cli_gen 0.1.0-dev.4
cli_gen: ^0.1.0-dev.4 copied to clipboard
Build cli applications using code generation and macros.
cli-gen #
🚧 This package is in early preview and is subject to API changes.
Build CLI applications from plain Dart classes and functions.
| Before | After |
|---|---|
Table of Contents #
Motivation #
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
--helptext 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.
Quick Start #
Installation #
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.1
dev_dependencies:
build_runner: ^2.4.8
cli_gen: ^0.1.0-dev.1
# define an executable name (optional)
executables:
dart_git:
path: main # file name of `main()` in bin/ directory
You can optionally define an executable name and activate it using pub global activate.
Run Build Runner #
Once dependencies are installed, start the build_runner to begin code generation.
$ dart run build_runner watch -d
Define the Command Runner #
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 {
// ...
}
Define a Command #
Create a Command by simply creating a method on the class; any type can be used as a parameter.
@cliRunner
class GitRunner extends _$GitRunner {
@cliCommand
Future<void> merge({
required String branch,
MergeStrategy strategy = MergeStrategy.ort,
bool? commit,
}) async {
// ... `git merge` logic ...
}
}
Define a Subcommand #
As your application grows, you may want to separate your commands into their own groups.
You can create a Subcommand by annotating a class with @cliSubcommand and extending the generated superclass.
// Create your subcommand
@cliSubcommand
class StashSubcommand extends _$StashSubcommand {
@cliCommand
Future<void> push() async { /* ... */ }
@cliCommand
Future<void> pop() async { /* ... */ }
}
// Then mount it to your `CommandRunner` or a parent `Subcommand`
@cliRunner
class GitRunner extends _$GitRunner {
@mount
Command get stash => StashSubcommand();
}
Run the Application #
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.
Features #
Type-Safe Argument Parsing #
cli-gen automatically parses incoming string arguments into the correct type, and automatically informs your user if they've entered an invalid value.
Supported Types
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
Collection Types
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 {
// ...
}
Help Text Inference (--help) #
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.
Parameter Help Text
@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.
Command Descriptions
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 and Allowed Values
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.
Name Formatting #
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).
Named and Positional Parameters #
cli_gen can handle parameters no matter if they're positional or named; you're free to mix and match as you see fit.
@cliCommand
Future<void> myCustomCommand(
int? positionalParam, {
String? namedParam,
}) async {
// ...
}
Under the Hood #
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
CommandRunnerclass - has a
runmethod that should be passed args and run from yourmainfunction - mounts any nested commands as subcommands via
CommandRunner.addCommand
- generates a
@cliCommand- generates a
Commandclass - overrides the
runmethod with a call to your method or function
- generates a
@cliSubcommand- generates a
Commandclass - 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.
Design Goals #
TODO: a little blurb about the project goals (incl. what cli-gen is and what it is not).
Inspiration #
Several projects were researched as references of CLI ergonomics and macro libraries, including:
- clap - a declarative CLI parser for Rust
License #
cli-gen is released under the MIT License. See LICENSE for details.

