← Back to Releasing JavaScript
Building a command-line tool
Build a command-line app that, following the Unix philosophy, does one thing well and plays well with others via text.
Reading command-line arguments
POSIX (Portable Operating System Interface) guidelines: Utility conventions. GNU-style long options. Can be ambiguous.
I wrote opsh
to do as little as possible.
Node 18.3 added a native solution with util.parseArgs()
, which has more features out of the box.
Some general options: -h, --help
, -v, --version
.
Reading input
From a file on the disk
For all things file system, the node:fs
module.
The node:path
module works with file system paths at an abstract level, working ..
and .
.
Expanding globs
The user’s shell can probably already expand globs:
# glob is expanded by shell…
mytool pages/*.html
# …into something like below
mytool pages/some-file.html pages/some-other-file.html …
Support of various glob features varies among shells. For example, macOS still packages bash
3.2.57, a version released circa 2006, that does not support the **
globstar operator. And discrepancies between shells can sometimes cause bugs that go unnoticed.
Rather then depending on shell expansion, some tools may benefit from expanding globs themselves. The user can then provide a quoted glob argument that passes through to process.argv
unchanged.
# quoted glob doesn’t get expanded by shell
mytool 'pages/**/*.html'
Node.js 22 adds the glob()
function to the node:fs
module. For earlier Node.js versions, you can use the popular fast-glob
package.
Reading ancillary files
If the tool needs to refer to another file packaged along with it, the file’s path must be resolved against the tool’s path, and not relative to process.cwd()
. Assuming you’re writing your tool in ESM format, you can use import.meta.url
as the reference:
/*
Relative to `process.cwd()`:
what works when running the tool locally will fail
when the end user installs the tool globally.
*/
const content = await fs.readFile('templates/default.html', 'utf8');
/*
Relative to the current path:
works locally and for the end user.
*/
const content = await fs.readFile(
new URL('../templates/default.html', import.meta.url),
'utf8'
);
From an URL
Node.js 18 adds to its global scope a browser-compatible fetch()
function which makes it easy to retrieve the content of a URL. For earlier Node.js versions, node-fetch
is a helpful replacement.
It’s fine if your tool covers the basic scenario where you HTTP GET an URL. But a POST here, some credentials there, and things can get complicated fast. Unless fetching web pages is your tool’s main focus, don’t go overboard adding fetching options to the command-line interface. Instead, let the user defer to a dedicated tool such as curl
or wget
, which devote ample API space to this sort of configuration.
When working with web page content, JavaScript libraries to parse HTML may come in handy.
Two of my projects,
percollate
andhred
, operate on the content of web pages. Even when the content is fetched with a separate tool, the page’s original URL remains important for resolving relative links. For both tools you can pass the URL as an option with-u <url>
or--url=<url>
:curl https://danburzo.ro/ | hred "a@.href" -u https://danburzo.ro/
From stdin
Slurp the stdin
stream:
/* Slurp a Readable stream */
async function slurp(stream) {
// Switch to text mode (default is binary)
stream.setEncoding('utf8');
let result = '';
for await (let chunk of stream) result += chunk;
return result;
}
// Usage:
const content = await slurp(process.stdin);
From the clipboard
Node.js doesn’t have access to reading and writing to the system clipboard. Operating systems have their own: macOS has pbcopy
and pbpaste
, while Linux distributions often come with xsel
.
# pipe the content of the macOS clipboard
# to `mytool` via `stdin`
pbpaste | mytool
Writing output
stdout and stderr
Configuration
In order to make a JavaScript file into a CLI tool, you need to:
- Include
#!/usr/bin/env node
as the first line in the file - Make the file executable with
chmod +x ./index.js
- Add it to the
bin
object inpackage.json
Using the app
Instal it from npm
.
Use it directly with npx
.
Further reading
Node.js command-line tools are the topic of an entire book by Dr. Axel Rauschmayer, Shell scripting with Node.js.
References to work into the text
- The Open Group Base Specifications Issue 7, 2018 edition
- The Open Group Base Specifications Issue 7, 2018 edition
- Introduction
- Shell & Utilities
- A few facts about POSIX - Vorakl's notes
- bash - Short/long options with option argument - is this some sort of convention? - Stack Overflow
- What is the general syntax of a Unix shell command? - Stack Overflow
- Command Line Interface Guidelines
- 12 Factor CLI apps