Create Command-line Applications with Spring Shell

Create Command-line Applications with Spring Shell

Table Of Contents

Spring Shell allows us to build a command line (shell) application using the Spring framework and all the advantages it provides.

Example Code

This article is accompanied by a working code example on GitHub.

What Is a Shell Anyways?

A shell provides us with an interface to a system (usually an operating system) to which we give commands and parameters. The shell in turn does some useful tasks for us and provides an output.

Creating a Basic Shell

First, we have to get the SpringShell dependency from Maven central, which has everything we need.

In Gradle, the dependency will look something like this:

dependencies {  
  implementation 'org.springframework.shell:spring-shell-starter:2.0.1.RELEASE'  
}  

Then, since it’s a Spring Boot application, our main method has to be annotated with @SpringBootApplication

@SpringBootApplication 
public class SpringShellApplication 
{    
   public static void main(String[] args) 
   {    
      SpringApplication.run(SpringShellApplication.class, args);    
	} 
}  

Now, let’s create our first shell command which simulates an SSH command:

@ShellComponent 
public class SSHCommand 
{    
    Logger log = Logger.getLogger(SSHCommand.class.getName());    
    
    @ShellMethod(value = "connect to remote server")    
    public void ssh(@ShellOption(value = "-s") String remoteServer)    
    {    
        log.info(format("Logged to machine '%s'", remoteServer));    
	} 
}  

The annotation @ShellComponent tells Spring Shell that an annotated class may contain shell methods, which are annotated with @ShellMethod.

As for the @ShellMethod annotation, it’s used to mark a method as invokable via Spring Shell. We can also see the value property which is used to describe the command.

The @ShellOption annotation simply states that this command takes a parameter named -s.

So as a result when we run the application we get a shell that has a command called ssh which takes a parameter -s and all it does is logging the passed parameter value to the command line.

shell:>ssh -s my-machine  
2022-02-11 15:44:04.065  INFO 5648 --- [           main] j.t.springshell.command.SSHCommand       : Logged to machine 'my-machine'shell:>  

Modifying the Command Name

The default naming convention for Spring Shell, as we’ve seen, is taking the method name ssh and turning it into the command name.

  • If we wrote the name in camel case Spring would turn camelCase humps into “-”.
  • So customSsh would translate to custom-ssh.

We can also add a name of our own using the key property of the ShellMethod annotation:

@ShellComponent  
public class SSHCommand 
{
	...
	@ShellMethod(key = "my-ssh", value = "connect to remote server") 
	public void ssh(@ShellOption(value = "-s") String remoteServer) 
	{    
		log.info(format("Logged to machine '%s'", remoteServer)); 
	}  
}

Working with Command Parameters

Commands can take parameters as input from the user. Spring Shell offers a simple and easy way to introduce parameters.

Parameter Naming

As we’ve seen from the previous example, command parameters are expressed through method parameters.

We can specify the name of the parameter using the value property of the @ShellOption annotation.

If we don’t specify the value however, Spring Shell assigns it a default value of parameter name “-” separated prefixed by ShellMethod.prefix().

The default value for @ShellMethod.prefix() is “–":

@ShellComponent  
public class SSHCommand 
{
	...
	@ShellMethod(key = "my-ssh", prefix = "-", value = "connect to remote server") 
	public void ssh(@ShellOption String remoteServer) 
	{    
		log.info(format("Logged to machine '%s'", remoteServer)); 
	}  
}

Then, our command would be something like:

Cool Machine==> my-ssh -remote-server test
2022-02-27 12:39:14.800  INFO 11704 --- [           main] i.r.springshell.command.SSHCommand       : Logged to machine 'test'

Declaring Default Parameters Values

We can assign default values to parameters in case the user doesn’t specify any. Doing this also allows the user to treat those parameters as optional:

@ShellComponent  
public class SSHCommand 
{
	...
	@ShellMethod(value = "connect to remote server") 
	public void ssh(@ShellOption(value = "--s", defaultValue = "default-server") String remoteServer) 
	{    
		log.info(format("Logged to machine '%s'", remoteServer)); 
	}  
}

