Comparing version
@@ -62,2 +62,3 @@ export declare type ArgumentValue = any; | ||
name: string; | ||
version?: string; | ||
description?: string; | ||
@@ -64,0 +65,0 @@ opts: OptionDefinition[]; |
@@ -122,2 +122,3 @@ "use strict"; | ||
name: this.name, | ||
version: this.version, | ||
description: this.description, | ||
@@ -124,0 +125,0 @@ opts: Object.values(this.opts).map(o => this.dumpOption(o)), |
{ | ||
"name": "cilly", | ||
"version": "1.0.8", | ||
"version": "1.0.9", | ||
"description": "The last library you'll ever need for building intuitive, robust and flexible CLI tools with Node.js and TypeScript.", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
321
README.md
@@ -13,7 +13,12 @@ # Cilly | ||
- [Documentation](#documentation) | ||
- [Commands](#commands) | ||
- [parse()](#parse) | ||
- [process()](#process) | ||
- [Subcommands](#subcommands) | ||
- [Option inheritance](#option-inheritance) | ||
- [Arguments](#arguments) | ||
- [Variadic arguments](#variadic-arguments) | ||
- [Options](#options) | ||
- [Commands](#commands) | ||
- [Subcommands](#subcommands) | ||
- [Option inheritance](#option-inheritance) | ||
- [Option arguments](#option-arguments) | ||
- [Negating flags](#negating-flags) | ||
- [Validators](#validators) | ||
@@ -25,3 +30,3 @@ - [Hooks](#hooks) | ||
- [Custom help handlers](#custom-help-handlers) | ||
- [Custom exception handling](#custom-exception-handling) | ||
- [Exception handling](#exception-handling) | ||
@@ -34,3 +39,3 @@ # Installation | ||
# Basic usage | ||
With the file `build.ts`: | ||
With a file called `build.ts`: | ||
```typescript | ||
@@ -142,2 +147,114 @@ #!/usr/local/bin/ts-node | ||
## Commands | ||
Commands are represented by `CliCommand` class instances. | ||
The command constructor has the following signature: | ||
``` | ||
CliCommand(name: string, opts?: { inheritOpts?: boolean, consumeUnknownOpts?: boolean }) | ||
``` | ||
The `CliCommand` API looks as follows: | ||
```typescript | ||
new CliCommand('command') | ||
.withVersion() // Set the version | ||
.withDescription() // Set the description | ||
.withHandler() // Set the function to run when command is invoked | ||
.withArguments() // Register arguments | ||
.withOptions() // Register options | ||
.withSubCommands() // Register subcommands | ||
.withHelpHandler() // Custom handling of the --help flag | ||
.parse() // Generate { args, opts, extra } from process.argv, run onParse() hooks | ||
.process() // Run parse(), hooks, and call command handler | ||
.help() // Call the helpHandler | ||
.dump() // Dump the command description to an object (useful for documentation) | ||
``` | ||
### parse() | ||
The `parse()` method takes the command line arguments (`process.argv`) and parses it to produce the `{args, opts, extra}` objects passed to handlers. | ||
The `parse()` method does not call any hooks and does not invoke command handlers, and thus does not require a command handler to be defined. | ||
```typescript | ||
const cmd = new CliCommand('build') | ||
.withArguments({ name: 'address' }) | ||
.withOptions({ name: ['-g', '--garage'], negatable: true }) | ||
const { args, opts, extra } = cmd.parse(process.argv) | ||
``` | ||
#### Extra | ||
All arguments that cannot be parsed are put in the `extra` argument to command handlers. | ||
If desired, a command can choose to to treat unknown options similarly by setting the `consumeUnknownOpts` flag: | ||
```typescript | ||
const cmd = new CliCommand('build', { consumeUnknownOpts: true }) | ||
const { args, opts, extra } = cmd.parse(process.argv) | ||
console.log(extra) | ||
``` | ||
With the input: | ||
``` | ||
build --an --option --that --isnt --defined | ||
``` | ||
The above would print `['--an', '--option', '--that', '--isnt', '--defined']` | ||
### process() | ||
The `process()` method (asynchronous) calls `parse()`, runs argument and options hooks, validators, and invokes the appropriate command handler with the output of `parse()`. The result of `await process()` is whatever the command handler returns. | ||
```typescript | ||
const cmd = new CliCommand('build') | ||
.withArguments({ name: 'address' }) | ||
.withOptions({ name: ['-g', '--garage'], negatable: true }) | ||
// The args, opts, extra comes from .parse() | ||
.withHandler((args, opts, extra) => { | ||
return new House(args.address, opts.garage) | ||
}) | ||
const house = await cmd.process(process.argv) | ||
``` | ||
### Subcommands | ||
Commands can have an arbitrary number of subcommands, allowing developers to decouple their command handling logic. | ||
These are registered with the `withSubCommands()` method: | ||
```typescript | ||
new CliCommand('build') | ||
.withSubCommands( | ||
new CliCommand('house')..., | ||
new CliCommand('apartment')..., | ||
) | ||
``` | ||
A command **cannot** have both arguments and subcommands. This is because subcommands are invoked be essentially passing command names as arguments, and there would be no good way to tell the two apart. | ||
Subcommands are displayed in the help text: | ||
``` | ||
Usage: build [options] | ||
Options: | ||
... | ||
Commands: | ||
house <address> [state] [options] | ||
apartment <address> [options] | ||
``` | ||
#### Option inheritance | ||
Contrary to `commander.js`, subcommands can share options and arguments in the parent command(s). | ||
By setting the `inheritOpts` flag to true when constructing the command, the command inherits all options from the parent command: | ||
```typescript | ||
new CliCommand('build') | ||
.withOptions({ name: ['-vb', '--verbose'] }) | ||
.withSubCommands( | ||
new CliCommand('house', { inheritOpts: true }) | ||
.withOptions({ name: ['-r', '--rooms'] }) | ||
) | ||
``` | ||
The `opts` object in the `house` command handler will contain both `verbose` and `rooms`: | ||
```typescript | ||
opts: { | ||
verbose: ..., | ||
rooms: ... | ||
} | ||
``` | ||
## Arguments | ||
@@ -305,12 +422,198 @@ Arguments are provided to a command with the `withArguments()` chain method. | ||
## Commands | ||
### Subcommands | ||
### Option inheritance | ||
## Validators | ||
Options and arguments can be assigned validators that are called on `.process()`. | ||
A validator has the following signature: | ||
```typescript | ||
type Validator = (value: any, parsed: { args, opts, extra }) => string | boolean | Promise<string | boolean> | ||
``` | ||
1. The `value` argument is the value assigned to the option or argument. | ||
2. The `parsed` argument is the result of `.parsed()`; the result of parsing the command line arguments. | ||
If a validator returns `true`, the value is interpreted as valid. Otherwise, if the validator returns `false`, a `ValidationError` is thrown with a default error message. | ||
- If the validator returns a string, that string is used as the error message. | ||
```typescript | ||
new CliCommand('build') | ||
.withArguments({ name: 'address', validator: (value, parsed) => { | ||
if (!validators.isValidAddress(value)) { | ||
return `The address ${value} is not a valid address.` | ||
} | ||
return true | ||
}}) | ||
``` | ||
## Hooks | ||
It can be useful to intercept an option or argument before it's passed to the command handler. | ||
To do this, we can use `onParse()` and `onProcess()` hooks on both options and arguments. | ||
### onParse() | ||
When registered on an option or argument, an `onParse()` hook is called immediately when that argument or option is parsed from the command line. | ||
This is useful for implementing interrupting flags such as `--help`, `--version`, and so on. | ||
An `OnParseHook` has the following signature: | ||
```typescript | ||
type OnParseHook = (value: any, parsed: { args, opts, extra }) => void | ||
``` | ||
1. The `value` argument is the value assigned to the option or argument. | ||
2. The `parsed` argument is what the `.parse()` method has parsed so far. Note that this object may not be complete when the hook is invoked. | ||
```typescript | ||
new CliCommand('build') | ||
.withOptions({ name: ['-v', '--version'], onParse: (value, parsed) => { | ||
console.log(version) | ||
process.exit() | ||
}}) | ||
``` | ||
### onProcess() | ||
Contrary to `onParse()` hooks, `onProcess()` hooks are run after `parse()` has finished. | ||
Hooks also allow you to change the value of an option or argument at processing time, before the command handler is invoked. | ||
This can be very useful for designing "user-proof" CLIs that prompt the users for the information they need in a nice looking and robust manner. | ||
An `OnProcessHook` has the following signature: | ||
```typescript | ||
type OnProcessHook = (value: any, parsed: { args, opts, extra }, assign: (value: any) => Promise<void>) => void | Promise<void> | ||
``` | ||
1. The `value` argument is the value assigned to the option or argument | ||
2. The `parsed` argument is the result of `parse()` | ||
3. The `assign` argument is a function that, when called with a new value: | ||
1. Runs the value through the option/argument validator (if one exists) | ||
2. Assigns the value to the option/argument | ||
```typescript | ||
new CliCommadn('build') | ||
.withArguments({ name: 'address', onProcess: async (value, parsed, assign) => { | ||
if (value === undefined) { | ||
const address = await prompts.Prompt('Please enter your address') | ||
await assign(address) // Validate and assign | ||
} | ||
}}) | ||
``` | ||
## Generating documentation | ||
The `CliCommand.dump()` method dumps the entire command (and its subcommands) to an easily readable object of type `CommandDefinition`. This is useful for generating documentation. | ||
A `CommandDefinition` has the following signature: | ||
```typescript | ||
export type CommandDefinition = { | ||
name: string, | ||
version: string, | ||
description?: string, | ||
opts: OptionDefinition[], | ||
args: ArgumentDefinition[], | ||
subCommands: CommandDefinition[] | ||
} | ||
type OptionDefinition = { | ||
name: [string, string], | ||
args: ArgumentDefinition[], | ||
description?: string, | ||
required?: boolean, | ||
negatable?: boolean, | ||
defaultValue?: any | ||
} | ||
type ArgumentDefinition = { | ||
name: string, | ||
description?: string, | ||
required?: boolean, | ||
defaultValue?: any, | ||
variadic?: boolean | ||
} | ||
``` | ||
When printing the `help` text, this is done completely from the `CommandDefinition` objects. | ||
While out of scope for this specific package, one could dream of a package that could take a `CommandDefinition` object and generate a nice looking documentation page :eyes: | ||
Here's an example of a command dump: | ||
```typescript | ||
const cmd = new CliCommand('build') | ||
.withDescription('Build a home') | ||
.withArguments({ name: 'address', required: true }) | ||
.withOptions( | ||
{ name: ['-r', '--residents'], required: false, args: [ {name: 'residents', variadic: true} ] }, | ||
{ name: ['-d', '--doors'], defaultValue: 1 } | ||
) | ||
console.log(cmd.dump()) | ||
``` | ||
Produces: | ||
```typescript | ||
{ | ||
"name": "build", | ||
"description": "Build a home", | ||
"opts": [ | ||
{ | ||
"name": [ | ||
"-h", | ||
"--help" | ||
], | ||
"description": "Display help for command", | ||
"args": [] | ||
}, | ||
{ | ||
"name": [ | ||
"-r", | ||
"--residents" | ||
], | ||
"required": false, | ||
"args": [ | ||
{ | ||
"name": "residents", | ||
"variadic": true | ||
} | ||
] | ||
}, | ||
{ | ||
"name": [ | ||
"-d", | ||
"--doors" | ||
], | ||
"args": [], | ||
"defaultValue": 1 | ||
} | ||
], | ||
"args": [ | ||
{ | ||
"name": "address", | ||
"required": true | ||
} | ||
], | ||
"subCommands": [] | ||
} | ||
``` | ||
## Custom help handlers | ||
## Custom exception handling | ||
You can use the `withHelpHandler()` method to override the default `help` text. | ||
```typescript | ||
new CliCommand('build') | ||
.withHelpHandler((command: CommandDefinition) => { | ||
console.log(`This is the documentation for ${command.name} (${command.definition})`) | ||
... | ||
process.exit() | ||
}) | ||
``` | ||
## Exception handling | ||
All exceptions thrown by `cilly` extend the `CillyException` class. If you want to catch each exception and handle them individually, here's the full list of exceptions thrown by `cilly`: | ||
```typescript | ||
class CillyException extends Error | ||
class UnknownOptionException extends CillyException | ||
class UnknownSubcommandException extends CillyException | ||
class InvalidNumOptionNamesException extends CillyException | ||
class InvalidShortOptionNameException extends CillyException | ||
class InvalidLongOptionNameException extends CillyException | ||
class InvalidCommandNameException extends CillyException | ||
class InvalidArgumentNameException extends CillyException | ||
class ExpectedButGotException extends CillyException | ||
class NoCommandHandlerException extends CillyException | ||
class DuplicateArgumentException extends CillyException | ||
class DuplicateOptionException extends CillyException | ||
class DuplicateCommandNameException extends CillyException | ||
class NoArgsAndSubCommandsException extends CillyException | ||
class ValidationError extends CillyException | ||
``` |
65919
19.3%1082
0.19%615
97.12%