Dart 73 🎯 Writing Command-Line Interfaces (CLIs) with package:args
A Dart command-line interface is a program that reads arguments from main(List<String> args), parses them into structured values, and routes execution to the correct logic. The package:args library provides two levels of abstraction for this: ArgParser for parsing flags and options in a single-command script, and CommandRunner for building multi-command CLIs with subcommands, automatic help generation, and usage error handling . The distinction between the two determines the architecture of the tool. A simple script that accepts a --name flag uses ArgParser directly. A tool like git that has commit, push, and status subcommands uses CommandRunner with a separate Command class for each subcommand.
The parser is not just a convenience. It handles the GNU/POSIX conventions that users expect: --name=value, --name value, -n value, combined short flags like -vfi, and the -- separator that stops option parsing so the remaining arguments are treated as positional . It also generates the help output automatically from the declared options, which means the --help text stays in sync with the code without manual maintenance. The parser throws a FormatException or ArgParserException when the input is invalid, and the CommandRunner converts those into a UsageException that prints the usage string and exits with a non-zero code.
Key point: Use ArgParser directly for simple scripts with flags and options, and CommandRunner with Command subclasses for multi-command CLIs; the parser handles GNU/POSIX conventions, generates help text from the declarations, and throws UsageException on invalid input, which the runner prints with the usage string.
Why package:args exists
The manual parsing problem. Without a library, parsing args means iterating over the list, checking for -- prefixes, handling = separators, and validating values. Every script reinvents the same logic, and every script has slightly different edge cases. ArgParser centralizes the logic and the conventions: it knows that --name=value and --name value are equivalent, that -n is an abbreviation for --name if declared, and that --no-flag negates a boolean flag . The parser also validates against a declared set of allowed values and throws a specific exception when the input does not match.
The subcommand problem. A CLI with subcommands—git commit, git push, docker run, docker build—needs to route the first positional argument to a command object, pass the remaining arguments to that command’s parser, and handle the case where the command does not exist. CommandRunner does this routing. It maintains a map of command names to Command instances, parses the top-level arguments to find the command, then delegates the rest to the command’s own ArgParser . Without the runner, the routing logic is a hand-written switch statement that grows with every new command.
The help generation problem. Every CLI needs --help. Writing the help text by hand means it drifts from the actual options. ArgParser generates the usage string from the declarations: the option names, abbreviations, help text, defaults, and allowed values all appear in the output automatically . CommandRunner generates a top-level help that lists the available commands and their descriptions, and each Command generates its own help. The help command is added automatically, so mycli help and mycli help subcommand both work .
The error handling problem. When the user types an invalid option, the CLI should print a clear error, show the usage string, and exit with a non-zero code. CommandRunner.run() catches ArgParserException, converts it to UsageException, prints the message and the usage, and sets the exit code . If the code uses run() inside a try/catch, the UsageException can be handled explicitly. The default behavior is to print to stderr and exit with code 64, which is the standard EX_USAGE code from sysexits.h .
a. ArgParser for single-command scripts
The ArgParser is created, configured with addFlag and addOption, and then parse() is called with the arguments from main. The result is an ArgResults object that provides typed access to the values.
// Import the args package for argument parsing
import 'package:args/args.dart';
void main(List<String> arguments) {
// Create a parser and declare the options
final parser = ArgParser()
// A string option named "name" with abbreviation "n"
// defaultsTo provides the value when the option is absent
// help is shown in the generated usage text
..addOption('name', abbr: 'n', defaultsTo: 'World', help: 'Who to greet.')
// A boolean flag named "verbose" with abbreviation "v"
// negatable: false means --no-verbose is not accepted
..addFlag('verbose', abbr: 'v', negatable: false, help: 'Enable verbose output.');
// Parse the arguments from the command line
// If parsing fails, ArgParserException is thrown
final results = parser.parse(arguments);
// Read the "name" option as a string
// The defaultsTo ensures it is never null
final name = results['name'] as String;
// Read the "verbose" flag as a boolean
final verbose = results['verbose'] as bool;
// Use the parsed values
print('Hello, $name!');
if (verbose) {
print('Verbose mode enabled.');
}
}
The addOption method declares a string option. The abbr parameter provides a single-character alias. The defaultsTo parameter provides the value when the option is not present on the command line. The addFlag method declares a boolean flag. By default, flags are negatable, which means --no-verbose sets the flag to false. The negatable: false parameter disables that behavior .
The parse() method returns an ArgResults object. Accessing a value uses the bracket operator: results['name'] returns the value of the name option. For flags, the value is a bool. For options, the value is a String? unless defaultsTo is set, in which case it is a String. The ArgResults also provides wasParsed() to check whether an option was explicitly provided, and rest for the positional arguments that were not consumed by options .
The parser supports allowed values for validation. If the user provides a value outside the allowed set, the parser throws an ArgParserException :
// Restrict the "mode" option to two values
parser.addOption('mode', allowed: ['debug', 'release']);
The mandatory parameter makes an option required. If the option is not provided, accessing it in the results throws an ArgumentError :
// The "mode" option must be provided
parser.addOption('mode', mandatory: true);
b. CommandRunner for multi-command CLIs
A CommandRunner orchestrates a set of Command objects. Each command has a name, a description, and its own ArgParser for command-specific options. The runner parses the top-level arguments, finds the command, and delegates to its run() method.
The Command class is extended for each subcommand. The name and description are overridden as fields. The argParser is configured in the constructor. The run() method contains the command’s logic and has access to argResults, which is the parsed result of the command’s own arguments.
// Import the command runner and the standard IO library
import 'dart:io';
import 'package:args/command_runner.dart';
// A command that greets a user
class GreetCommand extends Command {
// The name of the command as typed on the command line
@override
final String name = 'greet';
// The description shown in the help output
@override
final String description = 'Greet a user by name.';
GreetCommand() {
// Declare the options that this command accepts
argParser.addOption('name', abbr: 'n', help: 'The name to greet.');
argParser.addFlag('shout', help: 'Print the greeting in uppercase.');
}
// The logic that runs when the command is invoked
@override
void run() {
// argResults holds the parsed values for this command
// If "name" is not provided, use "World" as the fallback
final name = argResults?['name'] as String? ?? 'World';
// If "shout" is provided, the greeting is uppercased
final shout = argResults?['shout'] as bool? ?? false;
final greeting = 'Hello, $name!';
// Print the result, uppercased if the flag is set
print(shout ? greeting.toUpperCase() : greeting);
}
}
void main(List<String> arguments) async {
// Create the runner with the executable name and description
final runner = CommandRunner('mycli', 'A sample CLI built with package:args.')
// Register the commands that the CLI supports
..addCommand(GreetCommand());
// Run the command that matches the first argument
// If no command matches, the runner prints the usage and exits
await runner.run(arguments);
}
The runner.run(arguments) call does three things. It parses the top-level arguments to find the command name. It looks up the command in the internal map. It calls the command’s run() method with the remaining arguments parsed by the command’s own ArgParser . If the command name is not found, the runner throws a UsageException that prints the top-level usage and lists the available commands.
The runner adds the help command automatically. Running mycli help prints the top-level usage. Running mycli help greet prints the usage for the greet command, including the options declared in its argParser . The --help flag is also available on every command, so mycli greet --help produces the same output as mycli help greet.
c. Error handling, exit codes, and the usage exception
The CommandRunner.run() method catches ArgParserException and converts it to UsageException. The UsageException contains the error message and the usage string. The default behavior is to print both to stderr and set the exit code to 64 . If the application needs to handle the error differently—log it, show a custom message, or exit with a different code—it can catch the UsageException explicitly.
// Import the standard IO library for exit codes
import 'dart:io';
import 'package:args/command_runner.dart';
void main(List<String> arguments) async {
final runner = CommandRunner('mycli', 'A sample CLI.')
..addCommand(GreetCommand());
try {
// Run the command; if the arguments are invalid, UsageException is thrown
await runner.run(arguments);
} on UsageException catch (e) {
// Print the error message and the usage string to stderr
stderr.writeln(e.message);
stderr.writeln(e.usage);
// Exit with the standard usage error code
exit(64);
}
}
The UsageException has two properties: message is the specific error, and usage is the usage string for the command or runner that threw the exception . The exit(64) call sets the exit code to EX_USAGE, which is the standard code for a command-line usage error. A script that calls this CLI can check the exit code to determine whether the failure was a usage error or a runtime error.
The Command class also provides a usageException() method that throws a UsageException from inside the run() method. This is useful when the command’s logic determines that the arguments are valid syntactically but invalid semantically—for example, a deploy command that requires a --environment flag when the --production flag is set:
// Inside a Command's run() method
if (argResults?['production'] == true && argResults?['environment'] == null) {
// Throw a usage exception with a specific message
// The runner will catch it and print the usage string
usageException('--environment is required when --production is set.');
}
Complete Example Session
The following session builds a multi-command CLI with a greet command and an add command, shows the automatic help generation, and demonstrates the error handling.
// ============================================
// PART 1: THE GREET COMMAND
// ============================================
import 'dart:io';
import 'package:args/command_runner.dart';
class GreetCommand extends Command {
// The command name as typed on the command line
@override
final String name = 'greet';
// The description shown in the help output
@override
final String description = 'Greet a user.';
GreetCommand() {
// The name option with a default value
argParser.addOption('name', abbr: 'n', defaultsTo: 'World', help: 'Who to greet.');
// The shout flag, not negatable
argParser.addFlag('shout', negatable: false, help: 'Uppercase the greeting.');
}
@override
void run() {
// Read the values from argResults
final name = argResults?['name'] as String;
final shout = argResults?['shout'] as bool? ?? false;
final greeting = 'Hello, $name!';
print(shout ? greeting.toUpperCase() : greeting);
}
}
// ============================================
// PART 2: THE ADD COMMAND
// ============================================
class AddCommand extends Command {
@override
final String name = 'add';
@override
final String description = 'Add two numbers.';
AddCommand() {
// Two required numeric options
argParser.addOption('x', mandatory: true, help: 'First number.');
argParser.addOption('y', mandatory: true, help: 'Second number.');
}
@override
void run() {
// Parse the values as integers
final x = int.parse(argResults?['x'] as String);
final y = int.parse(argResults?['y'] as String);
print('$x + $y = ${x + y}');
}
}
// ============================================
// PART 3: THE RUNNER AND ERROR HANDLING
// ============================================
void main(List<String> arguments) async {
final runner = CommandRunner('mycli', 'A sample CLI with two commands.')
..addCommand(GreetCommand())
..addCommand(AddCommand());
try {
await runner.run(arguments);
} on UsageException catch (e) {
// Print the error and usage to stderr
stderr.writeln(e.message);
stderr.writeln(e.usage);
// Exit with the standard usage error code
exit(64);
}
}
// ============================================
// PART 4: EXAMPLE INVOCATIONS
// ============================================
// dart run bin/cli.dart greet
// → Hello, World!
//
// dart run bin/cli.dart greet --name=Alice --shout
// → HELLO, ALICE!
//
// dart run bin/cli.dart add --x=3 --y=4
// → 3 + 4 = 7
//
// dart run bin/cli.dart greet --bad-option
// → Could not find an option named "bad-option".
// → (usage string)
// → exit code 64
The four parts cover the greet command with a default value and a flag, the add command with two mandatory options, the runner with explicit UsageException handling, and the example invocations. What the session demonstrates is that the help output is generated from the declarations, the mandatory options throw when missing, and the invalid option produces a clear error with the usage string and a non-zero exit code.
Quick Reference
ArgParser Methods
| Method | Purpose |
|---|---|
addFlag(name) | Declare a boolean flag |
addOption(name) | Declare a string option |
addMultiOption(name) | Declare a repeatable option |
parse(args) | Parse the arguments |
addOption Parameters
| Parameter | Purpose |
|---|---|
abbr | Single-character abbreviation |
defaultsTo | Value when absent |
help | Text in usage output |
allowed | Validation set |
mandatory | Required option |
CommandRunner Methods
| Method | Purpose |
|---|---|
addCommand(cmd) | Register a command |
run(args) | Parse and dispatch |
usage | The top-level usage string |
Command Overrides
| Member | Purpose |
|---|---|
name | Command name |
description | Help text |
argParser | Command-specific parser |
run() | Command logic |
usageException(msg) | Throw a usage error |
Best Practices
✅ Do This:
// Use ArgParser for simple scripts
final parser = ArgParser()..addOption('name', abbr: 'n');
// Use CommandRunner for multi-command CLIs
final runner = CommandRunner('cli', 'Description')..addCommand(GreetCommand());
// Let the runner handle UsageException, or catch it explicitly
try { await runner.run(args); } on UsageException catch (e) { exit(64); }
// Use defaultsTo for optional values
parser.addOption('mode', defaultsTo: 'debug');
❌ Don’t Do This:
// Hand-parse args when package:args does it
if (args[0] == '--name') { ... } // reinventing the wheel
// Forget mandatory options cause a throw on access
argParser.addOption('x', mandatory: true); // access throws if missing
// Ignore UsageException and let the default print
// (fine, but the exit code is 64, not 1)
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
results['name'] is null | No defaultsTo and option absent | Use as String? ?? 'fallback' |
| Mandatory option throws | Accessing without providing | Check wasParsed() first |
| Command not found | Typo in command name | Runner prints available commands |
--help does not work | Parser not configured | Runner adds it automatically |
| Exit code is 64 | UsageException default | Catch and set custom code |
Real-World Examples
1. Basic option
parser.addOption('name', abbr: 'n', defaultsTo: 'World');
2. Basic flag
parser.addFlag('verbose', abbr: 'v', negatable: false);
3. Allowed values
parser.addOption('mode', allowed: ['debug', 'release']);
4. Mandatory option
parser.addOption('x', mandatory: true);
5. Command class
class GreetCommand extends Command {
final String name = 'greet';
final String description = 'Greet a user.';
}
6. Command option
GreetCommand() { argParser.addOption('name'); }
7. Command run
void run() { print('Hello, ${argResults?['name']}'); }
8. Runner setup
final runner = CommandRunner('cli', 'Description')..addCommand(GreetCommand());
9. Usage exception
try { await runner.run(args); } on UsageException catch (e) { exit(64); }
10. Custom usage error
usageException('--env is required.');
Visual
ArgParser Flow
┌──────────────────────────────────────────────┐
│ main(List<String> args) │
│ │ │
│ ▼ │
│ ArgParser.parse(args) │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ valid invalid │
│ │ │ │
│ ▼ ▼ │
│ ArgResults ArgParserException │
│ results['x']│ │
│ ▼ │
│ print error │
│ exit(64) │
└──────────────────────────────────────────────┘
CommandRunner Flow
┌──────────────────────────────────────────────┐
│ runner.run(args) │
│ │ │
│ ▼ │
│ Parse top-level args │
│ │ │
│ ▼ │
│ Find command by name │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ found not found │
│ │ │ │
│ ▼ ▼ │
│ cmd.run() UsageException │
│ with args (print usage, exit 64) │
└──────────────────────────────────────────────┘
Help Generation
┌──────────────────────────────────────────────┐
│ parser.addOption('name', │
│ abbr: 'n', │
│ defaultsTo: 'World', │
│ help: 'Who to greet.') │
│ │ │
│ ▼ │
│ --help output: │
│ -n, --name Who to greet. │
│ (defaults to "World") │
└──────────────────────────────────────────────┘
UsageException Handling
┌──────────────────────────────────────────────┐
│ Invalid option: --bad │
│ │ │
│ ▼ │
│ ArgParserException │
│ │ │
│ ▼ │
│ CommandRunner converts to UsageException │
│ │ │
│ ▼ │
│ try/catch in main │
│ │ │
│ ▼ │
│ stderr: error message + usage │
│ exit(64) │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Package | args |
| Single-command parser | ArgParser |
| Multi-command runner | CommandRunner |
| Subcommand base | Command |
| Flag method | addFlag |
| Option method | addOption |
| Parse result | ArgResults |
| Error type | UsageException |
| Default exit code | 64 (EX_USAGE) |
| Help command | Added automatically |
Key takeaways:
ArgParseris for single-command scripts. It declares flags and options, parses the arguments, and returns anArgResultsobject. The help output is generated from the declarations .CommandRunneris for multi-command CLIs. It routes the first positional argument to aCommandobject, delegates the remaining arguments to that command’s parser, and generates the top-level help with the list of commands .- The parser handles GNU/POSIX conventions.
--name=value,--name value,-n value, combined short flags, and--no-flagnegation all work without additional code . defaultsToandmandatorycontrol the option contract. A default value means the option is never null in the results. A mandatory option throws when accessed if it was not provided .CommandRunner.run()catches parse errors. It convertsArgParserExceptiontoUsageException, prints the message and usage to stderr, and exits with code 64 .- The
helpcommand is automatic.mycli helpandmycli help subcommandboth work without any code beyond the command registrations .
Remember: package:args is the standard library for Dart CLIs. The decision is between ArgParser for a simple script and CommandRunner for a tool with subcommands. The parser handles the conventions, the validation, and the help generation. The runner handles the routing and the error reporting. The UsageException is the contract for invalid input: catch it if you need a custom response, or let the runner print the usage and exit with code 64. The help text is not a separate document—it is derived from the same declarations that the parser uses, which is why it stays accurate as the CLI evolves.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!