diff --git a/src/Bash.ts b/src/Bash.ts index bfff63a1..1b800fc8 100644 --- a/src/Bash.ts +++ b/src/Bash.ts @@ -199,6 +199,11 @@ export interface ExecOptions { * Default: false */ rawScript?: boolean; + /** + * Standard input to pass to the script. + * This will be available to commands via stdin (e.g., for `bash -c 'cat'`). + */ + stdin?: string; } export class Bash { @@ -504,6 +509,8 @@ export class Bash { options: { ...this.state.options }, // Share hashTable reference - it should persist across exec calls hashTable: this.state.hashTable, + // Pass stdin through to commands (for bash -c with piped input) + groupStdin: options?.stdin, }; // Normalize indented multi-line scripts (unless rawScript is true) diff --git a/src/commands/bash/bash.test.ts b/src/commands/bash/bash.test.ts index 51afb09a..3ce6b942 100644 --- a/src/commands/bash/bash.test.ts +++ b/src/commands/bash/bash.test.ts @@ -200,4 +200,42 @@ cat /tmp/test.txt`, expect(result.exitCode).toBe(0); }); }); + + describe("stdin piping to nested bash -c", () => { + it("should handle stdin when piping to bash -c with command substitution", async () => { + const env = new Bash(); + // This is the key test case: piping stdin to a nested bash -c command + // The stdin should be available to commands inside the bash -c script + const result = await env.exec( + 'echo "hello world" | bash -c \'DATA=$(cat); echo "$DATA"\'', + ); + expect(result.stdout).toBe("hello world\n"); + expect(result.exitCode).toBe(0); + }); + + it("should handle stdin with multiple commands in bash -c", async () => { + const env = new Bash(); + const result = await env.exec( + 'echo "test data" | bash -c \'read LINE; echo "Got: $LINE"\'', + ); + expect(result.stdout).toBe("Got: test data\n"); + expect(result.exitCode).toBe(0); + }); + + it("should handle stdin piping to sh -c", async () => { + const env = new Bash(); + const result = await env.exec("echo \"from stdin\" | sh -c 'cat'"); + expect(result.stdout).toBe("from stdin\n"); + expect(result.exitCode).toBe(0); + }); + + it("should handle complex piping with bash -c", async () => { + const env = new Bash(); + const result = await env.exec( + "echo -e \"line1\\nline2\\nline3\" | bash -c 'grep line2'", + ); + expect(result.stdout).toBe("line2\n"); + expect(result.exitCode).toBe(0); + }); + }); }); diff --git a/src/commands/bash/bash.ts b/src/commands/bash/bash.ts index fa390bf6..e807454f 100644 --- a/src/commands/bash/bash.ts +++ b/src/commands/bash/bash.ts @@ -151,9 +151,11 @@ async function executeScript( // Execute the script as-is, preserving newlines for proper parsing // The parser needs to see the original structure to correctly handle // multi-line constructs like (( ... )) vs ( ( ... ) ) + // Pass stdin through to the nested script const result = await ctx.exec(scriptToRun, { env: positionalEnv, cwd: ctx.cwd, + stdin: ctx.stdin, }); return result; } diff --git a/src/types.ts b/src/types.ts index a98d8dfc..4da01d77 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,6 +33,11 @@ export interface CommandExecOptions { * Always pass `ctx.cwd` from the calling command's context. */ cwd: string; + /** + * Standard input to pass to the subcommand. + * Optional - if not provided, stdin will be empty. + */ + stdin?: string; } /**