Add support for exit codes

- New configurations to CommandRegistration
- Re-using exit code concepts from boot
- Handling exit codes only in non-interactive mode
- Adding e2e commands and tests for better coverage
- Fixes #431
This commit is contained in:
Janne Valkealahti
2022-05-30 21:26:11 +01:00
parent 08428c88cb
commit 3891a8b375
16 changed files with 707 additions and 20 deletions

60
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,60 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Sample interactive",
"request": "launch",
"mainClass": "org.springframework.shell.samples.SpringShellSample",
"projectName": "spring-shell-samples"
},
{
"type": "java",
"name": "Sample help",
"request": "launch",
"mainClass": "org.springframework.shell.samples.SpringShellSample",
"projectName": "spring-shell-samples",
"args": "help"
},
{
"type": "java",
"name": "Sample fail noarg",
"request": "launch",
"mainClass": "org.springframework.shell.samples.SpringShellSample",
"projectName": "spring-shell-samples",
"args": "fail"
},
{
"type": "java",
"name": "Sample fail arg",
"request": "launch",
"mainClass": "org.springframework.shell.samples.SpringShellSample",
"projectName": "spring-shell-samples",
"args": "fail --elementType TYPE"
},
{
"type": "java",
"name": "e2e exit-code noarg",
"request": "launch",
"mainClass": "org.springframework.shell.samples.SpringShellSample",
"projectName": "spring-shell-samples",
"args": "e2e reg exit-code"
},
{
"type": "java",
"name": "e2e exit-code arg hi",
"request": "launch",
"mainClass": "org.springframework.shell.samples.SpringShellSample",
"projectName": "spring-shell-samples",
"args": "e2e reg exit-code --arg1 hi"
},
{
"type": "java",
"name": "e2e exit-code arg fun",
"request": "launch",
"mainClass": "org.springframework.shell.samples.SpringShellSample",
"projectName": "spring-shell-samples",
"args": "e2e reg exit-code --arg1 fun"
}
]
}

View File

