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:
60
.vscode/launch.json
vendored
Normal file
60
.vscode/launch.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
153
e2e/spring-shell-e2e-tests/test/sample-e2e-exit-code.test.ts
Normal file
153
e2e/spring-shell-e2e-tests/test/sample-e2e-exit-code.test.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,\
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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[]
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user