@ratwizard/cli
v0.17.0
Published
Easy Node CLI framework
Downloads
60
Readme
@ratwizard/cli
Work-in-progress framework for quickly setting up CLI tools.
Quick Start
- Install with
npm i @ratwizard/cli
oryarn add @ratwizard/cli
. - See How does this thing work? to see how this thing works.
How does this thing work?
Contents
Initialize
The module exports a function to which you pass some optional metadata. You will receive a set of functions you can use to configure your CLI.
// in cli.js
const { program } = require("@ratwizard/cli");
We are destructuring the program
object, used to define a root command. The program
object has all the functions we'll need to do everything here in the main file.
Define A Command
Let's start with the most basic CLI definition; a single function with arguments and flags. The root command can have as many arguments and options as needed. All you need is the single command with a bunch of options if you want to structure your CLI like youtube-dl.
// in cli.js
program
.description("extends a greeting")
.arg("name", {
description: "name of the person to greet",
default: "world"
})
.option("-l, --loud", {
description: "SHOUT",
boolean: true // a true/false flag that takes no arguments
})
.option("--extra <text>", {
// Whereas this does take a value
description: "add more to your greeting"
})
.action({ args, options } => {
let message = `Hello, ${args.name}!`;
if (options.loud) {
message = message.toUpperCase() + "!!";
}
if (options.extra) {
message += options.extra + "!";
if (options.loud) {
message = message.toUpperCase() + "!!";
}
}
console.log(message);
})
.run(process.argv);
Notice the call to .run()
at the end of the chain. This will parse command line arguments and execute the command we just defined.
Now you can call your CLI like this:
$ node cli.js Tom --extra gaaaaa --loud
HELLO, TOM!!! GAAAAA!!!
You also get a nicely formatted --help
option out of the box. The help text includes any description
fields you add to your command, arguments and flags.
$ node cli.js --help
USAGE
cli.js [--flags] [<name>]
DESCRIPTION
extends a greeting
ARGUMENTS
name (string) name of the person to greet
FLAGS
-l, --loud SHOUT
--extra <text> add more to your greeting
Multiple Commands
Or, you can define several non-root commands. A user would invoke these commands by passing their name as an argument. If you are familiar with git
, think of commands as you would git commit
or git log
. They are sub-functions under the larger CLI umbrella.
Let's turn the single command above into a subcommand called "greet". We'll move its properties to an object and pass them to a .command() function on the program object. Let's also add another command for if we encounter any droids.
// in cli.js
program
.command("greet", {
description: "extends a greeting",
args: {
name: {
description: "name of the person to greet",
default: "world",
},
},
options: {
"-l, --loud": {
description: "SHOUT",
boolean: true,
},
"--extra <text>": {
description: "add more to your greeting",
},
},
action: ({ args, options }) => {
let message = `Hello, ${args.name}!`;
if (options.loud) {
message = message.toUpperCase() + "!!";
}
if (options.extra) {
message += options.extra + "!";
if (options.loud) {
message = message.toUpperCase() + "!!";
}
}
console.log(message);
},
})
.command("beep", {
description: "extends a greeting for droids",
action: () => {
console.log("boop");
},
})
.run(process.env);
Now both commands can be called by name through the CLI:
$ node cli.js greet Bob
Hello, Bob!
$ node cli.js beep
boop
If you enter a command name that doesn't exist you will see a listing of all valid commands. This is the same output you would see if you used the --help
flag.
$ node cli.js notreal
USAGE
cli.js <command> [<args>]
COMMANDS
run `cli.js <command> --help` to read about args and options for a specific command
greet .. extends a greeting
beep ... extends a greeting for droids
As it says, if you need help with a specific command, you can get it by using --help
with that command.
$ node cli.js greet --help
USAGE
cli.js greet [--flags] [<name>]
DESCRIPTION
extends a greeting
ARGUMENTS
name (string) name of the person to greet
FLAGS
-l, --loud SHOUT
--extra <text> add more to your greeting
Commands from other files
Of course, chaining multiple commands requires a change of syntax from chained functions to an object. If you prefer the function chaining syntax, if your commands have many args and options, or if you have many commands, it might be nicer to split each one off into its own separate file. You can do that!
You'll still initialize your command with a name, but we are moving the other calls into another file. The other file will export a top level command.
// in cli.js
program
.command("greet", {
description: "extends a greeting",
path: "./commands/greet",
})
.command("beep", {
description: "extends a greeting for droids",
path: "./commands/beep",
})
.run(process.env);
That's it! Everything other than the command name is configured in our new ./commands/greet.js
file. Running node cli.js greet
should have exactly the same effect it did before.
The only difference is that you'll define the command with new Command()
and not include a .run()
call at the end.
// in ./commands/greet.js
const { Command } = require("@ratwizard/cli");
module.exports = new Command()
.arg("name", {
description: "name of the person to greet",
default: "world"
})
.option("-l, --loud", {
description: "SHOUT",
boolean: true // a true/false flag that takes no arguments
})
.option("--extra <text>", {
// Whereas this does take a value
description: "add more to your greeting"
})
.action({ args, options } => {
let message = `Hello, ${args.name}!`;
if (options.loud) {
message = message.toUpperCase() + "!!";
}
if (options.extra) {
message += options.extra + "!";
if (options.loud) {
message = message.toUpperCase() + "!!";
}
}
console.log(message);
})
TIP Commands loaded from a path are only executed when invoked or when generating help. If the CLI arguments don't specify the command then the file is never touched. This can be a good way to keep even a complex CLI lean and fast.
Naming the CLI
You can give your CLI a name in a couple simple steps. This is an npm
feature that has nothing to do with this library, but chances are you're going to want to do this if you plan on publishing your CLI.
In your package.json
, add a bin
object and add your CLI's name as a key with the path to your entry file as a value. In this example, running greeter
will invoke our CLI.
{
...
"bin": {
"greeter": "./cli.js"
}
...
}
In cli.js
, add the Node hashbang at the top of the file. This tells your system which program is supposed to interpret the file. Our whole example file looks like this:
#!/usr/bin/env node
const { program } = require("@ratwizard/cli");
program
.command("greet", {
description: "extends a greeting",
args: {
name: {
description: "name of the person to greet",
default: "world",
},
},
options: {
"-l, --loud": {
description: "SHOUT",
boolean: true,
},
"--extra <text>": {
description: "add more to your greeting",
},
},
action: ({ args, options }) => {
let message = `Hello, ${args.name}!`;
if (options.loud) {
message = message.toUpperCase() + "!!";
}
if (options.extra) {
message += options.extra + "!";
if (options.loud) {
message = message.toUpperCase() + "!!";
}
}
console.log(message);
},
})
.command("beep", {
description: "extends a greeting for droids",
action: () => {
console.log("boop");
},
})
.run(process.env);
Now install the module globally through npm
:
$ npm i -g .
Now you should be able to run it with the name you've chosen:
$ greeter greet
Hello, world!
$ greeter greet Stranger
Hello, Stranger!
$ greeter beep
boop
The --help
command will automatically take the name the program is run with into account when generating help text. You'll notice the example usage now uses the name greeter
instead of cli.js
.
$ greeter --help
usage: greeter <command> [<args>]
COMMANDS
run `greeter <command> --help` to read about args and options for a specific command
greet .. extends a greeting
beep ... extends a greeting for droids
Utilities
There are some other utilities exported from the top level of the module:
print
and println
const { print, println } = require("@ratwizard/cli");
// Prints without line breaks
print("ba");
print("na");
print("na");
// Adds a line break at the end
println("one");
println("two");
println("three");
And in the terminal you see:
bananaone
two
three
You can also use tml
tags to style text for the terminal along with sprintf
style interpolation:
println(
"Oh, <italic>yeah</italic>, you can also <bg-lightgreen>%s</bg-lightgreen> with <red>%s</red>",
"format stuff",
"tml tags"
);
prompt
Ask the user for freeform text input.
const { ask } = require("@ratwizard/cli");
const answer = await ask("What color do you want?", {
validate: (input) => {
if (input.toLowerCase() !== "black") {
return "You can have any color as long as it's black".
}
}
});
console.log("Very good choice.");
And in the terminal:
What color do you want?
> red
You can have any color as long as it's black
> orange
You can have any color as long as it's black
> pink
You can have any color as long as it's black
> black
Very good choice.
confirm
Ask the user a yes or no question and get true
or false
back.
const { confirm } = require("@ratwizard/cli");
const confirmed = await confirm("Are you sure?");
if (confirmed) {
console.log("Thank you. Confirmed.");
} else {
console.log("Not confirmed.");
}
User enters their response and presses enter:
Are you sure? [Y/N] y
Thank you. Confirmed.
$|
Are you sure? [Y/N] n
Not confirmed.
$|
tabulate
Transforms a 2D array into a table.
const { tabulate } = require("@ratwizard/cli");
const table = tabulate(
[
[6, "two", "three", 1.44],
[1235, "two", null, 662.141],
[45, null, "hello\nworld", 89.00000000000001],
[331, "last", "last", 5],
],
{
showIndex: true,
headers: ["One", "Two", "Three", "Numbers"],
style: "box",
}
);
console.log(table);
And ye shall see:
┌───┬──────┬──────┬───────┬────────────────────┐
│ # │ One │ Two │ Three │ Numbers │
├───┼──────┼──────┼───────┼────────────────────┤
│ 0 │ 6 │ two │ three │ 1.44 │
│ 1 │ 1235 │ two │ │ 662.141 │
│ 2 │ 45 │ │ hello │ 89.00000000000001 │
│ │ │ │ world │ │
│ 3 │ 331 │ last │ last │ 5 │
└───┴──────┴──────┴───────┴────────────────────┘
tml
Transforms HTML-style tags into terminal styles and colors.
const { tml } = require("@ratwizard/cli");
const output = tml("This text is <italic>formatted</italic> with <red><bold>TML</bold> tags</red>!");
console.log(output);
API Reference
To be written as the library approaches v1.0.