Split sample app

- spring-shell-sample-commands and spring-shell-sample-e2e
- Needed changes in e2e tests and workflow
- Fixes #754
This commit is contained in:
Janne Valkealahti
2023-06-15 10:31:05 +01:00
parent 71ed64670f
commit c27b85fb0e
58 changed files with 257 additions and 31 deletions

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2017-2023 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.samples;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStyle;
import org.springframework.boot.Banner.Mode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.annotation.CommandScan;
import org.springframework.shell.jline.PromptProvider;
/**
* Main entry point for the application.
*
* <p>Creates the application context and start the REPL.</p>
*
* @author Eric Bottard
* @author Janne Valkealahti
*/
@SpringBootApplication
@CommandScan
public class SpringShellSample {
public static void main(String[] args) throws Exception {
SpringApplication application = new SpringApplication(SpringShellSample.class);
application.setBannerMode(Mode.OFF);
application.run(args);
// TODO: follow up with boot why spring.main.banner-mode=off doesn't work
// SpringApplication.run(SpringShellSample.class, args);
}
@Bean
public PromptProvider myPromptProvider() {
return () -> new AttributedString("my-shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW));
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.samples.standard;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class AliasCommands {
private final static String DESCRIPTION = "main1 with main2 as alias";
@ShellMethod(key = { "alias anno main1", "alias anno main2" }, group = "Alias Commands", value = DESCRIPTION)
public String annoMain1() {
return "Hello annoMain1";
}
@Bean
public CommandRegistration regMain1() {
return CommandRegistration.builder()
.command("alias", "reg", "main1")
.group("Alias Commands")
.description(DESCRIPTION)
.withAlias()
.command("alias", "reg", "main2")
.group("Alias Commands")
.and()
.withTarget()
.function(ctx -> {
return "Hello regMain1";
})
.and()
.build();
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2015-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.samples.standard;
import java.lang.annotation.ElementType;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import jakarta.validation.constraints.Size;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
/**
* Example commands for the Shell 2 Standard resolver.
*
* @author Eric Bottard
*/
@ShellComponent()
public class Commands {
@ShellMethod(value = "A command whose name looks the same as another one.", key = "help me out")
public void helpMeOut() {
System.out.println("You can go");
}
@ShellMethod("Change Password. Shows support for bean validation.")
public String changePassword(@Size(min = 8) String password) {
return "Password changed";
}
@ShellMethod(value = "Shows non trivial character encoding.")
public String helloWorld() {
return "こんにちは世界";
}
@ShellMethod("Shows support for boolean parameters, with arity=0.")
public void shutdown(@ShellOption(arity = 0) boolean force) {
System.out.println("You passed " + force);
}
@ShellMethod("Add numbers.")
public int add(int a, int b, int c) {
return a + b + c;
}
@ShellMethod("Concat strings.")
public String concat(String a, String b, String c) {
return a + b + c;
}
@ShellMethod("Fails with an exception. Shows enum conversion.")
public void fail(ElementType elementType) {
throw new IllegalArgumentException("You said " + elementType);
}
@ShellMethod("Add array numbers.")
public double addDoubles(@ShellOption(arity = 3) double[] numbers) {
return Arrays.stream(numbers).sum();
}
@ShellMethod("Get iterables.")
public Iterable<String> iterables() {
List<String> list = Arrays.asList("first", "second");
Iterable<String> iterable = new Iterable<String>() {
@Override
public Iterator<String> iterator() {
return list.iterator();
}
};
return iterable;
}
}

View File

@@ -0,0 +1,123 @@
/*
* 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.samples.standard;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.EnumValueProvider;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
import org.springframework.shell.standard.ValueProvider;
@ShellComponent
public class CompleteCommands {
@Bean
CommandRegistration completeCommandRegistration1() {
return CommandRegistration.builder()
.command("complete", "sample1")
.description("complete sample1")
.group("Complete Commands")
.withOption()
.longNames("arg1")
.completion(ctx -> {
CompletionProposal p1 = new CompletionProposal("arg1hi1");
CompletionProposal p2 = new CompletionProposal("arg1hi2");
return Arrays.asList(p1, p2);
})
.and()
.withOption()
.longNames("arg2")
.completion(ctx -> {
CompletionProposal p1 = new CompletionProposal("arg2hi1");
CompletionProposal p2 = new CompletionProposal("arg2hi2");
return Arrays.asList(p1, p2);
})
.and()
.withTarget()
.function(ctx -> {
String arg1 = ctx.getOptionValue("arg1");
return String.format("hi, arg1 value is '%s'", arg1);
})
.and()
.build();
}
@ShellMethod(value = "complete sample2", key = "complete sample2")
public String completeCommandSample2(@ShellOption(valueProvider = FunnyValuesProvider.class) String arg1) {
return "You said " + arg1;
}
@Bean
FunnyValuesProvider funnyValuesProvider() {
return new FunnyValuesProvider();
}
static class FunnyValuesProvider implements ValueProvider {
private final static String[] VALUES = new String[] {
"hello world",
"I am quoting \"The Daily Mail\"",
"10 \\ 3 = 3"
};
@Override
public List<CompletionProposal> complete(CompletionContext completionContext) {
return Arrays.stream(VALUES).map(CompletionProposal::new).collect(Collectors.toList());
}
}
@Bean
CommandRegistration completeCommandRegistration3() {
return CommandRegistration.builder()
.command("complete", "sample3")
.description("complete sample3")
.group("Complete Commands")
.withOption()
.longNames("arg1")
.type(MyEnums.class)
.completion(ctx -> {
CompletionProposal p1 = new CompletionProposal(MyEnums.E1.toString());
CompletionProposal p2 = new CompletionProposal(MyEnums.E2.toString());
CompletionProposal p3 = new CompletionProposal(MyEnums.E3.toString());
return Arrays.asList(p1, p2, p3);
})
.and()
.withTarget()
.function(ctx -> {
String arg1 = ctx.getOptionValue("arg1");
return String.format("You said '%s'", arg1);
})
.and()
.build();
}
@ShellMethod(value = "complete sample4", key = "complete sample4")
public String completeCommandSample4(@ShellOption(valueProvider = EnumValueProvider.class) MyEnums arg1) {
return "You said " + arg1;
}
static enum MyEnums {
E1, E2, E3
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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.samples.standard;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.springframework.shell.component.ConfirmationInput;
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
import org.springframework.shell.component.MultiItemSelector;
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
import org.springframework.shell.component.PathInput;
import org.springframework.shell.component.PathSearch;
import org.springframework.shell.component.PathInput.PathInputContext;
import org.springframework.shell.component.PathSearch.PathSearchConfig;
import org.springframework.shell.component.PathSearch.PathSearchContext;
import org.springframework.shell.component.SingleItemSelector;
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
import org.springframework.shell.component.StringInput;
import org.springframework.shell.component.StringInput.StringInputContext;
import org.springframework.shell.component.support.SelectorItem;
import org.springframework.shell.standard.AbstractShellComponent;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
import org.springframework.util.StringUtils;
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component string", value = "String input", group = "Components")
public String stringInput(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue");
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
if (mask) {
component.setMaskCharacter('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}
@ShellMethod(key = "component path input", value = "Path input", group = "Components")
public String pathInput() {
PathInput component = new PathInput(getTerminal(), "Enter value");
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
PathInputContext context = component.run(PathInputContext.empty());
return "Got value " + context.getResultValue();
}
@ShellMethod(key = "component path search", value = "Path search", group = "Components")
public String pathSearch(
@ShellOption(defaultValue = ShellOption.NULL) Integer maxPathsShow,
@ShellOption(defaultValue = ShellOption.NULL) Integer maxPathsSearch,
@ShellOption(defaultValue = "true") boolean searchForward,
@ShellOption(defaultValue = "false") boolean searchCaseSensitive,
@ShellOption(defaultValue = "false") boolean searchNormalize
) {
PathSearchConfig config = new PathSearch.PathSearchConfig();
if (maxPathsShow != null) {
config.setMaxPathsShow(maxPathsShow);
}
if (maxPathsSearch != null) {
config.setMaxPathsSearch(maxPathsSearch);
}
config.setSearchForward(searchForward);
config.setSearchCaseSensitive(searchCaseSensitive);
config.setSearchNormalize(searchNormalize);
PathSearch component = new PathSearch(getTerminal(), "Enter value", config);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
PathSearchContext context = component.run(PathSearchContext.empty());
return "Got value " + context.getResultValue();
}
@ShellMethod(key = "component confirmation", value = "Confirmation input", group = "Components")
public String confirmationInput(boolean no) {
ConfirmationInput component = new ConfirmationInput(getTerminal(), "Enter value", !no);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
ConfirmationInputContext context = component.run(ConfirmationInputContext.empty());
return "Got value " + context.getResultValue();
}
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
public String singleSelector(
@ShellOption(defaultValue = ShellOption.NULL) Boolean longKeys
) {
List<SelectorItem<String>> items = new ArrayList<>();
items.add(SelectorItem.of("key1", "value1"));
items.add(SelectorItem.of("key2", "value2"));
if (longKeys != null && longKeys == true) {
items.add(SelectorItem.of("key3 long long long long long", "value3"));
items.add(SelectorItem.of("key4 long long long long long long long long long long", "value4"));
}
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
SingleItemSelectorContext<String, SelectorItem<String>> context = component
.run(SingleItemSelectorContext.empty());
String result = context.getResultItem().flatMap(si -> Optional.ofNullable(si.getItem())).get();
return "Got value " + result;
}
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
public String multiSelector(
@ShellOption(defaultValue = ShellOption.NULL) Boolean longKeys
) {
List<SelectorItem<String>> items = new ArrayList<>();
items.add(SelectorItem.of("key1", "value1"));
items.add(SelectorItem.of("key2", "value2", false, true));
items.add(SelectorItem.of("key3", "value3"));
if (longKeys != null && longKeys == true) {
items.add(SelectorItem.of("key4 long long long long long", "value4", false, true));
items.add(SelectorItem.of("key5 long long long long long long long long long long", "value5"));
}
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
MultiItemSelectorContext<String, SelectorItem<String>> context = component
.run(MultiItemSelectorContext.empty());
String result = context.getResultItems().stream()
.map(si -> si.getItem())
.collect(Collectors.joining(","));
return "Got value " + result;
}
@ShellMethod(key = "component stringcustom", value = "String input", group = "Components")
public String stringInputCustom(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue",
new StringInputCustomRenderer());
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
if (mask) {
component.setMaskCharacter('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}
private static class StringInputCustomRenderer implements Function<StringInputContext, List<AttributedString>> {
@Override
public List<AttributedString> apply(StringInputContext context) {
AttributedStringBuilder builder = new AttributedStringBuilder();
builder.append(context.getName());
builder.append(" ");
if (context.getResultValue() != null) {
builder.append(context.getResultValue());
}
else {
String input = context.getInput();
if (StringUtils.hasText(input)) {
builder.append(input);
}
else {
builder.append("[Default " + context.getDefaultValue() + "]");
}
}
return Arrays.asList(builder.toAttributedString());
}
}
}

View File

@@ -0,0 +1,298 @@
/*
* 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.samples.standard;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.jline.terminal.impl.DumbTerminal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException;
import org.springframework.shell.command.CommandParser;
import org.springframework.shell.command.CommandParser.CommandParserException;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.component.flow.ComponentFlow;
import org.springframework.shell.component.flow.ComponentFlow.ComponentFlowResult;
import org.springframework.shell.component.flow.ResultMode;
import org.springframework.shell.component.flow.SelectItem;
import org.springframework.shell.standard.AbstractShellComponent;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
import org.springframework.util.StringUtils;
@ShellComponent
public class ComponentFlowCommands extends AbstractShellComponent {
@Autowired
private ComponentFlow.Builder componentFlowBuilder;
@ShellMethod(key = "flow showcase1", value = "Showcase", group = "Flow")
public void showcase1() {
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("key1", "value1");
single1SelectItems.put("key2", "value2");
List<SelectItem> multi1SelectItems = Arrays.asList(SelectItem.of("key1", "value1"),
SelectItem.of("key2", "value2"), SelectItem.of("key3", "value3"));
ComponentFlow flow = componentFlowBuilder.clone().reset()
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.and()
.withStringInput("field2")
.name("Field2")
.and()
.withConfirmationInput("confirmation1")
.name("Confirmation1")
.and()
.withPathInput("path1")
.name("Path1")
.and()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.and()
.withMultiItemSelector("multi1")
.name("Multi1")
.selectItems(multi1SelectItems)
.and()
.build();
flow.run();
}
@ShellMethod(key = "flow showcase2", value = "Showcase with options", group = "Flow")
public String showcase2(
@ShellOption(help = "Field1 value", defaultValue = ShellOption.NULL) String field1,
@ShellOption(help = "Field2 value", defaultValue = ShellOption.NULL) String field2,
@ShellOption(help = "Confirmation1 value", defaultValue = ShellOption.NULL) Boolean confirmation1,
@ShellOption(help = "Path1 value", defaultValue = ShellOption.NULL) String path1,
@ShellOption(help = "Single1 value", defaultValue = ShellOption.NULL) String single1,
@ShellOption(help = "Multi1 value", defaultValue = ShellOption.NULL) List<String> multi1
) {
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("key1", "value1");
single1SelectItems.put("key2", "value2");
List<SelectItem> multi1SelectItems = Arrays.asList(SelectItem.of("key1", "value1"),
SelectItem.of("key2", "value2"), SelectItem.of("key3", "value3"));
List<String> multi1ResultValues = multi1 != null ? multi1 : new ArrayList<>();
ComponentFlow flow = componentFlowBuilder.clone().reset()
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.resultValue(field1)
.resultMode(ResultMode.ACCEPT)
.and()
.withStringInput("field2")
.name("Field2")
.resultValue(field2)
.resultMode(ResultMode.ACCEPT)
.and()
.withConfirmationInput("confirmation1")
.name("Confirmation1")
.resultValue(confirmation1)
.resultMode(ResultMode.ACCEPT)
.and()
.withPathInput("path1")
.name("Path1")
.resultValue(path1)
.resultMode(ResultMode.ACCEPT)
.and()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.resultValue(single1)
.resultMode(ResultMode.ACCEPT)
.and()
.withMultiItemSelector("multi1")
.name("Multi1")
.selectItems(multi1SelectItems)
.resultValues(multi1ResultValues)
.resultMode(ResultMode.ACCEPT)
.and()
.build();
ComponentFlowResult result = flow.run();
StringBuilder buf = new StringBuilder();
result.getContext().stream().forEach(e -> {
buf.append(e.getKey());
buf.append(" = ");
buf.append(e.getValue());
buf.append("\n");
});
return buf.toString();
}
@Bean
public CommandRegistration showcaseRegistration() {
return CommandRegistration.builder()
.command("flow", "showcase3")
.description("Showcase")
.withOption()
.longNames("field1")
.and()
.withOption()
.longNames("field2")
.and()
.withOption()
.longNames("confirmation1")
.type(Boolean.class)
.and()
.withOption()
.longNames("path1")
.and()
.withOption()
.longNames("single1")
.and()
.withOption()
.longNames("multi1")
.and()
.withTarget()
.consumer(ctx -> {
String field1 = ctx.getOptionValue("field1");
String field2 = ctx.getOptionValue("field2");
Boolean confirmation1 = ctx.getOptionValue("confirmation1");
String path1 = ctx.getOptionValue("path1");
String single1 = ctx.getOptionValue("single1");
String asdf = ctx.getOptionValue("multi1");
List<String> multi1 = new ArrayList<>();
if (StringUtils.hasText(asdf)) {
multi1.add(asdf);
}
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("key1", "value1");
single1SelectItems.put("key2", "value2");
List<SelectItem> multi1SelectItems = Arrays.asList(SelectItem.of("key1", "value1"),
SelectItem.of("key2", "value2"), SelectItem.of("key3", "value3"));
ComponentFlow flow = componentFlowBuilder.clone().reset()
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.resultValue(field1)
.resultMode(ResultMode.ACCEPT)
.and()
.withStringInput("field2")
.name("Field2")
.resultValue(field2)
.resultMode(ResultMode.ACCEPT)
.and()
.withConfirmationInput("confirmation1")
.name("Confirmation1")
.resultValue(confirmation1)
.resultMode(ResultMode.ACCEPT)
.and()
.withPathInput("path1")
.name("Path1")
.resultValue(path1)
.resultMode(ResultMode.ACCEPT)
.and()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.resultValue(single1)
.resultMode(ResultMode.ACCEPT)
.and()
.withMultiItemSelector("multi1")
.name("Multi1")
.selectItems(multi1SelectItems)
.resultValues(multi1)
.resultMode(ResultMode.ACCEPT)
.and()
.build();
ComponentFlowResult result = flow.run();
boolean hasTty = !((ctx.getTerminal() instanceof DumbTerminal) && ctx.getTerminal().getSize().getRows() == 0);
if (hasTty) {
StringBuilder buf = new StringBuilder();
result.getContext().stream().forEach(e -> {
buf.append(e.getKey());
buf.append(" = ");
buf.append(e.getValue());
buf.append("\n");
});
ctx.getTerminal().writer().print(buf.toString());
ctx.getTerminal().writer().flush();
}
else {
List<CommandParser.CommandParserException> errors = new ArrayList<>();
result.getContext().stream().forEach(e -> {
if (e.getValue() == null) {
errors.add(CommandParserException.of(String.format("Missing option, longnames='%s'", e.getKey())));
}
});
if (!result.getContext().containsKey("single1")) {
errors.add(CommandParserException.of("Missing option, longnames='single'"));
}
if (!errors.isEmpty()) {
throw CommandParserExceptionsException.of("Missing options", errors);
}
}
})
.and()
.build();
}
@ShellMethod(key = "flow conditional", value = "Second component based on first", group = "Flow")
public void conditional() {
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("Field1", "field1");
single1SelectItems.put("Field2", "field2");
ComponentFlow flow = componentFlowBuilder.clone().reset()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.next(ctx -> ctx.getResultItem().get().getItem())
.and()
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.next(ctx -> null)
.and()
.withStringInput("field2")
.name("Field2")
.defaultValue("defaultField2Value")
.next(ctx -> null)
.and()
.build();
flow.run();
}
@ShellMethod(key = "flow autoselect", value = "Autoselect item", group = "Flow")
public void autoselect(
@ShellOption(defaultValue = "Field3") String defaultValue
) {
Map<String, String> single1SelectItems = IntStream.range(1, 10)
.boxed()
.collect(Collectors.toMap(i -> "Field" + i, i -> "field" + i));
ComponentFlow flow = componentFlowBuilder.clone().reset()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.defaultSelect(defaultValue)
.sort((o1, o2) -> o1.getName().compareTo(o2.getName()))
.and()
.build();
flow.run();
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2017-2021 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.samples.standard;
import org.springframework.core.convert.converter.Converter;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.stereotype.Component;
@ShellComponent
class ConversionCommands {
@ShellMethod("Shows conversion using Spring converter")
public Object conversionExample(DomainObject object) {
return object;
}
}
class DomainObject {
private final String value;
DomainObject(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "DomainObject [value=" + value + "]";
}
}
@Component
class CustomDomainConverter implements Converter<String, DomainObject> {
@Override
public DomainObject convert(String source) {
return new DomainObject(source);
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2017-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.samples.standard;
import org.springframework.shell.Availability;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellMethodAvailability;
/**
* Showcases dynamic command availability.
*
* @author Eric Bottard
*/
@ShellComponent
public class DynamicCommands {
private boolean connected;
private boolean authenticated;
public Availability authenticateAvailability() {
return connected ? Availability.available() : Availability.unavailable("you are not connected");
}
@ShellMethod(value = "Authenticate with the system", group = "Dynamic Commands")
public void authenticate(String credentials) {
authenticated = "sesame".equals(credentials);
}
@ShellMethod(value = "Connect to the system", group = "Dynamic Commands")
public void connect() {
connected = true;
}
@ShellMethod(value = "Disconnect from the system", group = "Dynamic Commands")
public void disconnect() {
connected = false;
}
@ShellMethod(value = "Blow Everything up", group = "Dynamic Commands")
@ShellMethodAvailability("dangerousAvailability")
public String blowUp() {
return "Boom!";
}
public Availability dangerousAvailability() {
return connected && authenticated ? Availability.available()
: Availability.unavailable("you failed to authenticate. Try 'sesame'.");
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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.samples.standard;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.command.CommandRegistration;
@Configuration
public class FunctionCommands {
@Bean
public CommandRegistration commandRegistration1() {
return CommandRegistration.builder()
.command("function", "command1")
.description("function sample")
.group("Function Commands")
.withTarget()
.function(ctx -> {
String arg1 = ctx.getOptionValue("arg1");
return String.format("hi, arg1 value is '%s'", arg1);
})
.and()
.withOption()
.longNames("arg1")
.and()
.build();
}
@Bean
public CommandRegistration commandRegistration2() {
return CommandRegistration.builder()
.command("function", "command2")
.description("function sample")
.group("Function Commands")
.withTarget()
.function(ctx -> {
Boolean a = ctx.getOptionValue("a");
Boolean b = ctx.getOptionValue("b");
Boolean c = ctx.getOptionValue("c");
return String.format("hi, boolean values for a, b, c are '%s' '%s' '%s'", a, b, c);
})
.and()
.withOption()
.shortNames('a')
.type(boolean.class)
.and()
.withOption()
.shortNames('b')
.type(boolean.class)
.and()
.withOption()
.shortNames('c')
.type(boolean.class)
.and()
.build();
}
@Bean
public CommandRegistration commandRegistration3() {
return CommandRegistration.builder()
.command("function", "command3")
.description("function sample")
.group("Function Commands")
.withTarget()
.consumer(ctx -> {
String arg1 = ctx.getOptionValue("arg1");
ctx.getTerminal().writer()
.println(String.format("hi, arg1 value is '%s'", arg1));
})
.and()
.withOption()
.longNames("arg1")
.and()
.build();
}
@Bean
public CommandRegistration commandRegistration4() {
return CommandRegistration.builder()
.command("function", "command4")
.description("function sample")
.group("Function Commands")
.withTarget()
.consumer(ctx -> {
ctx.getTerminal().writer()
.println(String.format("hi, command is '%s'", ctx.getCommandRegistration().getCommand()));
})
.and()
.withOption()
.longNames("arg1")
.and()
.build();
}
}

View File

@@ -0,0 +1,116 @@
/*
* 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.samples.standard;
import java.util.function.Function;
import org.springframework.shell.command.CommandContext;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.standard.AbstractShellComponent;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
@ShellComponent
public class RegisterCommands extends AbstractShellComponent {
private final static String GROUP = "Register Commands";
private final PojoMethods pojoMethods = new PojoMethods();
private final CommandRegistration registered1;
private final CommandRegistration registered2;
private final CommandRegistration registered3;
public RegisterCommands() {
registered1 = CommandRegistration.builder()
.command("register registered1")
.group(GROUP)
.description("registered1 command")
.withTarget()
.method(pojoMethods, "registered1")
.and()
.build();
registered2 = CommandRegistration.builder()
.command("register registered2")
.description("registered2 command")
.group(GROUP)
.withTarget()
.method(pojoMethods, "registered2")
.and()
.withOption()
.longNames("arg1")
.and()
.build();
registered3 = CommandRegistration.builder()
.command("register registered3")
.description("registered3 command")
.group(GROUP)
.withTarget()
.method(pojoMethods, "registered3")
.and()
.build();
}
@ShellMethod(key = "register add", value = "Register commands", group = GROUP)
public String register() {
getCommandCatalog().register(registered1, registered2, registered3);
registerFunctionCommand("register registered4");
return "Registered commands registered1, registered2, registered3, registered4";
}
@ShellMethod(key = "register remove", value = "Deregister commands", group = GROUP)
public String deregister() {
getCommandCatalog().unregister("register registered1", "register registered2", "register registered3",
"register registered4");
return "Deregistered commands registered1, registered2, registered3, registered4";
}
private void registerFunctionCommand(String command) {
Function<CommandContext, String> function = ctx -> {
String arg1 = ctx.getOptionValue("arg1");
return String.format("hi, arg1 value is '%s'", arg1);
};
CommandRegistration registration = CommandRegistration.builder()
.command(command)
.description("registered4 command")
.group(GROUP)
.withTarget()
.function(function)
.and()
.withOption()
.longNames("arg1")
.and()
.build();
getCommandCatalog().register(registration);
}
public static class PojoMethods {
@ShellMethod
public String registered1() {
return "registered1";
}
@ShellMethod
public String registered2(String arg1) {
return "registered2" + arg1;
}
@ShellMethod
public String registered3(@ShellOption(defaultValue = ShellOption.NULL) String arg1) {
return "registered3" + arg1;
}
}
}

View File

@@ -0,0 +1,144 @@
/*
* 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.samples.standard;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.shell.command.CommandRegistration;
import org.springframework.shell.command.CommandResolver;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
public class ResolvedCommands {
private static final String GROUP = "Resolve Commands";
@Configuration
public static class ResolvedCommandsConfiguration {
@Bean
Server1CommandResolver server1CommandResolver() {
return new Server1CommandResolver();
}
@Bean
Server2CommandResolver server2CommandResolver() {
return new Server2CommandResolver();
}
}
@ShellComponent
public static class ResolvedCommandsCommands {
private final Server1CommandResolver server1CommandResolver;
private final Server2CommandResolver server2CommandResolver;
ResolvedCommandsCommands(Server1CommandResolver server1CommandResolver,
Server2CommandResolver server2CommandResolver) {
this.server1CommandResolver = server1CommandResolver;
this.server2CommandResolver = server2CommandResolver;
}
@ShellMethod(key = "resolve enableserver1", group = GROUP)
public String server1Enable() {
server1CommandResolver.enabled = true;
return "Enabled server1";
}
@ShellMethod(key = "resolve disableserver1", group = GROUP)
public String server1Disable() {
server1CommandResolver.enabled = false;
return "Disabled server1";
}
@ShellMethod(key = "resolve enableserver2", group = GROUP)
public String server2Enable() {
server2CommandResolver.enabled = true;
return "Enabled server2";
}
@ShellMethod(key = "resolve disableserver2", group = GROUP)
public String server2Disable() {
server2CommandResolver.enabled = false;
return "Disabled server2";
}
}
static class Server1CommandResolver implements CommandResolver {
private final List<CommandRegistration> registrations = new ArrayList<>();
boolean enabled = false;
Server1CommandResolver() {
CommandRegistration resolved1 = CommandRegistration.builder()
.command("resolve server1 command1")
.group(GROUP)
.description("server1 command1")
.withTarget()
.function(ctx -> {
return "hi from server1 command1";
})
.and()
.build();
registrations.add(resolved1);
}
@Override
public List<CommandRegistration> resolve() {
return enabled ? registrations : Collections.emptyList();
}
}
static class Server2CommandResolver implements CommandResolver {
private final List<CommandRegistration> registrations = new ArrayList<>();
boolean enabled = false;
Server2CommandResolver() {
CommandRegistration resolved1 = CommandRegistration.builder()
.command("resolve server2 command1")
.group(GROUP)
.description("server2 command1")
.withTarget()
.function(ctx -> {
return "hi from server2 command1";
})
.and()
.build();
CommandRegistration resolved2 = CommandRegistration.builder()
.command("resolve server2 command2")
.group(GROUP)
.description("server2 command2")
.withTarget()
.function(ctx -> {
return "hi from server2 command2";
})
.and()
.build();
registrations.add(resolved1);
registrations.add(resolved2);
}
@Override
public List<CommandRegistration> resolve() {
return enabled ? registrations : Collections.emptyList();
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2017 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.samples.standard;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.table.*;
import java.util.Random;
@ShellComponent
public class TableCommands {
private static final String TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " +
"ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco " +
"laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in " +
"voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat " +
"non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
@ShellMethod(value = "Showcase Table rendering", group = "Tables")
public Table table() {
String[][] data = new String[3][3];
TableModel model = new ArrayTableModel(data);
TableBuilder tableBuilder = new TableBuilder(model);
Random r = new Random();
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
data[i][j] = TEXT.substring(0, TEXT.length() / 2 + r.nextInt(TEXT.length() / 2));
tableBuilder.on(at(i, j)).addAligner(SimpleHorizontalAligner.values()[j]);
tableBuilder.on(at(i, j)).addAligner(SimpleVerticalAligner.values()[i]);
}
}
return tableBuilder.addFullBorder(BorderStyle.fancy_light).build();
}
public static CellMatcher at(final int theRow, final int col) {
return new CellMatcher() {
@Override
public boolean matches(int row, int column, TableModel model) {
return row == theRow && column == col;
}
};
}
}

View File

@@ -0,0 +1,171 @@
/*
* 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.samples.standard;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
import org.springframework.shell.style.FigureSettings;
import org.springframework.shell.style.StyleSettings;
import org.springframework.shell.style.ThemeResolver;
@ShellComponent
public class ThemeCommands {
List<String> colorGround = Arrays.asList("fg", "bg");
List<String> colors = Arrays.asList("black", "red", "green", "yellow", "blue", "magenta", "cyan", "white");
List<String> named = Arrays.asList("default", "bold", "faint", "italic", "underline", "blink", "inverse",
"inverseneg", "conceal", "crossedout", "hidden");
List<String> rgbRedHue = Arrays.asList("#ff0000", "#ff4000", "#ff8000", "#ffbf00", "#ffff00", "#bfff00", "#80ff00",
"#40ff00", "#00ff00", "#00ff40", "#00ff80", "#00ffbf", "#00ffff", "#00bfff", "#0080ff", "#0040ff",
"#0000ff", "#4000ff", "#8000ff", "#bf00ff", "#ff00ff", "#ff00bf", "#ff0080", "#ff0040", "#ff0000");
List<String> themeTags = Arrays.asList(StyleSettings.TAG_TITLE, StyleSettings.TAG_VALUE, StyleSettings.TAG_LIST_KEY,
StyleSettings.TAG_LIST_VALUE, StyleSettings.TAG_LEVEL_INFO, StyleSettings.TAG_LEVEL_WARN,
StyleSettings.TAG_LEVEL_ERROR, StyleSettings.TAG_ITEM_ENABLED, StyleSettings.TAG_ITEM_DISABLED,
StyleSettings.TAG_ITEM_SELECTED, StyleSettings.TAG_ITEM_UNSELECTED, StyleSettings.TAG_ITEM_SELECTOR);
@Autowired
private ThemeResolver themeResolver;
@ShellMethod(key = "theme showcase values", value = "Showcase colors and styles", group = "Styles")
public AttributedString showcaseValues() {
AttributedStringBuilder builder = new AttributedStringBuilder();
combinations1().stream()
.forEach(spec -> {
AttributedStyle style = themeResolver.resolveStyle(spec);
AttributedString styledStr = new AttributedString(spec, style);
builder.append(String.format("%-25s", spec));
builder.append(" ");
builder.append(styledStr);
builder.append("\n");
});
return builder.toAttributedString();
}
@ShellMethod(key = "theme showcase rgb", value = "Showcase colors and styles with rgb", group = "Styles")
public AttributedString showcaseRgb() {
AttributedStringBuilder builder = new AttributedStringBuilder();
combinations2().stream()
.forEach(spec -> {
AttributedStyle style = themeResolver.resolveStyle(spec);
AttributedString styledStr = new AttributedString(spec, style);
builder.append(String.format("%-25s", spec));
builder.append(" ");
builder.append(styledStr);
builder.append("\n");
});
return builder.toAttributedString();
}
@ShellMethod(key = "theme style list", value = "List styles", group = "Styles")
public AttributedString styleList() {
AttributedStringBuilder builder = new AttributedStringBuilder();
themeTags.stream()
.forEach(tag -> {
String resolvedStyle = themeResolver.resolveStyleTag(tag);
AttributedStyle style = themeResolver.resolveStyle(resolvedStyle);
AttributedString styledStr = new AttributedString(tag, style);
builder.append(String.format("%-25s", tag));
builder.append(" ");
builder.append(styledStr);
builder.append("\n");
});
return builder.toAttributedString();
}
@ShellMethod(key = "theme style resolve", value = "Resolve given style", group = "Styles")
public AttributedString styleResolve(
@ShellOption(value = "--spec", defaultValue = "default") String spec
) {
AttributedStringBuilder builder = new AttributedStringBuilder();
AttributedStyle style = themeResolver.resolveStyle(spec);
AttributedString styledStr = new AttributedString(spec, style);
builder.append(styledStr);
builder.append("\n");
return builder.toAttributedString();
}
@ShellMethod(key = "theme expression resolve", value = "Resolve given style expression", group = "Styles")
public AttributedString expressionResolve(
@ShellOption(value = "--expression", defaultValue = "hi @{bold from} expression") String expression
) {
AttributedStringBuilder builder = new AttributedStringBuilder();
AttributedString styledStr = themeResolver.evaluateExpression(expression);
builder.append(styledStr);
builder.append("\n");
return builder.toAttributedString();
}
@ShellMethod(key = "theme figure list", value = "List figures", group = "Styles")
public AttributedString figureList() {
AttributedStringBuilder builder = new AttributedStringBuilder();
Stream.of(FigureSettings.tags())
.forEach(tag -> {
builder.append(String.format("%-25s", tag));
builder.append(" ");
String resolveFigureTag = themeResolver.resolveFigureTag(tag);
combinations3().stream().forEach(spec -> {
AttributedStyle style = themeResolver.resolveStyle(spec);
builder.append(" ");
builder.append(new AttributedString(resolveFigureTag, style));
});
builder.append("\n");
});
return builder.toAttributedString();
}
private List<String> combinations1() {
List<String> styles = new ArrayList<>();
colorGround.stream().forEach(ground -> {
colors.stream().forEach(color -> {
named.stream().forEach(named -> {
styles.add(String.format("%s,%s:%s", named, ground, color));
});
});
});
return styles;
}
private List<String> combinations2() {
List<String> styles = new ArrayList<>();
rgbRedHue.stream().forEach(rgb -> {
styles.add(String.format("inverse,fg-rgb:%s", rgb));
});
return styles;
}
private List<String> combinations3() {
List<String> styles = new ArrayList<>();
Arrays.asList("fg").stream().forEach(ground -> {
Arrays.asList("white").stream().forEach(color -> {
named.stream().forEach(named -> {
styles.add(String.format("%s,%s:%s", named, ground, color));
});
});
});
return styles;
}
}

View File

@@ -0,0 +1,34 @@
spring:
main:
banner-mode: off
shell:
## pick global default option naming
# option:
# naming:
# case-type: noop
# case-type: camel
# case-type: snake
# case-type: kebab
# case-type: pascal
config:
env: SPRING_SHELL_SAMPLES_USER_HOME
location: "{userconfig}/spring-shell-samples"
history:
name: spring-shell-samples-history.log
command:
help:
grouping-mode: group
completion:
root-command: spring-shell-samples
## disable console logging
logging:
pattern:
console:
## log debug from a cli
# file:
# name: shell.log
# level:
# root: debug
# org:
# springframework:
# shell: debug

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2022-2023 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.samples;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.Condition;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.shell.samples.standard.ResolvedCommands;
import org.springframework.shell.test.ShellAssertions;
import org.springframework.shell.test.ShellTestClient;
import org.springframework.shell.test.ShellTestClient.BaseShellSession;
import org.springframework.shell.test.ShellTestClient.InteractiveShellSession;
import org.springframework.shell.test.ShellTestClient.NonInteractiveShellSession;
import org.springframework.shell.test.autoconfigure.ShellTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
@ShellTest(terminalWidth = 120)
@Import(ResolvedCommands.ResolvedCommandsConfiguration.class)
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class AbstractSampleTests {
@Autowired
protected ShellTestClient client;
protected void assertScreenContainsText(BaseShellSession<?> session, String text) {
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen()).containsText(text);
});
}
protected void assertScreenNotContainsText(BaseShellSession<?> session, String textFound, String textNotFound) {
Condition<String> notCondition = new Condition<>(line -> line.contains(textNotFound),
String.format("Text '%s' not found", textNotFound));
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen()).containsText(textFound);
List<String> lines = session.screen().lines();
assertThat(lines).areNot(notCondition);
});
}
protected BaseShellSession<?> createSession(String command, boolean interactive) {
if (interactive) {
InteractiveShellSession session = client.interactive().run();
session.write(session.writeSequence().command(command).build());
return session;
}
else {
String[] commands = command.split(" ");
NonInteractiveShellSession session = client.nonInterative(commands).run();
return session;
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.samples.standard;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.shell.samples.AbstractSampleTests;
import org.springframework.shell.test.ShellAssertions;
import org.springframework.shell.test.ShellTestClient.BaseShellSession;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
public class ComponentCommandsTests extends AbstractSampleTests {
@ParameterizedTest
@CsvSource({
"component single,false",
"component single,true"
})
void componentSingle(String command, boolean interactive) {
BaseShellSession<?> session = createSession(command, interactive);
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.screen().lines()).anySatisfy(line -> {
assertThat(line).containsPattern("[>] key1");
});
});
session.write(session.writeSequence().keyDown().build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.screen().lines()).anySatisfy(line -> {
assertThat(line).containsPattern("[>] key2");
});
});
session.write(session.writeSequence().cr().build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen()).containsText("Got value value2");
});
}
@ParameterizedTest
@CsvSource({
"component multi,false",
"component multi,true"
})
void componentMulti(String command, boolean interactive) {
BaseShellSession<?> session = createSession(command, interactive);
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.screen().lines()).anySatisfy(line -> {
assertThat(line).containsPattern("[>] (☐|\\[ \\]) key1");
});
});
session.write(session.writeSequence().space().build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.screen().lines()).anySatisfy(line -> {
assertThat(line).containsPattern("[>] (☒|\\[x\\]) key1");
});
});
session.write(session.writeSequence().cr().build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen()).containsText("Got value value1,value2");
});
}
}