Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Executing a single command and exitting #159

Closed
woemler opened this issue Sep 22, 2017 · 11 comments
Closed

Executing a single command and exitting #159

woemler opened this issue Sep 22, 2017 · 11 comments

Comments

@woemler
Copy link

woemler commented Sep 22, 2017

I stumbled upon this question on Stack Overflow, explaining how to get a Spring Shell application to exit after calling it from the command line with a single command. However, testing this in 2.0.0 with Spring Boot, it does not seem to be the case any more that invoking the JAR with command arguments will execute that command and then exit. The shell just starts as normal without executing the supplied command. Is it still possible to do this? If not, would it be possible to pass the arguments from the JAR execution to Spring Shell and then trigger an exit after execution?

@ericbottard
Copy link
Member

Have a look at https://docs.spring.io/spring-shell/docs/2.0.0.BUILD-SNAPSHOT/reference/htmlsingle/#_customizing_command_line_options_behavior to find out how to either:

  • Do that "by hand" by putting your command in a file and invoking the Shell with @/path/to/the/file
  • Writing an ApplicationRunner that would mimic the Shell 1 behavior (and then some if you want) to do exactly what you want

Hope that helps

@woemler
Copy link
Author

woemler commented Sep 22, 2017

Thanks for the prompt response. The second option sounds promising, I will pursue that. Is there a reason why the single command execution was taken out of the project in version 2.0?

@ericbottard
Copy link
Member

No problem.

The previous approach was removed because accepting any program argument and assuming it is a command is too fragile, especially in the era of Spring Boot which makes using @ConfigurationProperties so easy (so users will typically pass --some.prefix.key=value

Another approach which may work (but I haven't entirely tested yet) is to pipe your command into the shell, ie echo help | java -jar sping-shell.jar

@ericbottard
Copy link
Member

Is it ok if I close this issue, or do you think there is more that needs to be done here?

@woemler woemler closed this as completed Oct 16, 2017
@woemler
Copy link
Author

woemler commented Oct 25, 2017

FYI, for anybody else looking to do the same thing, I found a nice little work-around. Rather than creating an ApplicationRunner that mimics the v1 behavior (which is tricky, since JLineInputProvider is a private class), I created one that is optionally loaded, based on active Spring profile. I used JCommander to define the CLI parameters, allowing me to have identical commands for the interactive shell and the one-off executions. Running the Spring Boot JAR with no args triggers the interactive shell. Running it with arguments triggers the one-and-done execution.

public class JCommanderImportCommands {

  public static enum DataType { SAMPLE, GENE, DATA }
  
  @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 */
}

@ShellComponent
public class ImportCommands {

  @ShellMethod(key = "import", value = "Imports the a file of a specified type.")
  public String jCommanderFileImport(@ShellOption(optOut = true) JCommanderImportCommands commands){
    System.out.println(String.format("Importing file=%s  dataType=%s  overwrite=%s", 
        commands.getFile(), commands.getDataType(), commands.getOverwrite()));
    return commands.getFile().getAbsolutePath();
  }

}

public class JCommanderShellRunner implements ApplicationRunner {

  @Override
  public void run(ApplicationArguments args) throws Exception {
    System.out.println(args.getNonOptionArgs());
    JCommanderImportCommands importCommands = new JCommanderImportCommands();
    JCommander.newBuilder()
        .addCommand("import", importCommands)
        .acceptUnknownOptions(true)
        .build()
        .parse(args.getSourceArgs());
    System.out.println(String.format("Importing file=%s  dataType=%s  overwrite=%s",
        importCommands.getFile(), importCommands.getDataType(), importCommands.getOverwrite()));
  }
}

@Configuration
@Profile({"jc"})
public class ShellConfig {

  @Bean
  public JCommanderShellRunner shellRunner(){
    return new JCommanderShellRunner();
  }

}

@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 args.length > 0 ? new String[]{"jc"} : new String[]{};
  }

}

Source code here.

@Lovett1991
Copy link

I had issues piping the command the command in (I think possibly because the input reader would get null on expecting the next command). The command would run fine, but the would get a bad exit.

I have the following which works for me (I am using jool and lombok) as above you can easily use profiles to inject this bean or use the interactive shell. pretty much copy paste of DefaultShellApplicationRunner method except it wraps the JLineInput. To run I just use:

