Migrating from Spring Shell 1.2 to 2.0

It was a coincidence that I discovered the upcoming 2.0.0.RELEASE of Spring Shell yesterday.

I decided to migrate our Planets CLI and write about it.

Since we have a Maven based setup let’s start with the Maven dependency:

<dependency>
  <groupId>org.springframework.shell</groupId>
  <artifactId>spring-shell</artifactId>
  <version>1.2.0.RELEASE</version>
</dependency>

becomes

<dependency>
  <groupId>org.springframework.shell</groupId>
  <artifactId>spring-shell-starter</artifactId>
  <version>2.0.0.RELEASE</version>
</dependency>

Tip: You did spot the change from spring-shell to spring-shell-starter, did you?

Bootstrapping the CLI

In version 1.2.0 we used org.springframework.shell.Bootstrap to bootstrap our shell:

public static void main(String[] args) throws IOException {
  Bootstrap.main(args);
}

With version 2.0.0 the shell is a regular Spring Boot application and is started accordingly with org.springframework.boot.SpringApplication.run:

@SpringBootApplication
public class PlanetsShell {

    public static void main(String[] args) {
        run(PlanetsShell.class, args);
    }
}

Shell commands

Previously our shell commands were annotated with @Component and implemented CommandMarker:

@Component
public class PlanetsCommands implements CommandMarker {
  
}

With the new 2.0 release comes a more specific version @ShellComponent and the marker interface is gone. Nice.

@ShellComponent
public class PlanetsCommands {
  
}

The former @CliCommand and @CliOption we used to annotate methods to appear in our CLI:

@CliCommand(value = "planets:login", help = "Login with your username/password")
public void login(@CliOption(key = "username", mandatory = true) String username,
                      @CliOption(key = "password", mandatory = true) String password) {

}

can be replaced with @ShellMethods and @ShellOption like this:

@ShellMethod("Login with your username/password")
public String login(@ShellOption String username, @ShellOption String password) {

}

For more details please refer to the Writing your own Commands.

Availability indication

The annotation @CliAvailablilityIndicator

@CliAvailabilityIndicator({"planets:showLocation"})
public boolean isActive() {
    return isAuthenticated();
}

is replaced by a method returning an Availability:

public Availability isAvailable() {
    return isAuthenticated() ? available() : unavailable("you are not logged in");
}

If you want to dive into the details I highly recommend to read the section Dynamic Command Availability

PromptProvider and BannerProvider

Finally the necessity to create a bean with @Order(1) to override the default providers is gone.

@Order(1) // Override the Spring Shell default Provider(s)
@Component
public class PlanetsShellTheme implements BannerProvider, PromptProvider {

}

Another notably change happened to the method PromptProvider.getPrompt(). The return type changed from a simple String to AttributedString:

@Bean
public PromptProvider promptProvider() {
    return () -> new AttributedString("Planets > ", DEFAULT.foreground(YELLOW));
}

Since we have a regular Spring Boot app the BannerProvider vanished completely. Simply name your banner file banner.txt and put it in src/main/resources. That’s it.

Tip: Just in case you want to update your banner when moving to Spring Shell 2.0.0…we polished our Online Banner Generator lately.

Troubleshooting the migration

The format of the history changed. You might encounter a NumberFormatException during startup:

18:14:49.114 WARN  - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'history' defined in class path resource [org/springframework/shell/jline/JLineShellAutoConfiguration$HistoryConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.jline.reader.History]: Factory method 'history' threw exception; nested exception is java.lang.NumberFormatException: For input string: "// null 1.2.0.RELEASE log opened at 2017-11-09 09"

Since the shell has an excellent tab completion I don’t care too much about the command history. so I simply deleted my spring-shell.log and started over!

A huge thanks to the team behind Spring Shell!

PS: Another small thing that has changed is how scripts are called. In Spring Shell 1.2 we used —script <filename>. With version 2.0 the only parameter needed is the script name prefixed with an @ like this: @<filename>.

PPS: Shell methods shouldn’t throw checked exceptions. This generates a shell error no application error. In our case we wrapped an IOException into an UncheckedIOException to avoid this misleading exceptions.