Typing only ssh to the console will give us:

shell:>ssh  
2022-02-11 19:55:05.133  INFO 4700 --- [           main] j.t.springshell.command.SSHCommand       : Logged to machine 'default-server'

Multi-valued Parameters

We can specify multiple values for a single parameter by using the arity() attribute of the @ShellOption annotation. Simply use a collection or array for the parameter type, and specify how many values are expected”:

@ShellComponent  
public class SSHCommand 
{
	...
	@ShellMethod(value = "add keys") 
	public void sshAdd(@ShellOption(value = "--k", arity = 2) String[] keys) 
	{    
		log.info(format("Adding keys '%s' '%s'", keys[0], keys[1])); 
	}  
}

Let’s try the command out in the shell:

shell:>ssh-add --k test1 test2  
2022-02-12 18:27:00.301  INFO 4928 --- [           main] j.t.springshell.command.SSHCommand       : Adding keys 'test1' 'test2'

Working with Boolean Parameters

Boolean parameters receive a special treatment by command-line utilities. The absence of the parameter in the command indicates a false value. On the other hand, its existence indicates true value:

@ShellComponent  
public class SSHCommand 
{
	...
	@ShellMethod(value = "sign in") 
	public void sshLogin(@ShellOption(value = "--r") boolean rememberMe) 
	{    
		log.info(format("remember me option is '%s'", rememberMe)); 
	}  
}

Let’s check it out in the command line:

shell:>ssh-login --r  
2022-02-12 18:41:34.903  INFO 10044 --- [           main] j.t.springshell.command.SSHCommand       : remember me option is 'true'

shell:>ssh-login  
2022-02-12 18:41:44.606  INFO 10044 --- [           main] j.t.springshell.command.SSHCommand       : remember me option is 'false'

Validating Command Parameters

Spring Shell integrates with the Bean Validation API to provide us with automatic and self-documenting constraints on command parameters. Validation annotations found on command parameters as well as annotations at the method level will trigger validation prior to the command executing.

Let’s try this in action by adding a @Size annotation to the method parameter:

@ShellComponent  
public class SSHCommand 
{
	...
	@ShellMethod(value = "ssh agent") public void sshAgent(    
	        @ShellOption(value = "--a")    
	@Size(min = 2, max = 10) String agent) 
	{    
		log.info(format("adding agent '%s'", agent)); 
	}  
}

Now, if we try to pass a parameter value with a length of 1 we will get an error stating the reason:

shell:>ssh-agent --a t  
The following constraints were not met:  --a string : size must be between 2 and 10 (You passed 't')  

Note that the @Size annotation is a part of the Jakarta Bean Validation which offers many more validation options like @NotEmpty @Max.

Dynamic Command Availability

Some commands only make sense when certain pre-conditions are met. For example, a sign-out command should be available only if a sign-in command has been issued, and if the user tries to run the sign-out command we want to warn them that it’s not possible.

Spring Shell offers us three ways to achieve our goal.

Create a Method to Check Availability

It checks our class for a method with a special name and with a return type of Availability.
The special name has to be in the format commandToCheckAvailability:

@ShellComponent 
public class SSHLoggingCommand 
{    
    Logger log = Logger.getLogger(SSHLoggingCommand.class.getName());    
    private boolean signedIn;    
    
    @ShellMethod(value = "sign in")    
    public void signIn()    
    {    
        this.signedIn = true;    
        log.info("Signed In!");    
    }    
    
    @ShellMethod(value = "sign out")    
    public void signOut()    
    {    
        this.signedIn = false;    
        log.info("Signed out!");    
    }    
    // note the naming     
   public Availability signOutAvailability()    
    {    
        return signedIn ?    
                Availability.available() : Availability.unavailable("Must be signed in first");    
	} 
}  

So if we try to run the sign-out command without first signing in we will get the following message:

shell:>sign-out  
Command 'sign-out' exists but is not currently available because Must be signed in firstDetails of the error have been omitted. You can use the stacktrace command to print the full stacktrace.shell:>  

