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 truepositionals 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.