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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:config/config.dart';
import 'completion/completion_command.dart';
import 'completion/completion_tool.dart' show CompletionScript;

import 'help_command_workaround.dart' show HelpCommandWorkaround;

/// A function type for executing code before running a command.
typedef OnBeforeRunCommand = Future<void> Function(BetterCommandRunner runner);

Expand Down Expand Up @@ -72,7 +74,7 @@ class BetterCommandRunner<O extends OptionDefinition, T>
/// The environment variables used for configuration resolution.
final Map<String, String> envVariables;

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

Configuration<O>? _globalConfiguration;
Expand Down Expand Up @@ -346,6 +348,13 @@ class BetterCommandRunner<O extends OptionDefinition, T>
await _onBeforeRunCommand?.call(this);

try {
// an edge case regarding `help -h`
final helpProxy = HelpCommandWorkaround(runner: this);
if (helpProxy.isUsageOfHelpCommandRequested(topLevelResults)) {
messageOutput?.logUsage(helpProxy.usage);
return null;
}
// normal cases
return await super.runCommand(topLevelResults);
} on UsageException catch (e) {
messageOutput?.logUsageException(e);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'package:args/args.dart' show ArgResults;
import 'package:args/command_runner.dart' show Command, CommandRunner;

/// A dummy to replicate the usage-text of upstream private `HelpCommand`.
///
/// It is intended to be used for an internal patch only and is
/// intentionally not part of the public API of this package.
final class HelpCommandWorkaround extends Command {
HelpCommandWorkaround({required this.runner});

/// Checks whether the main command seeks the
/// usage-text for `help` command.
///
/// Specifically, for a program `mock`, it checks
/// whether [topLevelResults] is of the form:
/// * `mock help -h`
/// * `mock help help`
bool isUsageOfHelpCommandRequested(final ArgResults topLevelResults) {
// check whether `help` command is chosen
final topLevelCommand = topLevelResults.command;
if (topLevelCommand == null) {
return false;
}
if (topLevelCommand.name != name) {
return false;
}
final helpCommand = topLevelCommand;
// check whether it's allowed to get the usage-text for `help`
if (!helpCommand.options.contains(name)) {
// extremely rare scenario (e.g. if `package:args` has a breaking change)
// fortunately, corresponding test-cases shall fail as it
// - tests the current behavior (e.g. args = ['help', '-h'])
// - notifies the publisher(s) of this breaking change
return false;
}
// case: `mock help -h`
if (helpCommand.flag(name)) {
return true;
}
// case: `mock help help`
if ((helpCommand.arguments.contains(name))) {
return true;
}
// aside: more cases may be added if necessary in future
return false;
}

@override
final CommandRunner runner;

@override
final name = 'help';

@override
String get description =>
'Display help information for ${runner.executableName}.';

@override
String get invocation => '${runner.executableName} $name [command]';

@override
Never run() => throw UnimplementedError(
'This class is meant to only obtain the Usage Text for `$name` command');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import 'dart:async' show ZoneSpecification, runZoned;

import 'package:args/command_runner.dart' show CommandRunner, UsageException;
import 'package:cli_tools/better_command_runner.dart'
show BetterCommandRunner, MessageOutput;
import 'package:test/test.dart';

void main() => _runTests();

const _exeName = 'mock';
const _exeDescription = 'A mock command to test `help -h` with MessageOutput.';
const _moMockText = 'SUCCESS: `MessageOutput.usageLogger`';
const _moExceptionText = 'SUCCESS: `MessageOutput.usageExceptionLogger`';
const _invalidOption = '-z';

CommandRunner _buildUpstreamRunner() => CommandRunner(
_exeName,
_exeDescription,
);

BetterCommandRunner _buildBetterRunner() => BetterCommandRunner(
_exeName,
_exeDescription,
messageOutput: MessageOutput(
usageLogger: (final usage) {
print(_moMockText);
print(usage);
},
usageExceptionLogger: (final exception) {
print(_moExceptionText);
print(exception.message);
print(exception.usage);
},
),
);

void _runTests() {
group('Given a BetterCommandRunner with custom MessageOutput', () {
for (final args in [
['help', '-h'],
['help', 'help'],
['help', '-h', 'help'],
['help', '-h', 'help', '-h'],
['help', '-h', 'help', '-h', 'help'],
['help', '-h', 'help', '-h', 'help', '-h'],
]) {
_testsForValidHelpUsageRequests(args);
}
for (final args in [
['help', '-h', _invalidOption],
['help', 'help', _invalidOption],
['help', _invalidOption, 'help'],
['help', 'help', '-h', _invalidOption],
['help', _invalidOption, 'help', '-h', _invalidOption],
]) {
_testsForInvalidHelpUsageRequests(args);
}
});
}

void _testsForValidHelpUsageRequests(final List<String> args) {
group('when $args is received', () {
final betterRunnerOutput = StringBuffer();
final upstreamRunnerOutput = StringBuffer();
late final Object? betterRunnerExitCode;
late final Object? upstreamRunnerExitCode;
setUpAll(() async {
betterRunnerExitCode = await runZoned(
() async => await _buildBetterRunner().run(args),
zoneSpecification: ZoneSpecification(
print: (final _, final __, final ___, final String line) {
betterRunnerOutput.writeln(line);
}),
);
upstreamRunnerExitCode = await runZoned(
() async => await _buildUpstreamRunner().run(args),
zoneSpecification: ZoneSpecification(
print: (final _, final __, final ___, final String line) {
upstreamRunnerOutput.writeln(line);
}),
);
});
test('then MessageOutput is not bypassed', () {
expect(
betterRunnerOutput.toString(),
contains(_moMockText),
);
});
test('then it can subsume upstream HelpCommand output', () {
expect(
betterRunnerOutput.toString(),
stringContainsInOrder([
_moMockText,
upstreamRunnerOutput.toString(),
]),
);
});
test('then Exit Code (null) matches that of upstream HelpCommand', () {
expect(betterRunnerExitCode, equals(null));
expect(betterRunnerExitCode, equals(upstreamRunnerExitCode));
});
});
}

void _testsForInvalidHelpUsageRequests(final List<String> args) {
group('when $args is received', () {
final betterRunnerOutput = StringBuffer();
final upstreamRunnerOutput = StringBuffer();
UsageException? betterRunnerException;
UsageException? upstreamRunnerException;
setUpAll(() async {
try {
await runZoned(
() async => await _buildBetterRunner().run(args),
zoneSpecification: ZoneSpecification(
print: (final _, final __, final ___, final String line) {
betterRunnerOutput.writeln(line);
},
),
);
} on UsageException catch (e) {
betterRunnerException = e;
}
try {
await runZoned(
() async => await _buildUpstreamRunner().run(args),
zoneSpecification: ZoneSpecification(
print: (final _, final __, final ___, final String line) {
upstreamRunnerOutput.writeln(line);
},
),
);
} on UsageException catch (e) {
upstreamRunnerException = e;
}
});
test('then it throws UsageException exactly like CommandRunner', () {
expect(betterRunnerException, isA<UsageException>());
expect(upstreamRunnerException, isA<UsageException>());
expect(
betterRunnerException!.message,
equals(upstreamRunnerException!.message),
);
expect(
betterRunnerException!.usage,
equals(upstreamRunnerException!.usage),
);
});
test('then MessageOutput is not bypassed', () {
expect(
betterRunnerOutput.toString(),
stringContainsInOrder(
[
_moExceptionText,
betterRunnerException!.message,
betterRunnerException!.usage,
],
),
);
});
test('then it can subsume upstream HelpCommand output', () {
expect(
betterRunnerOutput.toString(),
contains(upstreamRunnerOutput.toString()),
);
});
});
}