Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("toolExecuteBefore", () => {
it("should handle command with |", async () => {
mockOutput.args.command = "git log | head"
await toolExecuteBefore(mockInput, mockOutput)
expect(mockOutput.args.command).toBe("snip git log | snip head")
expect(mockOutput.args.command).toBe("snip git log | head")
})

it("should handle command with ;", async () => {
Expand Down Expand Up @@ -149,7 +149,7 @@ describe("toolExecuteBefore", () => {
it("should handle 2>&1 with pipe", async () => {
mockOutput.args.command = "find / -name \"*.log\" 2>&1 | grep error"
await toolExecuteBefore(mockInput, mockOutput)
expect(mockOutput.args.command).toBe("snip find / -name \"*.log\" 2>&1 | snip grep error")
expect(mockOutput.args.command).toBe("snip find / -name \"*.log\" 2>&1 | grep error")
})

it("should handle 2>&1 with chained commands", async () => {
Expand All @@ -158,4 +158,42 @@ describe("toolExecuteBefore", () => {
expect(mockOutput.args.command).toBe("snip cmd1 2>&1 && snip cmd2")
})
})
})

describe("pipe expressions with quotes", () => {
it("should not split pipes inside single quotes", async () => {
mockOutput.args.command = "cat file.json | jq '.content | .text'"
await toolExecuteBefore(mockInput, mockOutput)
expect(mockOutput.args.command).toBe("snip cat file.json | jq '.content | .text'")
})

it("should not split pipes inside double quotes", async () => {
mockOutput.args.command = 'cat file.json | jq ".content | .text"'
await toolExecuteBefore(mockInput, mockOutput)
expect(mockOutput.args.command).toBe('snip cat file.json | jq ".content | .text"')
})

it("should handle jq with fromjson", async () => {
mockOutput.args.command = "cat file.json | jq '.content[0].text | fromjson'"
await toolExecuteBefore(mockInput, mockOutput)
expect(mockOutput.args.command).toBe("snip cat file.json | jq '.content[0].text | fromjson'")
})

it("should handle multiple pipes in jq", async () => {
mockOutput.args.command = "cat file.json | jq '.a | .b | .c'"
await toolExecuteBefore(mockInput, mockOutput)
expect(mockOutput.args.command).toBe("snip cat file.json | jq '.a | .b | .c'")
})

it("should handle pipe with || operator", async () => {
mockOutput.args.command = "cmd1 || cmd2"
await toolExecuteBefore(mockInput, mockOutput)
expect(mockOutput.args.command).toBe("snip cmd1 || snip cmd2")
})

it("should handle mixed quotes and pipes", async () => {
mockOutput.args.command = 'echo "hello | world" | cat'
await toolExecuteBefore(mockInput, mockOutput)
expect(mockOutput.args.command).toBe('snip echo "hello | world" | cat')
})
})
})
35 changes: 33 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,30 @@ const ENV_VAR_RE = /^([A-Za-z_][A-Za-z0-9_]*=[^\s]* +)*/
const UNPROXYABLE_COMMANDS = new Set([
"cd", "source", ".", "export", "alias", "unset", "set", "shopt", "eval", "exec",
])
const OPERATOR_RE = /(\s*(?:&&|\|\||;|\|)\s*|\s&\s?)/
const OPERATOR_RE = /(\s*(?:&&|\|\||;)\s*|\s&\s?)/

function findFirstPipe(command: string): number {
let inSingleQuote = false
let inDoubleQuote = false

for (let i = 0; i < command.length; i++) {
const char = command[i]

if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote
} else if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote
} else if (char === '|' && !inSingleQuote && !inDoubleQuote) {
if (command[i + 1] === '|' || (i > 0 && command[i - 1] === '|')) {
i++
continue
}
return i
}
}

return -1
}

function snipCommand(command: string): string {
const envPrefix = (command.match(ENV_VAR_RE) ?? [""])[0]
Expand All @@ -21,6 +44,14 @@ export const toolExecuteBefore: NonNullable<Hooks["tool.execute.before"]> = asyn
if (!command || typeof command !== "string") return
if (command.startsWith("snip ")) return

if (findFirstPipe(command) !== -1) {
const pipeIdx = findFirstPipe(command)
const firstCmd = command.slice(0, pipeIdx).trimEnd()
const rest = command.slice(pipeIdx)
output.args.command = snipCommand(firstCmd) + ' ' + rest
return
}

const segments = command.split(OPERATOR_RE)

if (segments.length === 1) {
Expand All @@ -46,4 +77,4 @@ export const SnipPlugin: Plugin = async ({ $ }) => {
}
}

export default SnipPlugin
export default SnipPlugin