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
7 changes: 7 additions & 0 deletions src/Bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
38 changes: 38 additions & 0 deletions src/commands/bash/bash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
2 changes: 2 additions & 0 deletions src/commands/bash/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down