5. Options
Command line arguments can be separated into options and positional parameters. Following sections describes features how options are defined and used.
5.1. Definition
Options can be defined within a target method as annotations in a method arguments
or with programmatically with CommandRegistration
.
Having a target method with argument is automatically registered with a matching argument name.
public String example(String arg1) {
return "Hello " + arg1;
}
@ShellOption
annotation can be used to define an option name if you
don’t want it to be same as argument name.
public String example(@ShellOption(value = { "--argx" }) String arg1) {
return "Hello " + arg1;
}
If option name is defined without prefix, either -
or --
, it is discovered
from ShellMethod#prefix.
public String example(@ShellOption(value = { "argx" }) String arg1) {
return "Hello " + arg1;
}
Programmatic way with CommandRegistration
is to use method adding a long name.
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.and()
.build();
5.2. Short Format
Short style POSIX option in most is just a synonym to long format but
adds additional feature to combine those options together. Having short
options a, b, c can be used as -abc
.
Programmatically short option is defined by using short name function.
CommandRegistration.builder()
.withOption()
.shortNames('a')
.and()
.withOption()
.shortNames('b')
.and()
.withOption()
.shortNames('c')
.and()
.build();
Short option with combined format is powerful if type is defined as a flag
which means type is a boolean. That way you can define a presense of a flags
as -abc
, -abc true
or -abc false
.
CommandRegistration.builder()
.withOption()
.shortNames('a')
.type(boolean.class)
.and()
.withOption()
.shortNames('b')
.type(boolean.class)
.and()
.withOption()
.shortNames('c')
.type(boolean.class)
.and()
.build();
With annotation model you can define short argument directly.
public String example(
@ShellOption(value = { "-a" }) String arg1,
@ShellOption(value = { "-b" }) String arg2,
@ShellOption(value = { "-c" }) String arg3
) {
return "Hello " + arg1;
}
5.3. Arity
Sometimes, you want to have more fine control of how many parameters with an option are processed when parsing operations happen. Arity is defined as min and max values, where min must be zero or a positive integer and max has to be more or equal to min.
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.arity(0, 1)
.and()
.build();
Arity can also be defined as an OptionArity
enum, which are shortcuts
shown in below table OptionArity.
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.arity(OptionArity.EXACTLY_ONE)
.and()
.build();
Value | min/max |
---|---|
ZERO |
0 / 0 |
ZERO_OR_ONE |
0 / 1 |
EXACTLY_ONE |
1 / 1 |
ZERO_OR_MORE |
0 / Integer MAX |
ONE_OR_MORE |
1 / Integer MAX |
The annotation model supports defining only the max value of an arity.
public String example(@ShellOption(arity = 1) String arg1) {
return "Hello " + arg1;
}
One of a use cases to manually define arity is to impose restrictions how many parameters option accepts.
CommandRegistration.builder()
.command("arity-errors")
.withOption()
.longNames("arg1")
.type(String[].class)
.required()
.arity(1, 2)
.and()
.withTarget()
.function(ctx -> {
String[] arg1 = ctx.getOptionValue("arg1");
return "Hello " + Arrays.asList(arg1);
})
.and()
.build();
In above example we have option arg1 and it’s defined as type String[]. Arity defines that it needs at least 1 parameter and not more that 2. As seen in below spesific exceptions TooManyArgumentsOptionException and NotEnoughArgumentsOptionException are thrown to indicate arity mismatch.
shell:>e2e reg arity-errors --arg1
Not enough arguments --arg1 requires at least 1.
shell:>e2e reg arity-errors --arg1 one
Hello [one]
shell:>e2e reg arity-errors --arg1 one two
Hello [one, two]
shell:>e2e reg arity-errors --arg1 one two three
Too many arguments --arg1 requires at most 2.
5.4. Positional
Positional information is mostly related to a command target method:
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.position(0)
.and()
.build();
Be careful with positional parameters as it may soon become confusing which options those are mapped to. |
Usually arguments are mapped to an option when those are defined in a command line whether it’s a long or short option. Generally speaking there are options, option arguments and arguments where latter are the ones which are not mapped to any spesific option.
Unrecognised arguments can then have a secondary mapping logic where positional information is important. With option position you’re essentially telling command parsing how to interpret plain raw ambiguous arguments.
Let’s look what happens when we don’t define a position.
CommandRegistration.builder()
.command("arity-strings-1")
.withOption()
.longNames("arg1")
.required()
.type(String[].class)
.arity(0, 2)
.and()
.withTarget()
.function(ctx -> {
String[] arg1 = ctx.getOptionValue("arg1");
return "Hello " + Arrays.asList(arg1);
})
.and()
.build();
Option arg1 is required and there is no info what to do with argument
one
resulting error for missing option.
shell:>arity-strings-1 one
Missing mandatory option --arg1.
Now let’s define a position 0
.
CommandRegistration.builder()
.command("arity-strings-2")
.withOption()
.longNames("arg1")
.required()
.type(String[].class)
.arity(0, 2)
.position(0)
.and()
.withTarget()
.function(ctx -> {
String[] arg1 = ctx.getOptionValue("arg1");
return "Hello " + Arrays.asList(arg1);
})
.and()
.build();
Arguments are processed until we get up to 2 arguments.
shell:>arity-strings-2 one
Hello [one]
shell:>arity-strings-2 one two
Hello [one, two]
shell:>arity-strings-2 one two three
Hello [one, two]
5.5. Optional Value
An option is either required or not and, generally speaking, how it behaves depends on a command target:
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.required()
.and()
.build();
In the annotation model, there is no direct way to define if argument is
optional. Instead, it is instructed to be NULL
:
public String example(
@ShellOption(defaultValue = ShellOption.NULL) String arg1
) {
return "Hello " + arg1;
}
5.6. Default Value
Having a default value for an option is somewhat related to Optional Value, as there are cases where you may want to know if the user defined an option and change behavior based on a default value:
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.defaultValue("defaultValue")
.and()
.build();
The annotation model also supports defining default values:
public String example(
@ShellOption(defaultValue = "defaultValue") String arg1
) {
return "Hello " + arg1;
}
5.7. Validation
Spring Shell integrates with the Bean Validation API to support automatic and self-documenting constraints on command parameters.
Annotations found on command parameters and annotations at the method level are honored and trigger validation prior to the command executing. Consider the following command:
@ShellMethod("Change password.")
public String changePassword(@Size(min = 8, max = 40) String password) {
return "Password successfully set to " + password;
}
From the preceding example, you get the following behavior for free:
shell:>change-password hello The following constraints were not met: --password string : size must be between 8 and 40 (You passed 'hello')
5.8. Label
Option Label has no functional behaviour within a shell itself other than
what a default help
command outputs. Within a command documentation
a type of an option is documented but this is not always super useful. Thus
you may want to give better descriptive word for an option.
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.and()
.withOption()
.longNames("arg2")
.label("MYLABEL")
.and()
.build();
Defining label is then shown in help
.
my-shell:>help mycommand
NAME
mycommand -
SYNOPSIS
mycommand --arg1 String --arg2 MYLABEL
OPTIONS
--arg1 String
[Optional]
--arg2 MYLABEL
[Optional]
5.9. Types
This section talks about how particular data type is used as an option value.
5.9.1. String
String
is a most simplest type as there’s no conversion involved as what’s
coming in from a user is always a string.
String example(@ShellOption(value = "arg1") String arg1) {
return "Hello " + arg1;
}
While it’s not strictly required to define type as a String
it’s always
adviced to do so.
CommandRegistration.builder()
.command("example")
.withOption()
.longNames("arg1")
.type(String.class)
.required()
.and()
.withTarget()
.function(ctx -> {
String arg1 = ctx.getOptionValue("arg1");
return "Hello " + arg1;
})
.and()
.build();
5.9.2. Boolean
Using boolean types is a bit more involved as there are boolean
and
Boolean
where latter can be null. Boolean types are usually used as
flags meaning argument value may not be needed.
String example(
@ShellOption() boolean arg1,
@ShellOption(defaultValue = "true") boolean arg2,
@ShellOption(defaultValue = "false") boolean arg3,
@ShellOption() Boolean arg4,
@ShellOption(defaultValue = "true") Boolean arg5,
@ShellOption(defaultValue = "false") Boolean arg6
) {
return String.format("arg1=%s arg2=%s arg3=%s arg4=%s arg5=%s arg6=%s",
arg1, arg2, arg3, arg4, arg5, arg6);
}
shell:>example
arg1=false arg2=true arg3=false arg4=false arg5=true arg6=false
shell:>example --arg4
arg1=false arg2=true arg3=false arg4=true arg5=true arg6=false
shell:>example --arg4 false
arg1=false arg2=true arg3=false arg4=false arg5=true arg6=false
CommandRegistration.builder()
.command("example")
.withOption()
.longNames("arg1").type(boolean.class).and()
.withOption()
.longNames("arg2").type(boolean.class).defaultValue("true").and()
.withOption()
.longNames("arg3").type(boolean.class).defaultValue("false").and()
.withOption()
.longNames("arg4").type(Boolean.class).and()
.withOption()
.longNames("arg5").type(Boolean.class).defaultValue("true").and()
.withOption()
.longNames("arg6").type(Boolean.class).defaultValue("false").and()
.withTarget()
.function(ctx -> {
boolean arg1 = ctx.hasMappedOption("arg1")
? ctx.getOptionValue("arg1")
: false;
boolean arg2 = ctx.getOptionValue("arg2");
boolean arg3 = ctx.getOptionValue("arg3");
Boolean arg4 = ctx.getOptionValue("arg4");
Boolean arg5 = ctx.getOptionValue("arg5");
Boolean arg6 = ctx.getOptionValue("arg6");
return String.format("Hello arg1=%s arg2=%s arg3=%s arg4=%s arg5=%s arg6=%s",
arg1, arg2, arg3, arg4, arg5, arg6);
})
.and()
.build();
shell:>example
arg1=false arg2=true arg3=false arg4=null arg5=true arg6=false
shell:>example --arg4
arg1=false arg2=true arg3=false arg4=true arg5=true arg6=false
shell:>example --arg4 false
arg1=false arg2=true arg3=false arg4=false arg5=true arg6=false
5.9.3. Number
Numbers are converted as is.
String example(@ShellOption(value = "arg1") int arg1) {
return "Hello " + arg1;
}
CommandRegistration.builder()
.command("example")
.withOption()
.longNames("arg1")
.type(int.class)
.required()
.and()
.withTarget()
.function(ctx -> {
boolean arg1 = ctx.getOptionValue("arg1");
return "Hello " + arg1;
})
.and()
.build();
5.9.4. Enum
Conversion to enums is possible if given value is exactly matching enum itself. Currently you can convert assuming case insensitivity.
enum OptionTypeEnum {
ONE,TWO,THREE
}
String example(@ShellOption(value = "arg1") OptionTypeEnum arg1) {
return "Hello " + arg1;
}
CommandRegistration.builder()
.command("example")
.withOption()
.longNames("arg1")
.type(OptionTypeEnum.class)
.required()
.and()
.withTarget()
.function(ctx -> {
OptionTypeEnum arg1 = ctx.getOptionValue("arg1");
return "Hello " + arg1;
})
.and()
.build();
5.9.5. Array
Arrays can be used as is with strings and primitive types.
String example(@ShellOption(value = "arg1") String[] arg1) {
return "Hello " + arg1;
}
CommandRegistration.builder()
.command("example")
.withOption()
.longNames("arg1")
.type(String[].class)
.required()
.and()
.withTarget()
.function(ctx -> {
String[] arg1 = ctx.getOptionValue("arg1");
return "Hello " + arg1;
})
.and()
.build();
5.10. Naming
If there is a need to modify option long names that can be done
using OptionNameModifier
interface which is a simple
Function<String, String>
. In this interface original option
name goes in and modified name comes out.
Modifier can be defined per OptionSpec
in CommandRegistration
,
defaulting globally as bean or via configuration properties.
Modifier defined manually in OptionSpec
takes takes precedence
over one defined globally. There is no global modifier defined
on default.
You can define one with an option in CommandRegistration
.
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.nameModifier(name -> "x" + name)
.and()
.build();
Add one singleton bean as type OptionNameModifier
and that becomes
a global default.
@Bean
OptionNameModifier sampleOptionNameModifier() {
return name -> "x" + name;
}
It’s also possible to just add configuration property with
spring.shell.option.naming.case-type
which auto-configures
one based on a type defined.
noop
is to do nothing, camel
, snake
, kebab
, pascal
activates build-in modifiers for camelCase
, snake_case
,
kebab-case
or PascalCase
respectively.
If creating CommandRegistration beans directly, global
default via configuration properies only work if using
pre-configured Builder instance. See more
Programmatic Model.
|
spring:
shell:
option:
naming:
case-type: noop
# case-type: camel
# case-type: snake
# case-type: kebab
# case-type: pascal
For example options defined in an annotated method like this.
@ShellMethod(key = "option-naming-sample")
public void optionNamingSample(
@ShellOption("from_snake") String snake,
@ShellOption("fromCamel") String camel,
@ShellOption("from-kebab") String kebab,
@ShellOption("FromPascal") String pascal
) {}
On default help
for that command shows names coming
directly from @ShellOption
.
OPTIONS
--from_snake String
[Mandatory]
--fromCamel String
[Mandatory]
--from-kebab String
[Mandatory]
--FromPascal String
[Mandatory]
Define spring.shell.option.naming.case-type=kebab
and default
modifier is added and option names then look like.
OPTIONS
--from-snake String
[Mandatory]
--from-camel String
[Mandatory]
--from-kebab String
[Mandatory]
--from-pascal String
[Mandatory]