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.ts
Not enough positional arguments were specified. Expected: at least 1
$ ts-node main.ts hello
Args are: { foo: 'hello', bar: null }
$ ts-node main.ts hello world
Args 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.ts
Args are: { verbose: false }
$ ts-node main.ts -v
Args 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.ts
The following required flags were not specified: --count, --name
$ ts-node main.ts --count 2 --name bill
Args are: { count: 2, name: 'bill' }
$ ts-node main.ts --count invalid --name bob
Please provide a valid integer for '--count'. Received: invalid

The current types of valued flags are:

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.ts
The following required flags were not specified: -a
$ ts-node main.ts -a 1
Args are: { a: 1, b: 0, c: null }
$ ts-node main.ts -h
Usage: 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 -h
Usage: main.ts [options] <filename> [optarg]
My awesome program
Arguments:
filename Awesome description of a filename
optarg This argument is optional (optional)
Options:
--enable, --enable Awesome description of the --enable flag
--longboi, --longboi Text is automatically wrapped. Text is automatically
wrapped. Text is automatically wrapped. Text is
automatically 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 -h
Usage: main.ts COMMAND [options]
A description of my subcommand
Commands:
generate Generate something
clean Clean the output directory
this is nested
$ ts-node main.ts generate -h
Usage: main.ts generate <filename>
Generate something
$ ts-node main.ts generate foo.txt
Generating to foo.txt
$ ts-node main.ts this is nested -h
Usage: main.ts this is nested [options]
Options:
-x, -x
$ ts-node main.ts this is nested -x
This is a nested subcommand true