Skip to content

Commit

Permalink
Merge pull request #61 from carloscortonc/develop
Browse files Browse the repository at this point in the history
Release 0.12.0
  • Loading branch information
carloscortonc committed Sep 8, 2023
2 parents 333d27a + 19c0b69 commit 570a0b2
Show file tree
Hide file tree
Showing 34 changed files with 526 additions and 183 deletions.
57 changes: 41 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
# cli-er

[![NPM version](https://img.shields.io/npm/v/cli-er.svg)](https://www.npmjs.com/package/cli-er)
[![build](https://github.com/carloscortonc/cli-er/actions/workflows/build.yml/badge.svg)](https://github.com/carloscortonc/cli-er/actions/workflows/build.yml)

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.
</br>
<h1 align="center">cli-er</h1>

<p align="center">
<a href="https://www.npmjs.com/package/cli-er" target="_blank">
<img src="https://img.shields.io/npm/v/cli-er.svg" alt="NPM version">
</a>
<a href="https://github.com/carloscortonc/cli-er/actions/workflows/build.yml" target="_blank">
<img src="https://github.com/carloscortonc/cli-er/actions/workflows/build.yml/badge.svg" alt="build">
</a>
</p>

<p align="center">
Tool for building advanced CLI applications using a definition object.</br>
Implements a folder structure strategy that helps organize all the logic, also including help-generation.
</p>

<h4 align="center">
<a href="#features">Features</a>
<span>&nbsp;·&nbsp;</span>
<a href="#installation">Installation</a>
<span>&nbsp;·&nbsp;</span>
<a href="#usage">Usage</a>
<span>&nbsp;·&nbsp;</span>
<a href="./docs/definition.md">Definition</a>
<span>&nbsp;·&nbsp;</span>
<a href="./docs/cli-options.md">Options</a>
</h4>

_cli.js_:

Expand All @@ -29,6 +49,7 @@ Given the following definition (docker.js):
```js
const definition = {
builder: {
description: "Manage builds",
options: {
build: {
description: "Build an image from a Dockerfile",
Expand All @@ -44,7 +65,7 @@ const definition = {
},
debug: {
type: "boolean",
aliases: ["-D", "--debug"],
aliases: ["D", "debug"],
default: false,
},
};
Expand Down Expand Up @@ -78,6 +99,14 @@ This allows us to organize and structure the logic nicely.

You can check the full [docker-based example](./examples/docker) for a more in-depth demo.


## Features
- [**Intl support**](./docs/features.md#intl-support): support for internationalized messages.
- [**Routing**](./docs/features.md#routing): routes are generated where command handlers are expected to be found.
- [**Debug mode**](./docs/features.md#debug-mode): validate the definition and options, especially when upgrading to a new version.
- [**Typescript support**](./docs/features.md#typescript-support): build the cli with typescript.


## Installation

```sh
Expand All @@ -88,16 +117,12 @@ npm install cli-er

`cli-er` default-exports a class, which takes in a [definition object](./docs/definition.md) and an optional argument [CliOptions](./docs/cli-options.md). The available methods are the following:

- [parse(args)](./docs/api.md#parseargs): parse the given arguments and return an object containing the options, errors and calculated location.
- [run(args?)](./docs/api.md#runargs): parse the given arguments and execute the corresponding script found in the calculated location. Integrates help and version generation.
- [help(location?)](./docs/api.md#helplocation): generate help based on the definition. Can be scoped to a namespace/command.
- [version()](./docs/api.md#version): generate a formatted version of the application's version.
- [**parse(args)**](./docs/api.md#parseargs): parse the given arguments and return an object containing the options, errors and calculated location.
- [**run(args?)**](./docs/api.md#runargs): parse the given arguments and execute the corresponding script found in the calculated location. Integrates help and version generation.
- [**help(location?)**](./docs/api.md#helplocation): generate help based on the definition. Can be scoped to a namespace/command.
- [**version()**](./docs/api.md#version): generate a formatted version of the application's version.

#### Glossary
- **Namespace**: is used to group commands, but cannot be invoked. Can contain other namespaces, commands or options.
- **Command**: Is the final invocable element. Can only contain options.
- **Option**: arguments that hold values of different types, like string, boolean, list...

## Typescript cli

You can check [this example](./examples/ts-cli) on how to write a full typescript cli application.
29 changes: 27 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@ The available methods are described here:

## 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.
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. The form is:

```typescript
type ParsingOutput = {
/** Location based solely on supplied namespaces/commands */
location: string[];
/** Calculated options */
options: { _: string[]; [key: string]: any };
/** Errors originated while parsing */
errors: string[];
}
```
If no command is found in the parsing process, an error with a suggestion (the closest to the one suplied) will be returned.
_**TIP**: 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.__`
The execution of [this example](/README.md#example) would be:
```json
Expand All @@ -21,6 +33,19 @@ The execution of [this example](/README.md#example) would be:
}
```
### option-value syntax
The library support three ways for specifying the value of an option:
- `{alias} {value}` as separate arguments (e.g `--delay 10`)
- `{alias}={value}` for long aliases (more than one letter, e.g. `--delay=10`)
- `{alias}{value}` for short aliases (a single letter, e.g. `-d10`)
Boolean options with short aliases can also be concatenated with a single leading `-`, e.g:
```bash
$ ls -la # list also hidden files, in long format
$ ls -l -a # same as above
```
## run(args?)
Parses the given options by calling `parse(args)`, and executes the script in the computed location, forwarding the parsed options. If no arguments are provided, the args value defaults to `process.argv.slice(2)`.
Expand Down Expand Up @@ -78,7 +103,7 @@ const definition = {
description: "Description for global command",
},
globalOption: {
aliases: ["-g", "--global"],
aliases: ["g", "global"],
default: "globalvalue",
description: "Option shared between all commands",
},
Expand Down
4 changes: 2 additions & 2 deletions docs/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Whether to generate help option</br>
**Default**: `true`
##### `help.aliases`
Aliases to be used for help option</br>
**Default**: `["-h", "--help"]`
**Default**: `["h", "help"]`
##### `help.description`
Description for the option
##### `help.template`
Expand All @@ -51,7 +51,7 @@ Whether to generate version option</br>
**Default**: `true`
##### `version.aliases`
Aliases to be used for version option</br>
**Default**: `["-v", "--version"]`
**Default**: `["v", "version"]`
##### `version.description`
Description for the option

Expand Down
35 changes: 27 additions & 8 deletions docs/definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ type BaseElement = {
}
```
- **kind**: describes the type of element.
- **description**: description of the element, to be used when generating help.
- **hidden**: hide an element when generating help.
- **description**: description of the element, to be used when generating help. The library will search first in `CliOptions.messages` with an id generated from element's location and name, followed by `.description`. So for example, for a nested option `opt` inside a command `cmd`, the id will be `cmd.opt.description`.
- **hidden**: hide an element when generating help. Default: `false`
## Namespace
An element with `kind: "namespace"`, or since 0.11.0, when its kind is inferred to this one.
Expand All @@ -35,10 +35,12 @@ An element with `kind: "command"`, or since 0.11.0, when its kind is inferred to
```typescript
type Command = BaseElement & {
kind: "command";
aliases?: string[];
options?: Definition<Option>;
action?: (out: ParsingOutput) => void;
}
```
- **aliases**: alternative names for the command. If specified, the will added on top of command key. Default `[key]`
- **action**: method that will be called when the command is matched, receiving the output of the parsing process.
## Option
Expand All @@ -48,17 +50,19 @@ type Option = BaseElement & {
kind: "option";
aliases?: string[];
positional?: boolean | number;
negatable?: boolean;
default?: any;
required?: boolean;
type?: "string" | "boolean" | "list" | "number" | "float";
parser?: (input: ValueParserInput) => ValueParserOutput
}
```
- **aliases**: alternative names for the options, e.g. `["-h", "--help"]`. When not specified, the name of the option will be used preceded by "--".
- **positional**: enables [positional options](#positional-options).
- **aliases**: alternative names for the options, e.g. `["h", "help"]`. They should be specified without dashes, and final alias value will be calculated depending on the provided alias length: prefixed with `-` for single letters, and `--` in other cases. When not specified, the name of the option will be used. Default: `[key]`
- **positional**: enables [positional options](#positional-options). Default: `false`
- **negatable**: whether to include [negated aliases](#negated-aliases) in boolean options. Default: `false`
- **default**: default value for the option.
- **required**: specifies an option as required, generating an error if a value is not provided.
- **type**: type of option, to load the correct parser.
- **required**: specifies an option as required, generating an error if a value is not provided. Default: `false`
- **type**: type of option, to load the appropriate parser. Default: `string`
- **parser**: allows defining [custom parser](#custom-parser) for an option, instead of using the supported types.
### Positional options
Expand All @@ -78,6 +82,23 @@ new Cli({ files: { positional: true} }, { cliName: "format" });

**Example**: [jest-cli](/examples/jest-cli/)

### Negated aliases
For options with `type:boolean`, negated aliases can be included specifying `negatable:true`. These negated aliases are generated from original aliases, prefixing `no` and `no-`.

```js
new Cli({ debug: { type: "boolean", negatable: true }}, { cliName: "cli" })
// $ cli --debug
// => { options: { debug : true }}

// $ cli --no-debug
// => { options: { debug : false }}

// $ cli --nodebug
// => { options: { debug : false }}
```

**Example**: [secondary-cli](/examples/options-only/secondary-cli.js)

### Custom parser
```typescript
type ValueParserInput = {
Expand All @@ -87,8 +108,6 @@ type ValueParserInput = {
current: OptionValue;
/** Option definition */
option: Option & { key: string };
/** Method for formatting errors */
format: typeof CliError.format;
}

type ValueParserOutput = {
Expand Down
47 changes: 47 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
## Intl support
To internationalize library messages, the [`CliOptions.messages`](./cli-options.md#messages) can be used to override the default messages. Check the [intl-cli example](/examples/intl-cli) for a use case.
`CliOptions.messages` can be also be used to specify descriptions for element's definition. For this, the key must be the full route to such element, followed by `".description"`, e.g:
```javascript
new Cli({
nms: {
options: {
cmd: {
debug: { type: "boolean"}
}
}
}
}, {
messages: {
"nms.description": "Namespace",
"nms.cmd.description": "Command",
"nms.cmd.debug.description": "Option",
}
})
```

## Routing
A "location" is calculated and returned by [`Cli.parse`](./api.md#parseargs): this is the route to the final invocable command.
With this location, the [`Cli.run`](./api.md#runargs) method generates a list of candidate files to execute and forward the parsed options. For each element of the location list (`[...rest, element]`):
1. `{...rest}/{element}/index` file.
2. `{...rest}/{element}` file.
3. The name of the entrypoint file

For all of these candidates, only the two from the last element are imported with default import, the rest with named import (the name of the last element).
For single commands, the location is prefixed with [`CliOptions.rootCommand`](./cli-options.md#rootcommand), if declared as string.

### Example
If the location is `["nms", "cmd"]` for an entryfile `cli.js`, the list of candidates (in order) will be:
1. `/nms/cmd/index.js` - default import
2. `/nms/cmd.js` - default import
3. `/nms/index.js` - named import (`cmd`)
4. `/nms.js` - named import (`cmd`)
5. `/index.js` - named import (`cmd`)
6. `/cli.js` - named import (`cmd`)

## Debug mode
When active, the library will generate debug logs warning about problems, deprecations or suggestions.
If any is generated, the `exitCode` will be set to `1`, so a simple validation-workflow can be built with this.
To see how to enable it check [`CliOptions.debug`](./cli-options.md#debug).

## Typescript support
You can check [this example](./examples/ts-cli) on how to write a full typescript cli application.
6 changes: 4 additions & 2 deletions examples/custom-option-parser/custom-option-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ module.exports = (options) => {

/* Custom option-parser implementation */
/** @param {ValueParserInput} */
function dateParser({ value, format, option }) {
function dateParser({ value, option }) {
if (isNaN(Date.parse(value))) {
return { error: format("option_wrong_value", option.key, "yyyy/mm/dd", value) };
return {
error: Cli.formatMessage("option_wrong_value", { option: option.key, expected: "yyyy/mm/dd", found: value }),
};
}
return { value: new Date(value) };
}
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-option-parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"main": "custom-options-parser.js",
"author": "carloscortonc",
"dependencies": {
"cli-er": "0.10.0"
"cli-er": "0.12.0"
}
}
Loading

0 comments on commit 570a0b2

Please sign in to comment.