1. Overview
Simply put, the Spring Shell project provides an interactive shell for processing commands and building a full-featured CLI using the Spring programming model.
In this article, we’ll explore its features, key classes, and annotations, and implement several custom commands and customizations.
2. Maven Dependency
First, we need to add the spring-shell dependency to our pom.xml:
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
The latest version of this artifact can be found here.
3. Accessing the Shell
There are two main ways to access the shell in our applications.
The first is to bootstrap the shell in the entry point of our application and let the user enter the commands:
public static void main(String[] args) throws IOException {
Bootstrap.main(args);
}
The second is to obtain a JLineShellComponent and execute the commands programmatically:
Bootstrap bootstrap = new Bootstrap();
JLineShellComponent shell = bootstrap.getJLineShellComponent();
shell.executeCommand("help");
We’re going to use the first approach since its best suited for the examples in this article, however, in the source code you can find test cases that use the second form.
4. Commands
There are already several built-in commands in the shell, such as clear, help, exit, etc., that provide the standard functionality of every CLI.
Custom commands can be exposed by adding methods marked with the @CliCommand annotation inside a Spring component implementing the CommandMarker interface.
Every argument of that method must be marked with a @CliOption annotation, if we fail to do this, we’ll encounter several errors when trying to execute the command.
4.1. Adding Commands to the Shell
First, we need to let the shell know where our commands are. For this, it requires the file META-INF/spring/spring-shell-plugin.xml to be present in our project, there, we can use the component scanning functionality of Spring:
<beans ... >
<context:component-scan base-package="org.baeldung.shell.simple" />
</beans>
Once the components are registered and instantiated by Spring, they are registered with the shell parser, and their annotations are processed.
Let’s create two simple commands, one to grab the contents of an URL and display them, and other to save those contents to a file:
@Component
public class SimpleCLI implements CommandMarker {
@CliCommand(value = { "web-get", "wg" })
public String webGet(
@CliOption(key = "url") String url) {
return getContentsOfUrlAsString(url);
}
@CliCommand(value = { "web-save", "ws" })
public String webSave(
@CliOption(key = "url") String url,
@CliOption(key = { "out", "file" }) String file) {
String contents = getContentsOfUrlAsString(url);
try (PrintWriter out = new PrintWriter(file)) {
out.write(contents);
}
return "Done.";
}
}
Note that we can pass more than one string to the value and key attributes of @CliCommand and @CliOption respectively, this permits us to expose several commands and arguments that behave the same.
Now, let’s check if everything is working as expected:
spring-shell>web-get --url https://www.google.com
<!doctype html ...
spring-shell>web-save --url https://www.google.com --out contents.txt
Done.
4.2. Availability of Commands
We can use the @CliAvailabilityIndicator annotation on a method returning a boolean to change, at runtime, if a command should be exposed to the shell.
First, let’s create a method to modify the availability of the web-save command:
private boolean adminEnableExecuted = false;
@CliAvailabilityIndicator(value = "web-save")
public boolean isAdminEnabled() {
return adminEnableExecuted;
}
Now, let’s create a command to change the adminEnableExecuted variable:
@CliCommand(value = "admin-enable")
public String adminEnable() {
adminEnableExecuted = true;
return "Admin commands enabled.";
}
Finally, let’s verify it:
spring-shell>web-save --url https://www.google.com --out contents.txt
Command 'web-save --url https://www.google.com --out contents.txt'
was found but is not currently available
(type 'help' then ENTER to learn about this command)
spring-shell>admin-enable
Admin commands enabled.
spring-shell>web-save --url https://www.google.com --out contents.txt
Done.
4.3. Required Arguments
By default, all command arguments are optional. However, we can make them required with the mandatory attribute of the @CliOption annotation:
@CliOption(key = { "out", "file" }, mandatory = true)
Now, we can test that if we don’t introduce it, results in an error:
spring-shell>web-save --url https://www.google.com
You should specify option (--out) for this command
4.4. Default Arguments
An empty key value for a @CliOption makes that argument the default. There, we’ll receive the values introduced in the shell that are not part of any named argument:
@CliOption(key = { "", "url" })
Now, let’s check that it works as expected:
spring-shell>web-get https://www.google.com
<!doctype html ...
4.5. Helping Users
@CliCommand and @CliOption annotations provide a help attribute that allows us to guide our users when using the built-in help command or when tabbing to get auto-completion.
Let’s modify our web-get to add custom help messages:
@CliCommand(
// ...
help = "Displays the contents of an URL")
public String webGet(
@CliOption(
// ...
help = "URL whose contents will be displayed."
) String url) {
// ...
}
Now, the user can know exactly what our command does:
spring-shell>help web-get
Keyword: web-get
Keyword: wg
Description: Displays the contents of a URL.
Keyword: ** default **
Keyword: url
Help: URL whose contents will be displayed.
Mandatory: false
Default if specified: '__NULL__'
Default if unspecified: '__NULL__'
* web-get - Displays the contents of a URL.
* wg - Displays the contents of a URL.
5. Customization
There are three ways to customize the shell by implementing the BannerProvider, PromptProvider and HistoryFileNameProvider interfaces, all of them with default implementations already provided.
Also, we need to use the @Order annotation to allow our providers to take precedence over those implementations.
Let’s create a new banner to begin our customization:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleBannerProvider extends DefaultBannerProvider {
public String getBanner() {
StringBuffer buf = new StringBuffer();
buf.append("=======================================")
.append(OsUtils.LINE_SEPARATOR);
buf.append("* Baeldung Shell *")
.append(OsUtils.LINE_SEPARATOR);
buf.append("=======================================")
.append(OsUtils.LINE_SEPARATOR);
buf.append("Version:")
.append(this.getVersion());
return buf.toString();
}
public String getVersion() {
return "1.0.1";
}
public String getWelcomeMessage() {
return "Welcome to Baeldung CLI";
}
public String getProviderName() {
return "Baeldung Banner";
}
}
Note that we can also change the version number and welcome message.
Now, let’s change the prompt:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimplePromptProvider extends DefaultPromptProvider {
public String getPrompt() {
return "baeldung-shell";
}
public String getProviderName() {
return "Baeldung Prompt";
}
}
Finally, let’s modify the name of the history file:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleHistoryFileNameProvider
extends DefaultHistoryFileNameProvider {
public String getHistoryFileName() {
return "baeldung-shell.log";
}
public String getProviderName() {
return "Baeldung History";
}
}
The history file will record all commands executed in the shell and will be put alongside our application.
With everything in place, we can call our shell and see it in action:
=======================================
* Baeldung Shell *
=======================================
Version:1.0.1
Welcome to Baeldung CLI
baeldung-shell>
6. Converters
So far, we’ve only used simple types as arguments to our commands. Common types such as Integer, Date, Enum, File, etc., have a default converter already registered.
By implementing the Converter interface, we can also add our converters to receive custom objects.
Let’s create a converter that can transform a String into an URL:
@Component
public class SimpleURLConverter implements Converter<URL> {
public URL convertFromText(
String value, Class<?> requiredType, String optionContext) {
return new URL(value);
}
public boolean getAllPossibleValues(
List<Completion> completions,
Class<?> requiredType,
String existingData,
String optionContext,
MethodTarget target) {
return false;
}
public boolean supports(Class<?> requiredType, String optionContext) {
return URL.class.isAssignableFrom(requiredType);
}
}
Finally, let’s modify our web-get and web-save commands:
public String webSave(... URL url) {
// ...
}
public String webSave(... URL url) {
// ...
}
As you may have guessed, the commands behave the same.
7. Conclusion
In this article, we had a brief look at the core features of the Spring Shell project. We were able to contribute our commands and customize the shell with our providers, we changed the availability of commands according to different runtime conditions and created a simple type converter.
Complete source code for this article can be found over on GitHub.