@@ -0,0 +1,153 @@
import 'jest-extended';
import waitForExpect from 'wait-for-expect';
import { Cli } from 'spring-shell-e2e';
import {
nativeDesc,
jarDesc,
jarCommand,
nativeCommand,
jarOptions,
waitForExpectDefaultTimeout,
waitForExpectDefaultInterval,
testTimeout
} from '../src/utils';
describe('e2e commands exit-code', () => {
let cli: Cli;
let command: string;
let options: string[] = [];
const missingArgRet2Desc = 'missing arg returns 2';
const missingArgRet2Command = ['e2e reg exit-code'];
const missingArgRet2 = async (cli: Cli) => {
cli.run();
await waitForExpect(async () => {
const screen = cli.screen();
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('Missing option')]));
});
await expect(cli.exitCode()).resolves.toBe(2);
};
const customArgRet3Desc = 'custom arg returns 3';
const customArgRet3Command = ['e2e reg exit-code --arg1 ok'];
const customArgRet3 = async (cli: Cli) => {
cli.run();
await waitForExpect(async () => {
const screen = cli.screen();
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('ok')]));
});
await expect(cli.exitCode()).resolves.toBe(3);
};
const customArgRet4Desc = 'custom arg returns 4';
const customArgRet4Command = ['e2e reg exit-code --arg1 fun'];
const customArgRet4 = async (cli: Cli) => {
cli.run();
await waitForExpect(async () => {
const screen = cli.screen();
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('fun')]));
});
await expect(cli.exitCode()).resolves.toBe(4);
};
beforeEach(async () => {
waitForExpect.defaults.timeout = waitForExpectDefaultTimeout;
waitForExpect.defaults.interval = waitForExpectDefaultInterval;
}, testTimeout);
afterEach(async () => {
cli?.dispose();
}, testTimeout);
/**
* fatjar commands
*/
describe(jarDesc, () => {
beforeAll(() => {
command = jarCommand;
options = jarOptions;
});
it(
missingArgRet2Desc,
async () => {
cli = new Cli({
command: command,
options: [...options, ...missingArgRet2Command]
});
await missingArgRet2(cli);
},
testTimeout
);
it(
customArgRet3Desc,
async () => {
cli = new Cli({
command: command,
options: [...options, ...customArgRet3Command]
});
await customArgRet3(cli);
},
testTimeout
);
it(
customArgRet4Desc,
async () => {
cli = new Cli({
command: command,
options: [...options, ...customArgRet4Command]
});
await customArgRet4(cli);
},
testTimeout
);
});
/**
* native commands
*/
describe(nativeDesc, () => {
beforeAll(() => {
command = nativeCommand;
options = [];
});
it(
missingArgRet2Desc,
async () => {
cli = new Cli({
command: command,
options: [...options, ...missingArgRet2Command]
});
await missingArgRet2(cli);
},
testTimeout
);
it(
customArgRet3Desc,
async () => {
cli = new Cli({
command: command,
options: [...options, ...customArgRet3Command]
});
await customArgRet3(cli);
},
testTimeout
);
it(
customArgRet4Desc,
async () => {
cli = new Cli({
command: command,
options: [...options, ...customArgRet4Command]
});
await customArgRet4(cli);
},
testTimeout
);
});
});

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.boot;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.springframework.boot.ExitCodeExceptionMapper;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.command.CommandExecution;
import org.springframework.shell.exit.ExitCodeMappings;
/**
* {@link EnableAutoConfiguration Auto-configuration} for exit codes.
*
* @author Janne Valkealahti
*/
@Configuration(proxyBeanMethods = false)
public class ExitCodeAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ShellExitCodeExceptionMapper shellExitCodeExceptionMapper() {
return new ShellExitCodeExceptionMapper();
}
@Bean
@ConditionalOnMissingBean
public ShellExitCodeMappingsExceptionMapper shellExitCodeMappingsExceptionMapper() {
return new ShellExitCodeMappingsExceptionMapper();
}
static class ShellExitCodeExceptionMapper implements ExitCodeExceptionMapper {
@Override
public int getExitCode(Throwable exception) {
if (exception.getCause() instanceof CommandExecution.CommandParserExceptionsException) {
return 2;
}
return 1;
}
}
static class ShellExitCodeMappingsExceptionMapper implements ExitCodeExceptionMapper, ExitCodeMappings {
private final List<Function<Throwable, Integer>> functions = new ArrayList<>();
@Override
public void reset(List<Function<Throwable, Integer>> functions) {
this.functions.clear();
if (functions != null) {
this.functions.addAll(functions);
}
}
@Override
public int getExitCode(Throwable exception) {
int exitCode = 0;
for (Function<Throwable, Integer> function : functions) {
Integer code = function.apply(exception.getCause());
if (code != null) {
if (code > 0 && code > exitCode || code < 0 && code < exitCode) {
exitCode = code;
}
}
}
return exitCode;
}
}
}

View File

