2.1.0-M4
© 2017 - 2022 VMware, Inc.
Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
1. What is Spring Shell?
Not all applications need a fancy web user interface. Sometimes, interacting with an application through an interactive terminal is the most appropriate way to get things done.
Spring Shell lets you create such a runnable application, where the user enters textual commands that are run until the program terminates. The Spring Shell project provides the infrastructure to create such a REPL (Read, Eval, Print Loop) application, letting you concentrate on implementing commands by using the familiar Spring programming model.
Spring Shell includes advanced features (such as parsing, tab completion, colorization of output, fancy ASCII-art table display, input conversion, and validation), freeing you to focus on core command logic.
2. Getting Started
To see what Spring Shell has to offer, we can write a trivial shell application that has a simple command to add two numbers.
2.1. Writing a Simple Boot Application
Starting with version 2, Spring Shell has been rewritten from the ground up with various enhancements in mind, one of which is easy integration with Spring Boot.
For the purpose of this tutorial, we create a simple Boot application by
using start.spring.io. This minimal application depends only on spring-boot-starter
and configures the spring-boot-maven-plugin
to generate an executable über-jar:
...
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
...
</dependencies>
2.2. Adding a Dependency on Spring Shell
The easiest way to get going with Spring Shell is to depend on the {starter-artifactId}
artifact.
This comes with everything you need to use Spring Shell and plays nicely with Boot,
configuring only the necessary beans as needed:
...
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.1.0-M4</version>
</dependency>
...
Given that Spring Shell starts the REPL (Read-Eval-Print-Loop) because this dependency is present,
you need to either skip tests when you build (-DskipTests ) throughout this tutorial or remove the sample integration test
that was generated by start.spring.io. If you do not remove it, the integration test creates
the Spring ApplicationContext and, depending on your build tool, stays stuck in the eval loop or crashes with a NPE.
|
2.3. Your First Command
Now we can add our first command. To do so, create a new class (named whatever you want) and
annotate it with @ShellComponent
(a variation of @Component
that is used to restrict
the set of classes that are scanned for candidate commands).
Then we can create an add
method that takes two ints (a
and b
) and returns their sum. We need to annotate it
with @ShellMethod
and provide a description of the command in the annotation (the only piece of
information that is required):
package com.example.demo;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellComponent;
@ShellComponent
public class MyCommands {
@ShellMethod("Add two integers together.")
public int add(int a, int b) {
return a + b;
}
}
2.4. Trying the Application
To build the application and run the generated jar, run the following command:
./mvnw clean install -DskipTests
[...]
java -jar target/demo-0.0.1-SNAPSHOT.jar
You are greeted by the following screen (the banner comes from Spring Boot and can be customized:
shell:>
A yellow shell:>
prompt invites you to type commands. Type add 1 2
, press ENTER
, and admire the magic:
shell:>add 1 2
3
You should play with the shell (hint: there is a help
command). When you are done, type exit
and press ENTER
.
The rest of this document delves deeper into the whole Spring Shell programming model.
3. Using Spring Shell
This section describes how to use Spring Shell.
Spring Shell 2.1.x is a major rework to bring the codebase up to date with existing Spring Boot versions, adding new features and, especially, making it work with GraalVM which makes command-line applications much more relevant in a Java space. Moving to a new major version also lets us clean up the codebase and make some needed breaking changes. |
3.1. Basics
This section covers the basics of Spring Shell. Before going on to define actual commands and options, we need to go through some of the fundamental concepts of Spring Shell.
Essentially, a few things needs to happen before you have a working Spring Shell application:
-
Create a Spring Boot application.
-
Define commands and options.
-
Package the application.
-
Run the application, either interactively or non-interactively.
You can get a full working Spring Shell application without defining any user-level commands
as some basic built-in commands (such as help
and history
) are provided.
Throughout this documentation, we make references to configuring something by using
annotations (mostly relates to use of The programmatic model is how things are actually registered, even if you use annotations.
The |
3.2. Commands
In this section, we go through an actual command registration and leave command options and execution for later in a documentation. You can find more detailed info in Command Registration.
There are two different ways to define a command: through an annotation model and through a programmatic model. In the annotation model, you define your methods in a class and annotate the class and the methods with specific annotations. In the programmatic model, you use a more low level approach, defining command registrations (either as beans or by dynamically registering with a command catalog).
3.2.1. Annotation Model
When you use the standard API, methods on beans are turned into executable commands, provided that:
-
The bean class bears the
@ShellComponent
annotation. (This is used to restrict the set of beans that are considered.) -
The method bears the
@ShellMethod
annotation.
The You can customize the name of the created bean by using the |
@ShellComponent
static class MyCommands {
@ShellMethod
public void mycommand() {
}
}
The only required attribute of the @ShellMethod
annotation is its value
attribute, which should have
a short, one-sentence, description of what the command does. This lets your users
get consistent help about your commands without having to leave the shell (see Help).
The description of your command should be short — no more than one or two sentences. For better consistency, it should start with a capital letter and end with a period. |
By default, you need not specify the key for your command (that is, the word(s) that should be used
to invoke it in the shell). The name of the method is used as the command key, turning camelCase names into
dashed, gnu-style, names (for example, sayHello()
becomes say-hello
).
You can, however, explicitly set the command key, by using the key
attribute of the annotation:
@ShellMethod(value = "Add numbers.", key = "sum")
public int add(int a, int b) {
return a + b;
}
The key attribute accepts multiple values.
If you set multiple keys for a single method, the command is registered with those different aliases.
|
The command key can contain pretty much any character, including spaces. When coming up with names though, keep in mind that consistency is often appreciated by users. That is, you should avoid mixing dashed-names with spaced names and other inconsistencies. |
3.2.2. Programmatic Model
In the programmatic model, CommandRegistration
is defined as a @Bean
, and it is automatically registered:
@Bean
CommandRegistration commandRegistration() {
return CommandRegistration.builder()
.command("mycommand")
.build();
}
3.2.3. Organizing Commands
When your shell starts to provide a lot of functionality, you may end up
with a lot of commands, which could be confusing for your users. By typing help
,
they would see a daunting list of commands, organized in alphabetical order,
which may not always be the best way to show the available commands.
To alleviate this possible confusion, Spring Shell provides the ability to group commands together,
with reasonable defaults. Related commands would then end up in the same group (for example, User Management Commands
)
and be displayed together in the help screen and other places.
By default, commands are grouped according to the class they are implemented in,
turning the camelCase class name into separate words (so URLRelatedCommands
becomes URL Related Commands
).
This is a sensible default, as related commands are often already in the class anyway,
because they need to use the same collaborating objects.
If, however, this behavior does not suit you, you can override the group for a command in the following ways, in order of priority:
-
Specify a
group()
in the@ShellMethod
annotation. -
Place a
@ShellCommandGroup
on the class in which the command is defined. This applies the group for all commands defined in that class (unless overridden, as explained earlier). -
Place a
@ShellCommandGroup
on the package (throughpackage-info.java
) in which the command is defined. This applies to all the commands defined in the package (unless overridden at the method or class level, as explained earlier).
The following listing shows an example:
public class UserCommands {
@ShellMethod(value = "This command ends up in the 'User Commands' group")
public void foo() {}
@ShellMethod(value = "This command ends up in the 'Other Commands' group",
group = "Other Commands")
public void bar() {}
}
...
@ShellCommandGroup("Other Commands")
public class SomeCommands {
@ShellMethod(value = "This one is in 'Other Commands'")
public void wizz() {}
@ShellMethod(value = "And this one is 'Yet Another Group'",
group = "Yet Another Group")
public void last() {}
}
3.2.4. Dynamic Command Availability
Registered commands do not always make sense, due to the internal state of the application.
For example, there may be a download
command, but it only works once the user has used connect
on a remote
server. Now, if the user tries to use the download
command, the shell should explain that
the command exists but that it is not available at the time.
Spring Shell lets you do that, even letting you provide a short explanation of the reason for
the command not being available.
There are three possible ways for a command to indicate availability.
They all use a no-arg method that returns an instance of Availability
.
Consider the following example:
@ShellComponent
public class MyCommands {
private boolean connected;
@ShellMethod("Connect to the server.")
public void connect(String user, String password) {
[...]
connected = true;
}
@ShellMethod("Download the nuclear codes.")
public void download() {
[...]
}
public Availability downloadAvailability() {
return connected
? Availability.available()
: Availability.unavailable("you are not connected");
}
}
The connect
method is used to connect to the server (details omitted), altering the state
of the command through the connected
boolean when done.
The download
command as marked as unavailable until the user has connected, thanks to the presence
of a method named exactly as the download
command method with the Availability
suffix in its name.
The method returns an instance of Availability
, constructed with one of the two factory methods.
If the command is not available, an explanation has to be provided.
Now, if the user tries to invoke the command while not being connected, here is what happens:
shell:>download
Command 'download' exists but is not currently available because you are not connected.
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
Information about currently unavailable commands is also used in the integrated help. See Help.
The reason provided when the command is not available should read nicely if appended after “Because”. You should not start the sentence with a capital or add a final period |
If naming the availability method after the name of the command method does not suit you, you
can provide an explicit name by using the @ShellMethodAvailability
annotation:
@ShellMethod("Download the nuclear codes.")
@ShellMethodAvailability("availabilityCheck") (1)
public void download() {
[...]
}
public Availability availabilityCheck() { (1)
return connected
? Availability.available()
: Availability.unavailable("you are not connected");
}
1 | the names have to match |
Finally, it is often the case that several commands in the same class share the same internal state and, thus,
should all be available or unavailable as a group. Instead of having to stick the @ShellMethodAvailability
on all command methods, Spring Shell lets you flip things around and put the @ShellMethodAvailabilty
annotation on the availability method, specifying the names of the commands that it controls:
@ShellMethod("Download the nuclear codes.")
public void download() {
[...]
}
@ShellMethod("Disconnect from the server.")
public void disconnect() {
[...]
}
@ShellMethodAvailability({"download", "disconnect"})
public Availability availabilityCheck() {
return connected
? Availability.available()
: Availability.unavailable("you are not connected");
}
The default value for the
|
Spring Shell does not impose many constraints on how to write commands and how to organize classes. However, it is often good practice to put related commands in the same class, and the availability indicators can benefit from that. |
3.3. Options
Command line arguments can be separated into options and positional parameters. Following sections describes features how options are defined and used.
3.3.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;
}
Programmatic way with CommandRegistration
is to use method adding a long name.
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.and()
.build();
3.3.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;
}
3.3.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 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
within the following table:
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;
}
3.3.4. Positional
Positional information is mostly related to a command target method:
CommandRegistration.builder()
.withOption()
.longNames("arg1")
.position(0)
.and()
.build();
3.3.5. Optional Value
An option is either required or not and, generally speaking, how it behavesit 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;
}
3.3.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;
}
3.3.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')
Applies to All Command Implementations
It is important to note that bean validation applies to all command implementations, whether they use the “standard” API or any other API, through the use of an adapter (see Supporting Other APIs). |
3.4. Building
This section covers how to build a Spring Shell application.
3.4.1. Native Support
Version 2.1.x includes experimental support for compiling Spring Shell applications into native applications with GraalVM and Spring Native. Because the underlying JLine library works with GraalVM, most things should just work.
You can compile the project with a native profile to get a native application:
$ ./mvnw clean package -Pnative
You can then run the application in either interactive or non-interactive mode:
$ ./spring-shell-samples/target/spring-shell-samples help AVAILABLE COMMANDS Built-In Commands completion bash: Generate bash completion script help: Display help about available commands. history: Display or save the history of previously run commands script: Read and execute commands from a file. ...
3.5. Components
Components are a set of features which are either build-in or something you can re-use or extend for your own needs. Components in question are either built-in commands or UI side components providing higher level features within commands itself.
3.5.1. Built-In Commands
Any application built by using the {starter-artifactId}
artifact
(which equates to the spring-shell-standard-commands
dependency) comes with a set of built-in commands.
You can override or disable these commands individually (see [overriding-or-disabling-built-in-commands]).
However, if they are not overridden or disabled, this section describes their behavior.
Help
Running a shell application often implies that the user is in a graphically limited
environment. Also, while we are nearly always connected in the era of mobile phones,
accessing a web browser or any other rich UI application (such as a PDF viewer) may not always
be possible. This is why it is important that the shell commands are correctly self-documented, and this is where the help
command comes in.
Typing help
+ ENTER
lists all the commands known to the shell (including unavailable commands)
and a short description of what they do, similar to the following:
my-shell:>help
AVAILABLE COMMANDS
Built-In Commands
exit: Exit the shell.
help: Display help about available commands
stacktrace: Display the full stacktrace of the last error.
clear: Clear the shell screen.
quit: Exit the shell.
history: Display or save the history of previously run commands
completion bash: Generate bash completion script
version: Show version info
script: Read and execute commands from a file.
Typing help <command>
shows more detailed information about a command, including the available parameters, their
type, whether they are mandatory or not, and other details.
The following listing shows the help
command applied to itself:
my-shell:>help help
NAME
help - Display help about available commands
SYNOPSIS
help --command String
OPTIONS
--command or -C String
The command to obtain help for.
[Optional]
Help is templated and can be customized if needed. Settings are under spring.shell.command.help
where you can use
enabled
to disable command, grouping-mode
taking group
or flat
if you want to hide groups by flattening
a structure, command-template
to define your template for output of a command help, commands-template
to define
output of a command list.
If spring.shell.command.help.grouping-mode=flat
is set, then help would show:
my-shell:>help help
AVAILABLE COMMANDS
exit: Exit the shell.
help: Display help about available commands
stacktrace: Display the full stacktrace of the last error.
clear: Clear the shell screen.
quit: Exit the shell.
history: Display or save the history of previously run commands
completion bash: Generate bash completion script
version: Show version info
script: Read and execute commands from a file.
Clear
The clear
command does what you would expect and clears the screen, resetting the prompt
in the top left corner.
Exit
The quit
command (also aliased as exit
) requests the shell to quit, gracefully
closing the Spring application context. If not overridden, a JLine History
bean writes a history of all
commands to disk, so that they are available again (see [interacting-with-the-shell]) on the next launch.
Stacktrace
When an exception occurs inside command code, it is caught by the shell and a simple, one-line message is displayed so as not to overflow the user with too much information. There are cases, though, when understanding what exactly happened is important (especially if the exception has a nested cause).
To this end, Spring Shell remembers the last exception that occurred, and the user can later use the stacktrace
command to print all the details on the console.
Script
The script
command accepts a local file as an argument and replays commands found there, one at a time.
Reading from the file behaves exactly like inside the interactive shell, so lines starting with //
are considered
to be comments and are ignored, while lines ending with \
trigger line continuation.
History
The history
command shows the history of commands that has been executed.
There are a few configuration options that you can use to configure behavior
of a history. History is kept in a log file, which is enabled by default and can
be turned off by setting spring.shell.history.enabled
. The name of a log file
is resolved from spring.application.name
and defaults to spring-shell.log
,
which you can change by setting spring.shell.history.name
.
By default, a log file is generated to a current working directory, which you can dictate
by setting spring.shell.config.location
. This property can contain
a placeholder ({userconfig}
), which resolves to a common shared config directory.
Run the Spring Shell application to see how the sample application works as it uses these options. |
Completion
The completion
command set lets you create script files that can be used
with am OS shell implementations to provide completion. This is very useful when
working with non-interactive mode.
Currently, the only implementation is for bash, which works with bash
sub-command.
Version
The version
command shows existing build and git info by integrating into
Boot’s BuildProperties
and GitProperties
if those exist in the shell application.
By default, only version information is shown, and you can enable other information through configuration
options.
The relevant settings are under spring.shell.command.version
, where you can use enabled
to
disable a command and, optionally, define your own template with template
. You can use the
show-build-artifact
, show-build-group
, show-build-name
, show-build-time
,
show-build-version
, show-git-branch
, show-git-commit-id
,
show-git-short-commit-id
and show-git-commit-time
commands to control
fields in a default template.
The template defaults to classpath:template/version-default.st
, and you can define
your own, as the following example shows:
<buildVersion>
This setting would output something like the following:
X.X.X
You can add the following attributes to the default template rendering: buildVersion
, buildGroup
,
buildGroup
, buildName
, buildTime
, gitShortCommitId
, gitCommitId
,
gitBranch
, and gitCommitTime
.
3.5.2. Flow
When you use Flow Components to build something that involves
use of a multiple components, your implementation may become a bit cluttered.
To ease these use cases, we added a
ComponentFlow
that can hook multiple component executions together
as a “flow”.
The following listings show examples of flows and their output in a shell:
static class FlowSampleComplex {
@Autowired
private ComponentFlow.Builder componentFlowBuilder;
public void runFlow() {
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("key1", "value1");
single1SelectItems.put("key2", "value2");
List<SelectItem> multi1SelectItems = Arrays.asList(SelectItem.of("key1", "value1"),
SelectItem.of("key2", "value2"), SelectItem.of("key3", "value3"));
ComponentFlow flow = componentFlowBuilder.clone().reset()
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.and()
.withStringInput("field2")
.name("Field2")
.and()
.withConfirmationInput("confirmation1")
.name("Confirmation1")
.and()
.withPathInput("path1")
.name("Path1")
.and()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.and()
.withMultiItemSelector("multi1")
.name("Multi1")
.selectItems(multi1SelectItems)
.and()
.build();
flow.run();
}
}
static class FlowSampleConditional {
@Autowired
private ComponentFlow.Builder componentFlowBuilder;
public void runFlow() {
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("Field1", "field1");
single1SelectItems.put("Field2", "field2");
ComponentFlow flow = componentFlowBuilder.clone().reset()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.next(ctx -> ctx.getResultItem().get().getItem())
.and()
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.next(ctx -> null)
.and()
.withStringInput("field2")
.name("Field2")
.defaultValue("defaultField2Value")
.next(ctx -> null)
.and()
.build();
flow.run();
}
}
The result from running a flow returns ComponentFlowResult , which you can
use to do further actions.
|
3.5.3. Flow Components
Starting from version 2.1.x, a new component model provides an easier way to create higher-level user interaction for the usual use cases, such as asking for input in various forms. These usually are just plain text input or choosing something from a list.
Templates for built-in components are in the
org/springframework/shell/component
classpath.
Built-in components generally follow this logic:
-
Enter a run loop for user input.
-
Generate component-related context.
-
Render the runtime status of a component state.
-
Exit.
-
Render the final status of a component state.
Flow gives better interface for defining the flow of components that are better suited for defining interactive command flows. |
Component Render
You can implement component rendering in either of two ways: fully
programmatically or by using a ANTLR Stringtemplate.
Strictly speaking, there is a simple Function
renderer interface
that takes Context
as an input and outputs a list of AttributedString
.
This lets you choose between templating and code.
Templating is a good choice if you do not need to do anything complex or you just want to slightly modify existing component layouts. Rendering through code then gives you flexibility to do whatever you need.
The programmatic way to render is to create a Function
:
class StringInputCustomRenderer implements Function<StringInputContext, List<AttributedString>> {
@Override
public List<AttributedString> apply(StringInputContext context) {
AttributedStringBuilder builder = new AttributedStringBuilder();
builder.append(context.getName());
builder.append(" ");
if (context.getResultValue() != null) {
builder.append(context.getResultValue());
}
else {
String input = context.getInput();
if (StringUtils.hasText(input)) {
builder.append(input);
}
else {
builder.append("[Default " + context.getDefaultValue() + "]");
}
}
return Arrays.asList(builder.toAttributedString());
}
}
Then you can hook it to a component:
@ShellMethod(key = "component stringcustom", value = "String input", group = "Components")
public String stringInputCustom(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue",
new StringInputCustomRenderer());
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
if (mask) {
component.setMaskCharater('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}
Components have their own context but usually share some functionality from a parent component types. The following tables show those context variables:
Key | Description |
---|---|
|
The value after a component renders its result. |
|
The name of a component — that is, its title. |
|
The possible message set for a component. |
|
The level of a message — one of |
|
Return |
|
Return |
|
Return |
|
The raw user input. |
Key | Description |
---|---|
|
The name of a component — that is, its title. |
|
The raw user input — mostly used for filtering. |
|
The full list of item states. |
|
The visible list of item states. |
|
Return |
|
The current cursor row in a selector. |
String Input
The string input component asks a user for simple text input, optionally masking values if the content contains something sensitive. The following listing shows an example:
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component string", value = "String input", group = "Components")
public String stringInput(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue");
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
if (mask) {
component.setMaskCharater('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}
}
The following image shows typical output from a string input component:
The context object is StringInputContext
. The following table lists its context variables:
Key | Description |
---|---|
|
The default value, if set. Otherwise, null. |
|
The masked input value |
|
The masked result value |
|
The mask character, if set. Otherwise, null. |
|
|
|
The parent context variables (see TextComponentContext Template Variables). |
Path Input
The path input component asks a user for a Path
and gives additional information about a path itself.
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component path", value = "Path input", group = "Components")
public String pathInput() {
PathInput component = new PathInput(getTerminal(), "Enter value");
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
PathInputContext context = component.run(PathInputContext.empty());
return "Got value " + context.getResultValue();
}
}
The following image shows typical output from a path input component:
The context object is PathInputContext
. The following table describes its context variables:
Key | Description |
---|---|
|
The parent context variables (see TextComponentContext Template Variables). |
Confirmation
The confirmation component asks a user for a simple confirmation. It is essentially a yes-or-no question.
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component confirmation", value = "Confirmation input", group = "Components")
public String confirmationInput(boolean no) {
ConfirmationInput component = new ConfirmationInput(getTerminal(), "Enter value", !no);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
ConfirmationInputContext context = component.run(ConfirmationInputContext.empty());
return "Got value " + context.getResultValue();
}
}
The following image shows the typical output from a confirmation component:
The context object is ConfirmationInputContext
. The following table describes its context variables:
Key | Description |
---|---|
|
The default value — either |
|
The parent context variables (see TextComponentContext Template Variables). |
Single Select
A single select component asks a user to choose one item from a list. It is similar to a simple dropbox implementation. The following listing shows an example:
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
public String singleSelector() {
SelectorItem<String> i1 = SelectorItem.of("key1", "value1");
SelectorItem<String> i2 = SelectorItem.of("key2", "value2");
List<SelectorItem<String>> items = Arrays.asList(i1, i2);
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
SingleItemSelectorContext<String, SelectorItem<String>> context = component
.run(SingleItemSelectorContext.empty());
String result = context.getResultItem().flatMap(si -> Optional.ofNullable(si.getItem())).get();
return "Got value " + result;
}
}
The following image shows typical output for a single select component:
The context object is SingleItemSelectorContext
. The following table describes its context variables:
Key | Description |
---|---|
|
The returned value when the component exists. |
|
The visible items, where rows contains maps of name and selected items. |
|
The parent context variables (see SelectorComponentContext Template Variables). |
You can pre-select an item by defining it to get exposed. This is
useful if you know the default and lets the user merely press Enter
to make a choice.
The following listing sets a default:
SelectorItem<String> i1 = SelectorItem.of("key1", "value1");
SelectorItem<String> i2 = SelectorItem.of("key2", "value2");
List<SelectorItem<String>> items = Arrays.asList(i1, i2);
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setDefaultExpose(i2);
Multi Select
The multi select component asks a user to select multiple items from a list. The following listing shows an example:
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
public String multiSelector() {
List<SelectorItem<String>> items = new ArrayList<>();
items.add(SelectorItem.of("key1", "value1"));
items.add(SelectorItem.of("key2", "value2", false));
items.add(SelectorItem.of("key3", "value3"));
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
MultiItemSelectorContext<String, SelectorItem<String>> context = component
.run(MultiItemSelectorContext.empty());
String result = context.getResultItems().stream()
.map(si -> si.getItem())
.collect(Collectors.joining(","));
return "Got value " + result;
}
}
The following image shows a typical multi-select component:
The context object is MultiItemSelectorContext
. The following table describes its context variables:
Key | Description |
---|---|
|
The values returned when the component exists. |
|
The visible items, where rows contain maps of name, selected, on-row, and enabled items. |
|
The parent context variables (see SelectorComponentContext Template Variables). |
3.6. Customization
This section describes how you can customize the shell.
Unresolved directive in using-shell-customization.adoc - include::using-shell-customization-generic.adoc[]
3.6.1. Styling
Version 2.1.x introduced support for centrally handling styling and theming.
You can change the default theme (named default
)by setting the
spring.shell.theme.name
property.
To create a new theme, register a new Theme
bean with custom ThemeSettings
. This new bean
lets you tweak styles. The following example shows how to do so:
@Configuration
static class CustomThemeConfig {
@Bean
public Theme myTheme() {
return new Theme() {
@Override
public String getName() {
return "mytheme";
}
@Override
public ThemeSettings getSettings() {
return new MyThemeSettings();
}
};
}
}
static class MyThemeSettings extends ThemeSettings {
}
You can use ThemeResolver
to resolve styles if you want to create
JLine-styled strings programmatically. The following example shows how to do so:
@Autowired
private ThemeResolver themeResolver;
String resolvedStyle = themeResolver.resolveTag(TAG_TITLE);
AttributedStyle style = themeResolver.resolveStyle(resolvedStyle);
3.7. Execution
This section describes how to set up a Spring Shell to work in interactive mode.
3.7.1. Interaction Mode
Version 2.1.x introduced built-in support to distinguish between interactive and non-interactive modes. This makes it easier to use the shell as a simple command-line tool without requiring customization.
Currently, interactive mode is entered if any command line options are passed when starting or running a shell from a command line. This works especially well when a shell application is compiled with Native Support.
Some commands may not have any useful meanings when they run in interactive mode
or (conversely) in non-interactive mode. For example, a built-in exit
command would
have no meaning in non-interactive mode, because it is used to exit interactive mode.
The @ShellMethod
annotation has a field called interactionMode
that you can use to inform
shell about when a particular command is available.
Appendix A: Appendix: Techical Introduction
This appendix contains information for developers and others who would like to know more about how Spring Shell works internally and what its design decisions are.
A.1. Command Registration
Defining a command registration is a first step to introducing the structure of a command and its options and parameters. This is loosely decoupled from what happens later, such as parsing command-line input and running actual target code. Essentially, it is the definition of a command API that is shown to a user.
A.1.1. Commands
A command in a spring-shell
structure is defined as an array of commands. This yields a
structure similar to the following example:
command1 sub1
command2 sub1 subsub1
command2 sub2 subsub1
command2 sub2 subsub2
We do not currently support mapping commands to an explicit parent if sub-commands are defined.
For example, command1 sub1 and command1 sub1 subsub1 cannot both be registered.
|
A.1.2. Interaction Mode
Spring Shell has been designed to work on two modes: interactive (which essentially
is a REPL
where you have an active shell instance throughout a series of commands) and
non-interactive (where commands are executed one by one from a command line).
Differentation between these modes is mostly around limitations about what can be done in each mode. For example, it would not be feasible to show what was a previous stacktrace of a command if the shell is no longer active. Generally, whether the shell is still active dictates the available information.
Also, being on an active REPL
session may provide more information about what the user has been
doing within an active session.
A.1.3. Options
Options can be defined as long and short, where the prefixing is --
and -
, respectively.
The following examples show long and short options:
CommandRegistration.builder()
.withOption()
.longNames("myopt")
.and()
.build();
CommandRegistration.builder()
.withOption()
.shortNames('s')
.and()
.build();
A.1.4. Target
The target defines the execution target of a command. It can be a method in a POJO,
a Consumer
, or a Function
.
Method
Using a Method
in an existing POJO is one way to define a target.
Consider the following class:
public static class CommandPojo {
String command(String arg) {
return arg;
}
}
Given the existing class shown in the preceding listing, you can then register its method:
CommandPojo pojo = new CommandPojo();
CommandRegistration.builder()
.command("command")
.withTarget()
.method(pojo, "command")
.and()
.withOption()
.longNames("arg")
.and()
.build();
Function
Using a Function
as a target gives a lot of flexibility to handle what
happens in a command execution, because you can handle many things manually by using
a CommandContext
given to a Function
. The return type from a Function
is
then what gets printed into the shell as a result. Consider the following example:
CommandRegistration.builder()
.command("command")
.withTarget()
.function(ctx -> {
String arg = ctx.getOptionValue("arg");
return String.format("hi, arg value is '%s'", arg);
})
.and()
.withOption()
.longNames("arg")
.and()
.build();
Consumer
Using a Consumer
is basically the same as using a Function
, with the difference being
that there is no return type. If you need to print something into a shell,
you can get a reference to a Terminal
from a context and print something
through it. Consider the following example:
CommandRegistration.builder()
.command("command")
.withTarget()
.consumer(ctx -> {
String arg = ctx.getOptionValue("arg");
ctx.getTerminal().writer()
.println(String.format("hi, arg value is '%s'", arg));
})
.and()
.withOption()
.longNames("arg")
.and()
.build();
A.2. Command Parser
Before a command can be executed, we need to parse the command and whatever options the user may have provided. Parsing comes between command registration and command execution.
A.3. Command Execution
When command parsing has done its job and command registration has been resolved, command execution does the hard work of running the code.
A.4. Command Context
The CommandContext
interface gives access to a currently running
context. You can use it to get access to options:
String arg = ctx.getOptionValue("arg");
If you need to print something into a shell, you can get a Terminal
and use its writer to print something:
ctx.getTerminal().writer().println("hi");
A.5. Command Catalog
The CommandCatalog
interface defines how command registrations exist in
a shell application. It is possible to dynamically register and de-register
commands, which gives flexibility for use cases where possible commands
come and go, depending on a shell’s state. Consider the following example:
CommandRegistration registration = CommandRegistration.builder().build();
catalog.register(registration);
A.5.1. Command Resolver
You can implement the CommandResolver
interface and define a bean to dynamically
resolve mappings from a command’s name to its CommandRegistration
instances. Consider
the following example:
static class CustomCommandResolver implements CommandResolver {
List<CommandRegistration> registrations = new ArrayList<>();
CustomCommandResolver() {
CommandRegistration resolved = CommandRegistration.builder()
.command("resolve command")
.build();
registrations.add(resolved);
}
@Override
public List<CommandRegistration> resolve() {
return registrations;
}
}
A current limitation of a CommandResolver is that it is used every time commands are resolved.
Thus, we advise not using it if a command resolution call takes a long time, as it would
make the shell feel sluggish.
|
A.5.2. Command Catalog Customizer
You can use the CommandCatalogCustomizer
interface to customize a CommandCatalog
.
Its main use is to modify a catalog. Also, within spring-shell
auto-configuration, this
interface is used to register existing CommandRegistration
beans into a catalog.
Consider the following example:
static class CustomCommandCatalogCustomizer implements CommandCatalogCustomizer {
@Override
public void customize(CommandCatalog commandCatalog) {
CommandRegistration registration = CommandRegistration.builder()
.command("resolve command")
.build();
commandCatalog.register(registration);
}
}
You can create a CommandCatalogCustomizer
as a bean, and Spring Shell handles the rest.