Une CLI avec Spring Shell

1. Vue d'ensemble

En termes simples, le projet Spring Shell fournit un shell interactif pour le traitement des commandes et la création d'une CLI complète à l'aide du modèle de programmation Spring.

Dans cet article, nous explorerons ses fonctionnalités, ses classes clés et ses annotations, et implémenterons plusieurs commandes et personnalisations personnalisées.

2. Dépendance de Maven

Tout d'abord, nous devons ajouter la dépendance spring-shell à notre pom.xml :

 org.springframework.shell spring-shell 1.2.0.RELEASE 

La dernière version de cet artefact peut être trouvée ici.

3. Accès au Shell

Il existe deux manières principales d'accéder au shell dans nos applications.

La première consiste à amorcer le shell dans le point d'entrée de notre application et à laisser l'utilisateur entrer les commandes:

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

La seconde consiste à obtenir un JLineShellComponent et à exécuter les commandes par programme:

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

Nous allons utiliser la première approche car elle est la mieux adaptée aux exemples de cet article, cependant, dans le code source, vous pouvez trouver des cas de test qui utilisent la deuxième forme.

4. Commandes

Il existe déjà plusieurs commandes intégrées dans le shell, telles que clear , help , exit , etc., qui fournissent les fonctionnalités standard de chaque CLI.

Les commandes personnalisées peuvent être exposées en ajoutant des méthodes marquées de l' annotation @CliCommand dans un composant Spring implémentant l' interface CommandMarker .

Chaque argument de cette méthode doit être marqué avec une annotation @CliOption , si nous ne parvenons pas à le faire, nous rencontrerons plusieurs erreurs en essayant d'exécuter la commande.

4.1. Ajout de commandes au shell

Tout d'abord, nous devons indiquer au shell où se trouvent nos commandes. Pour cela, il faut que le fichier META-INF / spring / spring-shell-plugin.xml soit présent dans notre projet, là, nous pouvons utiliser la fonctionnalité d'analyse des composants de Spring:

Une fois que les composants sont enregistrés et instanciés par Spring, ils sont enregistrés auprès de l'analyseur du shell et leurs annotations sont traitées.

Créons deux commandes simples, l'une pour saisir le contenu d'une URL et l'afficher, et l'autre pour enregistrer ce contenu dans un fichier:

@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."; } }

Notez que nous pouvons passer plus d'une chaîne aux attributs value et key de @CliCommand et @CliOption respectivement, cela nous permet d'exposer plusieurs commandes et arguments qui se comportent de la même manière.

Maintenant, vérifions si tout fonctionne comme prévu:

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

4.2. Disponibilité des commandes

On peut utiliser l' annotation @CliAvailabilityIndicator sur une méthode retournant un booléen pour changer, à l'exécution, si une commande doit être exposée au shell.

Commençons par créer une méthode pour modifier la disponibilité de la commande web-save :

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

Maintenant, créons une commande pour changer la variable adminEnableExecuted :

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

Enfin, vérifions-le:

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. Arguments requis

Par défaut, tous les arguments de commande sont facultatifs. Cependant, nous pouvons les rendre obligatoires avec l' attribut obligatoire de l' annotation @CliOption :

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

Maintenant, nous pouvons tester que si nous ne l'introduisons pas, cela entraîne une erreur:

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

4.4. Arguments par défaut

Une valeur de clé vide pour un @CliOption fait de cet argument la valeur par défaut. Là, nous recevrons les valeurs introduites dans le shell qui ne font partie d'aucun argument nommé:

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

Maintenant, vérifions que cela fonctionne comme prévu:

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.