Tools for command line applications. TypeScript-safe command line parser and ANSI codes
@webhare/cli provides access to CLI support functions, like ANSI commands and
a command-line parser.
You can use the command-line parser for simple scripts like:
``typescript
// @webhare/cli: An example command using @webhare/cli
import { run } from "@webhare/cli";
run({
flags: {
// Flags are stored with the last name of their list of names, in this case "verbose"
"v,verbose": { description: "Show extra info" },
},
options: {
// Options have associated values. Also, the names are camelcased, this one is stored as "withData".
"with-data": { description: "string option" },
},
arguments: [
{ name: "
],
main({ args, opts, specifiedOpts }) {
// Type types of args and opts
console.log(Load file ${args.file});Verbose: ${opts.verbose ? "yes" : "no"}
console.log();WithData: ${opts.withData ?? "
console.log();List of specified options: ${specifiedOpts.join(", ")}
console.log(); // prints the full names`
}
});
For scripts with sub-commands you can use the following form:
`typescript
// @webhare/cli: An example with subcommands
import { run } from "@webhare/cli";
// runData is filled with data, containing the global options
const runData = run({
/// Global options
flags: {
// Option are stored with the last name of their list of names, in this case "verbose"
"v,verbose": { default: false, description: "Show extra info" },
},
options: {
// Also, the names are camelcased, this one is stored as "withData".
"with-data": { default: "", description: "string option" },
},
subCommands: {
command1: {
shortDescription: "Executes command1",
description: "This command executes the stuff a command1 command should execute",
flags: {
"fast": { description: "Do it fast" }
},
arguments: [
{ name: "
{ name: "[output]", description: "Optional output" },
{ name: "[more...]", description: "More arguments, all optional" },
],
main({ opts, args }) {
// The global and per-command options are passed in opts
console.log(Verbose: ${opts.verbose}, fast: ${opts.fast});WithData: ${opts.withData ?? "
console.log();File: ${args.file}
console.log();Output: ${args.output ?? "N/A"}
console.log();More: ${args.more.join(", ")}
console.log();
// run() returns before the main() is called, so runData can be used
// to read the global options in shared functions
console.log(Verbose: ${runData.globalOpts.verbose});`
}
}
}
});
that will not trigger side effects outside its
call to run() (ie nothing happens until run() is invoked) should mark itself
with a // @webhare/cli: [short description] header. This will be used to generate
an overview of available commands and signals autocompletion that it will be safe
to invoke this script to extract autocompletions.$3
Flags are simple switches (of type boolean) that can be true or false.
The value is set to true when the switch is provided in the command line (even
when the default is set to true!)$3
Options have a required associated value (there is no mechanism to make it
optional).The default type of an options is
string. If there are rules for the
form of the argument, or it needs a specific parser, an argument type can be
specified with a type: CLIArgumentType property. The type of the option
then becomes Type.If no default is provided, the property passed to the main function will
be optional (eg:
{ args: { file?: string } }). If a default is passed (which
must be of same type as the option, it is not added to the type), the property
will become required (eg: { args: { file: string } }).Example:
`typescript
import { intOption, run } from "@webhare/cli";run({
options: {
"withoutDefault": { description: "Option without default" },
"withDefault": { default: "", description: "Option with default" },
"intWithDefault": { default: -1, description: "Option with default", type: intOption() },
},
main({ opts }) {
// opts has type { withoutDefault?: string; withDefault: string; intWithDefault: number }
console.log(
withoutDefault: ${opts.withoutDefault});
console.log(withDefault: ${opts.withDefault ?? ");
console.log(intWithDefault: ${opts.intWithDefault});
}
});
`To see if an option was provided on the command-line, use specifiedOpts (or
specifiedGlobalOpts for global options in the return value of
run()).$3
There are four forms of arguments:
- : a required argument
- [arg2]: an optional argument
- : a list of arguments, with at least one item
- [arg4...]: a list of arguments, none requiredThe default type of an argument is
string (or string[] for lists). If a
type: CLIArgumentType is provided, the arguments are parsed with
that type and will change to Type and Type[] respectively.$3
The main functions execute the program or subcommand. They are executed
_after_ the run() function returns, to be able to store the object with
the global options so it can be accessed by the executed code.The main function is called with the following signature:
`typescript
main(data: {
/// Executed command, not provided if no subcommands are present
command?: string;
/// Options, for every option there is a property with the actual option value
opts: Record; // real type inferred by the options and arguments (eg { verbose: boolean; str: string })
/// List of options specified on the command-line
specifiedOptions: string[]; // real type inferred by the options (eg Array<"verbose" | "fast>)
/// Options, for every argument there is a property with the actual argument value
args: object; // real type inferred by the arguments (eg { arg1: string; arg2?: string; arg4: string[] })
}): CommandReturn;
`The allowed return types of a main function are:
-
void
- number - if not set yet or 0, the process exit code is set to this value
- Promise
- Promise - if not set yet or 0, the process exit code is set to this
valueSpecial handling is invoked when an exception of type
CLIRuntimeError is
received. If the message of the exception is set, that message will be printed.
If the option showHelp of that exception that is set to true, the help will be
printed to stderr (if set, the help for the command in option command). If
the property exitCode is set, the process exit code will be set to that value
(and to 1 if not).$3
The following types have been predefined:
- intOption: Accepts integers (optional with minimum and maximum value)
- floatOption: Accepts numbers (optional with minimum and maximum value)
- enumOption: Accepts a specific set of strings$3
To enable autocompletion for an option you can need to define a custom type
with parseValue and autoComplete eg:`typescript
const assetPackOption = {
parseValue: (arg: string) => arg,
autoComplete: (mask: string) => {
//first complete to module name, then to the full name
const allpacks = getExtractedConfig("assetpacks").map(assetpack => assetpack.name);
return mask.includes(':') ? allpacks : [...new Set(allpacks.map(name => name.split(':')[0] + ':*'))];
}
};...
arguments: [{ name: "[assetpack]", description: "Asset packs to list", type: assetPackOption }],
`Autocomplete results should end in a wildcard (
*) if the argument is not yet complete (eg a partial filename)The API for bash autocompletion support is still under development, and should
be considered experimental (and subject to change in the future).
For now, completion is handled by getting the command line (usually in COMP_LINE)
and slicing it at the cursor position (COMP_POINT). Then the command line
is parsed with
parseCommandLine. The last string in the array returned by
that function is the one that will be completed. If the cursor is set at the end
of the line, after whitespace (eg bla bla ), an empty string is returned
at the end of the list.Example for completion:
` typescript
import { autoCompleteCLIRunScript, enableAutoCompleteMode, parseCommandLine } from "@webhare/cli/src/run-autocomplete";/// Split the command line in words using bash word splitting rules
const words = parseCommandLine(commandline);
/// Run autocomplete
const completes = await runAutoComplete(words);
`To run autocomplete for a file using
run(), the cli library must be placed in
a mode that run() calls aren't executed, but their configuration is stored
for autocompletion instead.Example:
`typescript
import { enableAutoCompleteMode } from "@webhare/cli/src/run-autocomplete";/** registerAsDynamicLoader is called with the node module of the autocompletion
* library, this can be used to register that library as one that does
*
require() calls, and should not be reloaded when a loaded library changes
*/
enableAutoCompleteMode({ registerAsDynamicLoader: (module) => { / ... / } });const autocompletes = autoCompleteCLIRunScript("/tmp/file.ts", [ "param1" ]);
`This returns a list of autocompletion options. It must be postfixed with
\n
when it is not a partial autocompletion, and a space should be added after
the argument when accepted.TODO: describe how to register an autocomplete handler, process the
result with COMP_WORDBREAKS, example to call it with
curl.
commander -> run conversion guide
`typescript
const parsedArgs = program
.option("--days ", "Number of days to sync (default 7)", "7")
.option("--debug", "Debug")
.parse();const days = parseInt(parsedArgs.opts().days) || 7;
const debug = Boolean(parsedArgs.opts().debug);
async function main() { ... }
`becomes:
`typescript
run({
flags: {
"debug": { description: "Debug" },
},
options: {
"days": { default: 7, description: "Number of days to sync (default 7)", type: intOption({ start: 1 }) },
},
main: async function ({ opts }) { ... }
});
``