A CLI with Spring Shell – 一个带有Spring Shell的CLI

最后修改: 2017年 4月 2日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Simply put, the Spring Shell project provides an interactive shell for processing commands and building a full-featured CLI using the Spring programming model.

简单地说,Spring Shell 项目提供了一个用于处理命令和使用Spring编程模型构建全功能CLI的交互式外壳。

In this article, we’ll explore its features, key classes, and annotations, and implement several custom commands and customizations.

在这篇文章中,我们将探讨它的特点、关键类和注解,并实现几个自定义命令和定制。

2. Maven Dependency

2.Maven的依赖性

First, we need to add the spring-shell dependency to our pom.xml:

首先,我们需要将spring-shell依赖性添加到我们的pom.xml

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

The latest version of this artifact can be found here.

该工件的最新版本可以在这里找到。

3. Accessing the Shell

3.访问外壳

There are two main ways to access the shell in our applications.

在我们的应用程序中,有两种主要方式来访问shell。

The first is to bootstrap the shell in the entry point of our application and let the user enter the commands:

首先是在我们应用程序的入口处引导shell,让用户输入命令。

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

The second is to obtain a JLineShellComponent and execute the commands programmatically:

第二种是获得一个JLineShellComponent并以编程方式执行命令。

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

We’re going to use the first approach since its best suited for the examples in this article, however, in the source code you can find test cases that use the second form.

我们将使用第一种方法,因为它最适合本文中的例子,然而,在源代码中,你可以找到使用第二种形式的测试案例。

4. Commands

4.指令

There are already several built-in commands in the shell, such as clear, help, exit, etc., that provide the standard functionality of every CLI.

shell中已经有几个内置的命令,如clearhelpexit等,它们提供了每个CLI的标准功能。

Custom commands can be exposed by adding methods marked with the @CliCommand annotation inside a Spring component implementing the CommandMarker interface.

通过在实现CommandMarker接口的Spring组件中添加标有@CliCommand注解的方法,可以公开自定义命令。

Every argument of that method must be marked with a @CliOption annotation, if we fail to do this, we’ll encounter several errors when trying to execute the command.

该方法的每个参数都必须用@CliOption注解来标记,如果我们没有这样做,在试图执行该命令时,我们会遇到一些错误。

4.1. Adding Commands to the Shell

4.1.在外壳中添加命令

First, we need to let the shell know where our commands are. For this, it requires the file META-INF/spring/spring-shell-plugin.xml to be present in our project, there, we can use the component scanning functionality of Spring:

首先,我们需要让shell知道我们的命令在哪里。为此,它需要在我们的项目中存在META-INF/spring/spring-shell-plugin.xml文件,在那里,我们可以使用Spring的组件扫描功能。

<beans ... >
    <context:component-scan base-package="org.baeldung.shell.simple" />
</beans>

Once the components are registered and instantiated by Spring, they are registered with the shell parser, and their annotations are processed.

一旦组件被Spring注册并实例化,它们就会被注册到shell解析器中,并对其注释进行处理。

Let’s create two simple commands, one to grab the contents of an URL and display them, and other to save those contents to a file:

让我们创建两个简单的命令,一个是抓取一个URL的内容并显示,另一个是将这些内容保存到一个文件中。

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

Note that we can pass more than one string to the value and key attributes of @CliCommand and @CliOption respectively, this permits us to expose several commands and arguments that behave the same.

注意,我们可以向@CliCommand@CliOptionvaluekey属性分别传递一个以上的字符串,这允许我们公开几个行为相同的命令和参数。

Now, let’s check if everything is working as expected:

现在,让我们检查一下一切是否按预期工作。

spring-shell>web-get --url https://www.google.com
<!doctype html ... 
spring-shell>web-save --url https://www.google.com --out contents.txt
Done.

4.2. Availability of Commands

4.2.命令的可用性

We can use the @CliAvailabilityIndicator annotation on a method returning a boolean to change, at runtime, if a command should be exposed to the shell.

我们可以在返回boolean的方法上使用@CliAvailabilityIndicator注解,以便在运行时改变一个命令是否应该暴露给shell。

First, let’s create a method to modify the availability of the web-save command:

首先,让我们创建一个方法来修改web-save命令的可用性。

private boolean adminEnableExecuted = false;

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

Now, let’s create a command to change the adminEnableExecuted variable:

现在,让我们创建一个命令来改变adminEnableExecuted变量。

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

Finally, let’s verify it:

最后,让我们来验证一下。

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

4.3.必要的参数

By default, all command arguments are optional. However, we can make them required with the mandatory attribute of the @CliOption annotation:

默认情况下,所有的命令参数都是可选的。然而,我们可以通过@CliOption注解的mandatory属性使它们成为必需的。

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

Now, we can test that if we don’t introduce it, results in an error:

现在,我们可以测试一下,如果我们不引入它,结果是错误的。

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

4.4. Default Arguments

4.4.默认参数

An empty key value for a @CliOption makes that argument the default. There, we’ll receive the values introduced in the shell that are not part of any named argument:

对于一个@CliOption来说,一个空的key值使该参数成为默认值。在那里,我们将接收shell中引入的不属于任何命名参数的值。

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

Now, let’s check that it works as expected:

现在,让我们检查一下它是否像预期的那样工作。

spring-shell>web-get https://www.google.com
<!doctype html ...

4.5. Helping Users

4.5.帮助用户

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

@CliCommand@CliOption注解提供了一个help属性,允许我们在使用内置的help命令或通过标签获得自动完成时指导用户。

Let’s modify our web-get to add custom help messages:

让我们修改我们的web-get以添加自定义的帮助信息。

@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

5.定制化

There are three ways to customize the shell by implementing the BannerProvider, PromptProvider and HistoryFileNameProvider interfaces, all of them with default implementations already provided.

有三种方法可以通过实现BannerProviderPromptProviderHistoryFileNameProvider接口来定制shell,它们都已经提供了默认实现。

Also, we need to use the @Order annotation to allow our providers to take precedence over those implementations.

另外,我们需要使用@Order注解,以允许我们的提供者优先于这些实现。

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.

历史文件将记录在shell中执行的所有命令,并将与我们的应用程序放在一起。

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

6.转换器[/strong

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.

到目前为止,我们只使用简单的类型作为我们的命令的参数。常见的类型如Integer, Date, Enum, File等,已经注册了一个默认的转换器。

By implementing the Converter interface, we can also add our converters to receive custom objects.

通过实现Converter接口,我们也可以添加我们的转换器来接收自定义对象。

Let’s create a converter that can transform a String into an URL:

让我们创建一个转换器,可以将String转换成URL

@Component
public class SimpleURLConverter implements Converter<URL> {

    public URL convertFromText(
      String value, Class<?> requiredType, String optionContext) {
        return new URL(value);
    }

    public boolean getAllPossibleValues(
      List<Completion> 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:

最后,让我们修改我们的web-getweb-save命令。

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

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

As you may have guessed, the commands behave the same.

正如你可能已经猜到的,这些命令的行为是一样的。

7. Conclusion

7.结论

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.

在这篇文章中,我们简单了解了Spring Shell项目的核心功能。我们能够贡献我们的命令,用我们的提供者定制Shell,我们根据不同的运行时间条件改变命令的可用性,并创建一个简单的类型转换器。

Complete source code for this article can be found over on GitHub.

本文的完整源代码可以在GitHub上找到over