Commander
For nest-commander
, a command
is something that the CLI runner should do. This could be like
@nestjs/cli
’s start
or build
commands, or it could be something like git
’s add
or
commit
. To specify a command, all you need to do is decorate a class that extends the
CommandRunner
abstract class with the @Command()
decorator. No need to use @Injectable()
or
anything else, the @Command()
decorator will take care of allowing dependencies to be injected
into the class.
Setting the Command Name and Arguments
The @Command()
decorator takes in a set of options that allows the underlying commander
package
to handle the command properly. The options are things like the command’s name
, its arguments
,
the description
, the argsDescription
and general options
, all of which will get passed to
commander and handled as described by their docs.
To set a command’s name, and make it the default actions, you would need to use the decorator as
follows. Let’s make a command with the name my-exec
. It will also take in a shell command to
execute. We’ll call this, task
.
@Command({
name: 'my-exec',
arguments: '<task>',
options: { isDefault: true }
})
export class TaskRunner extends CommandRunner {
async run(inputs: string[], options: Record<string, any>): Promise<void> {}
}
You’ll notice for the arguments
we use angle brackets around the argument name. This specifies
that the argument is required for the command, and commander will throw an error if it is not
supplied. If, however, we wanted to make the argument optional, we could wrap it in square brackets
instead like [task]
.
Now, to run this command, we’ll need to set up the CommandFactory and make use of one
of the execution methods as described later in the docs. For now, we’ll just assume this application
is installed globally under the crun
name. Running the above command would then look like
crun my-exec 'echo Hello World!'
# OR
crun 'echo Hello World!'
We can use either of these because we set up the options: { isDefault: true }
options.
This command doesn’t do anything yet, but we’ll get to the implementation of it later. For now, let’s explore how we can set options for the command.
Setting Options for the Command
Options allow users to change certain behaviors of the command. Going back to Nest’s CLI the start
command can take a --watch
or --debug
option to start up in different modes. For
nest-commander
each of these options goes on its own method of the class it modifies. Going back
to our run
command, say we wanted to allow the use of a different shell for running the command.
The easiest way to add this in would be to allow the user to use a flag, like -s
and then the name
of the shell to use. Adding this option would then make our class look like the following
@Command({
name: 'my-exec',
arguments: '<task>',
options: { isDefault: true }
})
export class TaskRunner extends CommandRunner {
async run(inputs: string[], options: Record<string, any>): Promise<void> {}
@Option({
flags: '-s, --shell <shell>',
description: 'A different shell to spawn than the default'
})
parseShell(val: string) {
return val;
}
}
The reason for each option to be tied to a method handler is because options from the command line
come in as strings. Most of the time, this is fine and works well, but there are times where it ends
up having problems, like passing booleans. These methods allow developers to set up their own
parsing methods for each option, so that when my-exec
is called, all of the inputs are validated
and ready to be used immediately.
You’ll also notice that the flags
property has several parts to it, a -s
short flag, a --shell
long flag, and <shell>
. This means that the user can either use -s
or --shell
to add this
option, but if either is used they must provide a shell option. The shell
name ends up being a
key of the options
parameter for the my-exec
method, and can be retrieved later using
options.shell
.
The description
is used when the --help
flag is passed, and will help provide more information
to the CLI consumer.
You can add as many of these @Option()
methods as necessary for your command, so long as they do
not duplicate option names. Each method may only have one @Option()
decorator as well.
You can also make an option completely required, like an argument, by setting required: true
in
the metadata for the option.
Variadic Options
Options also allow for variadic inputs but you will need to provide an option parser that accumulates each option.
@Option({
flags: '-c, --options <options...>',
description: 'Specify options',
})
parseOptions(option: string, optionsAccumulator: string[] = []): string[] {
optionsAccumulator.push(option);
return optionsAccumulator;
}
Setting Choices for your Options
Commander also allows us to set up pre-defined choices for options. To do so we have two options:
setting the choices
array directly as a part of the @Option()
decorator, or using the
@OptionChoiceFor()
decorator and a class method, similar to the InquirerService.
With using the @OptionChoiceFor()
decorator, we are also able to make use of class providers that
are injected into the command via Nest’s DI which allows devs to read for the choices from a file or
database if that happens to be necessary.
import { Option, OptionChoiceFor } from 'nest-commander';
@Command({ name: 'my-exec' })
export class RunCommand extends CommandRunner {
constructor(
private readonly choiceProvider: {
getChoicesForRun: () => string[];
}
) {
super();
}
async run(args: any, options: { runWithColor: 'yes' | 'no' }) {
console.log(options);
}
@Option({
flags: '-c, --color [runWithColor]',
name: 'withColor',
description: 'Should the command use color in the output'
})
parseColorOption(option: string) {
return option;
}
@OptionChoiceFor({ name: 'withColor' }) // make sure this matches the `name` of an `@Options()` decorator
getColorChoices() {
return this.choiceProvider.getChoicesForRun();
}
}
Adding Custom Help
By default, commander
sets help to the --help
or -h
flag. If you need extra help added to the
command, you can use the @Help()
decorator on a class method that returns a string. The valid
values for the @Help()
decorator are before
, beforeAll
, after
and afterAll
, just like for
commander’s addHelpText
method.
Getting the Commander Instance
If for some reason you need access to the commander
instance, as of nest-commander@2.4.0
you can
use @InjectCommander()
to get the instance used.
Getting the Current Command
Similarly, if you need to get the current Commander commander instance, you can access it via
this.command
Sub Commands
It may also be that you want to add subcommands to your command, similar to docker compose up
.
This is possible with the @SubCommand()
decorator. Using this decorator, you can have your
original implementation for the @Command()
decorator, with arguments as normal, and you can have
sub commands, as specific arguments that take in even more options. With our my-exec
example
above, lets say we wanted to add a subcommand, foo
. We’d make use of the parents
option for the
my-exec
command and reference the subcommand class, like so:
@Command({
name: 'my-exec',
arguments: '<task>',
subCommands: [FooCommand]
})
export class RunCommand extends CommandRunner {}
Now we just make a subcommand with the same metadata options as the @Command()
decorator
@SubCommand({ name: 'foo', arguments: '[phooey]' })
export class FooCommand extends CommandRunner {
// command runner implementation
}
After adding the subcommand to the appropriate module’s providers
array, nest-commander will set
up the command so you can call crun my-exec foo hello!
and the FooCommand#run
method will be ran
instead of RunCommand#run
. You can also chain commands as deep as you want, by adding
subCommands
to the subcommand’s metadata.
Subcommands can also take an aliases
array for sub command aliases. We could add aliases: ['f']
to the above FooCommand
and run it with my-exec f
instead of my-exec foo
and get the same
result. aliases
must be passed as an array.
You can also use the options: { isDefault: true }
option of the@SubCommand()
decorator to set a
default subcommand for the command.
Request Scoped Commands
In a CLI environment requests don’t exist in the traditional HTTP sense. Because of this, REQUEST
scoped providers are problematic inside of nest-commander
commands. However, thanks to the
@RequestModule()
decorator it is possible to create a mock value for the command to make use of so
that the command and all of its dependencies remain SINGLETON
or DEFAULT
scoped allowing the
original providers of the application to be used without a major bit of re-architecting. The
decorator works just like @Module()
with one extra field, requestObject
that gets merged into
the providers
array under the REQUEST
provider token from @nestjs/core
. This makes Nest
resolve the “proper” value for REQUEST
because it exists inside the current module.
@RequestModule({
providers: [SimpleCommand, RequestScopedProvider],
requestObject: { headers: { Authorization: 'Bearer token' } }
})
export class RequestScopedCommandModule {}
While we call it RequestScoped
, it is very much set to be singleton, which is a win for re-using
existing providers.
Using a Command as the Root Command
Sometimes using options: { isDefault: true }
isn’t enough for the use case, like if you want to
have the --help
flag output the default command’s arguments and options. Fortunately, there is the
@RootCommand()
decorator to replace the base commander
command with your own command. This
command will be read, parsed, and set up before all of the other commands in your application to
allow for everything to keep working as is. It works exactly like @Command()
, but does not
require a name
argument to be passed.
The Full Command
Let’s say all we want to do is have our my-exec
command run the task in another shell, and that’s
it. If we take our above command we can see that it can be ran like so
crun my-exec 'echo Hello World!'
# OR
crun 'echo Hello World!'
# OR
crun 'echo Hello World!' --shell zsh
# OR
crun my-exec 'echo Hello World!' -s zsh
To create this kind of program, we can do the following:
import { spawn } from 'child_process';
import { Command, CommandRunner, Option } from 'nest-commander';
import { userInfo } from 'os';
@Command({
name: 'my-exec',
arguments: '<task>',
options: { isDefault: true }
})
export class TaskRunner extends CommandRunner {
async run(inputs: string[], options: Record<string, string>): Promise<void> {
const echo = spawn(inputs[0], {
shell: options.shell ?? userInfo().shell
});
echo.stdout.on('data', (data: Buffer) => {
console.log(data.toString());
});
}
@Option({
flags: '-s, --shell <shell>',
description: 'A different shell to spawn than the default'
})
parseShell(val: string) {
return val;
}
}
And now the TaskRunner
is setup and ready to be used.
The above command is meant to be a basic example, and should not be taken as a fully fleshed out CLI example. There’s error handling, input validation, and security that should all be considered. Please do not use the above command in a production environment without adding the mentioned necessities at the very least.
Register Commands
Though you’ll find the implementation details in the factory page, you must register
all of your commands including the sub commands as providers in a module class that the
CommandFactory
ends up registering. For convenience, given that we register examples in Sub
Commands section and set them as providers in app.module.ts
that set as root module to
CommandFactory
.
@Module({
providers: [RunCommand, FooCommand]
})
export class AppModule {}
If you have many sub commands and nested directories for that, it may feel tough to import all of
them. For this case, the static registerWithSubCommands
method is available in all classes
inheriting CommandRunner
which returns a list of itself and all sub commands. This means you can
write the setting like followed by example instead of the previous example.
@Module({
providers: [...RunCommand.registerWithSubCommands()]
})
export class AppModule {}
This example works even if the RunCommand
has more and nested subcommands and doesn’t interfere
with registering other providers or commands if using spread operator or concat method of Array
.