Skip to content

Commit 95e2829

Browse files
committed
Revisit positional arguments
- Add better mapping logic - Add better type conversion - More docs for arity and positional option configuration - Fixes #616
1 parent b107868 commit 95e2829

File tree

8 files changed

+266
-20
lines changed

8 files changed

+266
-20
lines changed

spring-shell-core/src/main/java/org/springframework/shell/command/CommandParser.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -269,7 +269,10 @@ public CommandParserResults parse(List<CommandOption> options, String[] args) {
269269
// don't do anything if first arg looks like an option as if we are here
270270
// then we'd might remove wrong required option
271271
if (!oargs.isEmpty() && !oargs.get(0).startsWith("-")) {
272-
results.add(new DefaultCommandParserResult(o, oargs.stream().collect(Collectors.joining(" "))));
272+
// as we now have a candicate option, try to see if there is a
273+
// conversion we can do and the use it.
274+
Object value = convertOptionType(o, oargs);
275+
results.add(new DefaultCommandParserResult(o, value));
273276
requiredOptions.remove(o);
274277
}
275278
});
@@ -285,6 +288,15 @@ public CommandParserResults parse(List<CommandOption> options, String[] args) {
285288
return new DefaultCommandParserResults(results, positional, errors);
286289
}
287290

291+
private Object convertOptionType(CommandOption option, Object value) {
292+
if (conversionService != null && option.getType() != null && value != null) {
293+
if (conversionService.canConvert(value.getClass(), option.getType().getRawClass())) {
294+
value = conversionService.convert(value, option.getType().getRawClass());
295+
}
296+
}
297+
return value;
298+
}
299+
288300
private static class ParserResult {
289301
private CommandOption option;
290302
private List<String> args;
@@ -335,12 +347,7 @@ ParserResults visit(List<List<String>> lexerResults, List<CommandOption> options
335347
if (holder.error != null) {
336348
return Stream.of(ParserResult.of(o, subArgs, null, holder.error));
337349
}
338-
Object value = holder.value;
339-
if (conversionService != null && o.getType() != null && value != null) {
340-
if (conversionService.canConvert(value.getClass(), o.getType().getRawClass())) {
341-
value = conversionService.convert(value, o.getType().getRawClass());
342-
}
343-
}
350+
Object value = convertOptionType(o, holder.value);
344351
Stream<ParserResult> unmapped = holder.unmapped.stream()
345352
.map(um -> ParserResult.of(null, Arrays.asList(um), null, null));
346353
Stream<ParserResult> res = Stream.of(ParserResult.of(o, subArgs, value, null));

spring-shell-core/src/test/java/org/springframework/shell/command/AbstractCommandTests.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -60,6 +60,8 @@ protected static class Pojo1 {
6060
public int method7Arg3;
6161
public int method8Count;
6262
public float[] method8Arg1;
63+
public int method9Count;
64+
public String[] method9Arg1;
6365

6466
public void method1(CommandContext ctx) {
6567
method1Ctx = ctx;
@@ -107,5 +109,10 @@ public void method8(float[] arg1) {
107109
method8Arg1 = arg1;
108110
method8Count++;
109111
}
112+
113+
public void method9(String[] arg1) {
114+
method9Arg1 = arg1;
115+
method9Count++;
116+
}
110117
}
111118
}

spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -195,7 +195,47 @@ public void testMethodMultiPositionalArgsAll() {
195195
.build();
196196
execution.evaluate(r1, new String[]{"myarg1value1", "myarg1value2"});
197197
assertThat(pojo1.method4Count).isEqualTo(1);
198-
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value1 myarg1value2");
198+
assertThat(pojo1.method4Arg1).isEqualTo("myarg1value1,myarg1value2");
199+
}
200+
201+
@Test
202+
public void testMethodMultiPositionalArgsAllToArray1() {
203+
CommandRegistration r1 = CommandRegistration.builder()
204+
.command("command1")
205+
.description("help")
206+
.withOption()
207+
.longNames("arg1")
208+
.description("some arg1")
209+
.position(0)
210+
.arity(OptionArity.ONE_OR_MORE)
211+
.and()
212+
.withTarget()
213+
.method(pojo1, "method9")
214+
.and()
215+
.build();
216+
execution.evaluate(r1, new String[]{"myarg1value1", "myarg1value2"});
217+
assertThat(pojo1.method9Count).isEqualTo(1);
218+
assertThat(pojo1.method9Arg1).isEqualTo(new String[] { "myarg1value1", "myarg1value2" });
219+
}
220+
221+
@Test
222+
public void testMethodMultiPositionalArgsAllToArray2() {
223+
CommandRegistration r1 = CommandRegistration.builder()
224+
.command("command1")
225+
.description("help")
226+
.withOption()
227+
.longNames("arg1")
228+
.description("some arg1")
229+
.position(0)
230+
.arity(OptionArity.ONE_OR_MORE)
231+
.and()
232+
.withTarget()
233+
.method(pojo1, "method8")
234+
.and()
235+
.build();
236+
execution.evaluate(r1, new String[]{"1", "2"});
237+
assertThat(pojo1.method8Count).isEqualTo(1);
238+
assertThat(pojo1.method8Arg1).isEqualTo(new float[] { 1, 2 });
199239
}
200240

201241
@Test

spring-shell-core/src/test/java/org/springframework/shell/command/CommandParserTests.java

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -198,7 +198,8 @@ public void testNonMappedArgWithoutOption() {
198198
CommandParserResults results = parser.parse(options, args);
199199
assertThat(results.results()).hasSize(1);
200200
assertThat(results.results().get(0).option()).isSameAs(option1);
201-
assertThat(results.results().get(0).value()).isEqualTo("value foo");
201+
// no type so we get raw list
202+
assertThat(results.results().get(0).value()).isEqualTo(Arrays.asList("value", "foo"));
202203
assertThat(results.positional()).containsExactly("value", "foo");
203204
}
204205

@@ -210,7 +211,7 @@ public void testNonMappedArgWithoutOptionHavingType() {
210211
CommandParserResults results = parser.parse(options, args);
211212
assertThat(results.results()).hasSize(1);
212213
assertThat(results.results().get(0).option()).isSameAs(option1);
213-
assertThat(results.results().get(0).value()).isEqualTo("value foo");
214+
assertThat(results.results().get(0).value()).isEqualTo("value,foo");
214215
assertThat(results.positional()).containsExactly("value", "foo");
215216
}
216217

@@ -331,10 +332,35 @@ public void testArityErrors() {
331332
assertThat(results2.results().get(0).value()).isNull();
332333
}
333334

335+
@Test
336+
public void testMapToIntArray() {
337+
CommandOption option1 = CommandOption.of(
338+
new String[] { "arg1" },
339+
null,
340+
null,
341+
ResolvableType.forType(int[].class),
342+
false,
343+
null,
344+
0,
345+
1,
346+
2,
347+
null,
348+
null);
349+
350+
351+
List<CommandOption> options = Arrays.asList(option1);
352+
String[] args = new String[]{"1", "2"};
353+
CommandParserResults results = parser.parse(options, args);
354+
assertThat(results.errors()).hasSize(0);
355+
assertThat(results.results()).hasSize(1);
356+
assertThat(results.results().get(0).option()).isSameAs(option1);
357+
assertThat(results.results().get(0).value()).isEqualTo(new int[] { 1, 2 });
358+
}
359+
334360
@Test
335361
public void testMapPositionalArgs1() {
336-
CommandOption option1 = longOption("arg1", 0, 1, 1);
337-
CommandOption option2 = longOption("arg2", 1, 1, 2);
362+
CommandOption option1 = longOption("arg1", ResolvableType.forType(String.class), false, 0, 1, 1);
363+
CommandOption option2 = longOption("arg2", ResolvableType.forType(String.class), false, 1, 1, 2);
338364
List<CommandOption> options = Arrays.asList(option1, option2);
339365
String[] args = new String[]{"--arg1", "1", "2"};
340366
CommandParserResults results = parser.parse(options, args);
@@ -346,9 +372,25 @@ public void testMapPositionalArgs1() {
346372
}
347373

348374
@Test
349-
public void testMapPositionalArgs2() {
375+
public void testMapPositionalArgs11() {
350376
CommandOption option1 = longOption("arg1", 0, 1, 1);
351377
CommandOption option2 = longOption("arg2", 1, 1, 2);
378+
List<CommandOption> options = Arrays.asList(option1, option2);
379+
String[] args = new String[]{"--arg1", "1", "2"};
380+
CommandParserResults results = parser.parse(options, args);
381+
assertThat(results.results()).hasSize(2);
382+
assertThat(results.results().get(0).option()).isSameAs(option1);
383+
assertThat(results.results().get(1).option()).isSameAs(option2);
384+
assertThat(results.results().get(0).value()).isEqualTo("1");
385+
// no type so we get raw list
386+
assertThat(results.results().get(1).value()).isEqualTo(Arrays.asList("2"));
387+
}
388+
389+
@Test
390+
public void testMapPositionalArgs2() {
391+
CommandOption option1 = longOption("arg1", ResolvableType.forType(String.class), false, 0, 1, 1);
392+
CommandOption option2 = longOption("arg2", ResolvableType.forType(String.class), false, 1, 1, 2);
393+
352394
List<CommandOption> options = Arrays.asList(option1, option2);
353395
String[] args = new String[]{"1", "2"};
354396
CommandParserResults results = parser.parse(options, args);

spring-shell-docs/src/main/asciidoc/using-shell-options-arity.adoc

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
44

55
Sometimes, you want to have more fine control of how many parameters with an option
66
are processed when parsing operations happen. Arity is defined as min and max
7-
values, where min must be a positive integer and max has to be more or equal to min.
7+
values, where min must be zero or a positive integer and max has to be more or equal to min.
88

99
====
1010
[source, java, indent=0]
@@ -14,7 +14,7 @@ include::{snippets}/OptionSnippets.java[tag=option-registration-arityints]
1414
====
1515

1616
Arity can also be defined as an `OptionArity` enum, which are shortcuts
17-
within the following table:
17+
shown in below table <<using-shell-options-arity-optionarity-table>>.
1818

1919
====
2020
[source, java, indent=0]
@@ -23,6 +23,7 @@ include::{snippets}/OptionSnippets.java[tag=option-registration-arityenum]
2323
----
2424
====
2525

26+
[[using-shell-options-arity-optionarity-table]]
2627
.OptionArity
2728
|===
2829
|Value |min/max
@@ -51,3 +52,35 @@ The annotation model supports defining only the max value of an arity.
5152
include::{snippets}/OptionSnippets.java[tag=option-with-annotation-arity]
5253
----
5354
====
55+
56+
One of a use cases to manually define arity is to impose restrictions how
57+
many parameters option accepts.
58+
59+
====
60+
[source, java, indent=0]
61+
----
62+
include::{snippets}/OptionSnippets.java[tag=option-registration-aritystrings-sample]
63+
----
64+
====
65+
66+
In above example we have option _arg1_ and it's defined as type _String[]_. Arity
67+
defines that it needs at least 1 parameter and not more that 2. As seen in below
68+
spesific exceptions _TooManyArgumentsOptionException_ and
69+
_NotEnoughArgumentsOptionException_ are thrown to indicate arity mismatch.
70+
71+
====
72+
[source, bash]
73+
----
74+
shell:>e2e reg arity-errors --arg1
75+
Not enough arguments --arg1 requires at least 1.
76+
77+
shell:>e2e reg arity-errors --arg1 one
78+
Hello [one]
79+
80+
shell:>e2e reg arity-errors --arg1 one two
81+
Hello [one, two]
82+
83+
shell:>e2e reg arity-errors --arg1 one two three
84+
Too many arguments --arg1 requires at most 2.
85+
----
86+
====

spring-shell-docs/src/main/asciidoc/using-shell-options-positional.adoc

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,61 @@ Positional information is mostly related to a command target method:
1010
include::{snippets}/OptionSnippets.java[tag=option-registration-positional]
1111
----
1212
====
13+
14+
NOTE: Be careful with positional parameters as it may soon
15+
become confusing which options those are mapped to.
16+
17+
Usually arguments are mapped to an option when those are defined in a
18+
command line whether it's a long or short option. Generally speaking
19+
there are _options_, _option arguments_ and _arguments_ where latter
20+
are the ones which are not mapped to any spesific option.
21+
22+
Unrecognised arguments can then have a secondary mapping logic where
23+
positional information is important. With option position you're
24+
essentially telling command parsing how to interpret plain raw
25+
ambiguous arguments.
26+
27+
Let's look what happens when we don't define a position.
28+
29+
====
30+
[source, java, indent=0]
31+
----
32+
include::{snippets}/OptionSnippets.java[tag=option-registration-aritystrings-noposition]
33+
----
34+
====
35+
36+
Option _arg1_ is required and there is no info what to do with argument
37+
`one` resulting error for missing option.
38+
39+
====
40+
[source, bash]
41+
----
42+
shell:>arity-strings-1 one
43+
Missing mandatory option --arg1.
44+
----
45+
====
46+
47+
Now let's define a position `0`.
48+
49+
====
50+
[source, java, indent=0]
51+
----
52+
include::{snippets}/OptionSnippets.java[tag=option-registration-aritystrings-position]
53+
----
54+
====
55+
56+
Arguments are processed until we get up to 2 arguments.
57+
58+
====
59+
[source, bash]
60+
----
61+
shell:>arity-strings-2 one
62+
Hello [one]
63+
64+
shell:>arity-strings-2 one two
65+
Hello [one, two]
66+
67+
shell:>arity-strings-2 one two three
68+
Hello [one, two]
69+
----
70+
====

0 commit comments

Comments
 (0)