java -jar app.jar <<< $(echo command)

@Order(0)
public class SingleCommandShellRunner extends DefaultShellApplicationRunner{
    
    private final LineReader lineReader;
    private final PromptProvider promptProvider;
    private final Parser parser;
    private final Shell shell;
    
    public SingleCommandShellRunner(LineReader lineReader, PromptProvider promptProvider, Parser parser, Shell shell) {
        super(lineReader, promptProvider, parser, shell);
        this.lineReader = lineReader;
        this.promptProvider = promptProvider;
        this.parser = parser;
        this.shell = shell;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        List<File> scriptsToRun = args.getNonOptionArgs().stream()
                .filter(s -> s.startsWith("@"))
                .map(s -> new File(s.substring(1)))
                .collect(Collectors.toList());

        if (scriptsToRun.isEmpty()) {
            InputProvider inputProvider = new SingleCommandInputProvider(new JLineInputProvider(lineReader, promptProvider));
            shell.run(inputProvider);
        } else {
            for (File file : scriptsToRun) {
                try (Reader reader = new FileReader(file); FileInputProvider inputProvider = new FileInputProvider(reader, parser)) {
                    shell.run(inputProvider);
                }
            }
        }
    }
    
    public static class SingleCommandInputProvider implements InputProvider{
        
        private boolean hasCommandRun = false;
        private final InputProvider inputProvider;
        
        public SingleCommandInputProvider(InputProvider inputProvider) {
            this.inputProvider = inputProvider;
        }
        
        @Override
        public Input readInput() {
            if (!hasCommandRun) {
                hasCommandRun = true;
                return inputProvider.readInput();
            }
            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);
            }
        }
        
    }
            
}

@bigbasti
Copy link

bigbasti commented Feb 21, 2018

Hello @Lovett1991, i'm not really sure how to use your solution. could you elaborate a little on how to integrate this class?
I implemented this class and added some System.out commands to it. But it seems like it is never being used. At least i see no different behavior when i call java -jar script.jar <<< $(echo command param) so i still get the exception after the command has been executed successful :

java.lang.IllegalStateException: Failed to execute ApplicationRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:726)
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:713)
	at org.springframework.boot.SpringApplication.afterRefresh(SpringApplication.java:703)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:304)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1118)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1107)
	at c.t.r.CliApplication.main(CliApplication.java:74)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
	at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
	at org.springframework.boot.loader.Launcher.launch(Launcher.java:50)
	at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:51)
Caused by: java.lang.NullPointerException: null
	at org.springframework.shell.jline.ParsedLineInput.words(ParsedLineInput.java:44)
	at org.springframework.shell.Shell.noInput(Shell.java:194)
	at org.springframework.shell.Shell.evaluate(Shell.java:150)
	at org.springframework.shell.Shell.run(Shell.java:133)
	at org.springframework.shell.jline.DefaultShellApplicationRunner.run(DefaultShellApplicationRunner.java:80)
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:723)
	... 14 common frames omitted

@Lovett1991
Copy link

In your config you'll need to define the bean:

@Bean
public ApplicationRunner shellRunner(LineReader lineReader, PromptProvider promptProvider, Parser parser, Shell shell) { 
  return new SingleCommandShellRunner(lineReader, promptProvider, parser, shell);
}

@bigbasti
Copy link

That was the missing piece! Thanks!

@LuboVarga
Copy link

LuboVarga commented Mar 23, 2018

What about to exit cleanly in echo solution and do not code more?
echo "help\nexit" | java -jar sping-shell.jar
It works OK for me.

@ptitvert
Copy link

ptitvert commented Oct 15, 2018

@ericbottard I am not sure to understand the reason why "The previous approach was removed because accepting any program argument and assuming it is a command is too fragile".
If you can use the "pipe" method, or "using @ConfigurationProperties so easy (so users will typically pass --some.prefix.key=value)" then it would be also "fragile", the same fragility as to use command line arguments.
Or am I missing something?
I ask that, because there is a obvious need for that, and the choice to remove it doesn't make sense for me, according to the explanation, especially if the other other options seems to be as fragile as the original method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants