Skip to content

Commit

Permalink
[#761] API: Added method ParseResult.matchedArgs() that returns all…
Browse files Browse the repository at this point in the history
… matched options and positional params

* Add `ParseResult.matchedArgs()` method to return all matched arguments in order;
* change `ParseResult.matchedOptions()` and `ParseResult.matchedPositionals()` to return the full list of matched options and positional parameters, including duplicates if the option or positional parameter was matched multiple times.
* Add new `ParseResult.matchedOptionSet()` and `ParseResult.matchedPositionalSet()` methods that return a `Set`.
  • Loading branch information
remkop committed Jul 3, 2019
1 parent 927a27b commit bbcc485
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 37 deletions.
5 changes: 4 additions & 1 deletion RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Picocli follows [semantic versioning](http://semver.org/).
## <a name="4.0.0-rc-1-fixes"></a> Fixed issues
- [#696][#741] Automatically split lines in TextTable. Thanks to [Sualeh Fatehi](https://github.com/sualeh) for the pull request.
- [#756] API: Make synopsis indent for multi-line synopsis configurable (related to #739).
- [#761] API: Add `ParseResult.matchedArgs()` method to return all matched arguments in order; change `ParseResult.matchedOptions()` and `ParseResult.matchedPositionals()` to return the full list of matched options and positional parameters, including duplicates if the option or positional parameter was matched multiple times. Thanks to [Michael D. Adams](https://github.com/adamsmd) for the feature request.
- [#739] Bugfix: infinite loop or exception when command name plus synopsis heading length equals or exceeds usage help message width. Thanks to [Arturo Alonso](https://github.com/thefang12) for raising this.
- [#746] Bugfix: Apply default values to options and positional parameters in argument groups. Thanks to [Andreas Deininger](https://github.com/deining) for raising this.
- [#742] Bugfix: Default values prevent correct parsing in argument groups. Thanks to [Andreas Deininger](https://github.com/deining) for raising this.
Expand All @@ -44,7 +45,9 @@ Picocli follows [semantic versioning](http://semver.org/).
## <a name="4.0.0-rc-1-deprecated"></a> Deprecations

## <a name="4.0.0-rc-1-breaking-changes"></a> Potential breaking changes

`ParseResult.matchedOptions()` and `ParseResult.matchedPositionals()` now return the full list of matched options and positional parameters, including duplicates if the option or positional parameter was matched multiple times.
Prior to this release, these methods would return a list that did not contain duplicates.
Applications interested in the old behavior should use the new `matchedOptionSet()` and `matchedPositionalSet()` methods that return a `Set`.

# <a name="4.0.0-beta-2"></a> Picocli 4.0.0-beta-2
The picocli community is pleased to announce picocli 4.0.0-beta-2.
Expand Down
71 changes: 43 additions & 28 deletions src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -9956,8 +9956,11 @@ interface ILookup {
* @since 3.0 */
public static class ParseResult {
private final CommandSpec commandSpec;
private final Set<OptionSpec> matchedUniqueOptions;
private final Set<PositionalParamSpec> matchedUniquePositionals;
private final List<ArgSpec> matchedArgs;
private final List<OptionSpec> matchedOptions;
private final List<PositionalParamSpec> matchedUniquePositionals;
private final List<PositionalParamSpec> matchedPositionals;
private final List<String> originalArgs;
private final List<String> unmatched;
private final List<List<PositionalParamSpec>> matchedPositionalParams;
Expand All @@ -9972,10 +9975,13 @@ public static class ParseResult {
private ParseResult(ParseResult.Builder builder) {
commandSpec = builder.commandSpec;
subcommand = builder.subcommand;
matchedOptions = new ArrayList<OptionSpec>(builder.options);
matchedOptions = new ArrayList<OptionSpec>(builder.matchedOptionsList);
matchedUniqueOptions = new LinkedHashSet<OptionSpec>(builder.options);
unmatched = new ArrayList<String>(builder.unmatched);
originalArgs = new ArrayList<String>(builder.originalArgList);
matchedUniquePositionals = new ArrayList<PositionalParamSpec>(builder.positionals);
matchedArgs = new ArrayList<ArgSpec>(builder.matchedArgsList);
matchedUniquePositionals = new LinkedHashSet<PositionalParamSpec>(builder.positionals);
matchedPositionals = new ArrayList<PositionalParamSpec>(builder.matchedPositionalsList);
matchedPositionalParams = new ArrayList<List<PositionalParamSpec>>(builder.positionalParams);
errors = new ArrayList<Exception>(builder.errors);
usageHelpRequested = builder.usageHelpRequested;
Expand Down Expand Up @@ -10066,11 +10072,28 @@ public List<PositionalParamSpec> matchedPositionals(int position) {
/** Returns whether the specified positional parameter was matched on the command line. */
public boolean hasMatchedPositional(PositionalParamSpec positional) { return matchedUniquePositionals.contains(positional); }

/** Returns a list of matched options, in the order they were found on the command line. */
public List<OptionSpec> matchedOptions() { return Collections.unmodifiableList(matchedOptions); }
/** Returns a set of matched options.
* @since 4.0 */
public Set<OptionSpec> matchedOptionsSet() { return Collections.unmodifiableSet(matchedUniqueOptions); }

/** Returns a list of matched options, in order they were matched on the command line.
* The returned list may contain the same {@code OptionSpec} multiple times, if the option was matched multiple times on the command line.
*/
public List<OptionSpec> matchedOptions() { return Collections.unmodifiableList(matchedOptions); }

/** Returns a set of matched positional parameters.
* @since 4.0 */
public Set<PositionalParamSpec> matchedPositionalsSet() { return Collections.unmodifiableSet(matchedUniquePositionals); }

/** Returns a list of matched positional parameters. */
public List<PositionalParamSpec> matchedPositionals() { return Collections.unmodifiableList(matchedUniquePositionals); }
/** Returns a list of matched positional parameters, in order they were matched on the command line.
* The returned list may contain the same {@code PositionalParamSpec} multiple times, if the parameter was matched multiple times on the command line.
*/
public List<PositionalParamSpec> matchedPositionals() { return Collections.unmodifiableList(matchedPositionals); }

/** Returns a list of matched options and positional parameters, in order they were matched on the command line.
* The returned list may contain an {@code OptionSpec} or {@code PositionalParamSpec} multiple times, if the option or parameter was matched multiple times on the command line.
* @since 4.0 */
public List<ArgSpec> matchedArgs() { return Collections.unmodifiableList(matchedArgs); }

/** Returns a list of command line arguments that did not match any options or positional parameters. */
public List<String> unmatched() { return Collections.unmodifiableList(unmatched); }
Expand Down Expand Up @@ -10127,6 +10150,9 @@ void validateGroups() {
/** Builds immutable {@code ParseResult} instances. */
public static class Builder {
private final CommandSpec commandSpec;
private final List<ArgSpec> matchedArgsList = new ArrayList<ArgSpec>();
private final List<OptionSpec> matchedOptionsList = new ArrayList<OptionSpec>();
private final List<PositionalParamSpec> matchedPositionalsList = new ArrayList<PositionalParamSpec>();
private final Set<OptionSpec> options = new LinkedHashSet<OptionSpec>();
private final Set<PositionalParamSpec> positionals = new LinkedHashSet<PositionalParamSpec>();
private final List<String> unmatched = new ArrayList<String>();
Expand Down Expand Up @@ -10163,19 +10189,27 @@ public Builder add(ArgSpec arg, int position) {
} else {
addPositionalParam((PositionalParamSpec) arg, position);
}
afterMatchingGroupElement(arg, position);
return this;
}

/** Adds the specified {@code OptionSpec} to the list of options that were matched on the command line. */
public Builder addOption(OptionSpec option) { if (!isInitializingDefaultValues) {options.add(option);} return this; }
public Builder addOption(OptionSpec option) {
if (!isInitializingDefaultValues) {
options.add(option);
matchedOptionsList.add(option);
matchedArgsList.add(option);
}
return this;
}
/** Adds the specified {@code PositionalParamSpec} to the list of parameters that were matched on the command line.
* @param positionalParam the matched {@code PositionalParamSpec}
* @param position the command line position at which the {@code PositionalParamSpec} was matched.
* @return this builder for method chaining */
public Builder addPositionalParam(PositionalParamSpec positionalParam, int position) {
if (isInitializingDefaultValues) { return this; }
positionals.add(positionalParam);
matchedPositionalsList.add(positionalParam);
matchedArgsList.add(positionalParam);
while (positionalParams.size() <= position) { positionalParams.add(new ArrayList<PositionalParamSpec>()); }
positionalParams.get(position).add(positionalParam);
return this;
Expand Down Expand Up @@ -10231,25 +10265,6 @@ void beforeMatchingGroupElement(ArgSpec argSpec) throws Exception {
this.groupMatchContainer.findOrCreateMatchingGroup(argSpec, commandSpec.commandLine);
}
}

private void afterMatchingGroupElement(ArgSpec argSpec, int position) {
// ArgGroupSpec group = argSpec.group();
// if (group == null || isInitializingDefaultValues) { return; }
// GroupMatchContainer groupMatchContainer = this.groupMatchContainer.findOrCreateMatchingGroup(argSpec, commandSpec.commandLine);
// promotePartiallyMatchedGroupToMatched(group, groupMatchContainer, true);
}

private void promotePartiallyMatchedGroupToMatched(ArgGroupSpec group, GroupMatchContainer groupMatchContainer, boolean allRequired) {
if (!groupMatchContainer.matchedFully(allRequired)) { return; }

// FIXME: before promoting the child group, check to see if the parent is matched, given the child group

Tracer tracer = commandSpec.commandLine.tracer;
if (groupMatchContainer.matchedMaxElements()) {
tracer.info("Marking matched group %s as complete: max elements reached. User object: %s%n", groupMatchContainer, groupMatchContainer.group.userObject());
groupMatchContainer.complete(commandSpec.commandLine());
}
}
}

static class GroupValidationResult {
Expand Down
28 changes: 20 additions & 8 deletions src/test/java/picocli/ModelParseResultTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,26 +79,38 @@ class App {
ParseResult result = new CommandLine(new App()).parseArgs(args);
assertEquals(Arrays.asList(args), result.originalArgs());

assertSame(result.commandSpec().positionalParameters().get(0), result.tentativeMatch.get(0));
assertSame(result.commandSpec().positionalParameters().get(0), result.tentativeMatch.get(1));
assertSame(result.commandSpec().positionalParameters().get(1), result.tentativeMatch.get(2));
assertSame(result.commandSpec().positionalParameters().get(1), result.tentativeMatch.get(3));
assertSame(result.commandSpec().positionalParameters().get(1), result.tentativeMatch.get(4));
List<PositionalParamSpec> positionals = result.commandSpec().positionalParameters();
assertSame(positionals.get(0), result.tentativeMatch.get(0));
assertSame(positionals.get(0), result.tentativeMatch.get(1));
assertSame(positionals.get(1), result.tentativeMatch.get(2));
assertSame(positionals.get(1), result.tentativeMatch.get(3));
assertSame(positionals.get(1), result.tentativeMatch.get(4));

assertTrue(result.unmatched().isEmpty());
assertFalse(result.hasSubcommand());
assertFalse(result.isUsageHelpRequested());
assertFalse(result.isVersionHelpRequested());

assertEquals(Collections.emptyList(), result.matchedOptions());
assertEquals(3, result.matchedPositionals().size());
assertEquals(3, result.matchedPositionalsSet().size());
assertEquals(new LinkedHashSet<PositionalParamSpec>(positionals), result.matchedPositionalsSet());

assertEquals(5 + 2 + 4, result.matchedPositionals().size());
assertEquals(Range.valueOf("0..1"), result.matchedPositionals().get(0).index());
assertEquals(Range.valueOf("0..*"), result.matchedPositionals().get(1).index());
assertEquals(Range.valueOf("1..*"), result.matchedPositionals().get(2).index());
assertEquals(Range.valueOf("0..1"), result.matchedPositionals().get(2).index());
assertEquals(Range.valueOf("0..*"), result.matchedPositionals().get(3).index());
assertEquals(Range.valueOf("1..*"), result.matchedPositionals().get(4).index());
assertEquals(Range.valueOf("0..*"), result.matchedPositionals().get(5).index());
assertEquals(Range.valueOf("1..*"), result.matchedPositionals().get(6).index());
assertEquals(Range.valueOf("0..*"), result.matchedPositionals().get(7).index());
assertEquals(Range.valueOf("1..*"), result.matchedPositionals().get(8).index());
assertEquals(Range.valueOf("0..*"), result.matchedPositionals().get(9).index());
assertEquals(Range.valueOf("1..*"), result.matchedPositionals().get(10).index());

assertArrayEquals(args, (String[]) result.matchedPositionals().get(1).getValue());
assertArrayEquals(new String[]{"a", "b"}, (String[]) result.matchedPositionals().get(0).getValue());
assertArrayEquals(new String[]{"b", "c", "d", "e"}, (String[]) result.matchedPositionals().get(2).getValue());
assertArrayEquals(new String[]{"b", "c", "d", "e"}, (String[]) result.matchedPositionals().get(4).getValue());

for (int i = 0; i < args.length; i++) {
assertTrue(result.hasMatchedPositional(i));
Expand Down
61 changes: 61 additions & 0 deletions src/test/java/picocli/OrderedOptionsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package picocli;

import org.junit.Test;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.ArgSpec;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.OptionSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParseResult;
import picocli.CommandLine.Spec;

import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.*;

// https://github.com/remkop/picocli/issues/761
public class OrderedOptionsTest {
@Command(name = "rsync")
static class Rsync implements Runnable {

@Option(names = "--include")
List<String> includes;

@Option(names = "--exclude")
List<String> excludes;

@Spec CommandSpec spec;

public void run() {
ParseResult pr = spec.commandLine().getParseResult();
List<ArgSpec> optionSpecs = pr.matchedArgs();
// do something
}
}

@Test
public void testOrderWithParseResult() {
CommandLine cmd = new CommandLine(new Rsync());
ParseResult parseResult = cmd.parseArgs("--include", "a", "--exclude", "b", "--include", "c", "--exclude", "d");
List<ArgSpec> argSpecs = parseResult.matchedArgs();
assertEquals(4, argSpecs.size());
assertEquals("--include", ((OptionSpec) argSpecs.get(0)).longestName());
assertEquals("--exclude", ((OptionSpec) argSpecs.get(1)).longestName());
assertEquals("--include", ((OptionSpec) argSpecs.get(2)).longestName());
assertEquals("--exclude", ((OptionSpec) argSpecs.get(3)).longestName());

List<OptionSpec> matchedOptions = parseResult.matchedOptions();
assertEquals(4, matchedOptions.size());

assertEquals("--include", matchedOptions.get(0).longestName());
assertSame(matchedOptions.get(0), matchedOptions.get(2));
assertEquals(Arrays.asList("a"), matchedOptions.get(0).typedValues().get(0));
assertEquals(Arrays.asList("c"), matchedOptions.get(2).typedValues().get(1));

assertEquals("--exclude", matchedOptions.get(1).longestName());
assertSame(matchedOptions.get(1), matchedOptions.get(3));
assertEquals(Arrays.asList("b"), matchedOptions.get(1).typedValues().get(0));
assertEquals(Arrays.asList("d"), matchedOptions.get(3).typedValues().get(1));
}
}

0 comments on commit bbcc485

Please sign in to comment.