UNPKG

26.5 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6const fslib_1 = require("@yarnpkg/fslib");
7const parsers_1 = require("@yarnpkg/parsers");
8const fast_glob_1 = __importDefault(require("fast-glob"));
9const stream_1 = require("stream");
10const pipe_1 = require("./pipe");
11const pipe_2 = require("./pipe");
12function cloneState(state, mergeWith = {}) {
13 const newState = { ...state, ...mergeWith };
14 newState.environment = { ...state.environment, ...mergeWith.environment };
15 newState.variables = { ...state.variables, ...mergeWith.variables };
16 return newState;
17}
18const BUILTINS = new Map([
19 [`cd`, async ([target, ...rest], opts, state) => {
20 const resolvedTarget = fslib_1.ppath.resolve(state.cwd, fslib_1.npath.toPortablePath(target));
21 const stat = await fslib_1.xfs.statPromise(resolvedTarget);
22 if (!stat.isDirectory()) {
23 state.stderr.write(`cd: not a directory\n`);
24 return 1;
25 }
26 else {
27 state.cwd = resolvedTarget;
28 return 0;
29 }
30 }],
31 [`pwd`, async (args, opts, state) => {
32 state.stdout.write(`${fslib_1.npath.fromPortablePath(state.cwd)}\n`);
33 return 0;
34 }],
35 [`true`, async (args, opts, state) => {
36 return 0;
37 }],
38 [`false`, async (args, opts, state) => {
39 return 1;
40 }],
41 [`exit`, async ([code, ...rest], opts, state) => {
42 return state.exitCode = parseInt(code, 10);
43 }],
44 [`echo`, async (args, opts, state) => {
45 state.stdout.write(`${args.join(` `)}\n`);
46 return 0;
47 }],
48 [`__ysh_run_procedure`, async (args, opts, state) => {
49 const procedure = state.procedures[args[0]];
50 const exitCode = await pipe_2.start(procedure, {
51 stdin: new pipe_2.ProtectedStream(state.stdin),
52 stdout: new pipe_2.ProtectedStream(state.stdout),
53 stderr: new pipe_2.ProtectedStream(state.stderr),
54 }).run();
55 return exitCode;
56 }],
57 [`__ysh_set_redirects`, async (args, opts, state) => {
58 let stdin = state.stdin;
59 let stdout = state.stdout;
60 const stderr = state.stderr;
61 const inputs = [];
62 const outputs = [];
63 let t = 0;
64 while (args[t] !== `--`) {
65 const type = args[t++];
66 const count = Number(args[t++]);
67 const last = t + count;
68 for (let u = t; u < last; ++t, ++u) {
69 switch (type) {
70 case `<`:
71 {
72 inputs.push(() => {
73 return fslib_1.xfs.createReadStream(fslib_1.ppath.resolve(state.cwd, fslib_1.npath.toPortablePath(args[u])));
74 });
75 }
76 break;
77 case `<<<`:
78 {
79 inputs.push(() => {
80 const input = new stream_1.PassThrough();
81 process.nextTick(() => {
82 input.write(`${args[u]}\n`);
83 input.end();
84 });
85 return input;
86 });
87 }
88 break;
89 case `>`:
90 {
91 outputs.push(fslib_1.xfs.createWriteStream(fslib_1.ppath.resolve(state.cwd, fslib_1.npath.toPortablePath(args[u]))));
92 }
93 break;
94 case `>>`:
95 {
96 outputs.push(fslib_1.xfs.createWriteStream(fslib_1.ppath.resolve(state.cwd, fslib_1.npath.toPortablePath(args[u])), { flags: `a` }));
97 }
98 break;
99 }
100 }
101 }
102 if (inputs.length > 0) {
103 const pipe = new stream_1.PassThrough();
104 stdin = pipe;
105 const bindInput = (n) => {
106 if (n === inputs.length) {
107 pipe.end();
108 }
109 else {
110 const input = inputs[n]();
111 input.pipe(pipe, { end: false });
112 input.on(`end`, () => {
113 bindInput(n + 1);
114 });
115 }
116 };
117 bindInput(0);
118 }
119 if (outputs.length > 0) {
120 const pipe = new stream_1.PassThrough();
121 stdout = pipe;
122 for (const output of outputs) {
123 pipe.pipe(output);
124 }
125 }
126 const exitCode = await pipe_2.start(makeCommandAction(args.slice(t + 1), opts, state), {
127 stdin: new pipe_2.ProtectedStream(stdin),
128 stdout: new pipe_2.ProtectedStream(stdout),
129 stderr: new pipe_2.ProtectedStream(stderr),
130 }).run();
131 // Close all the outputs (since the shell never closes the output stream)
132 await Promise.all(outputs.map(output => {
133 // Wait until the output got flushed to the disk
134 return new Promise(resolve => {
135 output.on(`close`, () => {
136 resolve();
137 });
138 output.end();
139 });
140 }));
141 return exitCode;
142 }],
143]);
144async function executeBufferedSubshell(ast, opts, state) {
145 const chunks = [];
146 const stdout = new stream_1.PassThrough();
147 stdout.on(`data`, chunk => chunks.push(chunk));
148 await executeShellLine(ast, opts, cloneState(state, { stdout }));
149 return Buffer.concat(chunks).toString().replace(/[\r\n]+$/, ``);
150}
151async function applyEnvVariables(environmentSegments, opts, state) {
152 const envPromises = environmentSegments.map(async (envSegment) => {
153 const interpolatedArgs = await interpolateArguments(envSegment.args, opts, state);
154 return {
155 name: envSegment.name,
156 value: interpolatedArgs.join(` `),
157 };
158 });
159 const interpolatedEnvs = await Promise.all(envPromises);
160 return interpolatedEnvs.reduce((envs, env) => {
161 envs[env.name] = env.value;
162 return envs;
163 }, {});
164}
165async function interpolateArguments(commandArgs, opts, state) {
166 const redirections = new Map();
167 const interpolated = [];
168 let interpolatedSegments = [];
169 const split = (raw) => {
170 return raw.match(/[^ \r\n\t]+/g) || [];
171 };
172 const push = (segment) => {
173 interpolatedSegments.push(segment);
174 };
175 const close = () => {
176 if (interpolatedSegments.length > 0)
177 interpolated.push(interpolatedSegments.join(``));
178 interpolatedSegments = [];
179 };
180 const pushAndClose = (segment) => {
181 push(segment);
182 close();
183 };
184 const redirect = (type, target) => {
185 let targets = redirections.get(type);
186 if (typeof targets === `undefined`)
187 redirections.set(type, targets = []);
188 targets.push(target);
189 };
190 for (const commandArg of commandArgs) {
191 switch (commandArg.type) {
192 case `redirection`:
193 {
194 const interpolatedArgs = await interpolateArguments(commandArg.args, opts, state);
195 for (const interpolatedArg of interpolatedArgs) {
196 redirect(commandArg.subtype, interpolatedArg);
197 }
198 }
199 break;
200 case `argument`:
201 {
202 for (const segment of commandArg.segments) {
203 switch (segment.type) {
204 case `text`:
205 {
206 push(segment.text);
207 }
208 break;
209 case `glob`:
210 {
211 const matches = await opts.glob.match(segment.pattern, { cwd: state.cwd });
212 if (!matches.length)
213 throw new Error(`No file matches found: "${segment.pattern}". Note: Glob patterns currently only support files that exist on the filesystem (Help Wanted)`);
214 for (const match of matches.sort()) {
215 pushAndClose(match);
216 }
217 }
218 break;
219 case `shell`:
220 {
221 const raw = await executeBufferedSubshell(segment.shell, opts, state);
222 if (segment.quoted) {
223 push(raw);
224 }
225 else {
226 const parts = split(raw);
227 for (let t = 0; t < parts.length - 1; ++t)
228 pushAndClose(parts[t]);
229 push(parts[parts.length - 1]);
230 }
231 }
232 break;
233 case `variable`:
234 {
235 switch (segment.name) {
236 case `#`:
237 {
238 push(String(opts.args.length));
239 }
240 break;
241 case `@`:
242 {
243 if (segment.quoted) {
244 for (const raw of opts.args) {
245 pushAndClose(raw);
246 }
247 }
248 else {
249 for (const raw of opts.args) {
250 const parts = split(raw);
251 for (let t = 0; t < parts.length - 1; ++t)
252 pushAndClose(parts[t]);
253 push(parts[parts.length - 1]);
254 }
255 }
256 }
257 break;
258 case `*`:
259 {
260 const raw = opts.args.join(` `);
261 if (segment.quoted) {
262 push(raw);
263 }
264 else {
265 for (const part of split(raw)) {
266 pushAndClose(part);
267 }
268 }
269 }
270 break;
271 default:
272 {
273 const argIndex = parseInt(segment.name, 10);
274 if (Number.isFinite(argIndex)) {
275 if (!(argIndex >= 0 && argIndex < opts.args.length)) {
276 throw new Error(`Unbound argument #${argIndex}`);
277 }
278 else {
279 push(opts.args[argIndex]);
280 }
281 }
282 else {
283 if (Object.prototype.hasOwnProperty.call(state.variables, segment.name)) {
284 push(state.variables[segment.name]);
285 }
286 else if (Object.prototype.hasOwnProperty.call(state.environment, segment.name)) {
287 push(state.environment[segment.name]);
288 }
289 else if (segment.defaultValue) {
290 push((await interpolateArguments(segment.defaultValue, opts, state)).join(` `));
291 }
292 else {
293 throw new Error(`Unbound variable "${segment.name}"`);
294 }
295 }
296 }
297 break;
298 }
299 }
300 break;
301 }
302 }
303 }
304 break;
305 }
306 close();
307 }
308 if (redirections.size > 0) {
309 const redirectionArgs = [];
310 for (const [subtype, targets] of redirections.entries())
311 redirectionArgs.splice(redirectionArgs.length, 0, subtype, String(targets.length), ...targets);
312 interpolated.splice(0, 0, `__ysh_set_redirects`, ...redirectionArgs, `--`);
313 }
314 return interpolated;
315}
316/**
317 * Executes a command chain. A command chain is a list of commands linked
318 * together thanks to the use of either of the `|` or `|&` operators:
319 *
320 * $ cat hello | grep world | grep -v foobar
321 */
322function makeCommandAction(args, opts, state) {
323 if (!opts.builtins.has(args[0]))
324 args = [`command`, ...args];
325 const nativeCwd = fslib_1.npath.fromPortablePath(state.cwd);
326 let env = state.environment;
327 if (typeof env.PWD !== `undefined`)
328 env = { ...env, PWD: nativeCwd };
329 const [name, ...rest] = args;
330 if (name === `command`) {
331 return pipe_1.makeProcess(rest[0], rest.slice(1), opts, {
332 cwd: nativeCwd,
333 env,
334 });
335 }
336 const builtin = opts.builtins.get(name);
337 if (typeof builtin === `undefined`)
338 throw new Error(`Assertion failed: A builtin should exist for "${name}"`);
339 return pipe_1.makeBuiltin(async ({ stdin, stdout, stderr }) => {
340 state.stdin = stdin;
341 state.stdout = stdout;
342 state.stderr = stderr;
343 return await builtin(rest, opts, state);
344 });
345}
346function makeSubshellAction(ast, opts, state) {
347 return (stdio) => {
348 const stdin = new stream_1.PassThrough();
349 const promise = executeShellLine(ast, opts, cloneState(state, { stdin }));
350 return { stdin, promise };
351 };
352}
353async function executeCommandChain(node, opts, state) {
354 let current = node;
355 let pipeType = null;
356 let execution = null;
357 while (current) {
358 // Only the final segment is allowed to modify the shell state; all the
359 // other ones are isolated
360 const activeState = current.then
361 ? { ...state }
362 : state;
363 let action;
364 switch (current.type) {
365 case `command`:
366 {
367 const args = await interpolateArguments(current.args, opts, state);
368 const environment = await applyEnvVariables(current.envs, opts, state);
369 action = current.envs.length
370 ? makeCommandAction(args, opts, cloneState(activeState, { environment }))
371 : makeCommandAction(args, opts, activeState);
372 }
373 break;
374 case `subshell`:
375 {
376 const args = await interpolateArguments(current.args, opts, state);
377 // We don't interpolate the subshell because it will be recursively
378 // interpolated within its own context
379 const procedure = makeSubshellAction(current.subshell, opts, activeState);
380 if (args.length === 0) {
381 action = procedure;
382 }
383 else {
384 let key;
385 do {
386 key = String(Math.random());
387 } while (Object.prototype.hasOwnProperty.call(activeState.procedures, key));
388 activeState.procedures = { ...activeState.procedures };
389 activeState.procedures[key] = procedure;
390 action = makeCommandAction([...args, `__ysh_run_procedure`, key], opts, activeState);
391 }
392 }
393 break;
394 case `envs`:
395 {
396 const environment = await applyEnvVariables(current.envs, opts, state);
397 activeState.environment = { ...activeState.environment, ...environment };
398 action = makeCommandAction([`true`], opts, activeState);
399 }
400 break;
401 }
402 if (typeof action === `undefined`)
403 throw new Error(`Assertion failed: An action should have been generated`);
404 if (pipeType === null) {
405 // If we're processing the left-most segment of the command, we start a
406 // new execution pipeline
407 execution = pipe_2.start(action, {
408 stdin: new pipe_2.ProtectedStream(activeState.stdin),
409 stdout: new pipe_2.ProtectedStream(activeState.stdout),
410 stderr: new pipe_2.ProtectedStream(activeState.stderr),
411 });
412 }
413 else {
414 if (execution === null)
415 throw new Error(`The execution pipeline should have been setup`);
416 // Otherwise, depending on the exaxct pipe type, we either pipe stdout
417 // only or stdout and stderr
418 switch (pipeType) {
419 case `|`:
420 {
421 execution = execution.pipeTo(action);
422 }
423 break;
424 case `|&`:
425 {
426 execution = execution.pipeTo(action);
427 }
428 break;
429 }
430 }
431 if (current.then) {
432 pipeType = current.then.type;
433 current = current.then.chain;
434 }
435 else {
436 current = null;
437 }
438 }
439 if (execution === null)
440 throw new Error(`Assertion failed: The execution pipeline should have been setup`);
441 return await execution.run();
442}
443/**
444 * Execute a command line. A command line is a list of command shells linked
445 * together thanks to the use of either of the `||` or `&&` operators.
446 */
447async function executeCommandLine(node, opts, state) {
448 if (!node.then)
449 return await executeCommandChain(node.chain, opts, state);
450 const code = await executeCommandChain(node.chain, opts, state);
451 // If the execution aborted (usually through "exit"), we must bailout
452 if (state.exitCode !== null)
453 return state.exitCode;
454 // We must update $?, which always contains the exit code from
455 // the right-most command
456 state.variables[`?`] = String(code);
457 switch (node.then.type) {
458 case `&&`:
459 {
460 if (code === 0) {
461 return await executeCommandLine(node.then.line, opts, state);
462 }
463 else {
464 return code;
465 }
466 }
467 break;
468 case `||`:
469 {
470 if (code !== 0) {
471 return await executeCommandLine(node.then.line, opts, state);
472 }
473 else {
474 return code;
475 }
476 }
477 break;
478 default:
479 {
480 throw new Error(`Unsupported command type: "${node.then.type}"`);
481 }
482 break;
483 }
484}
485async function executeShellLine(node, opts, state) {
486 let rightMostExitCode = 0;
487 for (const command of node) {
488 rightMostExitCode = await executeCommandLine(command, opts, state);
489 // If the execution aborted (usually through "exit"), we must bailout
490 if (state.exitCode !== null)
491 return state.exitCode;
492 // We must update $?, which always contains the exit code from
493 // the right-most command
494 state.variables[`?`] = String(rightMostExitCode);
495 }
496 return rightMostExitCode;
497}
498function locateArgsVariableInSegment(segment) {
499 switch (segment.type) {
500 case `variable`:
501 {
502 return segment.name === `@` || segment.name === `#` || segment.name === `*` || Number.isFinite(parseInt(segment.name, 10)) || (!!segment.defaultValue && segment.defaultValue.some(arg => locateArgsVariableInArgument(arg)));
503 }
504 break;
505 case `shell`:
506 {
507 return locateArgsVariable(segment.shell);
508 }
509 break;
510 default:
511 {
512 return false;
513 }
514 break;
515 }
516}
517function locateArgsVariableInArgument(arg) {
518 switch (arg.type) {
519 case `redirection`:
520 {
521 return arg.args.some(arg => locateArgsVariableInArgument(arg));
522 }
523 break;
524 case `argument`:
525 {
526 return arg.segments.some(segment => locateArgsVariableInSegment(segment));
527 }
528 break;
529 default:
530 throw new Error(`Unreacheable`);
531 }
532}
533function locateArgsVariable(node) {
534 return node.some(command => {
535 while (command) {
536 let chain = command.chain;
537 while (chain) {
538 let hasArgs;
539 switch (chain.type) {
540 case `subshell`:
541 {
542 hasArgs = locateArgsVariable(chain.subshell);
543 }
544 break;
545 case `command`:
546 {
547 hasArgs = chain.envs.some(env => env.args.some(arg => {
548 return locateArgsVariableInArgument(arg);
549 })) || chain.args.some(arg => {
550 return locateArgsVariableInArgument(arg);
551 });
552 }
553 break;
554 }
555 if (hasArgs)
556 return true;
557 if (!chain.then)
558 break;
559 chain = chain.then.chain;
560 }
561 if (!command.then)
562 break;
563 command = command.then.line;
564 }
565 return false;
566 });
567}
568async function execute(command, args = [], { builtins = {}, cwd = fslib_1.npath.toPortablePath(process.cwd()), env = process.env, stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, variables = {}, glob = {
569 isGlobPattern: fast_glob_1.default.isDynamicPattern,
570 match: (pattern, { cwd, fs = fslib_1.xfs }) => fast_glob_1.default(pattern, {
571 cwd: fslib_1.npath.fromPortablePath(cwd),
572 // @ts-ignore: `fs` is wrapped in `PosixFS`
573 fs: new fslib_1.PosixFS(fs),
574 }),
575}, } = {}) {
576 const normalizedEnv = {};
577 for (const [key, value] of Object.entries(env))
578 if (typeof value !== `undefined`)
579 normalizedEnv[key] = value;
580 const normalizedBuiltins = new Map(BUILTINS);
581 for (const [key, builtin] of Object.entries(builtins))
582 normalizedBuiltins.set(key, builtin);
583 // This is meant to be the equivalent of /dev/null
584 if (stdin === null) {
585 stdin = new stream_1.PassThrough();
586 stdin.end();
587 }
588 const ast = parsers_1.parseShell(command, glob);
589 // If the shell line doesn't use the args, inject it at the end of the
590 // right-most command
591 if (!locateArgsVariable(ast) && ast.length > 0 && args.length > 0) {
592 let command = ast[ast.length - 1];
593 while (command.then)
594 command = command.then.line;
595 let chain = command.chain;
596 while (chain.then)
597 chain = chain.then.chain;
598 if (chain.type === `command`) {
599 chain.args = chain.args.concat(args.map(arg => {
600 return {
601 type: `argument`,
602 segments: [{
603 type: `text`,
604 text: arg,
605 }],
606 };
607 }));
608 }
609 }
610 return await executeShellLine(ast, {
611 args,
612 builtins: normalizedBuiltins,
613 initialStdin: stdin,
614 initialStdout: stdout,
615 initialStderr: stderr,
616 glob,
617 }, {
618 cwd,
619 environment: normalizedEnv,
620 exitCode: null,
621 procedures: {},
622 stdin,
623 stdout,
624 stderr,
625 variables: Object.assign(Object.create(variables), {
626 [`?`]: 0,
627 }),
628 });
629}
630exports.execute = execute;