1 | "use strict";
|
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
|
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
|
4 | };
|
5 | Object.defineProperty(exports, "__esModule", { value: true });
|
6 | const fslib_1 = require("@yarnpkg/fslib");
|
7 | const parsers_1 = require("@yarnpkg/parsers");
|
8 | const fast_glob_1 = __importDefault(require("fast-glob"));
|
9 | const stream_1 = require("stream");
|
10 | const pipe_1 = require("./pipe");
|
11 | const pipe_2 = require("./pipe");
|
12 | function 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 | }
|
18 | const 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 |
|
132 | await Promise.all(outputs.map(output => {
|
133 |
|
134 | return new Promise(resolve => {
|
135 | output.on(`close`, () => {
|
136 | resolve();
|
137 | });
|
138 | output.end();
|
139 | });
|
140 | }));
|
141 | return exitCode;
|
142 | }],
|
143 | ]);
|
144 | async 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 | }
|
151 | async 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 | }
|
165 | async 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 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 | function 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 | }
|
346 | function 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 | }
|
353 | async function executeCommandChain(node, opts, state) {
|
354 | let current = node;
|
355 | let pipeType = null;
|
356 | let execution = null;
|
357 | while (current) {
|
358 |
|
359 |
|
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 |
|
378 |
|
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 |
|
406 |
|
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 |
|
417 |
|
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 |
|
445 |
|
446 |
|
447 | async 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 |
|
452 | if (state.exitCode !== null)
|
453 | return state.exitCode;
|
454 |
|
455 |
|
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 | }
|
485 | async function executeShellLine(node, opts, state) {
|
486 | let rightMostExitCode = 0;
|
487 | for (const command of node) {
|
488 | rightMostExitCode = await executeCommandLine(command, opts, state);
|
489 |
|
490 | if (state.exitCode !== null)
|
491 | return state.exitCode;
|
492 |
|
493 |
|
494 | state.variables[`?`] = String(rightMostExitCode);
|
495 | }
|
496 | return rightMostExitCode;
|
497 | }
|
498 | function 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 | }
|
517 | function 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 | }
|
533 | function 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 | }
|
568 | async 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 |
|
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 |
|
584 | if (stdin === null) {
|
585 | stdin = new stream_1.PassThrough();
|
586 | stdin.end();
|
587 | }
|
588 | const ast = parsers_1.parseShell(command, glob);
|
589 |
|
590 |
|
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 | }
|
630 | exports.execute = execute;
|