Skip to content

Commit

Permalink
[#326] Enhancement and API Change: Add parser configuration to treat …
Browse files Browse the repository at this point in the history
…unmatched options as positional parameters.

Closes #326.
  • Loading branch information
remkop committed Apr 6, 2018
1 parent 5390056 commit ccd2b97
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 8 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ No features have been promoted in this picocli release.

## <a name="3.0.0-alpha-5-fixes"></a> Fixed issues

- [#326] Enhancement and API Change: Add parser configuration to treat unmatched options as positional parameters.
- [#283] Enhancement and API Change: Provide `getMissing` method on MissingParameterException to get a reference to the problematic options and positional parameters. Thanks to [jcapsule](https://github.com/jcapsule) for the suggestion.
- [#323] Enhancement: Remove dependency on java.sql package: picocli should only require the java.base module when running in Java 9.
- [#325] Enhancement: Allow custom type converter to map empty String to custom default value for empty options. Thanks to [jesselong](https://github.com/jesselong) for the suggestion.
Expand Down
13 changes: 7 additions & 6 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -857,7 +857,7 @@ If picocli finds a field annotated with `@Unmatched`, it automatically sets `unm
so no `UnmatchedArgumentException` is thrown when a command line argument cannot be assigned to an option or positional parameter.

=== Unknown Options
A special case of unmatched input are arguments that look like options but don't match any of the defined options.
A special case of unmatched input are arguments that resemble options but don't match any of the defined options.
For example:

[source,java]
Expand All @@ -874,7 +874,7 @@ The above defines options `-a` and `-b`, but what should the parser do with inpu
The `-x` argument "looks like" an option but there is no `-x` option defined...

One possibility is to silently accept such values as positional parameters but this is often not desirable.
From version 1.0, picocli determines if the unmatched argument "looks like an option"
From version 1.0, picocli determines if the unmatched argument "resembles an option"
by comparing its leading characters to the prefix characters of the known options.

When the unmatched value is similar to the known options, picocli throws an `UnmatchedArgumentException`
Expand All @@ -890,13 +890,14 @@ Arguments that are not considered similar to the known options are interpreted a
----
The above input is treated by the parser as one positional parameter (`x`) followed by the `-a` option and its value.

Use the <<Double dash (`--`),end-of-options delimiter>> (`--`) to ensure that arguments resembling an option are treated as positional parameters without `UnmatchedArgumentException`:
Picocli 3.0 introduced a `CommandLine.setUnmatchedOptionsArePositionalParams(boolean)` method that can be used to
force the parser to treat arguments resembling an option as positional parameters. For example:

----
<command> -- -x -a AAA
<command> -x -a AAA
----
Everything following the `--` end-of-options delimiter is treated as positional parameters by the parser, so
the above input is treated as three positional parameters.
When `unmatchedOptionsArePositionalParams` is set to `true`, the unknown option `-x` is treated as a positional parameter.
The next argument `-a` is recognized and processed as a known option like you would expect.

=== Stop At Unmatched
From picocli 2.3, applications can call `CommandLine.setStopAtUnmatched(true)` to force the parser to stop interpreting
Expand Down
38 changes: 37 additions & 1 deletion src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,35 @@ public CommandLine setStopAtUnmatched(boolean newValue) {
return this;
}

/** Returns whether arguments on the command line that resemble an option should be treated as positional parameters.
* The default is {@code false} and the parser behaviour depends on {@link #isUnmatchedArgumentsAllowed()}.
* @return {@code true} arguments on the command line that resemble an option should be treated as positional parameters, {@code false} otherwise
* @see #getUnmatchedArguments()
* @since 3.0
*/
public boolean isUnmatchedOptionsArePositionalParams() {
return getCommandSpec().parser().unmatchedOptionsArePositionalParams();
}

/** Sets whether arguments on the command line that resemble an option should be treated as positional parameters.
* <p>The specified setting will be registered with this {@code CommandLine} and the full hierarchy of its
* subcommands and nested sub-subcommands <em>at the moment this method is called</em>. Subcommands added
* later will have the default setting. To ensure a setting is applied to all
* subcommands, call the setter last, after adding subcommands.</p>
* @param newValue the new setting. When {@code true}, arguments on the command line that resemble an option should be treated as positional parameters.
* @return this {@code CommandLine} object, to allow method chaining
* @since 3.0
* @see #getUnmatchedArguments()
* @see #isUnmatchedArgumentsAllowed
*/
public CommandLine setUnmatchedOptionsArePositionalParams(boolean newValue) {
getCommandSpec().parser().unmatchedOptionsArePositionalParams(newValue);
for (CommandLine command : getCommandSpec().subcommands().values()) {
command.setUnmatchedOptionsArePositionalParams(newValue);
}
return this;
}

/** Returns whether the end user may specify arguments on the command line that are not matched to any option or parameter fields.
* The default is {@code false} and a {@link UnmatchedArgumentException} is thrown if this happens.
* When {@code true}, the last unmatched arguments are available via the {@link #getUnmatchedArguments()} method.
Expand Down Expand Up @@ -3220,6 +3249,7 @@ public static class ParserSpec {
private boolean expandAtFiles = true;
private boolean posixClusteredShortOptionsAllowed = true;
private boolean arityRestrictsCumulativeSize = false;
private boolean unmatchedOptionsArePositionalParams = false;

/** Returns the String to use as the separator between options and option parameters. {@code "="} by default,
* initialized from {@link Command#separator()} if defined.*/
Expand All @@ -3232,6 +3262,7 @@ public static class ParserSpec {
public boolean expandAtFiles() { return expandAtFiles; }
public boolean posixClusteredShortOptionsAllowed() { return posixClusteredShortOptionsAllowed; }
public boolean arityRestrictsCumulativeSize() { return arityRestrictsCumulativeSize; }
public boolean unmatchedOptionsArePositionalParams() { return unmatchedOptionsArePositionalParams; }

/** Sets the String to use as the separator between options and option parameters.
* @return this ParserSpec for method chaining */
Expand All @@ -3241,8 +3272,9 @@ public static class ParserSpec {
public ParserSpec overwrittenOptionsAllowed(boolean overwrittenOptionsAllowed) { this.overwrittenOptionsAllowed = overwrittenOptionsAllowed; return this; }
public ParserSpec unmatchedArgumentsAllowed(boolean unmatchedArgumentsAllowed) { this.unmatchedArgumentsAllowed = unmatchedArgumentsAllowed; return this; }
public ParserSpec expandAtFiles(boolean expandAtFiles) { this.expandAtFiles = expandAtFiles; return this; }
public ParserSpec posixClusteredShortOptionsAllowed(boolean posixClusteredShortOptionsAllowed) { this.posixClusteredShortOptionsAllowed = posixClusteredShortOptionsAllowed; return this; }
public ParserSpec arityRestrictsCumulativeSize(boolean arityRestrictsCumulativeSize) { this.arityRestrictsCumulativeSize = arityRestrictsCumulativeSize; return this; }
public ParserSpec posixClusteredShortOptionsAllowed(boolean posixClusteredShortOptionsAllowed) { this.posixClusteredShortOptionsAllowed = posixClusteredShortOptionsAllowed; return this; }
public ParserSpec unmatchedOptionsArePositionalParams(boolean unmatchedOptionsArePositionalParams) { this.unmatchedOptionsArePositionalParams = unmatchedOptionsArePositionalParams; return this; }
void initSeparator(String value) { if (initializable(separator, value, DEFAULT_SEPARATOR)) {separator = value;} }
public String toString() {
return String.format("posixClusteredShortOptionsAllowed=%s, stopAtPositional=%s, stopAtUnmatched=%s, separator=%s, overwrittenOptionsAllowed=%s, unmatchedArgumentsAllowed=%s, expandAtFiles=%s, arityRestrictsCumulativeSize=%s",
Expand Down Expand Up @@ -4663,6 +4695,10 @@ else if (config().posixClusteredShortOptionsAllowed() && arg.length() > 2 && arg
}
}
private boolean resemblesOption(String arg) {
if (commandSpec.parser().unmatchedOptionsArePositionalParams()) {
if (tracer.isDebug()) {tracer.debug("Parser is configured to treat all unmatched options as positional parameter%n", arg);}
return false;
}
if (commandSpec.options().isEmpty()) {
boolean result = arg.startsWith("-");
if (tracer.isDebug()) {tracer.debug("%s %s an option%n", arg, (result ? "resembles" : "doesn't resemble"));}
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/picocli/CommandLineModelTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ public void testOptionSpecRequiresAtLeastOneName() throws Exception {
}

@Test
public void testConversion() {
public void testConversion_TODO() {
// TODO convertion with aux types (abstract field types, generic map with and without explicit type attribute etc)
}

Expand Down
80 changes: 80 additions & 0 deletions src/test/java/picocli/CommandLineTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,86 @@ public void testParserArityRestrictsCumulativeSize_AfterSubcommandsAdded() {
assertTrue(grandChildCount > 0);
}

@Test
public void testParserUnmatchedOptionsArePositionalParams_BeforeSubcommandsAdded() {
@Command class TopLevel {}
CommandLine commandLine = new CommandLine(new TopLevel());
assertEquals(false, commandLine.isUnmatchedOptionsArePositionalParams());
commandLine.setUnmatchedOptionsArePositionalParams(true);
assertEquals(true, commandLine.isUnmatchedOptionsArePositionalParams());

int childCount = 0;
int grandChildCount = 0;
commandLine.addSubcommand("main", createNestedCommand());
for (CommandLine sub : commandLine.getSubcommands().values()) {
childCount++;
assertEquals("subcommand added afterwards is not impacted", false, sub.isUnmatchedOptionsArePositionalParams());
for (CommandLine subsub : sub.getSubcommands().values()) {
grandChildCount++;
assertEquals("subcommand added afterwards is not impacted", false, subsub.isUnmatchedOptionsArePositionalParams());
}
}
assertTrue(childCount > 0);
assertTrue(grandChildCount > 0);
}

@Test
public void testParserUnmatchedOptionsArePositionalParams_AfterSubcommandsAdded() {
@Command class TopLevel {}
CommandLine commandLine = new CommandLine(new TopLevel());
commandLine.addSubcommand("main", createNestedCommand());
assertEquals(false, commandLine.isUnmatchedOptionsArePositionalParams());
commandLine.setUnmatchedOptionsArePositionalParams(true);
assertEquals(true, commandLine.isUnmatchedOptionsArePositionalParams());

int childCount = 0;
int grandChildCount = 0;
for (CommandLine sub : commandLine.getSubcommands().values()) {
childCount++;
assertEquals("subcommand added before IS impacted", true, sub.isUnmatchedOptionsArePositionalParams());
for (CommandLine subsub : sub.getSubcommands().values()) {
grandChildCount++;
assertEquals("subsubcommand added before IS impacted", true, sub.isUnmatchedOptionsArePositionalParams());
}
}
assertTrue(childCount > 0);
assertTrue(grandChildCount > 0);
}

@Test
public void testParserUnmatchedOptionsArePositionalParams_False_unmatchedOptionThrowsUnmatchedArgumentException() {
class App {
@Option(names = "-a") String alpha;
@Parameters String[] remainder;
}
CommandLine app = new CommandLine(new App());
try {
app.parseArgs("-x", "-a", "AAA");
fail("Expected exception");
} catch (UnmatchedArgumentException ok) {
assertEquals("Unmatched argument [-x]", ok.getMessage());
}
}

@Test
public void testParserUnmatchedOptionsArePositionalParams_True_unmatchedOptionIsPositionalParam() {
class App {
@Option(names = "-a") String alpha;
@Parameters String[] remainder;
}
App app = new App();
CommandLine cmd = new CommandLine(app);
cmd.setUnmatchedOptionsArePositionalParams(true);
ParseResult parseResult = cmd.parseArgs("-x", "-a", "AAA");
assertTrue(parseResult.hasPositional(0));
assertArrayEquals(new String[]{"-x"}, parseResult.positionalValue(0, new String[0]));
assertTrue(parseResult.hasOption("a"));
assertEquals("AAA", parseResult.optionValue("a", null));

assertArrayEquals(new String[]{"-x"}, app.remainder);
assertEquals("AAA", app.alpha);
}

@Test
public void testOptionsMixedWithParameters() {
CompactFields compact = CommandLine.populateCommand(new CompactFields(), "-r -v p1 -o out p2".split(" "));
Expand Down

0 comments on commit ccd2b97

Please sign in to comment.