执行单个命令并退出Spring Shell 2

时间:2017-09-22 14:52:54

标签: java spring-boot spring-shell

我偶然发现了this question,解释了如何在使用单个命令从命令行调用Spring Shell应用程序后退出。但是,使用Spring Boot在2.0.0中测试它,似乎不再是用命令参数调用JAR将执行该命令然后退出的情况。 shell只是正常启动而不执行提供的命令。是否仍然可以这样做?如果没有,是否可以将参数从JAR执行传递给Spring Shell,然后在执行后触发退出?

例如,假设我有一个命令,import有两个选项。它可以在shell中运行,如下所示:

$ java -jar my-app.jar

> import -f /path/to/file.txt --overwrite
Successfully imported 'file.txt'

> exit

但是为了构建一个可以利用这个功能的脚本,能够简单地执行和退出会很好:

$ java -jar my-app.jar import -f /path/to/file.txt --overwrite
Successfully imported 'file.txt'

5 个答案:

答案 0 :(得分:3)

在不排除交互模式和脚本模式的情况下添加单个命令运行模式的方法(在 spring-shell-starter::2.0.0.RELEASE 上测试)。

类比于 ScriptShellApplicationRunner 创建一个运行器。

// Runs before ScriptShellApplicationRunner and InteractiveShellApplicationRunner
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 200)
public class SingleCommandApplicationRunner implements ApplicationRunner {

    private final Parser parser;
    private final Shell shell;
    private final ConfigurableEnvironment environment;
    private final Set<String> allCommandNames;

    public SingleCommandApplicationRunner(
            Parser parser,
            Shell shell,
            ConfigurableEnvironment environment,
            Set<CustomCommand> customCommands
    ) {
        this.parser = parser;
        this.shell = shell;
        this.environment = environment;
        this.allCommandNames = buildAllCommandNames(customCommands);
    }

    private Set<String> buildAllCommandNames(Collection<CustomCommand> customCommands) {
        final Set<String> result = new HashSet<>();
        customCommands.stream().map(CustomCommand::keys).flatMap(Collection::stream).forEach(result::add);
        // default spring shell commands
        result.addAll(asList("clear", "exit", "quit", "help", "script", "stacktrace"));
        return result;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        final boolean singleCommand = haveCommand(args.getSourceArgs());
        if (singleCommand) {
            InteractiveShellApplicationRunner.disable(environment);
            final String fullArgs = join(" ", args.getSourceArgs());
            try (Reader reader = new StringReader(fullArgs);
                 FileInputProvider inputProvider = new FileInputProvider(reader, parser)) {
                shell.run(inputProvider);
            }
        }
    }

    private boolean haveCommand(String... args) {
        for (String arg : args) {
            if (allCommandNames.contains(arg)) {
                return true;
            }
        }
        return false;
    }

}

将 runner 注册为 bean。

@Configuration
class ContextConfiguration {

    @Autowired
    private Shell shell;

    @Bean
    SingleCommandApplicationRunner singleCommandApplicationRunner(
            Parser parser,
            ConfigurableEnvironment environment,
            Set<CustomCommand> customCommands
    ) {
        return new SingleCommandApplicationRunner(parser, shell, environment, customCommands);
    }

}

为了使运行程序仅在发送命令时启动,我们创建了一个接口。

public interface CustomCommand {

    Collection<String> keys();

}

在每个命令中实现 CustomCommand 接口。

@ShellComponent
@RequiredArgsConstructor
class MyCommand implements CustomCommand {

    private static final String KEY = "my-command";

    @Override
    public Collection<String> keys() {
        return singletonList(KEY);
    }

    @ShellMethod(key = KEY, value = "My custom command.")
    public AttributedString version() {
        return "Hello, single command mode!";
    }

}

完成!

以交互模式运行:

java -jar myApp.jar

