Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: restrict non multi options #44

Merged
merged 2 commits into from
Dec 20, 2022
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
155 changes: 102 additions & 53 deletions example/test/integration/completion_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -419,42 +419,6 @@ void main() {
suggests(noSuggestions),
);
});

test(
'suggest all options when previous option is continuous with a value',
() async {
await expectLater(
'example_cli some_command --continuous="yeahoo" ',
suggests(allOptionsInThisLevel),
);
},
);

test(
'suggest all options when previous option is continuous with a value',
() async {
await expectLater(
'example_cli some_command --continuous yeahoo ',
suggests(allOptionsInThisLevel),
);
},
);
});

group('flags', () {
test('suggest all options when a flag was declared', () async {
await expectLater(
'example_cli some_command --flag ',
suggests(allOptionsInThisLevel),
);
});

test('suggest all options when a negated flag was declared', () async {
await expectLater(
'example_cli some_command --no-flag ',
suggests(allOptionsInThisLevel),
);
});
});
});

Expand Down Expand Up @@ -532,15 +496,6 @@ void main() {
);
});
});

group('flag', () {
test('suggest all options when a flag was declared', () async {
await expectLater(
'example_cli some_command -f ',
suggests(allOptionsInThisLevel),
);
});
});
});

group('invalid options', () {
Expand All @@ -552,15 +507,109 @@ void main() {
});
});

group(
'repeating options',
tags: 'known-issues',
() {
group('non multi options', () {});
group('repeating options', () {
group('non multi options', () {
test('do not include option after it is specified', () async {
await expectLater(
'example_cli some_command --discrete foo ',
suggests(allOptionsInThisLevel.except('--discrete')),
);
});

test('do not include abbr option after it is specified', () async {
await expectLater(
'example_cli some_command --discrete foo -',
suggests(allAbbreviationsInThisLevel.except('-d')),
);
});

test('do not include option after it is specified as abbr', () async {
await expectLater(
'example_cli some_command -d foo ',
suggests(allOptionsInThisLevel.except('--discrete')),
);
});

test(
'do not include option after it is specified as joined abbr',
() async {
await expectLater(
'example_cli some_command -dfoo ',
suggests(allOptionsInThisLevel.except('--discrete')),
);
},
tags: 'known-issues',
);

test('do not include flag after it is specified', () async {
await expectLater(
'example_cli some_command --flag ',
suggests(allOptionsInThisLevel.except('--flag')),
);
});

test('do not include flag after it is specified (abbr)', () async {
await expectLater(
'example_cli some_command -f ',
suggests(allOptionsInThisLevel.except('--flag')),
);
});

group('multi options', () {});
},
);
test('do not include negated flag after it is specified', () async {
await expectLater(
'example_cli some_command --no-flag ',
suggests(allOptionsInThisLevel.except('--flag')),
);
});

test('do not regard negation of non negatable flag', () async {
await expectLater(
'example_cli some_command --no-trueflag ',
suggests(allOptionsInThisLevel),
);
});
});

group('multi options', () {
test('include multi option after it is specified', () async {
await expectLater(
'example_cli some_command --multi-c yeahoo ',
suggests(allOptionsInThisLevel),
);
});

test('include multi option after it is specified (abbr)', () async {
await expectLater(
'example_cli some_command -n yeahoo ',
suggests(allOptionsInThisLevel),
);
});

test(
'include option after it is specified (abbr joined)',
() async {
await expectLater(
'example_cli some_command -nyeahoo ',
suggests(allOptionsInThisLevel),
);
},
tags: 'known-issues',
);

test('include discrete multi option value after it is specified',
() async {
await expectLater(
'example_cli some_command --multi-d bar -m ',
suggests({
'fii': 'fii help',
'bar': 'bar help',
'fee': 'fee help',
'i have space': 'an allowed option with space on it'
}),
);
});
});
});
});

