Skip to content

Commit 2beb888

Browse files
Add support for stdin
1 parent 7f1d822 commit 2beb888

12 files changed

Lines changed: 230 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
## [1.18.0] 2025-05-06
4+
5+
- Support stdin (#145)
6+
37
## [1.17.0] 2025-03-12
48

59
- Make input history persistent (#140)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ VSCode renders it like this:
9696

9797
* `command`: the system command to be executed (must be in PATH). If given as an array, the elements are joined by spaces.
9898
* `commandArgs`: if provided, `command` is interpreted as the binary to run and `commandArgs` are the arguments. This is useful if the binary you want to run has spaces (like `C:\Program Files\*`). This translates to `child_process.execFileSync(command, commandArgs)`.
99+
* `stdin`: if provided, this string is sent to the standard input of the command
99100
* `cwd`: the directory from within it will be executed
100101
* `env`: key-value pairs to use as environment variables (it won't append the variables to the current existing ones. It will replace instead)
101102
* `useFirstResult`: skip 'Quick Pick' dialog and use first result returned from the command

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Tasks Shell Input",
44
"description": "Use shell commands as input for your tasks",
55
"icon": "icon.png",
6-
"version": "1.17.0",
6+
"version": "1.18.0",
77
"publisher": "augustocdias",
88
"repository": {
99
"url": "https://github.com/augustocdias/vscode-shell-command"

src/lib/CommandHandler.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,77 @@ test("commandArgs", async () => {
248248
);
249249
});
250250

251+
describe("stdin", () => {
252+
test("command", async () => {
253+
const testDataPath = path.join(__dirname, "../test/testData/stdinCommand");
254+
255+
const tasksJson = await import(path.join(testDataPath, ".vscode/tasks.json"));
256+
const mockData = (await import(path.join(testDataPath, "mockData.ts"))).default;
257+
258+
mockVscode.setMockData(mockData);
259+
const input = tasksJson.inputs[0].args;
260+
const context = mockExtensionContext as unknown as vscode.ExtensionContext;
261+
const handler = new CommandHandler(
262+
input,
263+
new UserInputContext(context),
264+
context,
265+
child_process,
266+
);
267+
268+
const result = await handler.handle();
269+
270+
expect(execSpy).toHaveBeenCalledTimes(1);
271+
expect(execSpy).toHaveBeenCalledWith(
272+
"python",
273+
{
274+
cwd: testDataPath,
275+
encoding: "utf8",
276+
env: undefined,
277+
maxBuffer: undefined,
278+
},
279+
expect.anything(),
280+
);
281+
expect(execFileSpy).toHaveBeenCalledTimes(0);
282+
283+
expect(result).toBe("hello world");
284+
});
285+
286+
test("commandArgs", async () => {
287+
const testDataPath = path.join(__dirname, "../test/testData/stdincommandArgs");
288+
289+
const tasksJson = await import(path.join(testDataPath, ".vscode/tasks.json"));
290+
const mockData = (await import(path.join(testDataPath, "mockData.ts"))).default;
291+
292+
mockVscode.setMockData(mockData);
293+
const input = tasksJson.inputs[0].args;
294+
const context = mockExtensionContext as unknown as vscode.ExtensionContext;
295+
const handler = new CommandHandler(
296+
input,
297+
new UserInputContext(context),
298+
context,
299+
child_process,
300+
);
301+
302+
const result = await handler.handle();
303+
304+
expect(execSpy).toHaveBeenCalledTimes(0);
305+
expect(execFileSpy).toHaveBeenCalledTimes(1);
306+
expect(execFileSpy).toHaveBeenCalledWith(
307+
"python",
308+
["-v"],
309+
{
310+
cwd: testDataPath,
311+
encoding: "utf8",
312+
env: undefined,
313+
maxBuffer: undefined,
314+
},
315+
expect.anything(),
316+
);
317+
318+
expect(result).toBe("hello world");
319+
});
320+
});
321+
251322
test("stdio", async () => {
252323
const testDataPath = path.join(__dirname, "../test/testData/stdio");
253324

src/lib/CommandHandler.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,13 @@
11
import * as vscode from "vscode";
22
import * as child_process from "child_process";
3+
import * as stream from "stream";
34
import { ShellCommandOptions } from "./ShellCommandOptions";
45
import { VariableResolver, Input } from "./VariableResolver";
56
import { ShellCommandException } from "../util/exceptions";
67
import { UserInputContext } from "./UserInputContext";
78
import { QuickPickItem } from "./QuickPickItem";
89
import { parseBoolean } from "./options";
910

10-
function promisify<T extends unknown[], R>(fn: (...args: [...T, (err: Error | null, stdout: string, stderr: string) => void]) => R) {
11-
return (...args: [...T]) =>
12-
new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
13-
fn(...args, (err: Error | null, stdout: string, stderr: string) => {
14-
if (err) {
15-
reject(err);
16-
} else {
17-
resolve({ stdout, stderr });
18-
}
19-
});
20-
});
21-
}
22-
2311
export class CommandHandler {
2412
protected args: ShellCommandOptions;
2513
protected EOL = /\r\n|\r|\n/;
@@ -28,6 +16,7 @@ export class CommandHandler {
2816
protected input: Input;
2917
protected command: string;
3018
protected commandArgs: string[] | undefined;
19+
protected stdin: string | undefined;
3120
protected rememberKey: string | undefined;
3221
protected subprocess: typeof child_process;
3322

@@ -59,6 +48,7 @@ export class CommandHandler {
5948

6049
this.command = command;
6150
this.commandArgs = this.args.commandArgs as string[] | undefined;
51+
this.stdin = this.args.stdin;
6252

6353
this.input = this.resolveTaskToInput(this.args.taskId);
6454

@@ -171,7 +161,7 @@ export class CommandHandler {
171161
return result;
172162
}
173163

174-
protected async runCommand() {
164+
protected async runCommand(): Promise<{stdout: string, stderr: string}> {
175165
const options: child_process.ExecSyncOptionsWithStringEncoding = {
176166
encoding: "utf8",
177167
cwd: this.args.cwd,
@@ -180,16 +170,26 @@ export class CommandHandler {
180170
// shell: vscode.env.shell
181171
};
182172

183-
if (this.commandArgs !== undefined) {
184-
const execFile = promisify(this.subprocess.execFile);
185-
return await execFile(this.command, this.commandArgs, options);
186-
} else {
187-
const exec = promisify<
188-
[string, child_process.ExecOptionsWithStringEncoding],
189-
child_process.ChildProcess
190-
>(this.subprocess.exec);
191-
return exec(this.command, options);
192-
}
173+
return new Promise((resolve, reject) => {
174+
const callback = (error: Error | null, stdout: string, stderr: string) => {
175+
if (error) {
176+
reject(error);
177+
} else {
178+
resolve({ stdout, stderr });
179+
}
180+
};
181+
182+
const child = (this.commandArgs !== undefined)
183+
? this.subprocess.execFile(this.command, this.commandArgs, options, callback)
184+
: this.subprocess.exec(this.command, options, callback);
185+
186+
if ((this.stdin !== undefined) && (child.stdin !== null)) {
187+
const stdinStream = new stream.Readable();
188+
stdinStream.push(this.stdin);
189+
stdinStream.push(null);
190+
stdinStream.pipe(child.stdin);
191+
}
192+
});
193193
}
194194

195195
protected parseResult(commandOutput: { stdout: string, stderr: string }):
@@ -456,6 +456,7 @@ export class CommandHandler {
456456
// and command args. It can happen that we see the same input twice
457457
// because of workspaceFolders. We cannot detect this.
458458
return input.args.command === other.args.command &&
459+
input.args.stdin === other.args.stdin &&
459460
CommandHandler.compareCommandArgs(input.args.commandArgs,
460461
other.args.commandArgs);
461462
}

src/lib/ShellCommandOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface ShellCommandOptions
33
cwd?: string;
44
command: unknown;
55
commandArgs?: unknown;
6+
stdin?: string;
67
env?: { [s: string]: string };
78
useFirstResult?: boolean;
89
useSingleResult?: boolean;

src/lib/VariableResolver.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type Input = {
1111
taskId: string;
1212
command: string | string[];
1313
commandArgs: string[] | undefined;
14+
stdin: string | undefined;
1415
cwd: string;
1516
env: Record<string, string>;
1617
},
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "Test commandArgs",
6+
"type": "shell",
7+
"command": "echo ${input:inputTest}",
8+
"problemMatcher": []
9+
}
10+
],
11+
"inputs": [
12+
{
13+
"id": "inputTest",
14+
"type": "command",
15+
"command": "shellCommand.execute",
16+
"args": {
17+
"command": "python",
18+
"stdin": "print('hello world')",
19+
"useFirstResult": true
20+
}
21+
}
22+
]
23+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export default {
2+
"calls": {
3+
"workspace.getConfiguration().inspect()": {
4+
"[\"tasks\",null,\"inputs\"]": {
5+
"key": "tasks.inputs",
6+
"workspaceValue": [
7+
{
8+
"id": "inputTest",
9+
"type": "command",
10+
"command": "shellCommand.execute",
11+
"args": {
12+
"command": "python",
13+
"stdin": "print('hello world')",
14+
}
15+
}
16+
]
17+
}
18+
}
19+
},
20+
"staticData": {
21+
"window.activeTextEditor.document.fileName": `${__dirname}/.vscode/tasks.json`,
22+
"workspace.workspaceFolders": [
23+
{
24+
"uri": {
25+
"$mid": 1,
26+
"fsPath": `${__dirname}`,
27+
"external": `file://${__dirname}}`,
28+
"path": `${__dirname}`,
29+
"scheme": "file"
30+
},
31+
"name": "vscode-shell-command",
32+
"index": 0
33+
}
34+
]
35+
}
36+
};

0 commit comments

Comments
 (0)