Una CLI con Spring Shell

1. Panoramica

In poche parole, il progetto Spring Shell fornisce una shell interattiva per l'elaborazione dei comandi e la creazione di una CLI completa utilizzando il modello di programmazione Spring.

In questo articolo, esploreremo le sue caratteristiche, classi chiave e annotazioni e implementeremo diversi comandi e personalizzazioni personalizzati.

2. Dipendenza da Maven

Innanzitutto, dobbiamo aggiungere la dipendenza spring-shell al nostro pom.xml :

 org.springframework.shell spring-shell 1.2.0.RELEASE 

L'ultima versione di questo artefatto può essere trovata qui.

3. Accesso alla Shell

Esistono due modi principali per accedere alla shell nelle nostre applicazioni.

Il primo è avviare la shell nel punto di ingresso della nostra applicazione e consentire all'utente di immettere i comandi:

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

Il secondo è ottenere un JLineShellComponent ed eseguire i comandi a livello di codice :

Bootstrap bootstrap = new Bootstrap(); JLineShellComponent shell = bootstrap.getJLineShellComponent(); shell.executeCommand("help");

Useremo il primo approccio poiché è più adatto per gli esempi in questo articolo, tuttavia, nel codice sorgente puoi trovare casi di test che utilizzano la seconda forma.

4. Comandi

Esistono già diversi comandi incorporati nella shell, come clear , help , exit , ecc., Che forniscono le funzionalità standard di ogni CLI.

I comandi personalizzati possono essere esposti aggiungendo metodi contrassegnati con l' annotazione @CliCommand all'interno di un componente Spring che implementa l' interfaccia CommandMarker .

Ogni argomento di quel metodo deve essere contrassegnato con un'annotazione @CliOption , se non lo facciamo, riscontreremo diversi errori durante il tentativo di eseguire il comando.

4.1. Aggiunta di comandi alla shell

Per prima cosa, dobbiamo far sapere alla shell dove sono i nostri comandi. Per questo, richiede che il file META-INF / spring / spring-shell-plugin.xml sia presente nel nostro progetto, lì possiamo utilizzare la funzionalità di scansione dei componenti di Spring:

Una volta che i componenti sono registrati e istanziati da Spring, vengono registrati con il parser della shell e le loro annotazioni vengono elaborate.

Creiamo due semplici comandi, uno per afferrare il contenuto di un URL e visualizzarlo, e l'altro per salvare quei contenuti in un 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."; } }

Nota che possiamo passare più di una stringa al valore e agli attributi chiave di @CliCommand e @CliOption rispettivamente, questo ci permette di esporre diversi comandi e argomenti che si comportano allo stesso modo.

Ora, controlliamo se tutto funziona come previsto:

spring-shell>web-get --url //www.google.com web-save --url //www.google.com --out contents.txt Done.

4.2. Disponibilità di comandi

Possiamo usare l' annotazione @CliAvailabilityIndicator su un metodo che restituisce un valore booleano da modificare, in fase di esecuzione, se un comando deve essere esposto alla shell.

Per prima cosa, creiamo un metodo per modificare la disponibilità del comando web-save :

private boolean adminEnableExecuted = false; @CliAvailabilityIndicator(value = "web-save") public boolean isAdminEnabled() { return adminEnableExecuted; }

Ora creiamo un comando per modificare la variabile adminEnableExecuted :

@CliCommand(value = "admin-enable") public String adminEnable() { adminEnableExecuted = true; return "Admin commands enabled."; }

Infine, verificiamolo:

spring-shell>web-save --url //www.google.com --out contents.txt Command 'web-save --url //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 //www.google.com --out contents.txt Done.

4.3. Argomenti obbligatori

Per impostazione predefinita, tutti gli argomenti del comando sono facoltativi. Tuttavia, possiamo renderli obbligatori con l' attributo obbligatorio dell'annotazione @CliOption :

@CliOption(key = { "out", "file" }, mandatory = true)

Ora, possiamo verificare che se non lo introduciamo, si verifica un errore:

spring-shell>web-save --url //www.google.com You should specify option (--out) for this command

4.4. Argomenti predefiniti

Un valore chiave vuoto per @CliOption rende tale argomento l'impostazione predefinita. Lì, riceveremo i valori introdotti nella shell che non fanno parte di alcun argomento denominato:

@CliOption(key = { "", "url" })

Ora, controlliamo che funzioni come previsto:

spring-shell>web-get //www.google.com 

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 { public URL convertFromText( String value, Class requiredType, String optionContext) { return new URL(value); } public boolean getAllPossibleValues( List 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.