Specifying the Name of the Availability Method

Uses the @ShellMethodAvailability annotation, in which we specify the method name we want to use to Availability check:

@ShellComponent  
public  class  SSHLoggingCommand  
{
	...
	@ShellMethod(value = "sign out") 
	@ShellMethodAvailability("signOutCheck") 
	public void signOut() 
	{    
	    this.signedIn = false;    
		log.info("Signed out!"); 
	}  
		
	public Availability signOutCheck() 
	{    
	   return signedIn ?  Availability.available() : Availability.unavailable("Must be signed in first"); 
	}  
}

One Availability Method for Multiple Commands

It enables us to have several methods attached to a single availability method.
We are going to use the annotation ShellMethodAvailability with an array of the commands names (not method names):

@ShellComponent  
public  class  SSHLoggingCommand  
{
	...
	@ShellMethod(value = "sign out") public void signOut() 
	{    
	    this.signedIn = false;    
		log.info("Signed out!"); 
	}    

	 @ShellMethod(value = "Change password") 
	 public void changePass(@ShellOption String newPass) 
	 {    
		log.info(format("Changed password to '%s'", newPass)); 
	 }    
	 
	 @ShellMethodAvailability({"sign-out", "change-pass"}) 
	 public Availability signOutCheck() 
	 {    
	    return signedIn ? Availability.available() : Availability.unavailable("Must be signed in first"); 
	}  
}

Other Cool Features in Spring Shell

Since Spring Shell builds on top of JLine it inherits a lot of its features. Let’s look at some of them:

Tab Completion

Spring Shell allows us to use tab completion with command names and even with parameter names. Since this feature is a part of JLine we can use it out of the box with no need for any configuration.

Built-in Commands

Spring Shell offers us a set of useful built-in commands. Let’s take a look at two important ones:

  • help: lists all the commands known to the shell, including the built-in commands and commands we wrote.

  • script: accepts a local file as an argument and will replay commands found there, one at a time.

Styling the Shell

We can do so by registering a bean of type PromptProvider which includes information on how to render the Shell prompt.
For example, let’s change the prompt text to Cool Machine==> with a green color for the text:

@Component 
public class CustomPromptProvider implements PromptProvider 
{    
    @Override    
  public AttributedString getPrompt() 
  {    
            return new AttributedString(    
                    "Cool Machine" + "==> ",    
                    AttributedStyle.DEFAULT.background(AttributedStyle.GREEN));    
	} 
}  

This will give us a prompt like this

2022-02-26 23:37:49.949  INFO 6560 --- [           main] i.r.springshell.SpringShellApplication   : Started SpringShellApplication in 2.267 seconds (JVM running for 3.77)
Cool Machine==> 

Running the Shell from the Jar File

After obtaining the JAR file we run it using the command java -jar our-spring-shell-jar-name.jar. This will open our shell in the command line and have it ready for us to type in commands.

Summary

  • The Shell allows us to interface with a system using commands.

  • Spring Shell introduces a simple and quick way to build a Shell leveraging all the good sides of the Spring framework.

  • The three main building blocks of Spring Shell are

    • @ShellComponent
    • @ShellMethod
    • @ShellOption.
  • Spring Shell is built on top of JLine which offers useful features like tab completion and built in commands.

  • We can choose to make some commands available based on certain conditions.

  • We can style the command line as we like.

Written By:

Abdulcelil Cercenazi

Written By:

Abdulcelil Cercenazi

Software developer and craftsman. Loves working with Java | Spring | Docker. Always looking for new adventures

Recent Posts

Handling Timezones in a Spring Boot Application

It is common to encounter applications that run in different time zones. Handling date and time operations consistently across multiple layers of an application can be tricky.

Read more

JUnit 5 by Examples

In software development, unit testing means writing tests to verify that our code is working as logical units. Different people tend to think of a “logical unit” in different ways.

Read more

Scheduling Jobs with Node.js

Have you ever wanted to perform a specific task on your application server at specific times without physically running them yourself?

Read more