Const BUILTINS
BUILTINS: Map<string, function> = new Map<string, ShellBuiltin>([[`cd`, async ([target = homedir(), ...rest]: Array<string>, opts: ShellOptions, state: ShellState) => {const resolvedTarget = ppath.resolve(state.cwd, npath.toPortablePath(target));const stat = await opts.baseFs.statPromise(resolvedTarget).catch(error => {throw error.code === `ENOENT`? new ShellError(`cd: no such file or directory: ${target}`): error;});if (!stat.isDirectory())throw new ShellError(`cd: not a directory: ${target}`);state.cwd = resolvedTarget;return 0;}],[`pwd`, async (args: Array<string>, opts: ShellOptions, state: ShellState) => {state.stdout.write(`${npath.fromPortablePath(state.cwd)}\n`);return 0;}],[`:`, async (args: Array<string>, opts: ShellOptions, state: ShellState) => {return 0;}],[`true`, async (args: Array<string>, opts: ShellOptions, state: ShellState) => {return 0;}],[`false`, async (args: Array<string>, opts: ShellOptions, state: ShellState) => {return 1;}],[`exit`, async ([code, ...rest]: Array<string>, opts: ShellOptions, state: ShellState) => {return state.exitCode = parseInt(code ?? state.variables[`?`], 10);}],[`echo`, async (args: Array<string>, opts: ShellOptions, state: ShellState) => {state.stdout.write(`${args.join(` `)}\n`);return 0;}],[`sleep`, async ([time]: Array<string>, opts: ShellOptions, state: ShellState) => {if (typeof time === `undefined`)throw new ShellError(`sleep: missing operand`);// TODO: make it support unit suffixesconst seconds = Number(time);if (Number.isNaN(seconds))throw new ShellError(`sleep: invalid time interval '${time}'`);return await setTimeoutPromise(1000 * seconds, 0);}],[`__ysh_run_procedure`, async (args: Array<string>, opts: ShellOptions, state: ShellState) => {const procedure = state.procedures[args[0]];const exitCode = await start(procedure, {stdin: new ProtectedStream<Readable>(state.stdin),stdout: new ProtectedStream<Writable>(state.stdout),stderr: new ProtectedStream<Writable>(state.stderr),}).run();return exitCode;}],[`__ysh_set_redirects`, async (args: Array<string>, opts: ShellOptions, state: ShellState) => {let stdin = state.stdin;let stdout = state.stdout;let stderr = state.stderr;const inputs: Array<() => Readable> = [];const outputs: Array<Writable> = [];const errors: Array<Writable> = [];let t = 0;while (args[t] !== `--`) {const key = args[t++];const {type, fd} = JSON.parse(key);const pushInput = (readableFactory: () => Readable) => {switch (fd) {case null:case 0: {inputs.push(readableFactory);} break;default:throw new Error(`Unsupported file descriptor: "${fd}"`);}};const pushOutput = (writable: Writable) => {switch (fd) {case null:case 1: {outputs.push(writable);} break;case 2: {errors.push(writable);} break;default:throw new Error(`Unsupported file descriptor: "${fd}"`);}};const count = Number(args[t++]);const last = t + count;for (let u = t; u < last; ++t, ++u) {switch (type) {case `<`: {pushInput(() => {return opts.baseFs.createReadStream(ppath.resolve(state.cwd, npath.toPortablePath(args[u])));});} break;case `<<<`: {pushInput(() => {const input = new PassThrough();process.nextTick(() => {input.write(`${args[u]}\n`);input.end();});return input;});} break;case `<&`: {pushInput(() => getFileDescriptorStream(Number(args[u]), StreamType.Readable, state));} break;case `>`:case `>>`: {const outputPath = ppath.resolve(state.cwd, npath.toPortablePath(args[u]));if (outputPath === `/dev/null`) {pushOutput(new Writable({autoDestroy: true,emitClose: true,write(chunk, encoding, callback) {setImmediate(callback);},}),);} else {pushOutput(opts.baseFs.createWriteStream(outputPath, type === `>>` ? {flags: `a`} : undefined));}} break;case `>&`: {pushOutput(getFileDescriptorStream(Number(args[u]), StreamType.Writable, state));} break;default: {throw new Error(`Assertion failed: Unsupported redirection type: "${type}"`);}}}}if (inputs.length > 0) {const pipe = new PassThrough();stdin = pipe;const bindInput = (n: number) => {if (n === inputs.length) {pipe.end();} else {const input = inputs[n]();input.pipe(pipe, {end: false});input.on(`end`, () => {bindInput(n + 1);});}};bindInput(0);}if (outputs.length > 0) {const pipe = new PassThrough();stdout = pipe;for (const output of outputs) {pipe.pipe(output);}}if (errors.length > 0) {const pipe = new PassThrough();stderr = pipe;for (const error of errors) {pipe.pipe(error);}}const exitCode = await start(makeCommandAction(args.slice(t + 1), opts, state), {stdin: new ProtectedStream<Readable>(stdin),stdout: new ProtectedStream<Writable>(stdout),stderr: new ProtectedStream<Writable>(stderr),}).run();// Close all the outputs (since the shell never closes the output stream)await Promise.all(outputs.map(output => {// Wait until the output got flushed to the diskreturn new Promise<void>((resolve, reject) => {output.on(`error`, error => {reject(error);});output.on(`close`, () => {resolve();});output.end();});}));// Close all the errors (since the shell never closes the error stream)await Promise.all(errors.map(err => {// Wait until the error got flushed to the diskreturn new Promise<void>((resolve, reject) => {err.on(`error`, error => {reject(error);});err.on(`close`, () => {resolve();});err.end();});}));return exitCode;}],])
@yarnpkg/shell
A JavaScript implementation of a bash-like shell (we use it in Yarn 2 to provide cross-platform scripting). This package exposes an API that abstracts both the parser and the interpreter; should you only need the parser you can check out
@yarnpkg/parsers
, but you probably won't need it.Usage
import {execute} from '@yarnpkg/shell';
process.exitCode = await execute(
ls "$0" | wc -l
, [process.cwd()]);Features
ls *.txt
)Help Wanted
mv build/{index.js,index.build.js}
,echo {foo,bar}
,FOO=a,b echo {$FOO,x}
)Non-Goals