cli-of-mine
v2.1.0
Published
A no-fuss command-line framework
Downloads
9
Readme
cli-of-mine
"This CLI of mine, I'm gonna let it shine!"
This is a no-fuss CLI framework. It solves the CLI stuff, so you can focus on the other stuff.
What's it solve for you?
- Parsing and validating command-line arguments with command-line-args.
- Calling the correct function based on the command a user asked for.
- Arbitrary nesting of subcommands using an Express-like middleware pattern.
- Encapsulating all I/O streams for 100% testability -- assuming you use its logging primitives.
- Converting the metadata you provide into nice-looking help text with command-line-usage.
- Handling execution errors.
- Determining an appropriate exit code for the process.
- Resource-style commands (e.g.
myapp [verb] [noun]
)
See the online docs for API reference information.
Getting Started
Install cli-of-mine
from npm:
Node Versions:
cli-of-mine
is tested with the latest versions of node v8, v10, and v12. Earlier versions are not supported.
npm i cli-of-mine
The cli-of-mine
module exports a single function, exec. It expects an ExecConfig object that describes how the CLI should work.
A "hello world" example just needs to define a name
and a handler
:
const { exec } = require("cli-of-mine");
exec({
name: "my-app",
handler(ctx) {
ctx.console.log("Hello, world!");
}
}).then(result => process.exit(result.processExitCode));
For more information on the config cli-of-mine
supports, check the ExecConfig API reference.
Terminology
The following terms have a specific meaning when used in this documentation.
- config - A configuration option for
cli-of-mine
. Provided by the developer, not the user. - argument - A single command-line argument, provided by the user.
- command - An argument that matches the name of a command definition.
- option - An argument that starts with a
-
and might have one or more values, e.g.--force
or--file foo.txt
.
Handlers
When exec is called, it calls one or more Handlers based on the commands/subcommands that the user is invoking. The handlers implement the "business logic" or "application logic" for the commands.
For example, this handler prints "Hello, World!"
using the cli-of-mine
logging primitives.
const { exec } = require("cli-of-mine");
exec({
name: "my-app",
handler(ctx) {
ctx.console.log("Hello, world!");
}
});
Handlers are functions that get called with two arguments: a context
object (see HandlerContext) and a next
function (see HandlerCallback).
Handler Results
Handlers can return a value of any type. This provides a way for handlers to communicate with the code that called exec.
Whatever they return will be returned to the next tier of middleware and eventually put into the result
property of the ExecResult.
Also, if a handler returns a Promise
, the next subcommand handler won't execute until the promise is resolved.
For example, this will cause the ExecResult.result
property to be equal to ctx.args
:
function handler(ctx, next) {
return Promise.resolve(ctx.args);
}
Handler Errors
Any errors thrown within a handler will be caught and dealt with automatically.
Handlers can buy into improved error handling by throwing or rejecting with instances of AppError. It extends Error
with support for error codes, and it automatically "namespaces" your codes by prefixing them with APP_
, so they won't conflict with cli-of-mine
codes (or Node codes).
For example:
const { AppError } = require("cli-of-mine");
throw new AppError(
"SERVER_UNAVAILABLE", // error code
"The downstream server is unavailable." // message
);
Additionally, you can set the optional processExitCode
property to recommend an exit code to use, should this error become fatal.
You can construct an AppError from an existing error using fromError
:
try {
// do stuff
} catch (e) {
throw AppError.fromError(e);
}
Custom Error Classes
It's recommended that you extend AppError to implement your own error class, so you can document error codes more cleanly in your app. For example:
class FooError extends AppError {
code_prefix = "FOO_";
constructor(code: string | null, message: string) {
super(code, message);
Error.captureStackTrace(this, FooError);
}
}
try {
throw new FooError("MY_CODE", "my message");
} catch (e) {
console.log(e.code); // logs FOO_MY_CODE
}
Subcommands
cli-of-mine
is designed to support arbitrarily nested levels of subcommands using a middleware pattern inspired by frameworks like Express. This allows you to handle options at every subcommand level.
The special thing about middlewares is that your handler must choose when to relinquish control to the next handler by calling next()
(which returns a Promise). For example:
function handler(ctx, next) {
// things to do before subcommand starts
// next() runs the subcommand's handler
return next().then(result => {
// things to do after subcommand is finished
});
}
Your final handler (the "controller" in Express parlance) can call next
if it wants, but it doesn't have to. If it does, the next
is a no-op.
Subcommand Options
Each "level" of command/subcommand specifies its own set of options. The handler is only provided the options relevant to that specific subcommand, not the ones before or after it.
For example, assume widgets
is a subcommand of your application and list
is a subcommand of widgets
-- so users can run myapp widgets list
.
If someone runs: myapp -v widgets list --filter green
, then the options will be doled out like so:
- The root handler is given the
-v
argument. - The
widgets
handler is given no arguments. - The
list
handler is given the--filter green
argument.
If the list
handler actually cares about the -v
argument, the root handler has to give it that information explicitly using ctx.data
(see below).
Subcommands Sharing Data
You cannot provide arguments to next()
.
If you want to share data between subcommands, you should assign it to the ctx.data
object, which is reserved for arbitrary handler data.
For example, the following handler will initialize a database connection for the subcommand to use, then clean it up once the subcommand is finished.
Note: The
async
/await
syntax is used in this and remaining examples to improve readability. But good ol' Promise chains work just fine.
async function handler(ctx, next) {
const { args, data } = ctx;
// initialize dbConn before running subcommand
data.dbConn = await getFooDatabaseConn(args.db);
const result = await next();
// Clean up dbConn after subcommand is done
await data.dbConn.close();
// make sure to return the subcommand's result if you care about it.
return result;
}
Required Subcommands
Handlers have access to the subcommand
property of HandlerContext, which indicates which subcommand (if any) the user has requested. This can be used, for instance, to throw an error if no subcommands are specified:
function handler(ctx, next) {
if (!ctx.subcommand) {
throw new AppError("BAD_COMMAND", "Must specify command");
}
return next();
}
Resources
cli-of-mine
includes support for declaring resources and verbs using the resources
property of ExecConfig. This provides support for the myapp [verb] [noun]
invocation pattern, as used by kubectl
for instance.
As a trivial example, say we want to make a CLI that lets you run:
myapp add widget
myapp get widget
myapp rm widget
We can do this by defining a widget
resource:
exec({
name: "myapp",
resources: [
{
name: "widget",
commands: [
{
name: "add",
handler: ctx => ctx.console.log("add widget")
},
{
name: "get",
handler: ctx => ctx.console.log("get widget")
}
{
name: "rm",
handler: ctx => ctx.console.log("rm widget")
}
]
}
],
});
Resources and subcommands can be used together. If a resource and subcommand overlaps, the subcommand is chosen and executed instead. This can be used to define "default" behavior for a given verb, for example:
exec({
name: "myapp",
resources: [
{
name: "widget",
commands: [
{
name: "run",
handler: ctx => ctx.console.log("Running widget")
}
]
}
],
subcommands: [
{
name: "run",
handler: ctx => ctx.console.log("Default run behavior")
}
]
});
In this case, both these invocations are valid:
myapp run widget # prints "Running widget"
myapp run # prints "Default run behavior"
Help Text
cli-of-mine
will intercept --help
flags and handle them automatically. When this happens, none of your handlers will be called and the result
of the execution will be undefined
.
Different text is shown based on which command receives the --help
flag. For example:
# displays root help for myapp
myapp --help
# displays help for "widgets" command
myapp widgets --help
# displays help for "list" subcommand
myapp widgets list --help
You can disable this functionality by setting generateHelp: false
in your ExecConfig.
Version Text
cli-of-mine
will intercept --version
options (unless they are passed to a subcommand) and print version information based on the version
property of the ExecConfig. This behavior can be disabled by setting generateVersion: false
.
Error Strategies
exec provides three strategies for automated error handling, which you can pick using the errorStrategy
property of the ExecConfig.
The "log"
strategy is the default.
Log
When errorStrategy: "log"
, the exec function will automatically catch and log any errors that occur during execution, including errors that your handlers throw. This means it never rejects.
The ExecResult will contain a processExitCode
property that indicates what exit code is recommended to be used. exec will not exit the process automatically.
This is the default mode. It's useful if you want cli-of-mine
to handle as much as possible, but you don't want it to exit the process for you.
Throw
when errorStrategy: "throw"
, errors during execution will cause the returned Promise
to be rejected with an ExecutionError that you can inspect and handle manually.
This mode is useful for testing, or for cases where you want the code calling exec to be able to catch errors from within your handlers.
Exit
When errorStrategy: "exit"
, errors during execution will be logged to the user. If the execution would result in a nonzero exit code, the process will be automatically exited with that code.
This mode is useful if you want cli-of-mine
to completely manage error handling, and you don't need to run any code after exec is finished.
Testing
One core design goal of cli-of-mine
is to be testable from the user's perspective. In other words, it should be possible to easily write tests like:
"If I pass the --foo option, the output should include 'bar'."
This can be done by using the stdio-related parameters on ExecConfig, assuming your application uses the cli-of-mine
logging primitives. For example:
exec({
name: "testapp",
options: [{ name: "foo" }],
argv: ["--foo"],
stdout: "capture",
async handler(ctx, next) {
const { foo } = ctx.args;
ctx.console.log(foo ? "bar" : "bad request!");
}
}).then(result => {
expect(result.stdout).toEqual("bar");
});
FAQ
Q: Is there an @types/cli-of-mine package?
No -- cli-of-mine
is written in Typescript and includes the type definitions bundled in the package. No additional @types
are needed.
Q: Why does my application have to use your special console?
cli-of-mine
has a goal of "Capturing All I/O." But, it also has a strict "No Global Changes" rule and that applies to changing the global.console
object.
The only way we can capture all IO without global changes, is to allow our applications to be configured with a custom logger. Therefore, cli-of-mine
gives us the tools to do that.
If you don't care about global changes, your handler can assign the console for global use with:
global.console = ctx.console;
Q: What prompted you to create cli-of-mine?
When I was working on a Node CLI application, repost, one of my goals was to create an application that controls all its inputs and outputs, and has no global state or singletons.
As part of that, I wanted to make a CLI framework that allowed you to completely control your application's I/O, for instance to make assertions about output data. This is that framework.
Q: But what about all the existing CLI frameworks?
Surprisingly few CLI frameworks are designed for effective testing from the user's perspective. If I call the program with X arguments, do I see Y results? That can be a tough question to answer, even if you're using a CLI framework. The "special" thing about cli-of-mine
is that it tries to make this kind of assertion easier using the stdout: "capture"
option.
Having said that, the "established" Node CLI frameworks are still great, they just don't fit the grooves in my brain as well as this one does.
Q: When should I not use cli-of-mine?
- You just want to parse arguments and you don't like inversion of control. In that case, try command-line-args or minimist instead.
- You want all the batteries included. In that case, there are some larger frameworks like oclif and yargs that might be able to help you.
- You want a well-supported library with low bus-factor. This is a personal project and I do not commit to long-term support. (This may change in the future.) For a similarly-featured, very popular library, try commander.
- You use non-utf8 encodings. For now,
cli-of-mine
works best with utf8 encoded streams.
Copyright 2019 Luke Turner - Published under the MIT License.