carloscortonc / cli-er

Tool for building advanced cli applications

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

cli-er

NPM version build

Tool for building advanced CLI applications using a definition object. It implements a folder structure strategy that helps organize all the logic, also including help-generation

cli.js:

import Cli from "cli-er";

const definition = {...}

new Cli(definition).run();

Invocation:

node cli.js [namespace(s)|command] [OPTIONS]

Example

Given the following definition (docker.js):

const definition = {
  builder: {
    options: {
      build: {
        kind: "command",
        type: "string",
        description: "Build an image from a Dockerfile",
      },
    },
  },
  debug: {
    type: "boolean",
    aliases: ["-D", "--debug"],
    default: false,
  },
};

it will allow us to structure the code as follows:

.
├─ docker.js
└─ builder
   └── build.js

so we can then execute:

node docker.js builder build . -- someother-external-option

which will try to invoke, in order:

  1. /builder/build/index.js
  2. /builder/build.js
  3. /builder/index.js
  4. /builder.js
  5. /index.js
  6. /docker.js

with the parsed options (only the first two are default imports, the rest are named imports using the command name, in this case build). This allows us to organize and structure the logic nicely.

You can check the full docker-based example for a more in-depth demo.

Usage

cli-er default-exports a class, which takes in the definition object and an optional argument CliOptions. The available methods are the following:

parse(args)

Parses the given list of arguments based on the provided definition, and returns an object containing the resulting options, and the calculated location where the script is expected to be found. If any error is generated during the process, they will be registered inside an errors field. A custom parser for an Option may be used, with the following signature:

type parser = (input: ValueParserInput) => ValueParserOutput;

type ValueParserInput = {
  /** Value to process */
  value: string | undefined;
  /** Current value of the option */
  current: OptionValue;
  /** Option definition */
  option: Option & { key: string };
  /** Method for formatting errors */
  format: typeof CliError.format;
}

type ValueParserOutput = {
  /** Final value for the parsed option */
  value?: any;
  /** Number of additional arguments that the parser consumed. For example, a boolean option
   * might not consume any additional arguments ("--show-config", next=0) while a string option
   * would ("--path path-value", next=1). The main case of `next=0` is when incoming value
   * is `undefined` */
  next?: number;
  /** Error generated during parsing */
  error?: string;
}

This allows to create custom parsers for any type of input (check the custom-option-parser example for a hint), or to override the existing parsers logic.

If no command is found in the parsing process, an error with a suggestion (the closest to the one suplied) will be returned.

You can define an option as required (required: true), which will verify that such option is present in the provided arguments, setting an error otherwise.

This library also interprets the delimiter -- to stop parsing, including the remaning arguments as an array inside ParsingOutput.options.__

Positional options (Option.positional: boolean | number) allow asigning an option to a determinate position in the arguments (full example here), p.e:

new Cli({ projectName: { positional: 0} }, { cliName: "create-project" });

// $ create-project newproject [...]
// => { options: { projectName: "newproject" }}

The execution of the above example would be:

{ "options": { "build": ".", "debug": false, "_": ["someother-external-option"] }, "location": ["builder", "build"] }

run(args?)

Parses the given options by calling parse(args), and executes the script in the computed location, forwarding the parsed options. Alternatively, a command can be defined with an action function that will be called when the command is matched. If no arguments are provided, the args value defaults to process.argv.slice(2). With the following example (action.js):

new Cli({
  cmd: {
    options: {
      log: { type: "boolean" },
    },
    action: ({ options }) => {
      if (options.log) {
        console.log("Log from cmd");
      }
    },
  },
}).run();

invoking with node action.js cmd --log will print "Log from cmd" into the console.

This method has three main behaviours: print version, print help and execute a command:

  • print version: if autoincluded version is enabled and version option is provided, version will be printed.
  • print help: if autoincluded help is enabled and help option is provided, or a cli without rootCommand is invoked without location, or a namespace is invoked, help will be generated. If any errors configured in CliOptions.errors.onGenerateHelp are generated, they will be outputted before the help.
  • execute command: if any errors configured in CliOptions.errors.onExecuteCommand are generated, they will be printed and execution will end with status 1. Otherwise, the script location will be calculated, and the corresponding script executed.

If a cli application does not have registered a root command (logic executed without any supplied namespace/command), it should be configured with CliOptions.rootCommand: false. By doing this, when the cli application is invoked with no arguments, full help will be shown (see this docker example).

You also use CliOptions.rootCommand to define a default command to execute, when no command/namespace is supplied (check this webpack-cli example).

help(location?)

Generates and outputs help message based on the provided definition. Given the following code (test.js):

