Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

A command-line tool

A command-line tool (CLI) is a program controlled from the terminal with flags and answers to questions: npm, git and node work this way. For the blog we will write a blog tool that adds a post. Flags are parsed by the built-in parseArgs function from node:util, and missing data is asked for through node:readline.

Parsing flags

parseArgs() turns an array of arguments into an object. Each flag is described: type string (with a value) or boolean (present or not) and, optionally, a short name:

import { parseArgs } from 'node:util';
 
const { values, positionals } = parseArgs({
  args: ['add', '--title', 'Hello, CLI', '-d'],
  options: {
    title: { type: 'string', short: 't' },
    draft: { type: 'boolean', short: 'd', default: false },
  },
  allowPositionals: true,
});
 
console.log(positionals); // [ 'add' ]
console.log(values.title, values.draft); // Hello, CLI true

positionals are arguments without a flag, such as the command add. In a real program the args field is omitted — then process.argv.slice(2) is used.

An unknown flag

If you pass a flag that is not in options, parseArgs() throws. This protects against typos:

import { parseArgs } from 'node:util';
 
try {
  parseArgs({ args: ['--titel', 'Typo'], options: { title: { type: 'string' } } });
} catch (error) {
  console.log(error.code); // ERR_PARSE_ARGS_UNKNOWN_OPTION
}

Asking the user

node:readline/promises asks a question and waits for the answer with await. Create the file bin/blog.js:

// bin/blog.js
import { parseArgs } from 'node:util';
import readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import { addPost } from '../src/posts.js';
 
const { values } = parseArgs({
  options: {
    title: { type: 'string', short: 't' },
    help: { type: 'boolean', short: 'h' },
  },
});
 
if (values.help) {
  console.log('Usage: blog --title "Title"');
} else {
  const rl = readline.createInterface({ input, output });
  const title = values.title ?? (await rl.question('Post title: '));
  const body = await rl.question('Post text: ');
  rl.close();
 
  const post = await addPost(title, body);
  console.log(`Post #${post.id} saved`);
}

node bin/blog.js -t "Hello" asks only for the text; without the flag it asks for the title too. rl.close() is required: without it the program keeps waiting for input and never exits.

Running it by name

To call the tool with the word blog, add the line #!/usr/bin/env node at the top of bin/blog.js, and a bin field to package.json:

{
  "bin": {
    "blog": "./bin/blog.js"
  }
}

npm link in the project folder makes blog available everywhere in the terminal: blog --title "New post".

Common mistake

A boolean flag takes no value. --draft=no throws an error, and --draft no is even trickier: no becomes a separate positional argument (or an error if positionals are not allowed), and draft is still true:

import { parseArgs } from 'node:util';
 
const { values, positionals } = parseArgs({
  args: ['--draft', 'no'],
  options: { draft: { type: 'boolean' } },
  allowPositionals: true,
});
 
console.log(values.draft, positionals); // true [ 'no' ]

Declare flags that take a value with type string, and turn a boolean flag on simply by including it: --draft.

Thanks for reading!

Donate to the site — we work hard for you.