Skip to content

sgmonda/stdio

Repository files navigation

logo

The de facto standard input/output manager for Node.js

Build Status NPM version Known Vulnerabilities Maintainability codecov

After a very long time, finally version 2 is here. The cool stdio module you cannot live without has been rewritten and improved a lot, with Typescript support, promise-based usage and much more.

Note: Version 2 stops supporting non promise-based usage. Some functions have been renamed, too. So it is not compatible with older versions. If you're using an older version of stdio please, read this documentation carefully before upgrading.

Table of contents:

Installation

To install this module you can use npm:

$ npm install stdio

Then you can import it from your project, as a whole module or any of its parts independently:

import stdio from 'stdio';
import { getopt, read } from 'stdio';

Usage

This module contains the following static functions:

  • getopt(): a function to parse command-line arguments.
  • read(): an async function to read the standard input (or huge files) by lines, without having to worry about system resources.
  • readLine(): an async function to read a single line from the standard input.
  • ask(): an async function to ask questions in a terminal and wait for a user's response.

And the following classes:

  • ProgressBar: a class to create command-line progress bars.

Next sections will show how to use all of them.

getopt()

This function gives parsed UNIX-like command-line and options, preprocessed and ready to be used in an easy way. It is inspired by C standard library under UNIX.

import { getopt } from 'stdio';
const options = getopt({
  <option_name_1>: {<definition>},
  <option_name_2>: {<definition>},
  <option_name_3>: {<definition>},
  ...
});

Where <definition> is an object describing each option. These are the supported fields to define an option:

  • key (string): The short name for the option. It is a single-letter string.
  • description (string): The option description. A text for humans to understand what the option means.
  • required (boolean): If the option is mandatory or not.
  • args (number|string): The expected arguments count for the option (if the option expects arguments). This can be a number or the special string "*" when it is variable.
  • multiple (boolean): If the option should be specified for each value. This makes it mandatory to write things like -m 1 -m 2 -m 3 instead of -m 1 2 3.
  • default: (string[] or string): The default value for an option, in case it is not provided in the command.

Positional arguments (those not precedeed by an option) can be customized, too, using the special option _meta_, which supports some limits in the amount of required args:

import { getopt } from 'stdio';
const options = getopt({
  <option_name_1>: {<definition>},
  <option_name_2>: {<definition>},
  <option_name_3>: {<definition>},
  ...
  _meta_: { minArgs: <number>, maxArgs: <number>, args: <number> },
});

In case a required option is not defined or any option is not well used at runtime, an automatic help/usage message is printed, aborting the execution. This message is also shown automatically in case one of the special options -h, --help is provided.

USAGE: node example.js [OPTION1] [OPTION2]... arg1 arg2...
The following options are supported:
  -<key_1>, --<option_name_1>
  -<key_2>, --<option_name_2>
  ...
Behavior customization

In case you want to customize the automatic behavior when a command is wrong using your program, a second argument is supported by the getopt() call:

const options = getopt({...}, {<behavior_customizations>});

Here are the supported customizations:

  • printOnFailure (boolean): Print the usage/help message when your user writes a wrong command. This is true by default.
  • exitOnFailure (boolean): Kill the process with an exit code of failure. This is true by default.
  • throwOnFailure (boolean): Throw an exception in the getopt() call you can catch. This is false by default.

Please, note that exitOnFailure and throwOnFailure behavior customizations are not compatible. Only one of them is allowed at the same time.

The response of a getopt() call is a simple plain object with a value per option specified in the command. Each value can be one of the following:

  • boolean, for options not needing arguments.
  • string, for options expecting a single argument.
  • string[], for options expecting more than one argument.

See the following example for a better understanding of how to use getopt() and the expected resoponse:

Example

Here is a basic example of how to use getopt(). Please, note you'll find many more examples in the tests folder.

import { getopt } from 'stdio';

const options = getopt({
  name: { key: 'n', description: 'A name for the project', args: 1, required: true },
  keywords: { key: 'k', description: 'Some keywords to describe something', args: '*', multiple: true },
  languages: { args: '*' },
  finished: { description: 'If the project is finished' },
});

console.log('Stdio rocks!\n', options);

Here's a valid command for the previous options definition and the result of the getopt() response:

$ node example.js -n 'hello world' -k leisure -k health -k sport --languages javascript typescript c++ --finished
Stdio rocks!
 {
  name: 'hello world',
  keywords: [ 'leisure', 'health', 'sport' ],
  languages: [ 'javascript', 'typescript', 'c++' ],
  finished: true
}

On the other hand, if any option is not well used, the execution of our program will exit with an error result and the usage message will be shown. In this case, we omit the mandatory option --name, -n:

$ node example.js -k leisure -k health -k sport --languages javascript typescript c++ --finished
Missing option: "--name"
USAGE: node example.js [OPTION1] [OPTION2]... arg1 arg2...
The following options are supported:
  -n, --name <ARG1>          	A name for the project (required)
  -k, --keywords <ARG1>      	Some keywords to describe something (multiple)
  --languages <ARG1>...<ARGN>
  --finished                 	If the project is finished