const definition = {
  nms: {
    description: "Description for the namespace",
    options: {
      cmd: {
        kind: "command",
        type: "string",
        description: "Description for the command",
      },
    },
  },
  gcmd: {
    kind: "command",
    description: "Description for global command",
  },
  globalOption: {
    aliases: ["-g", "--global"],
    default: "globalvalue",
    description: "Option shared between all commands",
  },
};

new Cli(definition, { cliDescription: "Cli for testing purposes" }).help();

will output:

Usage:  test NAMESPACE|COMMAND [OPTIONS]

Cli for testing purposes

Namespaces:
  nms           Description for the namespace

Commands:
  gcmd          Description for global command

Options:
  -g, --global  Option shared between all commands (default: globalvalue)
  -h, --help    Display global help, or scoped to a namespace/command

The optional argument location enables the generation of scoped help. For the above definition, the following code:

new Cli(definition).help(["nms"]);

will output:

Usage:  test nms COMMAND [OPTIONS]

Description for the namespace

Commands:
  cmd  Description for the command

Options:
  -g, --global  Option shared between all commands (default: globalvalue)
  -h, --help    Display global help, or scoped to a namespace/command

Any DefinitionElement can be hidden from the generated help by using hidden:true on its definition.

Note
help-generation option is auto-included by default. This can be configured via CliOptions.help

version()

Prints the formatted version of the current cli application: finds the package.json for the current application, and prints its name and version.

Note
version-generation option is auto-included by default. This can be configured via CliOptions.version


CliOptions

A configuration object may be provided in the class constructor. The available options are:

baseLocation

Location of the main cli application.
Default: path.dirname(entryFile)

baseScriptLocation

Base path where the ProcessingOutput.location will start from
Default: path.dirname(entryFile)

commandsPath

Path where the single-command scripts (not contained in any namespace) are stored, starting from CliOptions.baseScriptLocation
Default: "commands"

errors

Configuration related to when errors should be displayed. The order of the lists containing the error-types matters, as it changes which error-messages are shown first (elements appearing first have a higher order of precedence). The available error-types are:

  • command_not_found: an unknown argument is encountered when a command was expected
  • option_wrong_value: the option parser considers the given value as incorrect (e.g. type:number when given "a123")
  • option_required: an option is marked as required but is not provided
  • option_missing_value: an option is provided without its corresponding value
  • option_not_found: an unknown argument is encountered when an option was expected
errors.onGenerateHelp

List of error-types that will get displayed before help
Default: ["command_not_found"]

errors.onExecuteCommand

List of error-types that will cause to end execution with exit(1)
Default: ["command_not_found", "option_wrong_value", "option_required", "option_missing_value", "option_not_found"]

help

Help-related configuration

help.autoInclude

Whether to generate help option
Default: true

help.aliases

Aliases to be used for help option
Default: ["-h", "--help"]

help.description

Description for the option

help.template

Template to be used when generating help. There are five distinct sections: usage, description, namespaces, commands and options. This can be used to include a header/footer, change the order of the sections, or remove a section altogether. If a section has no content, it will be removed along with any line-breaks that follow. You can see a use-case for this in the docker example
Default: \n{usage}\n{description}\n{namespaces}\n{commands}\n{options}\n

version

Version-related configuration

version.autoInclude

Whether to generate version option
Default: true

version.aliases

Aliases to be used for version option
Default: ["-v", "--version"]

version.description

Description for the option

rootCommand

Whether the cli implements a root command (invocation with no additional namespaces/commands). If a string is provided, it will be used as the default command to execute
Default: true

logger

Logger to be used by the cli. It contains two methods, log and error, that can be used to add a prefix to the log (e.g. "error ") or change the output color, as demonstrated in this docker example.
Default: ./src/cli-logger.ts

cliName

Cli name to be used instead of the one defined in package.json
Default: packageJson.name

cliVersion

Cli version to be used instead of the one defined in package.json
Default: packageJson.version

cliDescription

Cli description to be used instead of the one defined in package.json
Default: packageJson.description

debug

Enable debug mode. This is intended for the development phase of the cli. It will:

  • Print verbose errors when a script in not found in the expected path during Cli.run.
  • Print warnings for deprecated properties/methods, useful for detecting and applying these changes before updating to the next version.

Its value can also be configured using an enviroment variable:

$ CLIER_DEBUG=1 node cli.js

Default: process.env.CLIER_DEBUG

messages

Object containing the messages to be used in the Cli, to override the default ones defined by this library. This enables internationalization and customization of cli-native messages. You can see a use-case in this intl-cli example
Default: defined in ./src/cli-messages and ./src/cli-errors

Typescript cli

You can check this example on how to write a full typescript cli application.

About

Tool for building advanced cli applications

License:MIT License


Languages

Language:TypeScript 97.3%Language:JavaScript 2.7%