Examples
Introduction
This document specifies examples of using ProgramBuilder that should exemplify common use-cases.
Example invocations of the CLI program are presented using ts-node
and assuming that the main file is called main.ts
.
Positional arguments
Positional arguments are specified using the .arg
or .optionalArg
methods.
The first argument is the destination key into which the argument value will be stored.
.arg
is used to specify required arguments. If not enough required arguments are provided
to the program, it will exit with a non-zero code.
const program = ProgramBuilder.newBuilder().arg("foo").optionalArg("bar").build();program.exec(args => {// args: { foo: string, bar: string | null }console.log('Args are:', args);});
Example invocations:
$ ts-node main.tsNot enough positional arguments were specified. Expected: at least 1
$ ts-node main.ts helloArgs are: { foo: 'hello', bar: null }
$ ts-node main.ts hello worldArgs are: { foo: 'hello', bar: 'world' }
Promise-returning main functions
The function that you pass to exec
can be an async
or Promise-returning function.
If your function returns a Promise, it will be properly awaited on and if it throws an error that error will be output and the process will return a non-zero exit code.
const program = ProgramBuilder.newBuilder().build();program.exec(async args => {await someHTTPCall();});
Specifying your main function ahead of your builder
If you call program.exec
with a lambda, its args
argument will automatically assume
the correct type of the program's arguments.
However, if you want to specify your main function separately, you must use the
Arguments<T>
type helper and pass typeof program
.
typeof
returns the TypeScript type of a variable, and the Arguments
helper
(part of ProgramBuilder) unwraps the arguments type from a Program
object.
function main(args: Arguments<typeof program>) {// Do stuff}const program = ProgramBuilder.newBuilder().build();program.exec(main);
Boolean flags
Use .flag()
to specify a boolean flag. The first argument is the name of the flag,
including leading dashes (ex: "--count"
).
Multiple names can be specified by separating them with commas within the same string
(ex: "-c,--count"
).
The second argument is a list of options for the flag. The only required one of these
is dest
, which indicates where to store the flag value within the arguments object
that is passed to your main function.
const program = ProgramBuilder.newBuilder().flag("-v,--verbose", { dest: "verbose" }).build();program.exec(args => {// args: { verbose: boolean }console.log('Args are:', args)});
Example invocations:
$ ts-node main.tsArgs are: { verbose: false }
$ ts-node main.ts -vArgs are: { verbose: true }
Valued flags
Flags can also take on a value, which the user specifies by providing a string immediately succeeding the flag on the commandline.
Valued flags are specified by calling one of several methods that are specialized to the desired type of the flag: string, integer, etc.
The typed value is then stored in the specified dest
.
const program = ProgramBuilder.newBuilder().intFlag("--count", { dest: "count" }).stringFlag("--name", { dest: "name" }).build();program.exec(args => {// args: { count: number, name: string }console.log('Args are:', args);});
Example invocations:
$ ts-node main.tsThe following required flags were not specified: --count, --name
$ ts-node main.ts --count 2 --name billArgs are: { count: 2, name: 'bill' }
$ ts-node main.ts --count invalid --name bobPlease provide a valid integer for '--count'. Received: invalid
The current types of valued flags are:
stringFlag
intFlag
floatFlag
customFlag
– allows you to provide your own converter function from a string to your custom type.
Optional vs Required Flags
Valued flags are required unless a default
is specified. If a required flag
isn't provided, the program will exit with a non-zero return code.
The default may be null
, in which case the flag is typed as T | null
, or it
may be a value of the appropriate type, in which case that value will be assumed
in the case that the flag is omitted.
In help text, required flags are denoted by surrounding the variable in <braces>
,
whereas optional flags are denoted by surrounding the variable in [brackets]
.
const program = ProgramBuilder.newBuilder().intFlag('-a', { dest: 'a' }).intFlag('-b', { dest: 'b', default: 0 }).intFlag('-c', { dest: 'c', default: null }).build();program.exec(args => {// args: { a: number, b: number, c: number | null }console.log('Args are:', args);});
Example invocations:
$ ts-node main.tsThe following required flags were not specified: -a
$ ts-node main.ts -a 1Args are: { a: 1, b: 0, c: null }
$ ts-node main.ts -hUsage: main.ts [options]Options:-a <a>-b [b]-c [c]
Adding descriptions
Call .description()
on a ProgramBuilder
to set the program's description to be used in help text generation.
Add a description
key to the options for a flag or an argument to set a description there.
const program = ProgramBuilder.newBuilder().description(`My awesome program`).arg('filename', { description: `Awesome description of a filename` }).optionalArg('optarg', { description: `This argument is optional` }).flag('--enable', { dest: 'enable', description: `Awesome description of the --enable flag` }).flag('--longboi', { dest: 'longboi', description: `Text is automatically wrapped. Text is automatically wrapped. Text is automatically wrapped. Text is automatically wrapped.` }).build();program.exec(() => {});
Example invocations:
$ ts-node main.ts -hUsage: main.ts [options] <filename> [optarg]My awesome programArguments:filename Awesome description of a filenameoptarg This argument is optional (optional)Options:--enable, --enable Awesome description of the --enable flag--longboi, --longboi Text is automatically wrapped. Text is automaticallywrapped. Text is automatically wrapped. Text isautomatically wrapped.
Custom flags
For valued flags, you may also provide your own custom converter function from a string into your desired type.
This is how intFlag
and floatFlag
, etc are implemented.
To activate this functionality, use the customFlag
method, which is similar to
the other methods but takes a third argument which is a converter function.
The converter function should adhere to the Converter type's signature — it takes in two arguments, the string input and the argument name, and returns the converted value.
If the input is invalid, you should throw an Error
and use the argName
to enrich
your error message.
The return type of the converter (<T>
) will be added to the ProgramBuilder
's type.
const jsonConverter: Converter<any> = (input, argName) => JSON.parse(input);const program = ProgramBuilder.newBuilder().customFlag('--jsonInput', { dest: 'json' }, jsonConverter).build();program.exec(args => {// args: { json: any }console.log('Args are:', args);});
Example invocations:
$ ts-node main.ts --jsonInput '{"hello": "world"}'Args are: { json: { hello: 'world' } }
Subcommands
Use ProgramBuilder.buildWithSubcommands
to create a program with multiple sub-commands. This accepts a map from subcommand name to a
ProgramWithAction
representing the subcommand
flags and the action to be performed when executing the subcommand.
A ProgramWithAction
can be created by calling .bind()
on a ProgramBuilder
instead of .build()
. .bind()
accepts a MainFunction
as its argument,
indicating the function to be called when that subcommand is specified.
The map can be multiply nested, for subcommands that have child-subcommands which are separated
by spaces. For example {nested: {subcommand: someProgram}}
would result in a subcommand named
"nested subcommand"
.
The second argument to buildWithSubcommands
is an optional metadata object which can contain,
for example, the overall program description.
function generateMain(args: Arguments<typeof generate>) {console.log(`Generating to ${args.filename}`);}function cleanMain(args: Arguments<typeof clean>) {console.log('Cleaning output directory');}function nestedMain(args: Arguments<typeof nested>) {console.log(`This is a nested subcommand ${args.x}`);}const generate = ProgramBuilder.newBuilder().description(`Generate something`).arg('filename').bind(generateMain);const clean = ProgramBuilder.newBuilder().description('Clean the output directory').bind(cleanMain);const nested = ProgramBuilder.newBuilder().flag('-x', { dest: 'x' }).bind(nestedMain);const program = ProgramBuilder.buildWithSubcommands({generate,clean,this: {is: {nested}}}, {description: `A description of my subcommand`});// Execute the program. Note this doesn't take a main function, like// a normal Program.exec().program.exec();
Example invocations:
$ ts-node main.ts -hUsage: main.ts COMMAND [options]A description of my subcommandCommands:generate Generate somethingclean Clean the output directorythis is nested
$ ts-node main.ts generate -hUsage: main.ts generate <filename>Generate something
$ ts-node main.ts generate foo.txtGenerating to foo.txt
$ ts-node main.ts this is nested -hUsage: main.ts this is nested [options]Options:-x, -x
$ ts-node main.ts this is nested -xThis is a nested subcommand true