Creating CLI Applications in Dart

You can interact with computers in two primary ways: through a Graphical User Interface (GUI) or a Command-Line Interface (CLI). While GUIs are more user-friendly for general tasks, CLIs are ideal for scripting, developer tools, and system utilities.
Why Dart?
Dart is a great language for CLI because it's fast and produces cross-platform, self-contained executables. You donβt need to install extra dependencies, virtual machines, or libraries to run a Dart programβwhether on macOS, Windows, Linux, or Mobile.
Parsing Arguments
CLI applications use arguments to determine what action to perform. For example:
$ flutter analyze
$ flutter format
In the first example, the Flutter CLI runs the analyzer. In the second, it formats your code. So, a CLI should be able to interpret passed arguments properly.
Dart provides an args
package to parse command-line arguments. This package revolves around the ArgParser
class, which lets you define and process options and flags.
- Flags: Boolean values defined with
addFlag()
. They don't require a value (e.g.,--verbose
). - Options: Key-value pairs defined with
addOption()
(e.g.,--name Michael
).
Hereβs an example that uses both:
import 'package:args/args.dart';
void main(List<String> args) {
final parser = ArgParser()
..addOption('name', abbr: 'n', help: 'The name of the person to greet.')
..addFlag('spanish', abbr: 's', help: 'Greet in Spanish.');
final results = parser.parse(args);
final name = results['name'];
final isSpanish = results['spanish'] as bool;
if (name == null) {
print('Please provide a name using the --name option.');
return;
}
final greeting = isSpanish ? 'Hola, $name!' : 'Hello, $name!';
print(greeting);
}
Command Line App that greets userwo major ways to interact with computers are
Example usage:
$ dart run bin/hello.dart -n Michael
Hello, Michael!
$ dart run bin/hello.dart -n Michael -s
Hola, Michael!
π For small tools, a single ArgParser
is often sufficient. For complex tools with multiple subcommands, use CommandRunner
as described in the next chapter.
Defining Commands
For more complex applications, you can define commands using the Command
class from the args
package.
The following example defines a hello
command that greets a user. The command accepts two parameters: a --name
option and a --spanish
flag.
import 'package:args/command_runner.dart';
class HelloCommand extends Command<void> {
HelloCommand() {
argParser
..addOption(
'name',
abbr: 'n',
help: 'The name of the person to greet.',
)
..addFlag(
'spanish',
abbr: 's',
help: 'Greet in Spanish.',
);
}
@override
String get description => 'A command that greets the user.';
@override
String get name => 'hello';
@override
void run() {
final name = argResults?['name'] ?? 'World';
final isSpanish = argResults?['spanish'] as bool;
final greeting = isSpanish ? 'Β‘Hola, $name!' : 'Hello, $name!';
print(greeting);
}
}
Each command includes an argParser
, a description
that appears in help output, and a name
used to invoke the command from the terminal. This provides several benefits:
- Keeps command-specific logic modular and maintainable.
- Automatically generates usage and help documentation (e.g., when you run
--help
).
This is how the usage description looks like:
$ dart run bin/main.dart
A simple command-line tool that greets the user.
Usage: hello <command> [arguments]
Global options:
-h, --help Print this usage information.
Available commands:
hello A command that greets the user.
Run "hello help <command>" for more information about a command.
Creating a Command Runner
To make your commands executable, use the CommandRunner
class. It acts as the entry point for managing and dispatching commands.
class HelloCommandRunner extends CommandRunner<void> {
HelloCommandRunner()
: super(
'hello',
'A simple command-line tool that greets the user.',
) {
addCommand(HelloCommand());
}
}
CommandRunner
takes a command name and description.- Use
addCommand()
to register each command with the runner.
CommandRunner
per entry point. For example, HelloCommandRunner
might be used in bin/hello.dart
, while FooCommandRunner
handles bin/foo.dart
.Now, you can invoke the runner from your main function:
void main(List<String> args) {
final commandRunner = HelloCommandRunner();
commandRunner.run(args);
}
This structure allows you to support multiple commands, each with its own logic, while keeping the overall application organized and extensible.
Structuring CLI Application
This section explains how to structure the code for your CLI application. A typical structure looks like this:
.
βββ bin
β βββ lib.dart # From here you invoke command runner
βββ lib
β βββ lib.dart
β βββ src
β βββ cli
β β βββ commands
β β β βββ hello_command.dart
β β β βββ foo_command.dart
β β βββ runner.dart # This is your runner
β βββ core # The core logic of your library
β βββ hello
β βββ logic.dart
βββ pubspec.yaml
bin/
directory is the place for entrypoints. You can add as many files as needed, depending on how many commands or entry scripts your application requires.
cli/
directory is a good place for runners, commands, and any utilities specific to the CLI.
The core logic of your library should go in the core/
directory. For example, if library includes an SVG parser, implement and export all related functionality from core/
.
Alternatively, instead of combining everything in a single package, you can create a separate package for your CLI. This approach is especially useful for larger projects or when the CLI is optional for users of your library.
CLI Utilities & Techniques
To enhance your command-line tools, you can use the official cli_util
package. It provides convenient logging with ANSI support, as well as utilities for locating the Dart SDK and determining configuration directories.
The cli_util
package helps you display progress indicators, log messages at different verbosity levels, and format output in a user-friendly way.
Here's a simple example:
import 'package:cli_util/cli_logging.dart';
void main(List<String> args) async {
final verbose = args.contains('-v');
final logger = verbose ? Logger.verbose() : Logger.standard();
logger.stdout('Hello world!');
logger.trace('message 1');
await Future<void>.delayed(const Duration(milliseconds: 200));
logger.trace('message 2');
logger.trace('message 3');
final progress = logger.progress('doing some work');
await Future<void>.delayed(const Duration(seconds: 2));
progress.finish(showTiming: true);
logger.stdout('All ${logger.ansi.emphasized('done')}.');
}
In this example:
- The logger prints standard and trace-level messages, depending on whether the
-v
flag is passed. - It shows a progress spinner while simulating a task.
- The final message uses ANSI styling to emphasize the word done.
This is what it looks like:
Conclusion
Thanks for reading! Here are the key takeaways:
- Use the
args
package to parse command-line arguments. For structured commands and built-in--help
support, useCommand
andCommandRunner
. - To add a CLI to your package, place CLI sources in
lib/src/cli
and create an entry point inbin/<name>.dart
. For larger projects or reusable CLIs, consider creating a separate package. - Use the
cli_util
package for logging, ANSI styling, progress indicators, locating the Dart SDK, and accessing the configuration directory.