Build commands and subcommands
Use Artisanal's command runner when your program has subcommands, options,
generated help, or shell completion. It builds on package:args and connects
each command to Console, so parsing and user-facing output follow the same
conventions.
Quick start
import 'package:artisanal/args.dart';
class ServeCommand extends Command<void> {
String get name => 'serve';
String get description => 'Start the development server.';
Future<void> run() async {
io.title('Starting server...');
io.info('Listening on port 8080');
}
}
void main(List<String> args) async {
final runner = CommandRunner(
CommandRunner.detectExecutableName(),
'My Application',
);
runner.addCommand(ServeCommand());
await runner.run(args);
}
Shell Completion
Artisanal can automatically generate shell completion scripts for your CLI. This
is enabled by default on every CommandRunner — no extra setup required.
You do not need to add a completion command, read COMP_LINE, or call the
completion parser from your app.
How It Works
When the shell invokes your executable with completion -- <args>, Artisanal
detects the call, computes matching command names, option flags, and allowed
values, prints them to stdout, and exits. Normal runs are completely unaffected.
Installing
Print the completion script from your compiled binary (or dart run) and
source it in your shell rc file:
# One-shot evaluation (bash / zsh)
eval "$(./github_cli --completion-script)"
# Or for a dart-run script
eval "$(dart run bin/myapp.dart --completion-script)"
# Persistent: append to your rc file
./github_cli --completion-script >> ~/.bashrc # bash
./github_cli --completion-script >> ~/.zshrc # zsh
After adding to your rc file, restart your shell or source ~/.bashrc /
source ~/.zshrc.
Most apps never call completion APIs directly. For installers or release
tooling, ShellCompleter.generate('myapp') can produce the same script without
constructing a runner.
Opting Out
Disable completion per runner if you need to:
final runner = CommandRunner('myapp', 'My Application', enableShellCompletion: false);
Unknown Command Fallbacks
Shim-style CLIs sometimes need to delegate unknown commands to another
executable. Use unknownCommandFallback for that case instead of pre-parsing
the argument list before CommandRunner.run.
final runner = CommandRunner<void>(
'flutter-cli',
'Flutter CLI shim',
unknownCommandFallback: (args) async {
await runExternalFlutter(args);
},
);
The fallback runs only after the runner has handled built-in completion
requests, so completion -- ... and --completion-script remain automatic.
What Gets Completed
The completer walks your full command tree:
- Top-level commands — names and aliases
- Subcommands — recursively through nested
addSubcommandchains - Option names —
--flagand--no-flagfor negatable options - Allowed values — completion lists from
argParser.addOption(..., allowed: [...])
All of the above respect the same namespaceSeparator grouping your help already uses.
Advanced Programmatic Access
import 'package:artisanal/args.dart';
final runner = CommandRunner(
CommandRunner.detectExecutableName(),
'My Application',
);
runner.addCommand(ServeCommand());
// Lazy-initialized completer bound to this runner's arg tree.
// Most applications do not need to call this directly.
final completer = runner.shellCompleter;
// Generate a standalone completion script
final script = runner.shellCompletionScript;
ShellCompleter is also re-exported directly so you can call
ShellCompleter.generate('myapp') or ShellCompleter.generateAll(['myapp', 'myapp-dev'])
without a runner instance.
CommandRunner
Creating a Runner
void main(List<String> args) async {
final runner = CommandRunner(
CommandRunner.detectExecutableName(),
'My CLI application',
)
..addCommand(ServeCommand())
..addCommand(DbCommand())
..addCommand(DbMigrateCommand());
await runner.run(args);
}
Global Flags
The runner automatically adds these global flags:
| Flag | Description |
|---|---|
--help, -h | Display help information |
--ansi / --no-ansi | Force enable/disable ANSI colors |
--quiet, -q / --silent | Suppress all output |
--no-interaction, -n | Disable interactive prompts |
--verbose, -v, -vv, -vvv | Increase verbosity (verbose, very verbose, debug) |
--completion-script | Print a shell completion script to stdout |
Auto-detecting the Executable Name
Pass CommandRunner.detectExecutableName() as the executable name to have the
runner automatically pick up the correct name from the running script:
final runner = CommandRunner(
CommandRunner.detectExecutableName(),
'My CLI application',
);
When run as a compiled binary, the usage shows the binary's file name:
$ ./artisan --help
Usage: artisan <command> [arguments]
...
When run via dart run, the usage includes the script path:
$ dart run bin/myapp.dart --help
Usage: dart run bin/myapp.dart <command> [arguments]
...
You can always pass an explicit name if you prefer a fixed value:
// Always shows "myapp" regardless of how it's executed
final runner = CommandRunner('myapp', 'My CLI');
Constructor and I/O Hooks
CommandRunner supports dependency injection for tests and hosts:
final runner = CommandRunner(
'mycli',
'My CLI',
ansi: false,
namespaceSeparator: ':',
usageExitCode: 64,
out: (line) => output.add(line),
err: (line) => errors.add(line),
outRaw: (text) => rawOut.add(text),
errRaw: (text) => rawErr.add(text),
readLine: () => 'input',
setExitCode: (code) => exitCode = code,
usageLineLength: 80,
);
Important constructor options:
namespaceSeparator: custom command namespace separator (default:).usageExitCode: exit code used for argument/usage errors (default64).ansi: force or disable ANSI behavior globally (null= auto-detect).renderer: optional renderer override (StringRenderer,TerminalRenderer, etc.).out,err,outRaw,errRaw: output redirection hooks.readLine: custom stdin reader for prompts.setExitCode: custom exit-code sink.
Argument / Option Accessors
Artisanal's Command provides Laravel-style convenience helpers so you don't
need to reach for raw argResults! every time:
class GreetCommand extends Command<void> {
GreetCommand() {
argParser.addOption('name', abbr: 'n', help: 'Who to greet.');
argParser.addFlag('shout', help: 'SHOUT the greeting.');
}
String get name => 'greet';
String get description => 'Greet someone.';
void run() {
// --name value (or default)
final name = option('name') as String? ?? 'World';
// --shout flag (bool)
final shout = option('shout') as bool;
// First positional argument
final message = argument(0);
// All positional arguments
for (final arg in arguments) { ... }
// Check if --name was explicitly provided
if (hasOption('name')) { ... }
io.success('Hello, $name!');
}
}
Available Helpers
| Method | Returns | Description |
|---|---|---|
option(name) | Object? | Value of a named option/flag (e.g., --name, --force) |
hasOption(name) | bool | Whether the option was explicitly provided on the command line |
argument(index) | String? | Positional argument at index (0-based), or null if not provided |
arguments | List<String> | All positional arguments |
argumentCount | int | Number of positional arguments |
Command Namespaces
Commands can be grouped using the : separator:
class DbMigrateCommand extends Command<void> {
String get name => 'db:migrate';
String get description => 'Run database migrations';
void run() {
io.task('Running migrations', run: () async {
// ...
return TaskResult.success;
});
}
}
class DbSeedCommand extends Command<void> {
String get name => 'db:seed';
String get description => 'Seed the database';
void run() { ... }
}
final runner = CommandRunner('myapp', 'My Application')
..addCommand(DbMigrateCommand())
..addCommand(DbSeedCommand());
Run with:
dart run bin/myapp.dart db:migrate
Namespace Discovery (Symfony-style)
When a user types a namespace prefix (e.g., db where commands like db:migrate,
db:seed exist), the runner displays the subcommands in that namespace instead of
a "command not found" error:
$ dart run bin/myapp.dart db
Available commands for the "db" namespace:
db
db:migrate Run database migrations
db:seed Seed the database
Run "myapp db:<subcommand> --help" for more information about a command.
This works for nested namespaces too (e.g., cache:user lists cache:user:list,
cache:user:evict).
Like Symfony Console, namespace-only invocations exit with code 1 (no command was actually executed).
Namespace APIs
The runner exposes Symfony Console-style APIs for programmatic namespace inspection:
// Get all unique namespace prefixes
final namespaces = runner.getNamespaces();
// -> ['cache', 'cache:user', 'db', 'help', 'project']
// Get all commands within a namespace
final dbCommands = runner.allCommandsInNamespace('db');
// -> {'db:migrate': ..., 'db:seed': ...}
getNamespaces()-- equivalent to Symfony'sApplication::getNamespaces()allCommandsInNamespace(name)-- equivalent to Symfony'sApplication::all($namespace)
Help Output Grouping
Commands sharing a namespace prefix are automatically grouped under a heading in the help output:
Help Output
The runner generates beautiful, styled help output with namespaces grouped:
My Application
Usage: myapp <command> [arguments]
Options:
-h, --help Print this usage information.
--[no-]ansi Force (or disable with --no-ansi) ANSI output.
-q, --quiet Do not output any message.
--silent Alias for --quiet.
-n, --no-interaction Do not ask any interactive question.
-v, --verbose Increase verbosity of messages:
1 for verbose, 2 for very verbose, 3 for debug.
Available commands:
serve Start the development server
db
db:migrate Run database migrations
db:seed Seed the database
cache
cache:clear Clear the cache
cache:user:list List cached users
Run "myapp <command> --help" for more information about a command.
Commands without a namespace prefix appear first (e.g., serve), followed by
grouped commands under their namespace headings (db, cache).
HelpColorScheme
HelpColorScheme controls the colors applied to different elements of help
output. Pass it as the helpColorScheme argument to CommandRunner:
final runner = CommandRunner(
'myapp',
'My Application',
helpColorScheme: HelpColorScheme.dark(),
);
Built-in Presets
| Preset | Description |
|---|---|
HelpColorScheme.default_ | Default scheme (same amber/green palette, auto-adaptive) |
HelpColorScheme.dark() | Optimized for dark terminals (amber headings, green commands) |
HelpColorScheme.light() | Optimized for light terminals (darker shades) |
HelpColorScheme.minimal(color) | Single foreground color for all elements |
Customizing Colors
Each field accepts any Artisanal Color (AdaptiveColor, BasicColor,
AnsiColor, etc.). Use AdaptiveColor to supply different values for light
and dark backgrounds:
final runner = CommandRunner(
'myapp',
'My Application',
helpColorScheme: HelpColorScheme(
heading: AdaptiveColor(
light: BasicColor('#7c3aed'),
dark: BasicColor('#a78bfa'),
),
command: AdaptiveColor(
light: BasicColor('#0369a1'),
dark: BasicColor('#38bdf8'),
),
option: AdaptiveColor(
light: BasicColor('#047857'),
dark: BasicColor('#34d399'),
),
error: AdaptiveColor(
light: BasicColor('#b91c1c'),
dark: BasicColor('#f87171'),
),
),
);
Use copyWith to extend an existing scheme without respecifying every field:
final myScheme = HelpColorScheme.dark().copyWith(
heading: const BasicColor('#ff9900'),
namespace: const BasicColor('#cc77ff'),
);