Skip to content

Commit

Permalink
feat: restrict non multi optiobns
Browse files Browse the repository at this point in the history
  • Loading branch information
renancaraujo committed Dec 19, 2022
1 parent 53d1f08 commit 0cfce32
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 60 deletions.
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 (e) {
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

0 comments on commit 0cfce32

Please sign in to comment.