🚧 This package is in early preview and is subject to API changes.
Build CLI applications from plain Dart classes and functions.
Before | After |
---|---|
The ability to quickly whip up a command line script or application is a powerful skill for a developer to have. Compared to the Dart language itself, which offers a tremendous developer experience when building all kinds of apps, cli-based libraries like package:args
leave something to be desired when it comes to easily building and maintaining application logic.
cli-gen
aims to offer quality-of-life improvements for building and maintaining CLI apps, by allowing you to generate command line APIs from plain Dart functions. It achives this by providing the following features:
- parsing String arguments to any Dart type
--help
text inference from Method declarations, doc comments, and default values- proper error handling, without printing stack traces to the console
cli-gen
was designed to make writing CLI applications as intuitive as writing any other piece of Dart code.
Add cli_annotations
to your pubspec
dependencies and cli_gen
and build_runner
as dev dependencies.
name: dart_git
description: An implementation of the git CLI in Dart.
environment:
sdk: ^3.0.0
dependencies:
cli_annotations: ^0.1.0-dev.4
dev_dependencies:
build_runner: ^2.4.8
cli_gen: ^0.1.0-dev.4
# define an executable name (optional)
executables:
dart_git: main
You can optionally define an executable name and activate it using pub global activate.
Once dependencies are installed, start the build_runner
to begin code generation.
$ dart run build_runner watch -d
Create a CommandRunner
by annotating a class with @cliRunner
and extending the generated superclass (using the usual _$
prefix).
The generated code contains a single CommandRunner.run()
method, which is the entry point for your CLI application, to be called from the main
function.
@cliRunner
class GitRunner extends _$GitRunner {
// ...
}
Inside the CommandRunner
class, create a Command
by creating a method and annotating it with @cliCommand
. See the Features section for more information on the supported types and features.
@cliRunner
class GitRunner extends _$GitRunner {
@cliCommand
Future<void> merge({
required String branch,
MergeStrategy strategy = MergeStrategy.ort,
bool? commit,
}) async {
// ... `git merge` logic ...
}
}
You can create as many commands inside the CommandRunner
class as you'd like:
@cliRunner
class GitRunner extends _$GitRunner {
@cliCommand
Future<void> merge({
required String branch,
MergeStrategy strategy = MergeStrategy.ort,
bool? commit,
}) async { /* ... */ }
@cliCommand
Future<void> stashPush() async { /* ... */ }
@cliCommand
Future<void> stashPop() async { /* ... */ }
// ...
}
As your application grows, you may want to separate your commands into their own groups.
To do so, create a Subcommand
class by annotating the class with @cliSubcommand
and extending the generated superclass.
@cliSubcommand
class StashSubcommand extends _$StashSubcommand {
@cliCommand
Future<void> push() async { /* ... */ }
@cliCommand
Future<void> pop() async { /* ... */ }
}
Subcommands can then be connected to the main CommandRunner
class, or to another Subcommand
class, by using the @cliMount
annotation.
@cliRunner
class GitRunner extends _$GitRunner {
@cliMount
StashSubcommand get stash => StashSubcommand();
}
Finally, create a main
function that calls the run
method on your CommandRunner
.
void main(List<String> arguments) async {
final runner = GitRunner();
await runner.run(arguments);
}
Your application is ready to go! 🎉
Run a command to test out the generated help text and see the argument parsing in action.
# activate the executable (if executable is defined in `pubspec.yaml`)
$ dart pub global activate . --source=path
# run the application
$ dart_git merge --help
You should see the following output:
$ dart_git merge --help
Join two or more development histories together.
Usage: git-runner merge [arguments]
--branch (mandatory)
--commit
--options
Run "git-runner help" to see global options.
cli-gen
automatically parses incoming string arguments into the correct type, and automatically informs your user if they've entered an invalid value.
You can define your command methods with any Dart primitive type or enum, and cli-gen
will automatically parse the incoming string arguments into the correct type.
@cliCommand
Future<void> myCustomCommand({
// Use any supported type, nullable or not
Uri? outputFile,
// Enums can automatically be parsed from strings
MergeStrategy? strategy,
// Custom types can also be used, but require passing your own
// String parser to the `@Option` annotation
@Option(parser: Email.fromString) Email? email,
}) async {
// ...
}
NOTE: Types that can be automatically parsed are: String, int, double, bool, num, Uri, BigInt, and DateTime
The Collection types List
, Set
, and Iterable
are also supported, and can be used in combination with any of the above supported types.
@cliCommand
Future<void> myCustomCommand({
List<Uri>? inputFiles,
}) async {
// ...
}
CLI applications typically provide a --help
option that displays a list of available commands and descriptions of their parameters, to help users understand how they can interact with the application.
Rather than manually manitaining these details yourself, cli-gen
automatically generates help text from your annotated methods, based on the method and parameter names, doc comments, default values, and whether each parameter is required or not.
@cliCommand
Future<void> myCustomCommand({
// Required parameters will be shown to the user as `mandatory`
required String requiredParam,
// Optional and/or nullable parameters are not `mandatory`
int? optionalParam,
// Default values are also shown in the help text
String defaultPath = '~/',
// Use doc comments (i.e. 3 slashes) to display a description of the parameter
/// A parameter that uses a doc comment
String someDocumentedParameter,
}) async {
// ...
}
The above Dart function will generate a cli command with the following help text:
$ my-custom-command --help
Save your local modifications to a new stash.
Usage: git stash my-custom-command [arguments]
-h, --help Print this usage information.
--required-param (mandatory)
--optional-param
--default-path (defaults to "~/")
--some-documented-parameter A parameter that uses a doc comment
Run "git help" to see global options.
You can also generate descriptions for your commands and the entire application by using doc comments on the annotated classes and methods.
/// A dart implementation of the git CLI.
@cliRunner
class GitRunner extends _$GitRunner {
/// Merge two or more development histories together.
@cliCommand
Future<void> commit() async {
// ...
}
}
will generate:
$ git-runner --help
A dart implementation of the git CLI.
Usage: git-runner [arguments]
-h, --help Print this usage information.
Available commands:
commit Merge two or more development histories together.
Run "git help" to see global options.
Enums are unique in that they inherently define a finite set of allowable values. cli-gen
can use that information to generate a list of allowed values in the help text.
enum Values { a, b, c }
Using the above Values
enum as a parameter to a cliCommand
will generate the following help text:
--values (allowed: a, b, c)
If you ever need to override the default allowed values, you can do so by providing a list of values to the allowed
parameter of the @Option
annotation.
cli_gen
can handle trailing positional parameters.
For example, you may have a command that requires a path argument. Rather than define a named flag like git push --remote="origin"
, you can instead allow users to pass the argument using git push origin
.
Positional parameters can be defined using Dart's positional parameter syntax:
@cliCommand
Future<void> push(
String remote, // positional parameter
String? branch, { // positional parameter
bool force = false, // named parameter
}) async {
/* ... */
}
cli-gen
translates Dart class, method and parameter names to kebab-case, which is the convention for CLI commands and flags.
For example, a method named stashChanges
will be translated to stash-changes
, and a parameter named outputFile
will be translated to --output-file
.
To override the default behavior, simply provide a name
to the respective annotation (supported for @cliCommand
, @cliSubcommand
, @cliRunner
, and @Option
).
cli-gen
generates code that uses package:args
classes and utilities to manage command hierarchies and help text generation. The annotations included with this package are a 1:1 mapping to similar or identical concepts included with package:args
, for example:
@cliRunner
- generates a
CommandRunner
class - has a
run
method that should be passed args and run from yourmain
function - mounts any nested commands as subcommands via
CommandRunner.addCommand
- generates a
@cliCommand
- generates a
Command
class - overrides the
run
method with a call to your method or function
- generates a
@cliSubcommand
- generates a
Command
class - adds all nested commands as subcommands via
Command.addSubcommand
- generates a
Examples of generated code can be found in the example
project, within their respective .g.dart
files.
TODO: a little blurb about the project goals (incl. what cli-gen
is and what it is not).
Several projects were researched as references of CLI ergonomics and macro libraries, including:
- clap - a declarative CLI parser for Rust
cli-gen
is released under the MIT License. See LICENSE for details.