group('some_other_command', () {
Expand Down
6 changes: 6 additions & 0 deletions example/test/integration/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,9 @@ Future<Map<String, String?>> runCompletionCommand(

return map;
}

extension CompletionUtils on Map<String, String?> {
Map<String, String?> except(String key) {
return Map.from(this)..remove(key);
}
}
62 changes: 59 additions & 3 deletions lib/src/parser/arg_parser_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ bool isAbbr(String string) => _abbrRegex.hasMatch(string);
/// Extends [ArgParser] with utility methods that allow parsing a completion
/// input, which in most cases only regards part of the rules.
extension ArgParserExtension on ArgParser {
/// Tries to parse the minimal subset of valid [args] as valid options.
ArgResults? findValidOptions(List<String> args) {
final loosenOptionsGramamar = _looseOptions();
var currentArgs = args;
while (currentArgs.isNotEmpty) {
try {
return loosenOptionsGramamar.parse(currentArgs);
} catch (_) {
currentArgs = currentArgs.take(currentArgs.length - 1).toList();
}
}
return null;
}

/// Parses [args] with this [ArgParser]'s command structure only, ignore
/// option strict rules (mandatory, allowed values, non negatable flags,
/// default values, etc);
Expand All @@ -25,7 +39,7 @@ extension ArgParserExtension on ArgParser {
/// Returns null if there is an error when parsing, which means the given args
/// do not respect the known command structure.
ArgResults? tryParseCommandsOnly(Iterable<String> args) {
final commandsOnlyGrammar = _looseOptions();
final commandsOnlyGrammar = _cloneCommandsOnly();

final filteredArgs = args.where((element) {
return !isAbbr(element) && !isOption(element) && element.isNotEmpty;
Expand All @@ -40,16 +54,58 @@ extension ArgParserExtension on ArgParser {
}

/// Recursively copies this [ArgParser] without options.
ArgParser _looseOptions() {
ArgParser _cloneCommandsOnly() {
final clonedArgParser = ArgParser(
allowTrailingOptions: allowTrailingOptions,
);

for (final entry in commands.entries) {
final parser = entry.value._looseOptions();
final parser = entry.value._cloneCommandsOnly();
clonedArgParser.addCommand(entry.key, parser);
}

return clonedArgParser;
}

/// Copies this [ArgParser] with a less strict option mapping.
///
/// It preserves only the options names, types, abbreviations and aliases.
///
/// It disregard subcommands.
ArgParser _looseOptions() {
final clonedArgParser = ArgParser(
allowTrailingOptions: allowTrailingOptions,
);

for (final entry in options.entries) {
final option = entry.value;

if (option.isFlag) {
clonedArgParser.addFlag(
option.name,
abbr: option.abbr,
aliases: option.aliases,
negatable: option.negatable ?? true,
);
}

if (option.isSingle) {
clonedArgParser.addOption(
option.name,
abbr: option.abbr,
aliases: option.aliases,
);
}

if (option.isMultiple) {
clonedArgParser.addMultiOption(
option.name,
abbr: option.abbr,
aliases: option.aliases,
);
}
}

return clonedArgParser;
}
}
12 changes: 12 additions & 0 deletions lib/src/parser/completion_level.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class CompletionLevel {
@visibleForTesting
const CompletionLevel({
required this.grammar,
this.parsedOptions,
required this.rawArgs,
required this.visibleSubcommands,
required this.visibleOptions,
Expand Down Expand Up @@ -90,17 +91,24 @@ class CompletionLevel {
rawArgs = rootArgs.toList();
}

final validOptionsResult = originalGrammar.findValidOptions(rawArgs);

final visibleSubcommands = subcommands?.values.where((command) {
return !command.hidden;
}).toList() ??
[];

final visibleOptions = originalGrammar.options.values.where((option) {
final wasParsed = validOptionsResult?.wasParsed(option.name) ?? false;
if (wasParsed) {
return option.isMultiple;
}
return !option.hide;
}).toList();

return CompletionLevel(
grammar: originalGrammar,
parsedOptions: validOptionsResult,
rawArgs: rawArgs,
visibleSubcommands: visibleSubcommands,
visibleOptions: visibleOptions,
Expand All @@ -111,6 +119,10 @@ class CompletionLevel {
/// needs completion.
final ArgParser grammar;

/// An [ArgResults] that includes the valid options passed to the command on
/// completion level. Null if no valid options were passed.
final ArgResults? parsedOptions;

/// The user input that needs completion starting from the
/// command/sub_command being completed.
final List<String> rawArgs;
Expand Down
Loading