0.17.0 • Published 2 years ago

@ratwizard/cli v0.17.0

Weekly downloads
-
License
MIT
Repository
-
Last release
2 years ago

@ratwizard/cli

Work-in-progress framework for quickly setting up CLI tools.

Quick Start

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

See print README

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.

See prompt README

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.

See confirm README

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.

See tabulate README

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.

See tml README

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.

0.17.0

2 years ago

0.16.0

3 years ago

0.15.0

3 years ago

0.13.0

4 years ago

0.14.0

4 years ago

0.14.1

4 years ago

0.12.1

4 years ago

0.12.0

4 years ago

0.11.0

4 years ago

0.10.1

4 years ago

0.10.0

4 years ago

0.9.0

4 years ago

0.8.0

4 years ago

0.7.1

4 years ago

0.7.0

4 years ago

0.6.2

4 years ago

0.6.1

4 years ago

0.6.0

4 years ago

0.5.1

4 years ago

0.5.0

4 years ago

0.4.0

4 years ago

0.3.1

4 years ago

0.3.0

4 years ago

0.2.0

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago