Skip to content

Commit 3e102b5

Browse files
authored
Implements shell redirection (yarnpkg#245)
1 parent a17b358 commit 3e102b5

File tree

7 files changed

+167
-22
lines changed

7 files changed

+167
-22
lines changed

.pnp.js

+20-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/berry-fslib/sources/FakeFS.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type CreateReadStreamOptions = Partial<{
1010

1111
export type CreateWriteStreamOptions = Partial<{
1212
encoding: string,
13+
flags: 'a',
1314
}>;
1415

1516
export type WriteFileOptions = Partial<{

packages/berry-shell/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"devDependencies": {
1313
"@berry/pnpify": "workspace:0.0.4",
1414
"@types/cross-spawn": "6.0.0",
15+
"@types/tmp": "0.1.0",
16+
"tmp": "^0.0.33",
1517
"typescript": "^3.3.3333"
1618
},
1719
"scripts": {

packages/berry-shell/sources/index.ts

+89-2
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,95 @@ const BUILTINS = new Map<string, ShellBuiltin>([
8686
}],
8787

8888
[`setredirects`, async (args: Array<string>, opts: ShellOptions, state: ShellState) => {
89-
state.stderr.write(`Shell redirections aren't implemented yet.\n`);
90-
return 1;
89+
let stdin = state.stdin;
90+
let stdout = state.stdout;
91+
let stderr = state.stderr;
92+
93+
const inputs: Array<() => Readable> = [];
94+
const outputs: Array<Writable> = [];
95+
96+
let t = 0;
97+
98+
while (args[t] !== `--`) {
99+
const type = args[t++];
100+
101+
const count = Number(args[t++]);
102+
const last = t + count;
103+
104+
for (let u = t; u < last; ++t, ++u) {
105+
switch (type) {
106+
case `<`: {
107+
inputs.push(() => {
108+
return xfs.createReadStream(ppath.resolve(state.cwd, NodeFS.toPortablePath(args[u])));
109+
});
110+
} break;
111+
case `<<<`: {
112+
inputs.push(() => {
113+
const input = new PassThrough();
114+
process.nextTick(() => {
115+
input.write(`${args[u]}\n`);
116+
input.end();
117+
});
118+
return input;
119+
});
120+
} break;
121+
case `>`: {
122+
outputs.push(xfs.createWriteStream(ppath.resolve(state.cwd, NodeFS.toPortablePath(args[u]))));
123+
} break;
124+
case `>>`: {
125+
outputs.push(xfs.createWriteStream(ppath.resolve(state.cwd, NodeFS.toPortablePath(args[u])), {flags: `a`}));
126+
} break;
127+
}
128+
}
129+
}
130+
131+
if (inputs.length > 0) {
132+
const pipe = new PassThrough();
133+
stdin = pipe;
134+
135+
const bindInput = (n: number) => {
136+
if (n === inputs.length) {
137+
pipe.end();
138+
} else {
139+
const input = inputs[n]();
140+
input.pipe(pipe, {end: false});
141+
input.on(`end`, () => {
142+
bindInput(n + 1);
143+
});
144+
}
145+
};
146+
147+
bindInput(0);
148+
}
149+
150+
if (outputs.length > 0) {
151+
const pipe = new PassThrough();
152+
stdout = pipe;
153+
154+
for (const output of outputs) {
155+
pipe.pipe(output);
156+
}
157+
}
158+
159+
const exitCode = await start(makeCommandAction(args.slice(t + 1), opts, state), {
160+
stdin: new ProtectedStream<Readable>(stdin),
161+
stdout: new ProtectedStream<Writable>(stdout),
162+
stderr: new ProtectedStream<Writable>(stderr),
163+
}).run();
164+
165+
// Close all the outputs (since the shell never closes the output stream)
166+
await Promise.all(outputs.map(output => {
167+
output.end();
168+
169+
// Wait until the output got flushed to the disk
170+
return new Promise(resolve => {
171+
output.on(`close`, () => {
172+
resolve();
173+
});
174+
});
175+
}));
176+
177+
return exitCode;
91178
}],
92179
]);
93180

packages/berry-shell/tests/shell.test.ts

+46-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import {NodeFS, xfs} from '@berry/fslib';
12
import {execute} from '@berry/shell';
23
import {PassThrough} from 'stream';
4+
import {fileSync} from 'tmp';
35

46
const ifNotWin32It = process.platform !== `win32`
57
? it
@@ -404,51 +406,83 @@ describe(`Simple shell features`, () => {
404406
});
405407
});
406408

409+
it(`should support empty default arguments`, async () => {
410+
await expect(bufferResult(
411+
`echo "foo\${DOESNT_EXIST:-}bar"`,
412+
)).resolves.toMatchObject({
413+
stdout: `foobar\n`,
414+
});
415+
});
416+
407417
it(`should support input redirections (file)`, async () => {
418+
const file = NodeFS.toPortablePath(fileSync({discardDescriptor: true}).name);
419+
await xfs.writeFilePromise(file, `hello world\n`);
420+
408421
await expect(bufferResult(
409-
`echo foo < bar`,
422+
`cat < "${file}"`,
410423
)).resolves.toMatchObject({
411-
stderr: `Shell redirections aren't implemented yet.\n`,
424+
stdout: `hello world\n`,
412425
});
413426
});
414427

