This package contains tools for building great command line interfaces. These tools were developed for the Serverpod CLI but can be used in any Dart project.
The config library is a significant extension to the args package and enables typed options, environment variables and configuration files as input, and better error reporting.
We are happy to accept contributions. To contribute, please do the following:
- Fork the repository
- Create a feature branch
- Commit your changes
- Push to the branch
- Create a pull request
- Discuss and modify the pull request as necessary
- The pull request will be accepted and merged by the repository owner
Tests are required to accept any pull requests.
The config library is a significant extension to the Dart args package.
The main features are:
-
Typed arg options:
int
,DateTime
,Duration
, user-definedEnums
.- Automatic parsing and user-friendly error messages.
- Type-specific constraints, such as min/max for all
Comparable
option types. - Multivalued options are typed, e.g.
List<MyEnum>
. - Custom types can easily be added and combined with the existing ones.
- See Supported option types for the complete list.
-
Equal support for positional arguments, with proper validation.
- Arguments can be both positional and named, making the --name optional.
-
Equal support for environment variables.
- Options can be specified both via arguments and environment variables.
- Environment variables have the same typed values support as args.
-
Options can be fetched from configuration files as well.
- YAML/JSON configuration file support.
-
Options can have custom value-providing callbacks.
-
Named option groups are supported.
- A group can specify mutually exclusive options.
- A group can be mandatory in that at least one of its options is set.
-
Tracability - the information on an option's value source is retained.
-
The error handling is consistent, in contrast to the args package.
- Fail-fast, all validation is performed up-front.
- All errors are collected, avoiding the poor UX of fix-one-and-then-get-the-next-error.
- Well-defined exception behavior.
These tools were developed for the Serverpod CLI but can be used in any Dart project.
The ConfigParser
class is designed as a drop-in replacement for ArgParser
from the args
package. Its purpose is to make it easy to transition
to the config library - just replace the name ArgParser
with ConfigParser
.
It maintains almost complete compatibility with the original package while enabling direct use of the new features.
It achieves complete compatibility with the original package with the exception
of addCommand(), which you can replace with
BetterCommandRunner
.
-
Compatibility: The
ConfigParser
implements the same interface asArgParser
, and returns aConfigResults
object that implementsArgResults
. -
Usage: You can directly replace
ArgParser
in your existing code:final parser = ConfigParser(); // instead of ArgParser() parser.addFlag('verbose', abbr: 'v'); parser.addOption('port', defaultsTo: '8080'); parser.addOption('host', envName: 'HOST'); // using env feature final results = parser.parse(['--verbose', '--port', '3000']);
-
Key Differences:
- The
addCommand()
method is not supported (seeBetterCommandRunner
instead) - All validation is performed up-front with consistent error messages
- The parser supports additional configuration sources (environment variables, config files)
- The
-
Migration Path: You can start using
ConfigParser
as a direct replacement forArgParser
and gradually adopt its additional features as needed.
For transitioning existing code from ArgParser, see the drop-in replacement section above.
This library emphasizes a declarative style of defining options. Here is a real-life example, from a show logs command, that shows how to create a set of options for a particular command as an enum.
import 'package:cli_tools/config.dart';
enum LogOption<V> implements OptionDefinition<V> {
limit(IntOption(
argName: 'limit',
helpText: 'The maximum number of log records to fetch.',
defaultsTo: 50,
min: 0,
)),
utc(FlagOption(
argName: 'utc',
argAbbrev: 'u',
helpText: 'Display timestamps in UTC timezone instead of local.',
defaultsTo: false,
envName: 'DISPLAY_UTC',
)),
recent(DurationOption(
argName: 'recent',
argAbbrev: 'r',
argPos: 0,
helpText:
'Fetch records from the recent period. '
'Can also be specified as the first argument.',
min: Duration.zero,
)),
before(DateTimeOption(
argName: 'before',
helpText: 'Fetch records from before this timestamp.',
));
const LogOption(this.option);
@override
final ConfigOptionBase<V> option;
}
The enum form enables constant initialization, typed Configuration<LogType>
,
and easy reference.
Future<void> runWithConfig(
final Configuration<LogOption> commandConfig,
) async {
final limit = commandConfig.value(LogOption.limit);
final inUtc = commandConfig.value(LogOption.utc);
final recentOpt = commandConfig.optionalValue(LogOption.recent);
final beforeOpt = commandConfig.optionalValue(LogOption.before);
...
}
It is also possible to create them as a List:
abstract final class _ProjectOptions {
static const name = StringOption(
argName: 'name',
mandatory: true
);
static const enable = FlagOption(
argName: 'enable',
defaultsTo: false,
);
static createOptions = <OptionDefinition>[
name,
enable,
];
}
...
Future<void> runWithConfig(
final Configuration commandConfig,
) async {
final name = commandConfig.value(_ProjectOptions.name);
final enable = commandConfig.value(_ProjectOptions.enable);
...
}
Note that options that are mandatory or have a default value have a guaranteed value. They return a non-nullable type, while "optional" options return a nullable type.
An instance of the OptionDefinition class
defines an option.
This is an abstract class and implemented by option Enum types
as well as the base option class ConfigOptionBase
.
The latter is typically
not used directly, instead the typed subclasses are used such as StringOption
or IntOption
.
An instance of the Configuration class holds a configuration, i.e. the values for a set of option definitions.
The configuration library resolves each option value in a specific order, with earlier sources taking precedence over later ones.
-
Command-line arguments
- Named arguments (e.g.,
--verbose
or-v
) have top precedence - Positional arguments are resolved after named
- Specified using
argName
,argAbbrev
, andargPos
- Named arguments (e.g.,
-
Environment variables
- Environment variables have second precedence after CLI arguments
- Variable name is specified using
envName
-
Configuration files
- Values from configuration files (e.g. YAML/JSON)
- Lookup key is specified using
configKey
-
Custom value providers
- Values from custom callbacks
- Callbacks are allowed to depend on other option values (option definition order is significant in this case)
- Callback is specified using
fromCustom
-
Default values
- A default value guarantees that an option has a value
- Const values are specified using
defaultsTo
- Non-const values are specifed with a callback using
fromDefault
This order ensures that:
- Command-line arguments always take precedence, allowing users to override any other settings
- Environment variables can be used for values used across multiple command invocations, or to override other configuration sources
- Configuration files provide persistent settings
- Custom providers enable complex logic and integration with external systems
- Default values serve as a fallback when no other value is specified
Only the value sources provided to the Configuration.resolve
constructor are
actually included. This means that any precedence tiers can be skipped,
regardless of what the option definitions say.
This enables flexible inclusion of sources depending on context and helps constructing specific test cases.
The library provides a rich set of typed options out of the box. All option types support the common arguments like argName
, helpText
, mandatory
, etc. Below are the additional type-specific arguments:
Value Type | Option Class | Additional Settings | Description |
---|---|---|---|
String | StringOption |
None | String values |
Boolean | FlagOption |
negatable |
Whether the flag can be negated |
Integer | IntOption |
min max |
Minimum allowed value Maximum allowed value |
DateTime | DateTimeOption |
min max |
Minimum allowed date/time Maximum allowed date/time |
Duration | DurationOption |
min max |
Minimum allowed duration Maximum allowed duration |
Any Enum | EnumOption<E> |
None | Typed enum values |
File | FileOption |
mode |
Whether the file must exist, must not exist, or may exist |
Directory | DirOption |
mode |
Whether the directory must exist, must not exist, or may exist |
String List | MultiStringOption |
splitCommas |
Whether to split input on commas |
Any List | MultiOption<T> |
multiParser |
Parser for the element type |
It is easy to add custom option types, and to reuse the parsing code from existing option types. Just copy code from existing options and modify as needed.
All option types support:
- Command-line arguments (full name, abbreviated name, and positional)
- Environment variables
- Configuration file values
- Custom value-providing callback
- Default values
- Allowed values list validation
- Aliases
- Custom validation
- Help text and value descriptions
- Mandatory
- Hidden
- Option groups
This is an overview of how a Configuration is resolved.
sequenceDiagram
participant User Code
participant OptionDefinitions
participant Configuration
participant ArgParser
participant EnvVariables
participant ConfigSourceProvider
participant ConfigurationSource
User Code->>OptionDefinitions: Define options
User Code->>Configuration: Resolve with context (args, env, config, etc)
Configuration->>ArgParser: Parse command-line args
Configuration->>EnvVariables: Lookup environment variables
Configuration->>ConfigSourceProvider: (if needed) getConfigSource(cfg)
ConfigSourceProvider->>ConfigurationSource: Provide config data
ConfigurationSource-->>Configuration: Value for key
Configuration-->>User Code: Typed Configuration (with errors if any)
In Dart, commands are often implemented using Command
and CommandRunner
from the args
package.
To use the config library with these, they need to be subclassed
to modify the use of ArgParser
and introduce Configuration
. This has
already been done for you, with the BetterCommand
and BetterCommandRunner
classes in the better_command_runner
library in this package.
See the full example example/config_simple_example.dart.
To use configuration files as a source of option values,
a ConfigurationBroker
needs to be provided when resolving the
Configuration
.
Configuration.resolve(
options: options,
argResults: argResults,
env: envVariables,
configBroker: FileConfigBroker(),
);
A file-reading ConfigurationBroker can be implemented like this:
class FileConfigBroker implements ConfigurationBroker {
ConfigurationSource? _configSource;
FileConfigBroker();
@override
String? valueOrNull(final String key, final Configuration cfg) {
// By lazy-loading the config, the file path can depend on another option
_configSource ??= ConfigurationParser.fromFile(
cfg.value(TimeSeriesOption.configFile).path,
);
final value = _configSource?.valueOrNull(key);
return value is String ? value : null;
}
}
To reference a value from the configuration broker in an option definition,
specify the configKey
. In this example, the configuration file is a JSON or
YAML file and the JSON pointer syntax is used.
interval(DurationOption(
argName: 'interval',
argAbbrev: 'i',
configKey: '/interval', // JSON pointer
));
See the full example in example/config_file_example.dart.
By using the MultiDomainConfigBroker
, configuration sources
from multiple providers can be used, called configuration domains.
They are distinguished by the format used in the configKey, which needs to specify a so-called qualified key - qualifying the key with the domain it is found in.
Domains are matched using Pattern
, e.g. string prefixes or regular expressions.
For example, a simple prefix and colon syntax can be used:
dir(DirOption(
argName: 'dir',
configKey: 'local:/dir',
helpText: 'the local directory',
)),
host(StringOption(
argName: 'host',
configKey: 'remote:/host',
helpText: 'the remote host name',
));
The first domain that matches the qualified key is used to retrieve the value. This means that the order of the domains is significant if the matching patterns overlap.
Advanced pattern matching is supported via RegExp
, enabling complex qualifiers
including paths and URLs, such that the key pattern qualifies the domain.
When using regular expressions to identify the domain, the value key is derived from the qualified key depending on the capturing groups in the regex.
-
If the regex has no capturing groups:
- If the regex matches a shorter string than the qualified key, the value key is the remainder after the match.
This makes prefix matching simple. - If the regex matches the entire qualified key, the value key is the entire qualified key.
This can be used for specific syntaxes like URLs.
- If the regex matches a shorter string than the qualified key, the value key is the remainder after the match.
-
If the regex has one or more capturing groups:
The value key is the string captured by the first group.
For more information, see the
MultiDomainConfigBroker
source documentation.