// 2021-01-14 19:28:16.911 INFO 67313 --- [main] com.nao4j.example.Application: Starting Application v1.0.0 using Java 1.8.0_275 on Apple-MacBook-Pro-15.local with PID 67313 (/Users/nao4j/example/target/myApp.jar started by nao4j in /Users/nao4j/example/target)
// 2021-01-14 19:28:16.916 INFO 67313 --- [main] com.nao4j.example.Application: No active profile set, falling back to default profiles: default
// 2021-01-14 19:28:18.227 INFO 67313 --- [main] com.nao4j.example.Application: Started Application in 2.179 seconds (JVM running for 2.796)
// shell:>my-command
// Hello, single command mode!

从文件 script.txt 运行脚本(包含文本“my-command”):

java -jar myApp.jar @script.txt

// 2021-01-14 19:28:16.911 INFO 67313 --- [main] com.nao4j.example.Application: Starting Application v1.0.0 using Java 1.8.0_275 on Apple-MacBook-Pro-15.local with PID 67313 (/Users/nao4j/example/target/myApp.jar started by nao4j in /Users/nao4j/example/target)
// 2021-01-14 19:28:16.916 INFO 67313 --- [main] com.nao4j.example.Application: No active profile set, falling back to default profiles: default
// 2021-01-14 19:28:18.227 INFO 67313 --- [main] com.nao4j.example.Application: Started Application in 2.179 seconds (JVM running for 2.796)
// Hello, single command mode!

在单命令模式下运行:

java -jar myApp.jar my-command

// 2021-01-14 19:28:16.911 INFO 67313 --- [main] com.nao4j.example.Application: Starting Application v1.0.0 using Java 1.8.0_275 on Apple-MacBook-Pro-15.local with PID 67313 (/Users/nao4j/example/target/myApp.jar started by nao4j in /Users/nao4j/example/target)
// 2021-01-14 19:28:16.916 INFO 67313 --- [main] com.nao4j.example.Application: No active profile set, falling back to default profiles: default
// 2021-01-14 19:28:18.227 INFO 67313 --- [main] com.nao4j.example.Application: Started Application in 2.179 seconds (JVM running for 2.796)
// Hello, single command mode!

答案 1 :(得分:1)

使用@ my-script运行它,如下所示:

java -jar my-app.jar @my-script

其中my-script是包含您的命令的文件:

import -f /path/to/file.txt --overwrite

答案 2 :(得分:0)

我发现了一个很好的小解决方案。而不是创建模仿v1行为的ApplicationRunner(这是棘手的,因为JLineInputProvider是一个私有类),我创建了一个可选的加载,基于活动的Spring配置文件。我使用JCommander来定义CLI参数,允许我为交互式shell和一次性执行提供相同的命令。在没有args的情况下运行Spring Boot JAR会触发交互式s​​hell。使用参数运行它会触发一次性执行。

@Parameters
public class ImportParameters {

  @Parameter(names = { "-f", "--file" }, required = true, description = "Data file")
  private File file;

  @Parameter(names = { "-t", "--type" }, required = true, description = "Data type")
  private DataType dataType;

  @Parameter(names = { "-o", "--overwrite" }, description = "Flag to overwrite file if it exists")
  private Boolean overwrite = false;

  /* getters and setters */
}

public class ImportCommandExecutor {

  public void run(ImportParameters params) throws Exception {
    // import logic goes here
  }

}

/* Handles interactive shell command execution */
@ShellComponent
public class JLineInputExecutor {

  // All command executors are injected here
  @Autowired private ImportCommandExecutor importExecutor;
  ...

  @ShellMethod(key = "import", value = "Imports the a file of a specified type.")
  public String importCommand(@ShellOption(optOut = true) ImportParameters params) throws Exception {
    importCommandExecutor.run(params);
  }

  ...

}

/* Handles one-off command execution */
public class JCommanderInputExecutor implements ApplicationRunner {

  // All command executors are injected here
  @Autowired private ImportCommandExecutor importExecutor;
  ...

  @Override
  public void run(ApplicationArguments args) throws Exception {

    // Create all of the JCommander argument handler objects
    BaseParameters baseParameters = new BaseParameters();
    ImportParameters importParameters = new ImportParameters();
    ...

    JCommander jc = newBuilder().
      .acceptUnknownOptions(true)
      .addObject(baseParameters)
      .addCommand("import", importParameters)
      ...
      .build();

    jc.parse(args);
    String mainCommand = jc.getParsedCommand();

    if ("import".equals(mainCommand)){
      importExecutor.run(importParameters);
    } else if (...) {
      ...
    }  

  }
}

