diff --git a/example/main.dart b/example/main.dart index d8c724d..7343709 100644 --- a/example/main.dart +++ b/example/main.dart @@ -3,7 +3,6 @@ import 'dart:io' show exit; import 'package:args/command_runner.dart'; import 'package:cli_tools/cli_tools.dart'; -import 'package:cli_tools/config.dart'; void main(List args) async { var commandRunner = BetterCommandRunner( diff --git a/lib/cli_tools.dart b/lib/cli_tools.dart index 778d5d1..e3e969b 100644 --- a/lib/cli_tools.dart +++ b/lib/cli_tools.dart @@ -1,7 +1,8 @@ export 'analytics.dart'; export 'better_command_runner.dart'; +export 'config.dart'; +export 'docs_generator.dart'; export 'local_storage_manager.dart'; export 'logger.dart'; export 'package_version.dart'; -export 'docs_generator.dart'; export 'prompts.dart'; diff --git a/lib/src/better_command_runner/better_command.dart b/lib/src/better_command_runner/better_command.dart index a4d71b6..b62bd22 100644 --- a/lib/src/better_command_runner/better_command.dart +++ b/lib/src/better_command_runner/better_command.dart @@ -1,20 +1,25 @@ import 'dart:async' show FutureOr; +import 'dart:io' show Platform; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; -import 'package:cli_tools/better_command_runner.dart'; import 'package:cli_tools/config.dart'; -import 'config_resolver.dart'; +import 'better_command_runner.dart'; +/// An extension of [Command] with additional features. +/// +/// The [BetterCommand] class uses the config library to provide +/// a more enhanced command line interface for running commands and handling +/// command line arguments, environment variables, and configuration. abstract class BetterCommand extends Command { static const _defaultMessageOutput = MessageOutput(usageLogger: print); final MessageOutput? _messageOutput; final ArgParser _argParser; - /// The configuration resolver for this command. - ConfigResolver? _configResolver; + /// The environment variables used for configuration resolution. + final Map envVariables; /// The option definitions for this command. final List options; @@ -24,10 +29,13 @@ abstract class BetterCommand extends Command { /// - [messageOutput] is an optional [MessageOutput] object used to pass specific log messages. /// - [wrapTextColumn] is the column width for wrapping text in the command line interface. /// - [options] is a list of options, empty by default. - /// - [configResolver] is an optional custom [ConfigResolver] implementation. + /// - [env] is an optional map of environment variables. If not set then + /// [Platform.environment] will be used. /// - /// [configResolver] and [messageOutput] are optional and will default to the - /// values of the command runner (if any). + /// [messageOutput] is optional and will default to the + /// value of the command runner (if any). + /// + /// ## Options /// /// To define a bespoke set of options, it is recommended to define /// a proper options enum. It can included any of the default options @@ -48,17 +56,14 @@ abstract class BetterCommand extends Command { /// final ConfigOptionBase option; /// } /// ``` - /// - /// If [configResolver] is not provided then [DefaultConfigResolver] will be used, - /// which uses the command line arguments and environment variables as input sources. BetterCommand({ MessageOutput? messageOutput = _defaultMessageOutput, int? wrapTextColumn, this.options = const [], - ConfigResolver? configResolver, + Map? env, }) : _messageOutput = messageOutput, _argParser = ArgParser(usageLineLength: wrapTextColumn), - _configResolver = configResolver { + envVariables = env ?? Platform.environment { prepareOptionsForParsing(options, argParser); } @@ -72,15 +77,9 @@ abstract class BetterCommand extends Command { return _messageOutput; } - ConfigResolver get configResolver { - if (runner case BetterCommandRunner runner) { - return runner.configResolver; - } - return _configResolver ??= DefaultConfigResolver(); - } - @override - BetterCommand? get parent => super.parent as BetterCommand?; + BetterCommand? get parent => + super.parent as BetterCommand?; @override BetterCommandRunner? get runner => @@ -98,39 +97,38 @@ abstract class BetterCommand extends Command { /// Resolves the configuration (args, env, etc) and runs the command /// subclass via [runWithConfig]. /// + /// If there are errors resolving the configuration, + /// a UsageException is thrown with appropriate error messages. + /// /// Subclasses should override [runWithConfig], /// unless they want to handle the configuration resolution themselves. @override FutureOr? run() { final config = resolveConfiguration(argResults); + if (config.errors.isNotEmpty) { + final buffer = StringBuffer(); + final errors = config.errors.map(formatConfigError); + buffer.writeAll(errors, '\n'); + usageException(buffer.toString()); + } + return runWithConfig(config); } - /// Resolves the configuration for this command - /// using the preset [ConfigResolver]. - /// If there are errors resolving the configuration, - /// a UsageException is thrown with appropriate error messages. + /// Resolves the configuration for this command. /// /// This method can be overridden to change the configuration resolution - /// or error handling behavior. + /// behavior. Configuration resolveConfiguration(ArgResults? argResults) { - final config = configResolver.resolveConfiguration( + return Configuration.resolve( options: options, argResults: argResults, + env: envVariables, ); - - if (config.errors.isNotEmpty) { - final buffer = StringBuffer(); - final errors = config.errors.map(formatConfigError); - buffer.writeAll(errors, '\n'); - usageException(buffer.toString()); - } - - return config; } - /// Runs this command with prepared configuration (options). + /// Runs this command with the resolved configuration (option values). /// Subclasses should override this method. FutureOr? runWithConfig(final Configuration commandConfig); } diff --git a/lib/src/better_command_runner/better_command_runner.dart b/lib/src/better_command_runner/better_command_runner.dart index d72bd71..e48065b 100644 --- a/lib/src/better_command_runner/better_command_runner.dart +++ b/lib/src/better_command_runner/better_command_runner.dart @@ -1,9 +1,9 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'package:args/args.dart'; import 'package:args/command_runner.dart'; import 'package:cli_tools/config.dart'; -import 'package:cli_tools/src/better_command_runner/config_resolver.dart'; /// A function type for executing code before running a command. typedef OnBeforeRunCommand = Future Function(BetterCommandRunner runner); @@ -45,14 +45,15 @@ typedef SetLogLevel = void Function({ /// A function type for tracking events. typedef OnAnalyticsEvent = void Function(String event); -/// A custom implementation of [CommandRunner] with additional features. +/// An extension of [CommandRunner] with additional features. /// /// This class extends the [CommandRunner] class from the `args` package and adds /// additional functionality such as logging, setting log levels, tracking events, /// and handling analytics. /// -/// The [BetterCommandRunner] class provides a more enhanced command line interface -/// for running commands and handling command line arguments. +/// The [BetterCommandRunner] class uses the config library to provide +/// a more enhanced command line interface for running commands and handling +/// command line arguments, environment variables, and configuration. class BetterCommandRunner extends CommandRunner { static const foo = [ @@ -69,11 +70,12 @@ class BetterCommandRunner final OnBeforeRunCommand? _onBeforeRunCommand; OnAnalyticsEvent? _onAnalyticsEvent; + /// The environment variables used for configuration resolution. + final Map envVariables; + /// The gloabl option definitions. late final List _globalOptions; - final ConfigResolver _configResolver; - Configuration? _globalConfiguration; /// The current global configuration. @@ -101,7 +103,8 @@ class BetterCommandRunner /// - [onAnalyticsEvent] function is used to track events. /// - [wrapTextColumn] is the column width for wrapping text in the command line interface. /// - [globalOptions] is an optional list of global options. - /// - [configResolver] is an optional custom [ConfigResolver] implementation. + /// - [env] is an optional map of environment variables. If not set then + /// [Platform.environment] will be used. /// /// ## Message Output /// @@ -138,9 +141,6 @@ class BetterCommandRunner /// final ConfigOptionBase option; /// } /// ``` - /// - /// If [configResolver] is not provided then [DefaultConfigResolver] will be used, - /// which uses the command line arguments and environment variables as input sources. BetterCommandRunner( super.executableName, super.description, { @@ -151,12 +151,12 @@ class BetterCommandRunner OnAnalyticsEvent? onAnalyticsEvent, int? wrapTextColumn, List? globalOptions, - ConfigResolver? configResolver, + Map? env, }) : _messageOutput = messageOutput, _setLogLevel = setLogLevel, _onBeforeRunCommand = onBeforeRunCommand, _onAnalyticsEvent = onAnalyticsEvent, - _configResolver = configResolver ?? DefaultConfigResolver(), + envVariables = env ?? Platform.environment, super( usageLineLength: wrapTextColumn, ) { @@ -180,10 +180,6 @@ class BetterCommandRunner /// It is also used for the commands unless they have their own. MessageOutput? get messageOutput => _messageOutput; - /// The configuration resolver used for the global configuration. - /// It is also used for the command configurations unless they have their own. - ConfigResolver get configResolver => _configResolver; - /// Adds a list of commands to the command runner. void addCommands(List> commands) { for (var command in commands) { @@ -208,6 +204,20 @@ class BetterCommandRunner return Future.sync(() { var argResults = parse(args); globalConfiguration = resolveConfiguration(argResults); + + try { + if (globalConfiguration.errors.isNotEmpty) { + final buffer = StringBuffer(); + final errors = globalConfiguration.errors.map(formatConfigError); + buffer.writeAll(errors, '\n'); + usageException(buffer.toString()); + } + } on UsageException catch (e) { + messageOutput?.logUsageException(e); + _onAnalyticsEvent?.call(BetterCommandRunnerAnalyticsEvents.invalid); + rethrow; + } + return runCommand(argResults); }); } @@ -293,28 +303,17 @@ class BetterCommandRunner } } - /// Resolves the global configuration for this command runner - /// using the preset [ConfigResolver]. - /// If there are errors resolving the configuration, - /// a UsageException is thrown with appropriate error messages. + /// Resolves the global configuration for this command runner. /// /// This method can be overridden to change the configuration resolution - /// or error handling behavior. + /// behavior. Configuration resolveConfiguration(ArgResults? argResults) { - final config = _configResolver.resolveConfiguration( + return Configuration.resolve( options: _globalOptions, argResults: argResults, + env: envVariables, ignoreUnexpectedPositionalArgs: true, ); - - if (config.errors.isNotEmpty) { - final buffer = StringBuffer(); - final errors = config.errors.map(formatConfigError); - buffer.writeAll(errors, '\n'); - usageException(buffer.toString()); - } - - return config; } static CommandRunnerLogLevel _determineLogLevel(Configuration config) { @@ -384,3 +383,15 @@ abstract class BetterCommandRunnerAnalyticsEvents { /// An enum for the command runner log levels. enum CommandRunnerLogLevel { quiet, verbose, normal } + +/// Formats a configuration error message. +String formatConfigError(final String error) { + if (error.isEmpty) return error; + final suffix = _isPunctuation(error.substring(error.length - 1)) ? '' : '.'; + return '${error[0].toUpperCase()}${error.substring(1)}$suffix'; +} + +/// Returns true if the character is a punctuation mark. +bool _isPunctuation(final String char) { + return RegExp(r'\p{P}', unicode: true).hasMatch(char); +} diff --git a/lib/src/better_command_runner/config_resolver.dart b/lib/src/better_command_runner/config_resolver.dart deleted file mode 100644 index 5ae57bb..0000000 --- a/lib/src/better_command_runner/config_resolver.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:io' show Platform; - -import 'package:args/args.dart' show ArgResults; -import 'package:cli_tools/config.dart' show Configuration, OptionDefinition; - -/// {@template config_resolver} -/// Resolves a configuration for the provided options and arguments. -/// Subclasses can add additional configuration sources. -/// {@endtemplate} -/// -/// The purpose of this class is to delegate the configuration resolution -/// in BetterCommandRunner and BetterCommand to a separate object -/// they can be composed with. -/// -/// If invoked from global command runner or a command that has -/// subcommands, set [ignoreUnexpectedPositionalArgs] to true. -abstract interface class ConfigResolver { - /// {@macro config_resolver} - Configuration resolveConfiguration({ - required Iterable options, - ArgResults? argResults, - bool ignoreUnexpectedPositionalArgs = false, - }); -} - -/// The default behavior is to invoke this using the [argResults] and -/// [Platform.environment] as input. -class DefaultConfigResolver - implements ConfigResolver { - final Map _env; - - DefaultConfigResolver({Map? env}) - : _env = env ?? Platform.environment; - - @override - Configuration resolveConfiguration({ - required Iterable options, - ArgResults? argResults, - bool ignoreUnexpectedPositionalArgs = false, - }) { - return Configuration.resolve( - options: options, - argResults: argResults, - env: _env, - ignoreUnexpectedPositionalArgs: ignoreUnexpectedPositionalArgs, - ); - } -} - -/// Formats a configuration error message. -String formatConfigError(final String error) { - if (error.isEmpty) return error; - final suffix = _isPunctuation(error.substring(error.length - 1)) ? '' : '.'; - return '${error[0].toUpperCase()}${error.substring(1)}$suffix'; -} - -/// Returns true if the character is a punctuation mark. -bool _isPunctuation(final String char) { - return RegExp(r'\p{P}', unicode: true).hasMatch(char); -} diff --git a/lib/src/config/configuration.dart b/lib/src/config/configuration.dart index 413e5fa..ae73d08 100644 --- a/lib/src/config/configuration.dart +++ b/lib/src/config/configuration.dart @@ -787,6 +787,15 @@ class Configuration { presetValues: values, ); + /// Creates a configuration by copying the contents from another. + /// + /// This is a 1:1 copy including the errors. + Configuration.from({ + required final Configuration configuration, + }) : _options = List.from(configuration._options), + _config = Map.from(configuration._config), + _errors = List.from(configuration._errors); + /// Creates a configuration with option values resolved from the provided context. /// /// [argResults] is used if provided. Otherwise [args] is used if provided. diff --git a/test/documentation_generator/generate_markdown_test.dart b/test/documentation_generator/generate_markdown_test.dart index fcbb2a1..9effcde 100644 --- a/test/documentation_generator/generate_markdown_test.dart +++ b/test/documentation_generator/generate_markdown_test.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:args/command_runner.dart'; import 'package:cli_tools/cli_tools.dart'; -import 'package:cli_tools/src/config/configuration.dart'; import 'package:test/test.dart'; class AddSpiceCommand extends Command {