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
110 changes: 107 additions & 3 deletions example/main.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,120 @@
import 'dart:async' show FutureOr;
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(
'example',
'Example CLI command',
globalOptions: [
StandardGlobalOption.quiet,
StandardGlobalOption.verbose,
],
);
commandRunner.addCommand(TimeSeriesCommand());

try {
await commandRunner.run(args);
} on UsageException catch (e) {
print(e);
exit(1);
}

void main() async {
/// Simple example of using the [StdOutLogger] class.
var logger = StdOutLogger(LogLevel.info);
final LogLevel logLevel;
if (commandRunner.globalConfiguration.value(StandardGlobalOption.verbose)) {
logLevel = LogLevel.debug;
} else if (commandRunner.globalConfiguration
.value(StandardGlobalOption.quiet)) {
logLevel = LogLevel.error;
} else {
logLevel = LogLevel.info;
}
var logger = StdOutLogger(logLevel);

logger.info('An info message');
logger.error('An error message');
logger.debug(
'A debug message that will not be shown because log level is info',
'A debug message that will not be shown unless --verbose is set',
);
await logger.progress(
'A progress message',
() async => Future.delayed(const Duration(seconds: 3), () => true),
);
}

/// Options are defineable as enums as well as regular lists.
///
/// The enum approach is more distinct and type safe.
/// The list approach is more dynamic and permits non-const initialization.
enum TimeSeriesOption<V> implements OptionDefinition<V> {
until(DateTimeOption(
argName: 'until',
envName: 'SERIES_UNTIL', // can also be specified as environment variable
fromDefault: _defaultUntil,
helpText: 'The end timestamp of the series',
)),
length(IntOption(
argName: 'length',
argAbbrev: 'l',
argPos: 0, // can also be specified as positional argument
helpText: 'The number of elements in the series',
min: 1,
max: 100,
group: _granularityGroup,
)),
interval(DurationOption(
argName: 'interval',
argAbbrev: 'i',
helpText: 'The interval between the series elements',
min: Duration(seconds: 1),
max: Duration(days: 1),
group: _granularityGroup,
));

const TimeSeriesOption(this.option);

@override
final ConfigOptionBase<V> option;
}

/// Exactly one of the options in this group must be set.
const _granularityGroup = MutuallyExclusive(
'Granularity',
mode: MutuallyExclusiveMode.mandatory,
);

/// A function can be used as a const initializer.
DateTime _defaultUntil() => DateTime.now().add(const Duration(days: 1));

class TimeSeriesCommand extends BetterCommand<TimeSeriesOption, void> {
TimeSeriesCommand() : super(options: TimeSeriesOption.values);

@override
String get name => 'series';

@override
String get description => 'Generate a series of time stamps';

@override
FutureOr<void>? runWithConfig(Configuration<TimeSeriesOption> commandConfig) {
var start = DateTime.now();
var until = commandConfig.value(TimeSeriesOption.until);

// exactly one of these options is set
var length = commandConfig.optionalValue(TimeSeriesOption.length);
var interval = commandConfig.optionalValue(TimeSeriesOption.interval);
interval ??= (until.difference(start) ~/ length!);
if (interval < const Duration(milliseconds: 1)) {
interval = const Duration(milliseconds: 1);
}

while (start.isBefore(until)) {
print(start);
start = start.add(interval);
}
}
}
43 changes: 36 additions & 7 deletions lib/src/better_command_runner/better_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import 'package:cli_tools/config.dart';
import 'config_resolver.dart';

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.
final ConfigResolver<O> _configResolver;
ConfigResolver<O>? _configResolver;

/// The option definitions for this command.
final List<O> options;
Expand All @@ -21,9 +23,12 @@ 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 an optional list of options.
/// - [options] is a list of options, empty by default.
/// - [configResolver] is an optional custom [ConfigResolver] implementation.
///
/// [configResolver] and [messageOutput] are optional and will default to the
/// values of the command runner (if any).
///
/// To define a bespoke set of options, it is recommended to define
/// a proper options enum. It can included any of the default options
/// as well as any custom options. Example:
Expand All @@ -47,22 +52,46 @@ abstract class BetterCommand<O extends OptionDefinition, T> extends Command<T> {
/// 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,
MessageOutput? messageOutput = _defaultMessageOutput,
int? wrapTextColumn,
this.options = const [],
final ConfigResolver<O>? configResolver,
ConfigResolver<O>? configResolver,
}) : _messageOutput = messageOutput,
_argParser = ArgParser(usageLineLength: wrapTextColumn),
_configResolver = configResolver ?? DefaultConfigResolver<O>() {
_configResolver = configResolver {
prepareOptionsForParsing(options, argParser);
}

MessageOutput? get messageOutput {
if (_messageOutput != _defaultMessageOutput) {
return _messageOutput;
}
if (runner case BetterCommandRunner<O, T> runner) {
return runner.messageOutput;
}
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>?;

@override
BetterCommandRunner<dynamic, T>? get runner =>
super.runner as BetterCommandRunner<dynamic, T>?;

@override
ArgParser get argParser => _argParser;

@override
void printUsage() {
_messageOutput?.logUsage(usage);
messageOutput?.logUsage(usage);
}

/// Runs this command.
Expand All @@ -86,7 +115,7 @@ abstract class BetterCommand<O extends OptionDefinition, T> extends Command<T> {
/// This method can be overridden to change the configuration resolution
/// or error handling behavior.
Configuration<O> resolveConfiguration(ArgResults? argResults) {
final config = _configResolver.resolveConfiguration(
final config = configResolver.resolveConfiguration(
options: options,
argResults: argResults,
);
Expand Down
Loading