Une CLI avec Spring Shell

Une CLI avec Spring Shell

1. Vue d'ensemble

En termes simples, Spring Shellproject 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 allons explorer ses fonctionnalités, ses classes clés et ses annotations, et implémenter plusieurs commandes et personnalisations personnalisées.

2. Dépendance Maven

Tout d'abord, nous devons ajouter la dépendancespring-shell à nospom.xml:


    org.springframework.shell
    spring-shell
    1.2.0.RELEASE

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

3. Accéder 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 suivantes:

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

La seconde consiste à obtenir unJLineShellComponent 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 queclear,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 à l'intérieur d'un composant Spring implémentant l'interfaceCommandMarker.

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

Premièrement, nous devons faire savoir au shell où sont nos commandes. Pour cela, il faut que le fichierMETA-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 les composants enregistrés et instanciés par Spring, ils sont enregistrés avec l'analyseur de 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 attributsvalue etkey respectivement de@CliCommand et@CliOption, 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 https://www.google.com
web-save --url https://www.google.com --out contents.txt
Done.

4.2. Disponibilité des commandes

Nous pouvons utiliser l'annotation@CliAvailabilityIndicator sur une méthode retournant unboolean 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 commandeweb-save:

private boolean adminEnableExecuted = false;

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

Maintenant, créons une commande pour changer la variableadminEnableExecuted:

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

Enfin, vérifions-le:

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

Par défaut, tous les arguments de commande sont facultatifs. Cependant, nous pouvons les rendre obligatoires avec l'attributmandatory 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 https://www.google.com
You should specify option (--out) for this command

4.4. Arguments par défaut

Une valeurkey 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 https://www.google.com

4.5. Aider les utilisateurs

Les annotations@CliCommand et@CliOption fournissent un attributhelp qui nous permet de guider nos utilisateurs lors de l'utilisation de la commandehelp intégrée ou lors de la tabulation pour obtenir la saisie semi-automatique.

Modifions nosweb-get pour ajouter des messages d'aide personnalisés:

@CliCommand(
  // ...
  help = "Displays the contents of an URL")
public String webGet(
  @CliOption(
    // ...
    help = "URL whose contents will be displayed."
  ) String url) {
    // ...
}

Maintenant, l'utilisateur peut savoir exactement ce que fait notre commande:

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. Personnalisation

Il existe trois façons de personnaliser le shell en implémentant les interfacesBannerProvider,PromptProvider etHistoryFileNameProvider, toutes avec des implémentations par défaut déjà fournies.

De plus, nous devons utiliser l'annotation@Order pour permettre à nos fournisseurs de prendre le pas sur ces implémentations.

Créons une nouvelle bannière pour commencer notre personnalisation:

@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("*          example 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 example CLI";
    }

    public String getProviderName() {
        return "example Banner";
    }
}

Notez que nous pouvons également changer le numéro de version et le message de bienvenue.

Maintenant, modifions l'invite:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimplePromptProvider extends DefaultPromptProvider {

    public String getPrompt() {
        return "example-shell";
    }

    public String getProviderName() {
        return "example Prompt";
    }
}

Enfin, modifions le nom du fichier d’historique:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleHistoryFileNameProvider
  extends DefaultHistoryFileNameProvider {

    public String getHistoryFileName() {
        return "example-shell.log";
    }

    public String getProviderName() {
        return "example History";
    }

}

Le fichier historique enregistrera toutes les commandes exécutées dans le shell et sera placé à côté de notre application.

Avec tout en place, nous pouvons appeler notre shell et le voir en action:

=======================================
*          example Shell             *
=======================================
Version:1.0.1
Welcome to example CLI
example-shell>

6. Convertisseurs

Jusqu'à présent, nous n'avons utilisé que des types simples comme arguments pour nos commandes. Les types courants tels queInteger,Date,Enum,File, etc., ont un convertisseur par défaut déjà enregistré.

En implémentant l'interfaceConverter, nous pouvons également ajouter nos convertisseurs pour recevoir des objets personnalisés.

Créons un convertisseur capable de transformer unString enURL:

@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);
    }
}

Enfin, modifions nos commandesweb-get etweb-save:

public String webSave(... URL url) {
    // ...
}

public String webSave(... URL url) {
    // ...
}

Comme vous l'avez peut-être deviné, les commandes se comportent de la même manière.

7. Conclusion

Dans cet article, nous avons brièvement présenté les principales fonctionnalités du projet Spring Shell. Nous avons pu entrer nos commandes et personnaliser le shell avec nos fournisseurs, nous avons modifié la disponibilité des commandes en fonction de différentes conditions d'exécution et créé un convertisseur de type simple.

Le code source complet de cet article peut être trouvéover on GitHub.