A library to help test CLI. Originally intended to test inquirer.js driven CLIs.
- Should work for other CLI applications, but YMMV.
- Developed on OSX, tested on *nix, windows.
- Pull requests, DX enhancements welcomed.
npm install cli-inspector
constinspector=require('cli-inspector');inspector.run(cmd_line,// command-line to spawn child processinteractions,// array of Interactions (see below)options// extends child_process.spawnOptions);The gif below shows a build of cli-inspector, including tests, which use cli-inspector to test an example inquirer.js pizza-ordering application.
cli-inspector exposes a single method - run(). The idea is to provide it an
array of in-sequence CLI interaction.
An example interactions array: src/test/fixtures/inquirer/interactions.js.
/** * run an cli-test. * 1. spawn a child process with the cmd_line specified. * 2. Provide an array of individual interactions. See [tests](test/fixtures/inquirer/interactions.ts) * for a working example. `npm run build` to see it in action. * interactions.forEach( (interaction) => { * - wait for the prompt on interaction.stdout (string/regexp) * - pipe interaction.input to stdin * - wait for interaction.stdout/interaction.stderr if specified. * rinse and repeat * 3. Options provide control over the inspector and the child_process. * The most important control provided by `cli-inspector` are * - `delta`, the polling interval. Defaults to 1000 * - `timeout`, the time to wait in ms till * @export * @param {string} cmd_line * @param {Interaction[]} interactions * @param {Options} [options] */exportasyncfunctionrun(cmd_line: string,interactions: Interaction[],options?: Options);exportinterfaceInteraction{/** * await prompt before processing this element * * @type {(string | RegExp)} */prompt: string|RegExp;/** * when prompt found, input is piped into stdin of child process. * If an array, each element is sent with the delta timeout * * @type {((string | RegExp)[] | (string | RegExp))} */input: (string|RegExp)[]|(string|RegExp);/** * (Optional) message to await on stdout of child process, after input * * @type {(string | RegExp)} */stdout?: string|RegExp;/** * (Optional) message to await on stderr of child process, after input * * @type {(string | RegExp)} */stderr?: string|RegExp;/** * optional timeout for this step. * * @type {(number | null)} * @default (value of options.timeout) */timeout?: number|null;/** * debug this step only. useful to find problems deep in an interaction chain. * * @type {boolean} * @default false */debugStep?: boolean;}exportinterfaceOptionsextendsSpawnOptions{/** * prints all child_process stdin/stdout/stderr to process stdout/stderr * * @type {boolean} * @default false */debug?: boolean;/** * Total timeout for each prompt-input-response sequence. * Specified in milliseconds * Can also be customized per interaction, but this sets the default value * for all interactions when not specified. * * @type {number} * @default 5000 */timeout?: number;/** * delta time between polling intervals and between keyboard sequences. * Specified in milliseconds * * @type {number} * @default 1000 */delta?: number;/** * Normally, the child process is killed on exit. This allows control. * Generally, this is not very useful except for interactive debugging. * * @type {boolean} * @default false */killOnExit?: boolean;}CLIs have control sequences that make exact matching very cumbersome to create and maintain. It's much more convenient to use RegExps with wildcards and keywords.
This works well, but a few cautions:
- Prefer
[\s\S]*to.*with regular expressions that match multi-line strings with terminal control characters. - Escape all regexp special characters. There are a surprising number of them in regular CLIs. The set you should watch for:
^?[]()-{}!,*. - Currently, the spawned process inserts a
\nat column 80. The spawn operation however does not expose the underying TTY or control over it. Until we find a way around, the simplest thing is to add the[\n]?as an optional character in the match sequence. - The interactions array is a command-response sequence. Creating it can get very complicated quickly. To help,
cli-inspectoradds adetailsobject to erros thrown. Specifically,details.transcript, which records a step-by-step pattern match, making it easier to see what is actually being seen. A sample is shown below:
// Using err.details to get inspector state and debugging insight.try{inspector.run(cmd_line,interactions,options);}catch(err){console.log(JSON.stringify(err.details,null,2));throwerr;}git clone https://github.com/tufan-io/cli-inspector
cd cli-inspector
npm i
npm run build
- Spawns a child process with the supplied command line, under control of the cli-inspector.
- Pipes stdin, stdout and stderr appropriately.
+---------------+
| cli-inspector | +---------------+
| | stdin | child process |
| +----------------------> |
| | stdout | |
| <----------------------+ |
| | stderr | |
| <----------------------+ |
+---------------+ +---------------+
- Iterates over the supplied interactions, await interaction.prompt (on stdout) send interaction.userInput (on stdin) await interaction.response (on stdout) or interaction.error (on stderr)
- kills child-process on completion.
There are multiple debug controls provided, please look at the code or ask questions!
Apache 2.0
Bugs, PRs, comments, suggestions welcomed!