415428
it(`should support input redirections (string)`, async () => {
416429
await expect(bufferResult(
417-
`echo foo <<< bar`,
430+
`cat <<< "hello world"`,
418431
)).resolves.toMatchObject({
419-
stderr: `Shell redirections aren't implemented yet.\n`,
432+
stdout: `hello world\n`,
420433
});
421434
});
422435

423436
it(`should support output redirections (overwrite)`, async () => {
437+
const file = NodeFS.toPortablePath(fileSync({discardDescriptor: true}).name);
438+
424439
await expect(bufferResult(
425-
`echo foo > bar`,
440+
`echo "hello world" > "${file}"`,
426441
)).resolves.toMatchObject({
427-
stderr: `Shell redirections aren't implemented yet.\n`,
442+
stdout: ``,
428443
});
444+
445+
await expect(xfs.readFilePromise(file, `utf8`)).resolves.toEqual(`hello world\n`);
429446
});
430447

431448
it(`should support output redirections (append)`, async () => {
449+
const file = NodeFS.toPortablePath(fileSync({discardDescriptor: true}).name);
450+
await xfs.writeFilePromise(file, `foo bar baz\n`);
451+
432452
await expect(bufferResult(
433-
`echo foo >> bar`,
453+
`echo "hello world" >> "${file}"`,
434454
)).resolves.toMatchObject({
435-
stderr: `Shell redirections aren't implemented yet.\n`,
455+
stdout: ``,
436456
});
457+
458+
await expect(xfs.readFilePromise(file, `utf8`)).resolves.toEqual(`foo bar baz\nhello world\n`);
437459
});
438460

439461
it(`should support multiple outputs`, async () => {
462+
const file1 = NodeFS.toPortablePath(fileSync({discardDescriptor: true}).name);
463+
const file2 = NodeFS.toPortablePath(fileSync({discardDescriptor: true}).name);
464+
440465
await expect(bufferResult(
441-
`echo foo > bar > baz`,
466+
`echo "hello world" > "${file1}" > "${file2}"`,
442467
)).resolves.toMatchObject({
443-
stderr: `Shell redirections aren't implemented yet.\n`,
468+
stdout: ``,
444469
});
470+
471+
await expect(xfs.readFilePromise(file1, `utf8`)).resolves.toEqual(`hello world\n`);
472+
await expect(xfs.readFilePromise(file2, `utf8`)).resolves.toEqual(`hello world\n`);
445473
});
446474

447475
it(`should support multiple inputs`, async () => {
476+
const file1 = NodeFS.toPortablePath(fileSync({discardDescriptor: true}).name);
477+
await xfs.writeFilePromise(file1, `foo bar baz\n`);
478+
479+
const file2 = NodeFS.toPortablePath(fileSync({discardDescriptor: true}).name);
480+
await xfs.writeFilePromise(file2, `hello world\n`);
481+
448482
await expect(bufferResult(
449-
`echo foo < bar < baz`,
483+
`cat < "${file1}" < "${file2}"`,
450484
)).resolves.toMatchObject({
451-
stderr: `Shell redirections aren't implemented yet.\n`,
485+
stdout: `foo bar baz\nhello world\n`,
452486
});
453487
});
454488
});

yarn.lock

+9
Original file line numberDiff line numberDiff line change
@@ -1980,9 +1980,11 @@ __metadata:
19801980
"@berry/parsers": "workspace:0.0.2"
19811981
"@berry/pnpify": "workspace:0.0.4"
19821982
"@types/cross-spawn": "npm:6.0.0"
1983+
"@types/tmp": "npm:0.1.0"
19831984
cross-spawn: "npm:^6.0.5"
19841985
execa: "npm:^1.0.0"
19851986
stream-buffers: "npm:^3.0.2"
1987+
tmp: "npm:^0.0.33"
19861988
typescript: "npm:^3.3.3333"
19871989
languageName: unknown
19881990
linkType: soft
@@ -3441,6 +3443,13 @@ __metadata:
34413443
languageName: node
34423444
linkType: hard
34433445

3446+
"@types/tmp@npm:0.1.0":
3447+
version: 0.1.0
3448+
resolution: "@types/tmp@npm:0.1.0"
3449+
checksum: 8f5132c25ea5cf18e9694d807628fb6d654cca303de0a440197807ffabe7ed3edf2e95fbfe45d40e432f625fb2ae5ecc8e7d3f35c5be127bdcf375f46d037675
3450+
languageName: node
3451+
linkType: hard
3452+
34443453
"@types/tmp@npm:^0.0.32":
34453454
version: 0.0.32
34463455
resolution: "@types/tmp@npm:0.0.32"

0 commit comments

Comments
 (0)