@@ -34,6 +34,8 @@ import org.springframework.shell.ResultHandler;
import org.springframework.shell.ResultHandlerService;
import org.springframework.shell.Shell;
import org.springframework.shell.command.CommandCatalog;
import org.springframework.shell.context.ShellContext;
import org.springframework.shell.exit.ExitCodeMappings;
import org.springframework.shell.result.GenericResultHandlerService;
import org.springframework.shell.result.ResultHandlerConfig;
@@ -65,8 +67,9 @@ public class SpringShellAutoConfiguration {
@Bean
public Shell shell(ResultHandlerService resultHandlerService, CommandCatalog commandRegistry, Terminal terminal,
@Qualifier("shellConversionService") ConversionService shellConversionService) {
Shell shell = new Shell(resultHandlerService, commandRegistry, terminal);
@Qualifier("shellConversionService") ConversionService shellConversionService, ShellContext shellContext,
ExitCodeMappings exitCodeMappings) {
Shell shell = new Shell(resultHandlerService, commandRegistry, terminal, shellContext, exitCodeMappings);
shell.setConversionService(shellConversionService);
return shell;
}

View File

@@ -1,4 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.shell.boot.ExitCodeAutoConfiguration,\
org.springframework.shell.boot.ShellContextAutoConfiguration,\
org.springframework.shell.boot.SpringShellAutoConfiguration,\
org.springframework.shell.boot.ShellRunnerAutoConfiguration,\

View File

@@ -15,12 +15,12 @@
*/
package org.springframework.shell;
import java.io.IOException;
import java.lang.reflect.UndeclaredThrowableException;
import java.nio.channels.ClosedByInterruptException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.validation.Validator;
@@ -42,6 +42,9 @@ import org.springframework.shell.command.CommandExecution.CommandExecutionExcept
import org.springframework.shell.command.CommandExecution.CommandExecutionHandlerMethodArgumentResolvers;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.completion.CompletionResolver;
import org.springframework.shell.context.InteractionMode;
import org.springframework.shell.context.ShellContext;
import org.springframework.shell.exit.ExitCodeMappings;
/**
* Main class implementing a shell loop.
@@ -65,6 +68,8 @@ public class Shell {
protected List<CompletionResolver> completionResolvers = new ArrayList<>();
private CommandExecutionHandlerMethodArgumentResolvers argumentResolvers;
private ConversionService conversionService = new DefaultConversionService();
private final ShellContext shellContext;
private final ExitCodeMappings exitCodeMappings;
/**
* Marker object to distinguish unresolved arguments from {@code null}, which is a valid
@@ -74,10 +79,13 @@ public class Shell {
private Validator validator = Utils.defaultValidator();
public Shell(ResultHandlerService resultHandlerService, CommandCatalog commandRegistry, Terminal terminal) {
public Shell(ResultHandlerService resultHandlerService, CommandCatalog commandRegistry, Terminal terminal,
ShellContext shellContext, ExitCodeMappings exitCodeMappings) {
this.resultHandlerService = resultHandlerService;
this.commandRegistry = commandRegistry;
this.terminal = terminal;
this.shellContext = shellContext;
this.exitCodeMappings = exitCodeMappings;
}
@Autowired
@@ -108,7 +116,7 @@ public class Shell {
* (<em>e.g.</em> a {@literal script} command).
* </p>
*/
public void run(InputProvider inputProvider) throws IOException {
public void run(InputProvider inputProvider) throws Exception {
Object result = null;
while (!(result instanceof ExitRequest)) { // Handles ExitRequest thrown from Quit command
Input input;
@@ -130,6 +138,18 @@ public class Shell {
if (result != NO_INPUT && !(result instanceof ExitRequest)) {
resultHandlerService.handle(result);
}
// throw if not in interactive mode so that boot's exit code feature
// can contribute exit code. we can't throw when in interactive mode as
// that would exit a shell
if (this.shellContext != null && this.shellContext.getInteractionMode() != InteractionMode.INTERACTIVE) {
if (result instanceof CommandExecution.CommandParserExceptionsException) {
throw (CommandExecution.CommandParserExceptionsException) result;
}
else if (result instanceof Exception) {
throw (Exception) result;
}
}
}
}
@@ -169,6 +189,12 @@ public class Shell {
.findFirst();
if (commandRegistration.isPresent()) {
if (this.exitCodeMappings != null) {
List<Function<Throwable, Integer>> mappingFunctions = commandRegistration.get().getExitCode()
.getMappingFunctions();
this.exitCodeMappings.reset(mappingFunctions);
}
List<String> wordsForArgs = wordsForArguments(command, words);
Thread commandThread = Thread.currentThread();

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.command;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
/**
* Interface representing an exit code in a command.
*
* @author Janne Valkealahti
*/
public interface CommandExitCode {
/**
* Gets a function mappings from exceptions to exit codes.
*
* @return function mappings
*/
List<Function<Throwable, Integer>> getMappingFunctions();
/**
* Gets an instance of a default {@link CommandExitCode}.
*
* @return a command exit code
*/
public static CommandExitCode of() {
return of(new ArrayList<>());
}
/**
* Gets an instance of a default {@link CommandExitCode}.
*
* @param functions the function mappings
* @return a command exit code
*/
public static CommandExitCode of(List<Function<Throwable, Integer>> functions) {
return new DefaultCommandExitCode(functions);
}
static class DefaultCommandExitCode implements CommandExitCode {
private final List<Function<Throwable, Integer>> functions;
DefaultCommandExitCode( List<Function<Throwable, Integer>> functions) {
this.functions = functions;
}
@Override
public List<Function<Throwable, Integer>> getMappingFunctions() {
return functions;
}
}
}

View File

@@ -98,6 +98,13 @@ public interface CommandRegistration {
*/
List<CommandAlias> getAliases();
/**
* Gets an exit code.
*
* @return the exit code
*/
CommandExitCode getExitCode();
/**
* Gets a new instance of a {@link Buidler}.
*
@@ -386,6 +393,35 @@ public interface CommandRegistration {
Builder and();
}
/**
* Spec defining an exit code.
*/
public interface ExitCodeSpec {
/**
* Define mapping from exception to code.
*
* @param e the exception
* @param code the exit code
* @return a target spec for chaining
*/
ExitCodeSpec map(Class<? extends Throwable> e, int code);
/**
*
* @param function
* @return
*/
ExitCodeSpec map(Function<Throwable, Integer> function);
/**
* Return a builder for chaining.
*
* @return a builder for chaining
*/
Builder and();
}
/**
* Builder interface for {@link CommandRegistration}.
*/
@@ -456,6 +492,13 @@ public interface CommandRegistration {
*/
AliasSpec withAlias();
/**
* Define an exit code what this command should execute
*
* @return exit code spec for chaining
*/
ExitCodeSpec withExitCode();
/**
* Builds a {@link CommandRegistration}.
*
@@ -689,6 +732,39 @@ public interface CommandRegistration {
}
}
static class DefaultExitCodeSpec implements ExitCodeSpec {
private BaseBuilder builder;
private final List<Function<Throwable, Integer>> functions = new ArrayList<>();
DefaultExitCodeSpec(BaseBuilder builder) {
this.builder = builder;
}
@Override
public ExitCodeSpec map(Class<? extends Throwable> e, int code) {
Function<Throwable, Integer> f = t -> {
if (ObjectUtils.nullSafeEquals(t.getClass(), e)) {
return code;
}
return 0;
};
this.functions.add(f);
return this;
}
@Override
public ExitCodeSpec map(Function<Throwable, Integer> function) {
this.functions.add(function);
return this;
}
@Override
public Builder and() {
return builder;
}
}
static class DefaultCommandRegistration implements CommandRegistration {
private String command;
@@ -699,10 +775,11 @@ public interface CommandRegistration {
private List<DefaultOptionSpec> optionSpecs;
private DefaultTargetSpec targetSpec;
private List<DefaultAliasSpec> aliasSpecs;
private DefaultExitCodeSpec exitCodeSpec;
public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group,
String description, Supplier<Availability> availability, List<DefaultOptionSpec> optionSpecs,
DefaultTargetSpec targetSpec, List<DefaultAliasSpec> aliasSpecs) {
DefaultTargetSpec targetSpec, List<DefaultAliasSpec> aliasSpecs, DefaultExitCodeSpec exitCodeSpec) {
this.command = commandArrayToName(commands);
this.interactionMode = interactionMode;
this.group = group;
@@ -711,6 +788,7 @@ public interface CommandRegistration {
this.optionSpecs = optionSpecs;
this.targetSpec = targetSpec;
this.aliasSpecs = aliasSpecs;
this.exitCodeSpec = exitCodeSpec;
}
@Override
@@ -769,6 +847,16 @@ public interface CommandRegistration {
.collect(Collectors.toList());
}
@Override
public CommandExitCode getExitCode() {
if (this.exitCodeSpec == null) {
return CommandExitCode.of();
}
else {
return CommandExitCode.of(exitCodeSpec.functions);
}
}
private static String commandArrayToName(String[] commands) {
return Arrays.asList(commands).stream()
.flatMap(c -> Stream.of(c.split(" ")))
@@ -792,6 +880,7 @@ public interface CommandRegistration {
private List<DefaultOptionSpec> optionSpecs = new ArrayList<>();
private List<DefaultAliasSpec> aliasSpecs = new ArrayList<>();
private DefaultTargetSpec targetSpec;
private DefaultExitCodeSpec exitCodeSpec;
@Override
public Builder command(String... commands) {
@@ -848,7 +937,14 @@ public interface CommandRegistration {
DefaultAliasSpec spec = new DefaultAliasSpec(this);
this.aliasSpecs.add(spec);
return spec;
};
}
@Override
public ExitCodeSpec withExitCode() {
DefaultExitCodeSpec spec = new DefaultExitCodeSpec(this);
this.exitCodeSpec = spec;
return spec;
}
@Override
public CommandRegistration build() {
@@ -856,7 +952,7 @@ public interface CommandRegistration {
Assert.notNull(targetSpec, "target cannot be empty");
Assert.state(!(targetSpec.bean != null && targetSpec.function != null), "only one target can exist");
return new DefaultCommandRegistration(commands, interactionMode, group, description, availability,
optionSpecs, targetSpec, aliasSpecs);
optionSpecs, targetSpec, aliasSpecs, exitCodeSpec);
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.exit;
import java.util.List;
import java.util.function.Function;
/**
* Interface used with implementation of a boot's ExitCodeExceptionMapper
* in a context of spring-shell spesific one. Mostly needed not to have a
* direct dependencies to boot classes as currently only one implementation
* instance can exist which we need to reset between command executions.
*
* @author Janne Valkealahti
*/
public interface ExitCodeMappings {
/**
* Reset mappings into a given functions.
*
* @param functions the mapping functions
*/
void reset(List<Function<Throwable, Integer>> functions);
}

View File

@@ -15,7 +15,6 @@
*/
package org.springframework.shell;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -71,7 +70,7 @@ public class ShellTests {
}
@Test
public void commandMatch() throws IOException {
public void commandMatch() throws Exception {
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
doThrow(new Exit()).when(resultHandlerService).handle(any());
@@ -97,7 +96,7 @@ public class ShellTests {
}
@Test
public void commandNotFound() throws IOException {
public void commandNotFound() throws Exception {
when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?");
doThrow(new Exit()).when(resultHandlerService).handle(isA(CommandNotFound.class));
@@ -122,7 +121,7 @@ public class ShellTests {
@Test
// See https://github.com/spring-projects/spring-shell/issues/142
public void commandNotFoundPrefix() throws IOException {
public void commandNotFoundPrefix() throws Exception {
when(inputProvider.readInput()).thenReturn(() -> "helloworld how are you doing ?");
doThrow(new Exit()).when(resultHandlerService).handle(isA(CommandNotFound.class));
@@ -146,7 +145,7 @@ public class ShellTests {
}
@Test
public void noCommand() throws IOException {
public void noCommand() throws Exception {
when(inputProvider.readInput()).thenReturn(() -> "", () -> "hello world how are you doing ?", null);
doThrow(new Exit()).when(resultHandlerService).handle(any());
@@ -172,7 +171,7 @@ public class ShellTests {
}
@Test
public void commandThrowingAnException() throws IOException {
public void commandThrowingAnException() throws Exception {
when(inputProvider.readInput()).thenReturn(() -> "fail");
doThrow(new Exit()).when(resultHandlerService).handle(isA(SomeException.class));
@@ -199,7 +198,7 @@ public class ShellTests {
}
@Test
public void comments() throws IOException {
public void comments() throws Exception {
when(inputProvider.readInput()).thenReturn(() -> "// This is a comment", (Input) null);
shell.run(inputProvider);

View File

@@ -336,7 +336,7 @@ public class CommandRegistrationTests extends AbstractCommandTests {
assertThat(registration.getOptions()).hasSize(1);
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(0);
}
}
@Test
public void testArityViaEnum() {
@@ -353,8 +353,7 @@ public class CommandRegistrationTests extends AbstractCommandTests {
assertThat(registration.getOptions()).hasSize(1);
assertThat(registration.getOptions().get(0).getArityMin()).isEqualTo(0);
assertThat(registration.getOptions().get(0).getArityMax()).isEqualTo(0);
}
}
@Test
public void testAliases() {
@@ -381,4 +380,32 @@ public class CommandRegistrationTests extends AbstractCommandTests {
assertThat(registration.getAliases().get(0).getGroup()).isEqualTo("Alias Group");
assertThat(registration.getAliases().get(1).getGroup()).isEqualTo("Alias Group");
}
@Test
public void testExitCodes() {
CommandRegistration registration;
registration = CommandRegistration.builder()
.command("command1")
.withTarget()
.function(function1)
.and()
.build();
assertThat(registration.getExitCode()).isNotNull();
assertThat(registration.getExitCode().getMappingFunctions()).hasSize(0);
registration = CommandRegistration.builder()
.command("command1")
.withExitCode()
.map(RuntimeException.class, 1)
.map(IllegalArgumentException.class, 2)
.map(e -> 1)
.map(e -> 2)
.and()
.withTarget()
.function(function1)
.and()
.build();
assertThat(registration.getExitCode()).isNotNull();
assertThat(registration.getExitCode().getMappingFunctions()).hasSize(4);
}
}

View File

@@ -0,0 +1,39 @@
[[dynamic-command-exitcode]]
==== Exit Code
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
Many command line applications when applicable return an _exit code_ which running environment
can use to differentiate if command has been executed successfully or not. In a `spring-shell`
this mostly relates when a command is run on a non-interactive mode meaning one command
is always executed once with an instance of a `spring-shell`.
Default behaviour of an exit codes is as:
- Errors from a command option parsing will result code of `2`
- Any generic error will result result code of `1`
- Obviously in any other case result code is `0`
Every `CommandRegistration` can define its own mappings between _Exception_ and _exit code_.
Essentially we're bound to functionality in `Spring Boot` regarding _exit code_ and simply
integrate into that.
Assuming there is an exception show below which would be thrown from a command:
====
[source, java, indent=0]
----
include::{snippets}/ExitCodeSnippets.java[tag=my-exception-class]
----
====
It is possible to define a mapping function between `Throwable` and exit code. You can also
just configure a _class_ to _exit code_ which is just a syntactic sugar within configurations.
====
[source, java, indent=0]
----
include::{snippets}/ExitCodeSnippets.java[tag=example1]
----
====
NOTE: Exit codes cannot be customized with annotation based configuration

View File

@@ -18,3 +18,5 @@ include::using-shell-commands-programmaticmodel.adoc[]
include::using-shell-commands-organize.adoc[]
include::using-shell-commands-availability.adoc[]
include::using-shell-commands-exitcode.adoc[]

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.shell.docs;
import org.springframework.shell.command.CommandRegistration;
public class ExitCodeSnippets {
// tag::my-exception-class[]
static class MyException extends RuntimeException {
private final int code;
MyException(String msg, int code) {
super(msg);
this.code = code;
}
public int getCode() {
return code;
}
}
// end::my-exception-class[]
void dump1() {
// tag::example1[]
CommandRegistration.builder()
.withExitCode()
.map(MyException.class, 3)
.map(t -> {
if (t instanceof MyException) {
return ((MyException) t).getCode();
}
return 0;
})
.and()
.build();
// end::example1[]
}
}

View File

@@ -106,4 +106,41 @@ public class E2ECommands {
.build();
}
@Bean
public CommandRegistration testExitCodeRegistration() {
return CommandRegistration.builder()
.command("e2e", "reg", "exit-code")
.group("E2E Commands")
.withOption()
.longNames("arg1")
.required()
.and()
.withTarget()
.consumer(ctx -> {
String arg1 = ctx.getOptionValue("arg1");
throw new MyException(arg1);
})
.and()
.withExitCode()
.map(MyException.class, 3)
.map(t -> {
String msg = t.getMessage();
if (msg != null && msg.contains("ok")) {
return 0;
}
else if (msg != null && msg.contains("fun")) {
return 4;
}
return 0;
})
.and()
.build();
}
static class MyException extends RuntimeException {
MyException(String msg) {
super(msg);
}
}
}

View File

@@ -17,7 +17,6 @@ package org.springframework.shell.standard.commands;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import org.jline.reader.Parser;
@@ -57,7 +56,7 @@ public class Script extends AbstractShellComponent {
}
@ShellMethod(value = "Read and execute commands from a file.")
public void script(File file) throws IOException {
public void script(File file) throws Exception {
Reader reader = new FileReader(file);
try (FileInputProvider inputProvider = new FileInputProvider(reader, parser)) {
getShell().run(inputProvider);