diff --git a/src/commands/bash/bash.test.ts b/src/commands/bash/bash.test.ts index 3ce6b942..86c75250 100644 --- a/src/commands/bash/bash.test.ts +++ b/src/commands/bash/bash.test.ts @@ -237,5 +237,16 @@ cat /tmp/test.txt`, expect(result.stdout).toBe("line2\n"); expect(result.exitCode).toBe(0); }); + + it("should handle command substitution with grep and head in piped bash -c", async () => { + const env = new Bash(); + // This test demonstrates a bug where grep with no matches followed by head + // incorrectly passes through the original stdin instead of empty output + const result = await env.exec( + 'echo "test" | bash -c \'RESULT=$(cat | grep "nomatch" | head -1); echo "RESULT=[$RESULT]"\'', + ); + expect(result.stdout).toBe("RESULT=[]\n"); + expect(result.exitCode).toBe(0); + }); }); }); diff --git a/src/interpreter/pipeline-execution.ts b/src/interpreter/pipeline-execution.ts index aa588136..599cee3c 100644 --- a/src/interpreter/pipeline-execution.ts +++ b/src/interpreter/pipeline-execution.ts @@ -43,12 +43,21 @@ export async function executePipeline( for (let i = 0; i < node.commands.length; i++) { const command = node.commands[i]; const isLast = i === node.commands.length - 1; + const isFirst = i === 0; // In a multi-command pipeline, each command runs in a subshell context // where $_ starts empty (subshells don't inherit $_ from parent in same way) if (isMultiCommandPipeline) { // Clear $_ for each pipeline command - they each get fresh subshell context ctx.state.lastArg = ""; + + // After the first command, clear groupStdin so subsequent commands + // only see stdin from the pipeline (even if empty), not the original groupStdin + // This prevents commands like head from incorrectly falling back to groupStdin + // when they receive empty output from a previous command (e.g., grep with no matches) + if (!isFirst) { + ctx.state.groupStdin = undefined; + } } // Determine if this command runs in a subshell context