From 2f1ccfdf484f5acd0b5e6176c5e22a3aa957ac5d Mon Sep 17 00:00:00 2001 From: Robert Yates Date: Fri, 6 Feb 2026 13:32:22 -0500 Subject: [PATCH 1/3] first version with simple test --- src/Bash.ts | 7 +++++ src/commands/bash/bash.ts | 2 ++ src/test-final-summary.mjs | 57 ++++++++++++++++++++++++++++++++++++++ src/types.ts | 5 ++++ 4 files changed, 71 insertions(+) create mode 100644 src/test-final-summary.mjs 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.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/test-final-summary.mjs b/src/test-final-summary.mjs new file mode 100644 index 00000000..6e1423a6 --- /dev/null +++ b/src/test-final-summary.mjs @@ -0,0 +1,57 @@ +/** + * Final summary: The exact problem with just-bash + */ + +import { Bash, InMemoryFs } from 'just-bash'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +async function finalSummary() { + console.log('=== THE EXACT PROBLEM ===\n'); + + const testCommand = 'echo "hello world" | bash -c \'DATA=$(cat); echo "$DATA"\''; + + console.log('Command:', testCommand); + console.log(); + + // Test with native bash + console.log('1. Native bash (system):'); + try { + const { stdout } = await execAsync(testCommand); + console.log(' Result:', stdout.trim()); + console.log(' Status: ✅ WORKS\n'); + } catch (error) { + console.log(' Error:', error.message); + } + + // Test with just-bash + console.log('2. just-bash library:'); + const memFs = new InMemoryFs(); + const bash = new Bash({ + fs: memFs, + cwd: '/', + env: { HOME: '/', TMPDIR: '/tmp' }, + }); + + const result = await bash.exec(testCommand); + console.log(' Result:', result.stdout.trim() || '(empty)'); + console.log(' Status: ❌ FAILS\n'); + + console.log('=== ROOT CAUSE ===\n'); + console.log('just-bash does NOT properly handle stdin when piping to nested bash -c commands.'); + console.log('Specifically: `echo "data" | bash -c \'DATA=$(cat); ...\'` fails in just-bash.\n'); + + console.log('=== WORKAROUNDS FOR just-bash ===\n'); + console.log('Option 1: Write data to a file first, then read from file'); + console.log(' bash -c \'DATA=$(cat /path/to/file); ...\''); + console.log(); + console.log('Option 2: Break the command into separate bash.exec() calls'); + console.log(' (as demonstrated in test-bash-full-command.mjs)'); + console.log(); + console.log('Option 3: Avoid bash -c wrapper entirely'); + console.log(' Execute commands directly without nesting'); +} + +finalSummary(); \ No newline at end of file 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; } /** From 4a997f9ae479139f8d0cd3b7e5f04ac24bfcbd68 Mon Sep 17 00:00:00 2001 From: Robert Yates Date: Fri, 6 Feb 2026 13:36:57 -0500 Subject: [PATCH 2/3] moved the tests to the right spot --- src/commands/bash/bash.test.ts | 40 ++++++++++++++++++++++++ src/test-final-summary.mjs | 57 ---------------------------------- 2 files changed, 40 insertions(+), 57 deletions(-) delete mode 100644 src/test-final-summary.mjs diff --git a/src/commands/bash/bash.test.ts b/src/commands/bash/bash.test.ts index 51afb09a..d4ab364d 100644 --- a/src/commands/bash/bash.test.ts +++ b/src/commands/bash/bash.test.ts @@ -200,4 +200,44 @@ 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/test-final-summary.mjs b/src/test-final-summary.mjs deleted file mode 100644 index 6e1423a6..00000000 --- a/src/test-final-summary.mjs +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Final summary: The exact problem with just-bash - */ - -import { Bash, InMemoryFs } from 'just-bash'; -import { exec } from 'child_process'; -import { promisify } from 'util'; - -const execAsync = promisify(exec); - -async function finalSummary() { - console.log('=== THE EXACT PROBLEM ===\n'); - - const testCommand = 'echo "hello world" | bash -c \'DATA=$(cat); echo "$DATA"\''; - - console.log('Command:', testCommand); - console.log(); - - // Test with native bash - console.log('1. Native bash (system):'); - try { - const { stdout } = await execAsync(testCommand); - console.log(' Result:', stdout.trim()); - console.log(' Status: ✅ WORKS\n'); - } catch (error) { - console.log(' Error:', error.message); - } - - // Test with just-bash - console.log('2. just-bash library:'); - const memFs = new InMemoryFs(); - const bash = new Bash({ - fs: memFs, - cwd: '/', - env: { HOME: '/', TMPDIR: '/tmp' }, - }); - - const result = await bash.exec(testCommand); - console.log(' Result:', result.stdout.trim() || '(empty)'); - console.log(' Status: ❌ FAILS\n'); - - console.log('=== ROOT CAUSE ===\n'); - console.log('just-bash does NOT properly handle stdin when piping to nested bash -c commands.'); - console.log('Specifically: `echo "data" | bash -c \'DATA=$(cat); ...\'` fails in just-bash.\n'); - - console.log('=== WORKAROUNDS FOR just-bash ===\n'); - console.log('Option 1: Write data to a file first, then read from file'); - console.log(' bash -c \'DATA=$(cat /path/to/file); ...\''); - console.log(); - console.log('Option 2: Break the command into separate bash.exec() calls'); - console.log(' (as demonstrated in test-bash-full-command.mjs)'); - console.log(); - console.log('Option 3: Avoid bash -c wrapper entirely'); - console.log(' Execute commands directly without nesting'); -} - -finalSummary(); \ No newline at end of file From 27b7a741b64927b602de603be9d3d523153c15e5 Mon Sep 17 00:00:00 2001 From: Robert Yates Date: Fri, 6 Feb 2026 13:48:17 -0500 Subject: [PATCH 3/3] linter fixes --- src/commands/bash/bash.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/commands/bash/bash.test.ts b/src/commands/bash/bash.test.ts index d4ab364d..3ce6b942 100644 --- a/src/commands/bash/bash.test.ts +++ b/src/commands/bash/bash.test.ts @@ -224,9 +224,7 @@ cat /tmp/test.txt`, it("should handle stdin piping to sh -c", async () => { const env = new Bash(); - const result = await env.exec( - 'echo "from stdin" | sh -c \'cat\'', - ); + const result = await env.exec("echo \"from stdin\" | sh -c 'cat'"); expect(result.stdout).toBe("from stdin\n"); expect(result.exitCode).toBe(0); }); @@ -234,7 +232,7 @@ cat /tmp/test.txt`, 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\'', + "echo -e \"line1\\nline2\\nline3\" | bash -c 'grep line2'", ); expect(result.stdout).toBe("line2\n"); expect(result.exitCode).toBe(0);