export async function shell()

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}.`));
      }
    });
  });
}