Remember the same happens when --help or -h options are passed. They are reserved to be used to request help.

read()

This function reads the whole standard input by lines, waiting for a line to be processed successfully before reading the next one. This is perfect for huge files as lines are read only as you process them, so you don't have to worry about system resources.

import { read } from 'stdio';

async function onLine (line, index) {
  console.log('Processing line %d: %s', index, line);
  // Do your async stuff
}

read(onLine)
  .then(stats => console.log('Finished', stats))
  .catch(error => console.warn('Error', error));

Note that onLine is an async function, what means it returns a promise. read() call itself also returns a promise. In case one line fails being processed (its promise is rejected) the full read() promise will be rejected, too.

Once a read() successful call finishes (when all lines have been processed successfully), a small object with some stats is returned:

{
  length: number; // Number of lines
  times: Array<number>; // Duration of each line processor
  timesAverage: number; // Average duration of line processing
}
Example

The following command reads a huge file and pipes it to a simple program:

$ cat hugefile.txt | node myprogram.js

Where myprogram.js prints one line per second, including the line number at the begining:

import { read } from 'stdio';

function sleep (delay) {
  return new Promise((resolve) => {
    setTimeout(resolve, delay);
  });
}

async function onLine (line, index) {
  console.log(`#${index}: ${line}`);
  await sleep(1000);
}

read(onLine)
  .then((stats) => console.log('Finished', stats))
  .catch((error) => console.warn('Error', error));

The output is something like this:

#1: This is the first line of hugefile.txt
#2: Here the second one
#3: A third line...

readLine()

This function reads a single line from standard input. This is perfect for interactive terminal-based programs or just to read standard input on demand.

import { readLine } from 'stdio';

(async () => {
  ...
  const line = await readLine(<options>);
  ...
})()

Where <options> is an optional object with the following properties:

  • stream (Readable): An object implementing NodeJS.Readable interface, like a stream. By default, process.stdin is used.
  • close (boolean): An optional flag to close the reader after returning the line. This is useful if you want to stop listening before finishing your program execution.
Example

The following simple program lets the user introduce basic instructions and responds interactively:

import { readLine } from 'stdio';

(async () => {
  let command;
  do {
    command = await readLine();
    if (command === 'SAY_A') {
      console.log('A');
    } else if (command === 'SAY_B') {
      console.log('B');
    } else if (command === 'EXIT') {
      console.log('Good bye');
      await readLine({ close: true });
    }
  } while (command !== 'EXIT')
})()

Note we're closing the line reader. In this case it could be replaced by a simple process.exit(0), as our program doesn't do anything else.

ask()

This simple function let you ask questions to the user through the command line and wait for an answer:

import { ask } from 'stdio';
...
const answer = await ask(QUESTION_STRING, QUESTION_CONFIG);
...

Where QUESTION_STRING is just a string and QUESTION_CONFIG is an optional object including the following properties:

  • options (string[]): List of allowed values for the answer. If it is not provided, then any answer is accepted.
  • maxRetries (number): Only makes sense when options is provided. After maxRetries of wrong answers, the ask() returning promise is rejected with an error explaining that all retries have been spent with no successfull answer.
Example

Take a look at the following code

import { ask } from 'stdio';

async function main () {
	const name = await ask('What is your name?');
	const age = await ask('How old are you?');
	const gender = await ask('What is your gender?', { options: ['male', 'female'] });
	console.log('Your name is "%s". You are a "%s" "%s" years old.', name, gender, age);
}

main()
  .then(() => console.log('Finished'))
  .catch(error => console.warn(error));

Here is an example of the execution:

$ node example.js

What is your name?: John Doe
How old are you?: 34
What is your gender? [male/female]: other
Unexpected answer. 2 retries left.
What is your gender? [male/female]: male
Your name is "john doe". You are a "male" "34" years old.
Finished

ProgressBar

This utility let you create progress bar instances that are printed automatically in the terminal, using a beautiful format and estimating the remaining time of a task. Using it is as simple as follows:

import { ProgressBar } from 'stdio';

const bar = new ProgressBar(BAR_SIZE);
...
bar.tick();
bar.onFinish(() => console.log('FINISHED'));

Note that progress bars take the 100% of the terminal width where your code runs. No matter if you use a size of 10 or 10000 ticks. stdio takes care about the formatting so you don't have to worry about it. Your terminal will show something like the following:

00:01:12 23% [##############······················································] ETA 08:10:48
Example

The following code will create a progress bar of 345 pieces. It means the progress bar will be at 100% when we've called .tick() 345 times.

import { ProgressBar } from 'stdio';

var pbar = new ProgressBar(345);
var i = setInterval(() => pbar.tick(), 1000);
pbar.onFinish(() => {
	console.log('Finished!');
	clearInterval(i);
});

If you run the previous code, the following will be shown:

00:00:12 3% [###··································································] ETA 00:05:35

Contributing

Release new version:

  1. Update version in package.json
  2. Run npm publish

About

The de facto standard input/output manager for Node.js

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •