From 13f9f008360eea14b17177ac1c5fd3af76161d70 Mon Sep 17 00:00:00 2001 From: Christer Date: Wed, 7 May 2025 17:25:00 +0200 Subject: [PATCH 1/9] feat: Use platform env vars by default in ConfigParser --- lib/src/config/config_parser.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/src/config/config_parser.dart b/lib/src/config/config_parser.dart index c1fd048..7a1f24e 100644 --- a/lib/src/config/config_parser.dart +++ b/lib/src/config/config_parser.dart @@ -1,3 +1,5 @@ +import 'dart:io' show Platform; + import 'package:args/args.dart'; import 'package:args/command_runner.dart' show UsageException; @@ -213,6 +215,20 @@ class ConfigParser implements ArgParser { @override void addSeparator(final String text) => parser.addSeparator(text); + /// Parses [args], a list of command-line arguments, matches them against the + /// flags and options defined by this parser, and returns the result. + /// + /// Supports additional configuration sources: + /// - [env] is a map of environment variable names to their values. + /// - [configBroker] is a configuration broker that can provide values for + /// options, for example from a configuration file. + /// - [presetValues] is a map of options with predetermined values. + /// + /// If [env] is not provided it defaults to the current platform's environment + /// variables. + /// + /// Throws UsageException with appropriate error messages if any validation + /// errors occur. @override ConfigResults parse( final Iterable args, { @@ -227,7 +243,7 @@ class ConfigParser implements ArgParser { final configuration = Configuration.resolve( options: _optionDefinitions, argResults: argResults, - env: env, + env: env ?? Platform.environment, configBroker: configBroker, presetValues: presetValues, ignoreUnexpectedPositionalArgs: true, From c5e24cfb34e46e5383ac9b0af77253618a4b9044 Mon Sep 17 00:00:00 2001 From: Christer Date: Thu, 8 May 2025 09:57:28 +0200 Subject: [PATCH 2/9] chore: Include UsageException in export --- lib/config.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/config.dart b/lib/config.dart index f66c639..438c8d3 100644 --- a/lib/config.dart +++ b/lib/config.dart @@ -1 +1,3 @@ export 'src/config/config.dart'; + +export 'package:args/command_runner.dart' show UsageException; From 7e741ee38690930e1a6d7bcac6a0c33c782c3654 Mon Sep 17 00:00:00 2001 From: Christer Date: Thu, 8 May 2025 09:57:53 +0200 Subject: [PATCH 3/9] docs: Simplified main example --- example/main.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/example/main.dart b/example/main.dart index 7343709..737510c 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,10 +1,6 @@ -import 'dart:async' show FutureOr; -import 'dart:io' show exit; - -import 'package:args/command_runner.dart'; import 'package:cli_tools/cli_tools.dart'; -void main(List args) async { +Future main(List args) async { var commandRunner = BetterCommandRunner( 'example', 'Example CLI command', @@ -19,7 +15,7 @@ void main(List args) async { await commandRunner.run(args); } on UsageException catch (e) { print(e); - exit(1); + return 1; } /// Simple example of using the [StdOutLogger] class. @@ -43,6 +39,7 @@ void main(List args) async { 'A progress message', () async => Future.delayed(const Duration(seconds: 3), () => true), ); + return 0; } /// Options are defineable as enums as well as regular lists. @@ -99,7 +96,7 @@ class TimeSeriesCommand extends BetterCommand { String get description => 'Generate a series of time stamps'; @override - FutureOr? runWithConfig(Configuration commandConfig) { + void runWithConfig(Configuration commandConfig) { var start = DateTime.now(); var until = commandConfig.value(TimeSeriesOption.until); From b2f448e857edffb95a66cf61e22bc4b53a7fe984 Mon Sep 17 00:00:00 2001 From: Christer Date: Thu, 8 May 2025 10:29:57 +0200 Subject: [PATCH 4/9] docs: Example code for config use cases --- example/config.yaml | 1 + example/config_file_example.dart | 87 ++++++++++++++++++++++++++++++ example/config_simple_example.dart | 49 +++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 example/config.yaml create mode 100644 example/config_file_example.dart create mode 100644 example/config_simple_example.dart diff --git a/example/config.yaml b/example/config.yaml new file mode 100644 index 0000000..b300d25 --- /dev/null +++ b/example/config.yaml @@ -0,0 +1 @@ +interval: 3s diff --git a/example/config_file_example.dart b/example/config_file_example.dart new file mode 100644 index 0000000..b4553cd --- /dev/null +++ b/example/config_file_example.dart @@ -0,0 +1,87 @@ +import 'dart:io' show File; + +import 'package:args/args.dart' show ArgResults; +import 'package:cli_tools/cli_tools.dart'; + +Future main(List args) async { + var commandRunner = BetterCommandRunner( + 'example', + 'Example CLI command', + ); + commandRunner.addCommand(TimeSeriesCommand()); + + try { + await commandRunner.run(args); + } on UsageException catch (e) { + print(e); + return 1; + } + return 0; +} + +enum TimeSeriesOption implements OptionDefinition { + configFile(FileOption( + argName: 'config', + helpText: 'The path to the config file', + fromDefault: _defaultConfigFilePath, + mode: PathExistMode.mustExist, + )), + interval(DurationOption( + argName: 'interval', + argAbbrev: 'i', + configKey: '/interval', // JSON pointer + helpText: 'The interval between the series elements', + min: Duration(seconds: 1), + max: Duration(days: 1), + )); + + const TimeSeriesOption(this.option); + + @override + final ConfigOptionBase option; +} + +File _defaultConfigFilePath() => File('example/config.yaml'); + +class TimeSeriesCommand extends BetterCommand { + TimeSeriesCommand({super.env}) : super(options: TimeSeriesOption.values); + + @override + String get name => 'series'; + + @override + String get description => 'Generate a series of time stamps'; + + @override + void runWithConfig(Configuration commandConfig) { + var interval = commandConfig.optionalValue(TimeSeriesOption.interval); + print('interval: $interval'); + } + + @override + Configuration resolveConfiguration(ArgResults? argResults) { + return Configuration.resolve( + options: options, + argResults: argResults, + env: envVariables, + configBroker: FileConfigBroker(), + ); + } +} + +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; + } +} diff --git a/example/config_simple_example.dart b/example/config_simple_example.dart new file mode 100644 index 0000000..af803bd --- /dev/null +++ b/example/config_simple_example.dart @@ -0,0 +1,49 @@ +import 'package:cli_tools/cli_tools.dart'; + +Future main(List args) async { + var commandRunner = BetterCommandRunner( + 'example', + 'Example CLI command', + ); + commandRunner.addCommand(TimeSeriesCommand()); + + try { + await commandRunner.run(args); + } on UsageException catch (e) { + print(e); + return 1; + } + return 0; +} + +enum TimeSeriesOption implements OptionDefinition { + interval(DurationOption( + argName: 'interval', + argAbbrev: 'i', + helpText: 'The interval between the series elements', + mandatory: true, + min: Duration(seconds: 1), + max: Duration(days: 1), + )); + + const TimeSeriesOption(this.option); + + @override + final ConfigOptionBase option; +} + +class TimeSeriesCommand extends BetterCommand { + TimeSeriesCommand() : super(options: TimeSeriesOption.values); + + @override + String get name => 'series'; + + @override + String get description => 'Generate a series of time stamps'; + + @override + void runWithConfig(Configuration commandConfig) { + var interval = commandConfig.value(TimeSeriesOption.interval); + print('interval: $interval'); + } +} From dc97dfdf20b9dbbaf9f46412f7f7a9a524e4878f Mon Sep 17 00:00:00 2001 From: Christer Date: Thu, 8 May 2025 10:30:42 +0200 Subject: [PATCH 5/9] docs: A README for the config library --- README_config.md | 305 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 README_config.md diff --git a/README_config.md b/README_config.md new file mode 100644 index 0000000..6b7a17f --- /dev/null +++ b/README_config.md @@ -0,0 +1,305 @@ +# Config + +The config library is a significant extension to the Dart args package. + +The main features are: + +- Typed arg options: `int`, `DateTime`, `Duration`, specific `Enums`. + - Automatic parsing and user-friendly error messages. + - Type-specific constraints, such as min/max for int and DateTime options. + - Multi-valued options are typed, e.g. `List`. + - Custom types can easily be added and combined with the existing ones. + +- 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. + +## Drop-in replacement + +The `ConfigParser` class is designed as a drop-in replacement for `ArgParser` +from the `args` package. Its purpose is to make it very 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. + +- **Compatibility**: The `ConfigParser` implements the same interface as + `ArgParser`, and returns a `ConfigResults` object that implements `ArgResults`. +- **Usage**: You can directly replace `ArgParser` in your existing code: + ```dart + 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 (see `BetterCommand` instaed) + - All validation is performed up-front with consistent error messages + - The parser supports additional configuration sources (environment variables, config files) + +- **Migration Path**: You can start using `ConfigParser` as a direct replacement +for `ArgParser` and gradually adopt its additional features as needed. + +## Usage + +_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_. + +```dart +import 'package:cli_tools/config.dart'; + +enum LogOption implements OptionDefinition { + 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 option; +} +``` + +The enum form enables constant initialization, typed `Configuration`, +and easy reference. + +```dart + Future runWithConfig( + final Configuration 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: + +```dart +abstract final class _ProjectOptions { + static const name = StringOption( + argName: 'name', + mandatory: true + ); + static const enable = FlagOption( + argName: 'enable', + defaultsTo: false, + ); + + static createOptions = [ + name, + enable, + ]; +} +... + + Future runWithConfig( + final Configuration commandConfig, + ) async { + final name = commandConfig.value(_ProjectOptions.name); + final enambe = 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. + +### Supported option types + +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` | 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` | `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. + +#### Common option features + +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 + +### Resolving a Configuration + +This is an overview of how a Configuration is resolved. + +```mermaid +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) +``` + +## Integration with commands + +In Dart, commands are often implemented using `Command` and `CommandRunner` +from the `args` package. + +In order 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](example/config_simple_example.dart). + +## Using configuration files + +In order to use configuration files as a source of option values, +a `ConfigurationBroker` needs to be provided when resolving the +`Configuration`. + +```dart + Configuration.resolve( + options: options, + argResults: argResults, + env: envVariables, + configBroker: FileConfigBroker(), + ); +``` + +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. + +```dart + interval(DurationOption( + argName: 'interval', + argAbbrev: 'i', + configKey: '/interval', // JSON pointer + )); +``` + +See the full example [example/config_file_example.dart](example/config_file_example.dart). + +### Multiple configuration sources + +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. +For example, a simple prefix and colon syntax can be used: + +```dart + dir(DirOption( + argName: 'dir', + configKey: 'local:/dir', + helpText: 'the local directory', + )), + host(StringOption( + argName: 'host', + configKey: 'remote:/host', + helpText: 'the remote host name', + )); +``` + +Advanced pattern matching is also suppored, enabling complex keys including +paths and URLs. + +For more information, see the `MultiDomainConfigBroker` source documentation. + +## Contributing to the Project + +We are happy to accept contributions. To contribute, please do the following: + +1. Fork the repository +2. Create a feature branch +3. Commit your changes +4. Push to the branch +5. Create a pull request +6. Discuss and modify the pull request as necessary +7. The pull request will be accepted and merged by the repository owner + +Tests are required to accept any pull requests. + + +## Developed by Serverpod + +This library is developed, used, and maintained by Serverpod. + +![Serverpod banner](https://github.com/serverpod/serverpod/raw/main/misc/images/github-header.webp) From fc53cd9f08c2a3624d9698f0b9f3e49b9394bb8c Mon Sep 17 00:00:00 2001 From: Christer Date: Thu, 8 May 2025 10:31:23 +0200 Subject: [PATCH 6/9] docs: Link to config README from package README --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba3c5d2..26ba7fc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,16 @@ # CLI Tools -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. +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. + +## Config library + +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. + +[Config README](README_config.md) ## Contributing to the Project From 49be6f243b4fdc5ff41d613ca414277f7ab62b9e Mon Sep 17 00:00:00 2001 From: Christer Date: Thu, 8 May 2025 10:43:53 +0200 Subject: [PATCH 7/9] chore: Bump version to 0.5.0 --- CHANGELOG.md | 35 +++++++++-------------------------- pubspec.yaml | 2 +- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82b2cb0..ed4621a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,35 +1,18 @@ # Changelog -## 0.5.0-beta.5 +## 0.5.0 -- refactor: Support ConfigurationBroker that depends on dynamic state -- refactor: Support bespoke Configuration subclass -- refactor: Simplified default usage of BetterCommand/Runner -- refactor: Subcommands inherit output behavior from their command runner unless overridden +- feat: Introduced the `config` library for unified args and env parsing +- docs: Added README and code examples for the `config` library +- fix: BREAKING. `BetterCommand` constructor changed to use `MessageOutput` class for clearer specification of logging functions. +- feat: BREAKING. Simplified default usage of `BetterCommand/Runner` +- feat: Subcommands inherit output behavior from their command runner unless overridden - refactor: The default terminal usage output behavior is now the same as the args package `Command` / `CommandRunner` -- docs: A full example of using `BetterCommandRunner`, `BetterCommand`, and `Configuration` options in the example folder - -## 0.5.0-beta.4 - -- fix: BREAKING. Clarified behavior of mutually exclusive option groups - -## 0.5.0-beta.3 - -- fix: Replaced yaml_codec dependency with yaml in order to support Dart 3.3 -- chore: Require Dart 3.3 - -## 0.5.0-beta.2 - - feat: New `Logger.log` method with dynamically specified log level -- fix: Downgrade `collection` dependency to 1.18 to be compatible with Dart 3.3 -- fix: Improved usage help composition -- fix: BetterCommandRunner API improvements - -## 0.5.0-beta.1 - -- feat: Introduced the Config library for unified args and env parsing - fix: Include user input prompt feature in library export -- fix: BREAKING. BetterCommand's constructor changed to use MessageOutput class for clearer specification of logging functions. +- fix: Downgrade `collection` dependency to 1.18 to be compatible with Dart 3.3 +- fix: Replaced `yaml_codec` dependency with `yaml` in order to support Dart 3.3 +- chore: Require Dart 3.3 ## 0.4.0 diff --git a/pubspec.yaml b/pubspec.yaml index bdba4f6..e4f74dc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: cli_tools -version: 0.5.0-beta.5 +version: 0.5.0 description: A collection of tools for building great command-line interfaces. repository: https://github.com/serverpod/cli_tools homepage: https://serverpod.dev From 320b0129e097daa2a453c6f1ec57f46e7f3752c6 Mon Sep 17 00:00:00 2001 From: Christer Date: Thu, 8 May 2025 16:12:50 +0200 Subject: [PATCH 8/9] docs: Fixes according to review feedback --- README_config.md | 111 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 12 deletions(-) diff --git a/README_config.md b/README_config.md index 6b7a17f..7701606 100644 --- a/README_config.md +++ b/README_config.md @@ -4,10 +4,10 @@ The config library is a significant extension to the Dart args package. The main features are: -- Typed arg options: `int`, `DateTime`, `Duration`, specific `Enums`. +- Typed arg options: `int`, `DateTime`, `Duration`, user-defined `Enums`. - Automatic parsing and user-friendly error messages. - - Type-specific constraints, such as min/max for int and DateTime options. - - Multi-valued options are typed, e.g. `List`. + - Type-specific constraints, such as min/max for all Comparable option types. + - Multivalued options are typed, e.g. `List`. - Custom types can easily be added and combined with the existing ones. - Equal support for positional arguments, with proper validation. @@ -38,12 +38,16 @@ These tools were developed for the Serverpod CLI but can be used in any Dart pro ## Drop-in replacement The `ConfigParser` class is designed as a drop-in replacement for `ArgParser` -from the `args` package. Its purpose is to make it very easy to transition +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`](lib/src/better_command_runner/better_command_runner.dart). + - **Compatibility**: The `ConfigParser` implements the same interface as `ArgParser`, and returns a `ConfigResults` object that implements `ArgResults`. - **Usage**: You can directly replace `ArgParser` in your existing code: @@ -56,7 +60,8 @@ enabling direct use of the new features. ``` - **Key Differences**: - - The `addCommand()` method is not supported (see `BetterCommand` instaed) + - The `addCommand()` method is not supported + (see [`BetterCommandRunner`](lib/src/better_command_runner/better_command_runner.dart) instead) - All validation is performed up-front with consistent error messages - The parser supports additional configuration sources (environment variables, config files) @@ -157,6 +162,64 @@ abstract final class _ProjectOptions { > 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. +### Main classes + +An instance of the [OptionDefinition](lib/src/config/configuration.dart) 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](lib/src/config/configuration.dart) class +holds a configuration, i.e. the values for a set of option definitions. + +### Resolution order + +The configuration library resolves each option value in a specific order, with earlier sources taking precedence over later ones. + +1. **Command-line arguments** + - Named arguments (e.g., `--verbose` or `-v`) have top precedence + - Positional arguments are resolved after named + - Specified using `argName`, `argAbbrev`, and `argPos` + +2. **Environment variables** + - Environment variables have second precedence after CLI arguments + - Variable name is specified using `envName` + +3. **Configuration files** + - Values from configuration files (e.g. YAML/JSON) + - Lookup key is specified using `configKey` + +4. **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` + +5. **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 + +### Resolution sources + +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. + ### Supported option types 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: @@ -221,7 +284,7 @@ sequenceDiagram In Dart, commands are often implemented using `Command` and `CommandRunner` from the `args` package. -In order to use the config library with these, they need to be subclassed +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. @@ -230,7 +293,7 @@ See the full example [example/config_simple_example.dart](example/config_simple_ ## Using configuration files -In order to use configuration files as a source of option values, +To use configuration files as a source of option values, a `ConfigurationBroker` needs to be provided when resolving the `Configuration`. @@ -244,7 +307,7 @@ a `ConfigurationBroker` needs to be provided when resolving the ``` 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 +specify the `configKey`. In this example, the configuration file is a JSON or YAML file and the JSON pointer syntax is used. ```dart @@ -262,7 +325,11 @@ See the full example [example/config_file_example.dart](example/config_file_exam 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. +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: ```dart @@ -278,10 +345,30 @@ For example, a simple prefix and colon syntax can be used: )); ``` -Advanced pattern matching is also suppored, enabling complex keys including -paths and URLs. +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. + +#### RegExp domains + +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 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. +For more information, see the +[`MultiDomainConfigBroker`](lib/src/config/multi_config_source.dart) +source documentation. ## Contributing to the Project From 3206791505938a02933d9653375653446850a9bc Mon Sep 17 00:00:00 2001 From: Christer Date: Thu, 8 May 2025 17:50:13 +0200 Subject: [PATCH 9/9] docs: Fixes according to review feedback --- README_config.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/README_config.md b/README_config.md index 7701606..974ee90 100644 --- a/README_config.md +++ b/README_config.md @@ -6,9 +6,10 @@ The main features are: - Typed arg options: `int`, `DateTime`, `Duration`, user-defined `Enums`. - Automatic parsing and user-friendly error messages. - - Type-specific constraints, such as min/max for all Comparable option types. + - Type-specific constraints, such as min/max for all `Comparable` option types. - Multivalued options are typed, e.g. `List`. - Custom types can easily be added and combined with the existing ones. + - See [Supported option types](#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. @@ -17,7 +18,7 @@ The main features are: - 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. +- Options can be fetched from [configuration files](#using-configuration-files) as well. - YAML/JSON configuration file support. - Options can have custom value-providing callbacks. @@ -154,7 +155,7 @@ abstract final class _ProjectOptions { final Configuration commandConfig, ) async { final name = commandConfig.value(_ProjectOptions.name); - final enambe = commandConfig.value(_ProjectOptions.enable); + final enable = commandConfig.value(_ProjectOptions.enable); ... } ``` @@ -306,6 +307,26 @@ a `ConfigurationBroker` needs to be provided when resolving the ); ``` +A file-reading ConfigurationBroker can be implemented like this: + +```dart +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. @@ -318,7 +339,7 @@ YAML file and the JSON pointer syntax is used. )); ``` -See the full example [example/config_file_example.dart](example/config_file_example.dart). +See the full example in [example/config_file_example.dart](example/config_file_example.dart). ### Multiple configuration sources