Eine CLI mit Spring Shell

1. Übersicht

Einfach ausgedrückt bietet das Spring Shell-Projekt eine interaktive Shell zum Verarbeiten von Befehlen und zum Erstellen einer CLI mit vollem Funktionsumfang unter Verwendung des Spring-Programmiermodells.

In diesem Artikel werden die Funktionen, Schlüsselklassen und Anmerkungen erläutert und verschiedene benutzerdefinierte Befehle und Anpassungen implementiert.

2. Maven-Abhängigkeit

Zuerst müssen wir die Spring-Shell- Abhängigkeit zu unserer pom.xml hinzufügen :

 org.springframework.shell spring-shell 1.2.0.RELEASE 

Die neueste Version dieses Artefakts finden Sie hier.

3. Zugriff auf die Shell

In unseren Anwendungen gibt es zwei Möglichkeiten, auf die Shell zuzugreifen.

Die erste besteht darin, die Shell am Einstiegspunkt unserer Anwendung zu booten und den Benutzer die folgenden Befehle eingeben zu lassen:

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

Die zweite besteht darin, eine JLineShellComponent zu erhalten und die Befehle programmgesteuert auszuführen:

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

Wir werden den ersten Ansatz verwenden, da er für die Beispiele in diesem Artikel am besten geeignet ist. Im Quellcode finden Sie jedoch Testfälle, die das zweite Formular verwenden.

4. Befehle

In der Shell sind bereits mehrere Befehle integriert, z. B. Löschen , Hilfe , Beenden usw., die die Standardfunktionalität jeder CLI bereitstellen.

Benutzerdefinierte Befehle können durch Hinzufügen von Methoden mit der markierten ausgesetzt werden @CliCommand Annotation in einer Komponente Frühling die Umsetzung CommandMarker Schnittstelle.

Jedes Argument dieser Methode muss mit einer Annotation @CliOption gekennzeichnet sein . Wenn wir dies nicht tun, treten beim Versuch, den Befehl auszuführen, mehrere Fehler auf.

4.1. Hinzufügen von Befehlen zur Shell

Zuerst müssen wir der Shell mitteilen, wo sich unsere Befehle befinden. Dazu muss die Datei META-INF / spring / spring-shell-plugin.xml in unserem Projekt vorhanden sein. Dort können wir die Komponenten-Scan-Funktion von Spring verwenden:

Sobald die Komponenten von Spring registriert und instanziiert wurden, werden sie beim Shell-Parser registriert und ihre Anmerkungen werden verarbeitet.

Erstellen wir zwei einfache Befehle, einen zum Abrufen und Anzeigen des Inhalts einer URL und einen zum Speichern dieses Inhalts in einer Datei:

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

Beachten Sie, dass wir mehr als eine Zeichenfolge an die Wert- und Schlüsselattribute von @CliCommand bzw. @CliOption übergeben können. Auf diese Weise können wir mehrere Befehle und Argumente verfügbar machen , die sich gleich verhalten.

Lassen Sie uns nun überprüfen, ob alles wie erwartet funktioniert:

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

4.2. Verfügbarkeit von Befehlen

Wir können die Annotation @CliAvailabilityIndicator für eine Methode verwenden, die einen Booleschen Wert zurückgibt , um zur Laufzeit zu ändern, ob ein Befehl für die Shell verfügbar gemacht werden soll.

Lassen Sie uns zunächst eine Methode erstellen, um die Verfügbarkeit des Befehls web-save zu ändern :

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

Erstellen wir nun einen Befehl zum Ändern der Variablen adminEnableExecuted :

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

Lassen Sie es uns zum Schluss überprüfen:

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. Erforderliche Argumente

Standardmäßig sind alle Befehlsargumente optional. Wir können sie jedoch mit dem obligatorischen Attribut der Annotation @CliOption erforderlich machen :

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

Jetzt können wir testen, ob ein Fehler auftritt, wenn wir ihn nicht einführen:

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

4.4. Standardargumente

Ein leerer Schlüsselwert für eine @CliOption macht dieses Argument zum Standard. Dort erhalten wir die in der Shell eingeführten Werte, die nicht Teil eines benannten Arguments sind:

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

Lassen Sie uns nun überprüfen, ob es wie erwartet funktioniert:

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.