@Configuration
@Profile({"CLI"})
public class CommandLineInterfaceConfiguration {

  // All of my command executors are defined as beans here, as well as other required configurations for both modes of execution 
  @Bean
  public ImportCommandExecutor importExecutor (){
    return new ImportCommandExecutor();
  }
  ...

}

@Configuration
@Profile({"SINGLE_COMMAND"})
public class SingleCommandConfiguration {

  @Bean
  public JCommanderInputExecutor commandLineInputExecutor(){
    return new JCommanderInputExecutor();
  }

}

@SpringBootApplication
public class Application {

  public static void main(String[] args) throws IOException {
    String[] profiles = getActiveProfiles(args);
    SpringApplicationBuilder builder = new SpringApplicationBuilder(Application.class);
    builder.bannerMode((Mode.LOG));
    builder.web(false);
    builder.profiles(profiles);
    System.out.println(String.format("Command line arguments: %s  Profiles: %s",
        Arrays.asList(args), Arrays.asList(profiles)));
    builder.run(args);
  }

  private static String[] getActiveProfiles(String[] args){
    return Arrays.asList(args).contains("-X") ? new String[]{"CLI", "SINGLE_COMMAND"} : new String[]{"CLI"};
  }

}

所以现在我只需运行我的可执行文件JAR即可触发交互式客户端:

java -jar app.jar
> import -f /path/to/file.txt -t GENE -o
> quit()

或者,如果我通过&#39; -X&#39;在命令行上的参数,应用程序将执行然后退出:

java -jar app.jar -X import -f /path/to/file.txt -t GENE -o

答案 3 :(得分:0)

只是添加,我发现了另一种方法,它没有为您提供以交互模式运行的选项,但使用上面的配置文件,您当然可以交换配置。请注意我使用lombok和jool(以防任何人复制粘贴并获得有趣的问题!)

条目

@SpringBootApplication
public class Righter {

    public static void main(String[] args) {
        SpringApplication.run(Righter.class, args);
    }

    @Bean
    public ApplicationRunner shellRunner(Shell shell) {
        return new NonInteractiveShellRunner(shell);
    }

申请人:

@Order(0)
public class NonInteractiveShellRunner implements ApplicationRunner{

    private final Shell shell;

    public NonInteractiveShellRunner(Shell shell) {
        this.shell = shell;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        shell.run(new CommandInputProvider(args.getSourceArgs()));
    }

    public static class PredefinedInputProvider implements InputProvider{

        private final Input input;
        private boolean commandExecuted = false;

        public PredefinedInputProvider(String[] args) {
            this.input = new PredefinedInput(args);
        }

        @Override
        public Input readInput() {
            if (!commandExecuted){
                commandExecuted=true;
                return input;
            }
            return new PredefinedInput(new String[]{"exit"});
        }

        @AllArgsConstructor
        private static class PredefinedInput implements Input{

            private final String[] args;

            @Override
            public String rawText() {
                return Seq.of(args).toString(" ");
            }

            @Override
            public List<String> words(){
                return Arrays.asList(args);
            }
        }

    }

}

答案 4 :(得分:0)

除了Alex的答案,这是我制作的NonInteractiveApplicationRunner的简单版本。

@Component
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 100)
class NonInteractiveApplicationRunner implements ApplicationRunner {

    private final Shell shell;
    private final ConfigurableEnvironment environment;

    public NonInteractiveApplicationRunner(Shell shell, ConfigurableEnvironment environment) {
        this.shell = shell;
        this.environment = environment;
    }

    @Override
    public void run(ApplicationArguments args) {
        if (args.getSourceArgs().length > 0) {
            InteractiveShellApplicationRunner.disable(environment);
            var input = String.join(" ", args.getSourceArgs());
            shell.evaluate(() -> input);
            shell.evaluate(() -> "exit");
        }
    }
}

使用@Component,我们不需要添加bean方法。此外,与shell.evaluate()相比,使用shell.run(...)方法看起来要简单得多。