Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion example/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> args) async {
var commandRunner = BetterCommandRunner(
Expand Down
3 changes: 2 additions & 1 deletion lib/cli_tools.dart
Original file line number Diff line number Diff line change
@@ -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';
70 changes: 34 additions & 36 deletions lib/src/better_command_runner/better_command.dart
Original file line number Diff line number Diff line change
@@ -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<O extends OptionDefinition, T> extends Command<T> {
static const _defaultMessageOutput = MessageOutput(usageLogger: print);

final MessageOutput? _messageOutput;
final ArgParser _argParser;

/// The configuration resolver for this command.
ConfigResolver<O>? _configResolver;
/// The environment variables used for configuration resolution.
final Map<String, String> envVariables;

/// The option definitions for this command.
final List<O> options;
Expand All @@ -24,10 +29,13 @@ abstract class BetterCommand<O extends OptionDefinition, T> extends Command<T> {
/// - [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
Expand All @@ -48,17 +56,14 @@ abstract class BetterCommand<O extends OptionDefinition, T> extends Command<T> {
/// final ConfigOptionBase<V> 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<O>? configResolver,
Map<String, String>? env,
}) : _messageOutput = messageOutput,
_argParser = ArgParser(usageLineLength: wrapTextColumn),
_configResolver = configResolver {
envVariables = env ?? Platform.environment {
prepareOptionsForParsing(options, argParser);
}

Expand All @@ -72,15 +77,9 @@ abstract class BetterCommand<O extends OptionDefinition, T> extends Command<T> {
return _messageOutput;
}

ConfigResolver<O> get configResolver {
if (runner case BetterCommandRunner<O, T> runner) {
return runner.configResolver;
}
return _configResolver ??= DefaultConfigResolver<O>();
}

@override
BetterCommand<O, T>? get parent => super.parent as BetterCommand<O, T>?;
BetterCommand<dynamic, T>? get parent =>
super.parent as BetterCommand<dynamic, T>?;

@override
BetterCommandRunner<dynamic, T>? get runner =>
Expand All @@ -98,39 +97,38 @@ abstract class BetterCommand<O extends OptionDefinition, T> extends Command<T> {
/// 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<T>? 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<O> 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<T>? runWithConfig(final Configuration<O> commandConfig);
}
73 changes: 42 additions & 31 deletions lib/src/better_command_runner/better_command_runner.dart
Original file line number Diff line number Diff line change
@@ -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<void> Function(BetterCommandRunner runner);
Expand Down Expand Up @@ -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<O extends OptionDefinition, T>
extends CommandRunner<T> {
static const foo = <OptionDefinition>[
Expand All @@ -69,11 +70,12 @@ class BetterCommandRunner<O extends OptionDefinition, T>
final OnBeforeRunCommand? _onBeforeRunCommand;
OnAnalyticsEvent? _onAnalyticsEvent;

/// The environment variables used for configuration resolution.
final Map<String, String> envVariables;

/// The gloabl option definitions.
late final List<O> _globalOptions;

final ConfigResolver<O> _configResolver;

Configuration<O>? _globalConfiguration;

/// The current global configuration.
Expand Down Expand Up @@ -101,7 +103,8 @@ class BetterCommandRunner<O extends OptionDefinition, T>
/// - [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
///
Expand Down Expand Up @@ -138,9 +141,6 @@ class BetterCommandRunner<O extends OptionDefinition, T>
/// final ConfigOptionBase<V> 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, {
Expand All @@ -151,12 +151,12 @@ class BetterCommandRunner<O extends OptionDefinition, T>
OnAnalyticsEvent? onAnalyticsEvent,
int? wrapTextColumn,
List<O>? globalOptions,
ConfigResolver<O>? configResolver,
Map<String, String>? env,
}) : _messageOutput = messageOutput,
_setLogLevel = setLogLevel,
_onBeforeRunCommand = onBeforeRunCommand,
_onAnalyticsEvent = onAnalyticsEvent,
_configResolver = configResolver ?? DefaultConfigResolver<O>(),
envVariables = env ?? Platform.environment,
super(
usageLineLength: wrapTextColumn,
) {
Expand All @@ -180,10 +180,6 @@ class BetterCommandRunner<O extends OptionDefinition, T>
/// 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<O> get configResolver => _configResolver;

/// Adds a list of commands to the command runner.
void addCommands(List<Command<T>> commands) {
for (var command in commands) {
Expand All @@ -208,6 +204,20 @@ class BetterCommandRunner<O extends OptionDefinition, T>
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);
});
}
Expand Down Expand Up @@ -293,28 +303,17 @@ class BetterCommandRunner<O extends OptionDefinition, T>
}
}

/// 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<O> 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) {
Expand Down Expand Up @@ -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);
}
60 changes: 0 additions & 60 deletions lib/src/better_command_runner/config_resolver.dart

This file was deleted.

9 changes: 9 additions & 0 deletions lib/src/config/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,15 @@ class Configuration<O extends OptionDefinition> {
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<O> 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.
Expand Down
Loading