in packages/@aws-cdk-testing/cli-integ/lib/shell.ts [14:124]
export async function shell(command: string[], options: ShellOptions = {}): Promise<string> {
if (options.modEnv && options.env) {
throw new Error('Use either env or modEnv but not both');
}
const outputs = new Set(options.outputs);
const writeToOutputs = (x: string) => {
for (const outputStream of outputs) {
outputStream.write(x);
}
};
// Always output the command
writeToOutputs(`💻 ${command.join(' ')}\n`);
const show = options.show ?? 'always';
const env = options.env ?? (options.modEnv ? { ...process.env, ...options.modEnv } : process.env);
const tty = options.interact && options.interact.length > 0;
// Coerce to `any` because `ShellOptions` contains custom properties
// that don't exist in the underlying interfaces. We could either rebuild each options map,
// or just pass through and let the underlying implemenation ignore what it doesn't know about.
// We choose the lazy one.
const spawnOptions = { ...options, env } as any;
const child = tty
? Process.spawnTTY(command[0], command.slice(1), spawnOptions)
: Process.spawn(command[0], command.slice(1), spawnOptions);
// copy because we will be shifting it
const remainingInteractions = [...(options.interact ?? [])];
return new Promise<string>((resolve, reject) => {
const stdout = new Array<Buffer>();
const stderr = new Array<Buffer>();
const lastLine = new LastLine();
child.onStdout(chunk => {
if (show === 'always') {
writeToOutputs(chunk.toString('utf-8'));
}
stdout.push(chunk);
lastLine.append(chunk.toString('utf-8'));
const interaction = remainingInteractions[0];
if (interaction) {
if (interaction.prompt.test(lastLine.get())) {
// subprocess expects a user input now.
// first, shift the interactions to ensure the same interaction is not reused
remainingInteractions.shift();
// then, reset the last line to prevent repeated matches caused by tty echoing
lastLine.reset();
// now write the input with a slight delay to ensure
// the child process has already started reading.
setTimeout(() => {
child.writeStdin(interaction.input + (interaction.end ?? os.EOL));
}, 500);
}
}
});
if (tty && options.captureStderr === false) {
// in a tty stderr goes to the same fd as stdout
throw new Error('Cannot disable \'captureStderr\' in tty');
}
if (!tty) {
// in a tty stderr goes to the same fd as stdout, so onStdout
// is sufficient.
child.onStderr(chunk => {
if (show === 'always') {
writeToOutputs(chunk.toString('utf-8'));
}
if (options.captureStderr ?? true) {
stderr.push(chunk);
}
});
}
child.onError(reject);
child.onExit(code => {
const stderrOutput = Buffer.concat(stderr).toString('utf-8');
const stdoutOutput = Buffer.concat(stdout).toString('utf-8');
const out = (options.onlyStderr ? stderrOutput : stdoutOutput + stderrOutput).trim();
const logAndreject = (error: Error) => {
if (show === 'error') {
writeToOutputs(`${out}\n`);
}
reject(error);
};
if (remainingInteractions.length !== 0) {
// regardless of the exit code, if we didn't consume all expected interactions we probably
// did somethiing wrong.
logAndreject(new Error(`Expected more user interactions but subprocess exited with ${code}`));
return;
}
if (code === 0 || options.allowErrExit) {
resolve(out);
} else {
logAndreject(new Error(`'${command.join(' ')}' exited with error code ${code}.`));
}
});
});
}