diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java index 64f9b83d..3c742e87 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java @@ -210,6 +210,10 @@ public interface CommandExecution { this.parserExceptions = parserExceptions; } + public static CommandParserExceptionsException of(String message, List parserExceptions) { + return new CommandParserExceptionsException(message, parserExceptions); + } + public List getParserExceptions() { return parserExceptions; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/ConfirmationInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/ConfirmationInput.java index 112ea035..0ab3b967 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/ConfirmationInput.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/ConfirmationInput.java @@ -24,6 +24,8 @@ import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; import org.jline.terminal.Terminal; import org.jline.utils.AttributedString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext; import org.springframework.shell.component.context.ComponentContext; @@ -39,6 +41,7 @@ import org.springframework.util.StringUtils; */ public class ConfirmationInput extends AbstractTextComponent { + private final static Logger log = LoggerFactory.getLogger(ConfirmationInput.class); private final boolean defaultValue; private ConfirmationInputContext currentContext; @@ -78,6 +81,10 @@ public class ConfirmationInput extends AbstractTextComponent keyMap, ConfirmationInputContext context) { String operation = bindingReader.readBinding(keyMap); + log.debug("Binding read result {}", operation); + if (operation == null) { + return true; + } String input; switch (operation) { case OPERATION_CHAR: diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/MultiItemSelector.java b/spring-shell-core/src/main/java/org/springframework/shell/component/MultiItemSelector.java index bd75a5bf..08185735 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/MultiItemSelector.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/MultiItemSelector.java @@ -70,7 +70,10 @@ public class MultiItemSelector runInternal(MultiItemSelectorContext context) { super.runInternal(context); - loop(context); + // if there's no tty don't try to loop as it would then cause user interaction + if (hasTty()) { + loop(context); + } return context; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/PathInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/PathInput.java index f42509e7..f8784e47 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/PathInput.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/PathInput.java @@ -27,6 +27,8 @@ import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; import org.jline.terminal.Terminal; import org.jline.utils.AttributedString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.shell.component.PathInput.PathInputContext; import org.springframework.shell.component.context.ComponentContext; @@ -42,6 +44,7 @@ import org.springframework.util.StringUtils;; */ public class PathInput extends AbstractTextComponent { + private final static Logger log = LoggerFactory.getLogger(PathInput.class); private PathInputContext currentContext; private Function pathProvider = (path) -> Paths.get(path); @@ -75,6 +78,10 @@ public class PathInput extends AbstractTextComponent { @Override protected boolean read(BindingReader bindingReader, KeyMap keyMap, PathInputContext context) { String operation = bindingReader.readBinding(keyMap); + log.debug("Binding read result {}", operation); + if (operation == null) { + return true; + } String input; switch (operation) { case OPERATION_CHAR: diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/SingleItemSelector.java b/spring-shell-core/src/main/java/org/springframework/shell/component/SingleItemSelector.java index 859cc815..a00eeaae 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/SingleItemSelector.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/SingleItemSelector.java @@ -70,7 +70,10 @@ public class SingleItemSelector runInternal(SingleItemSelectorContext context) { super.runInternal(context); - loop(context); + // if there's no tty don't try to loop as it would then cause user interaction + if (hasTty()) { + loop(context); + } return context; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java index 9c965742..57386dd1 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java @@ -24,6 +24,8 @@ import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; import org.jline.terminal.Terminal; import org.jline.utils.AttributedString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.shell.component.StringInput.StringInputContext; import org.springframework.shell.component.context.ComponentContext; @@ -38,6 +40,7 @@ import org.springframework.util.StringUtils; */ public class StringInput extends AbstractTextComponent { + private final static Logger log = LoggerFactory.getLogger(StringInput.class); private final String defaultValue; private StringInputContext currentContext; private Character maskCharacter; @@ -83,6 +86,10 @@ public class StringInput extends AbstractTextComponent keyMap, StringInputContext context) { String operation = bindingReader.readBinding(keyMap); + log.debug("Binding read result {}", operation); + if (operation == null) { + return true; + } String input; switch (operation) { case OPERATION_CHAR: diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/context/ComponentContext.java b/spring-shell-core/src/main/java/org/springframework/shell/component/context/ComponentContext.java index f3872f36..d8278026 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/context/ComponentContext.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/context/ComponentContext.java @@ -56,6 +56,14 @@ public interface ComponentContext> { */ T get(Object key, Class type); + /** + * Check if a context contains a key. + * + * @param key the key + * @return true if context contains key + */ + boolean containsKey(Object key); + /** * Put an entry into a context. * diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractComponent.java b/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractComponent.java index 0e04ba63..e12b29b2 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractComponent.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractComponent.java @@ -33,6 +33,7 @@ import org.jline.keymap.KeyMap; import org.jline.terminal.Attributes; import org.jline.terminal.Size; import org.jline.terminal.Terminal; +import org.jline.terminal.impl.DumbTerminal; import org.jline.utils.AttributedString; import org.jline.utils.Display; import org.jline.utils.InfoCmp.Capability; @@ -153,7 +154,8 @@ public abstract class AbstractComponent> implement context = runPreRunHandlers(getThisContext(context)); T run = runInternal(getThisContext(context)); context = runPostRunHandlers(getThisContext(context)); - if (printResults) { + // if there's no tty don't try to print results as it'd be pointless + if (printResults && hasTty()) { printResults(context); } return run; @@ -186,6 +188,22 @@ public abstract class AbstractComponent> implement this.templateLocation = templateLocation; } + /** + * Checks if this component has an existing {@code tty}. + * + * @return true if component has tty + */ + protected boolean hasTty() { + boolean hasTty = true; + if (this.terminal instanceof DumbTerminal) { + if (this.terminal.getSize().getRows() == 0) { + hasTty = false; + } + } + log.debug("Terminal is {} with size {}, marking hasTty as {}", this.terminal, this.terminal.getSize(), hasTty); + return hasTty; + } + /** * Render a given template with attributes. * diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractSelectorComponent.java b/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractSelectorComponent.java index 9306b441..8e62ca2a 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractSelectorComponent.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractSelectorComponent.java @@ -26,6 +26,8 @@ import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; import org.jline.terminal.Terminal; import org.jline.utils.InfoCmp.Capability; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.shell.component.context.BaseComponentContext; import org.springframework.shell.component.context.ComponentContext; @@ -46,6 +48,7 @@ import static org.jline.keymap.KeyMap.key; public abstract class AbstractSelectorComponent, I extends Nameable & Matchable & Enableable & Itemable> extends AbstractComponent { + private final static Logger log = LoggerFactory.getLogger(AbstractSelectorComponent.class); protected final String name; private final List items; private Comparator comparator = (o1, o2) -> 0; @@ -155,6 +158,10 @@ public abstract class AbstractSelectorComponent> itemStateView = buildItemStateView.items; String operation = bindingReader.readBinding(keyMap); + log.debug("Binding read result {}", operation); + if (operation == null) { + return true; + } String input; switch (operation) { case OPERATION_SELECT: diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractTextComponent.java b/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractTextComponent.java index 1dfa7bd2..8d214bd1 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractTextComponent.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractTextComponent.java @@ -66,7 +66,10 @@ public abstract class AbstractTextComponent { try { diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/ConfirmationInputTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/ConfirmationInputTests.java index 50c0fb1b..087c66b7 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/ConfirmationInputTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/ConfirmationInputTests.java @@ -15,13 +15,17 @@ */ package org.springframework.shell.component; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import org.jline.terminal.impl.DumbTerminal; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -55,6 +59,33 @@ public class ConfirmationInputTests extends AbstractShellTests { service = null; } + @Test + void testNoTty() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8); + + ComponentContext empty = ComponentContext.empty(); + ConfirmationInput component1 = new ConfirmationInput(dumbTerminal, "component1"); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + + service.execute(() -> { + ConfirmationInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + ConfirmationInputContext run1Context = result1.get(); + + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isNull(); + } + @Test public void testResultUserInputEnterDefaultYes() throws InterruptedException, IOException { ComponentContext empty = ComponentContext.empty(); diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/MultiItemSelectorTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/MultiItemSelectorTests.java index 0d79f5b1..ce8031ac 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/MultiItemSelectorTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/MultiItemSelectorTests.java @@ -15,6 +15,9 @@ */ package org.springframework.shell.component; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; import java.util.List; @@ -25,6 +28,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import org.jline.terminal.Terminal; +import org.jline.terminal.impl.DumbTerminal; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -75,6 +80,19 @@ public class MultiItemSelectorTests extends AbstractShellTests { service = null; } + @Test + void testNoTty() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8); + + scheduleSelect(dumbTerminal); + awaitLatch(); + + List> selected = result.get(); + assertThat(selected).isNull(); + } + @Test public void testItemsShown() { scheduleSelect(); @@ -192,12 +210,21 @@ public class MultiItemSelectorTests extends AbstractShellTests { SELECTOR_ITEM_4)); } + private void scheduleSelect(Terminal terminal) { + scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3, SELECTOR_ITEM_4), null, + terminal); + } + private void scheduleSelect(List> items) { scheduleSelect(items, null); } private void scheduleSelect(List> items, Integer maxItems) { - MultiItemSelector> selector = new MultiItemSelector<>(getTerminal(), + scheduleSelect(items, maxItems, getTerminal()); + } + + private void scheduleSelect(List> items, Integer maxItems, Terminal terminal) { + MultiItemSelector> selector = new MultiItemSelector<>(terminal, items, "testSimple", null); selector.setResourceLoader(new DefaultResourceLoader()); selector.setTemplateExecutor(getTemplateExecutor()); diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/PathInputTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/PathInputTests.java index 2d819c2c..d9875706 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/PathInputTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/PathInputTests.java @@ -15,7 +15,10 @@ */ package org.springframework.shell.component; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; @@ -27,6 +30,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import com.google.common.jimfs.Jimfs; +import org.jline.terminal.impl.DumbTerminal; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -69,6 +73,36 @@ public class PathInputTests extends AbstractShellTests { pathProvider = null; } + @Test + void testNoTty() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8); + + Path path = fileSystem.getPath("tmp"); + Files.createDirectories(path); + ComponentContext empty = ComponentContext.empty(); + PathInput component1 = new PathInput(dumbTerminal, "component1"); + component1.setPathProvider(pathProvider); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + + service.execute(() -> { + PathInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().append("tmp").cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + PathInputContext run1Context = result1.get(); + + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isNull(); + } + @Test public void testResultUserInput() throws InterruptedException, IOException { Path path = fileSystem.getPath("tmp"); diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/SingleItemSelectorTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/SingleItemSelectorTests.java index c5077c43..d87de798 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/SingleItemSelectorTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/SingleItemSelectorTests.java @@ -15,6 +15,9 @@ */ package org.springframework.shell.component; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; import java.util.List; @@ -25,6 +28,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import org.jline.terminal.Terminal; +import org.jline.terminal.impl.DumbTerminal; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -92,6 +97,19 @@ public class SingleItemSelectorTests extends AbstractShellTests { "simplePojo3", "simplePojo4", "simplePojo5", "simplePojo6")); } + @Test + void testNoTty() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8); + + scheduleSelect(dumbTerminal); + awaitLatch(); + + Optional> selected = result.get(); + assertThat(selected).isEmpty(); + } + @Test public void testSelectFirst() throws InterruptedException { scheduleSelect(); @@ -156,9 +174,13 @@ public class SingleItemSelectorTests extends AbstractShellTests { assertThat(selected).isNotEmpty(); } + private void scheduleSelect(Terminal terminal) { + scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3, SELECTOR_ITEM_4), null, + terminal); + } + private void scheduleSelect() { - scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3, - SELECTOR_ITEM_4)); + scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3, SELECTOR_ITEM_4)); } private void scheduleSelect(List> items) { @@ -166,7 +188,11 @@ public class SingleItemSelectorTests extends AbstractShellTests { } private void scheduleSelect(List> items, Integer maxItems) { - SingleItemSelector> selector = new SingleItemSelector<>(getTerminal(), + scheduleSelect(items, maxItems, getTerminal()); + } + + private void scheduleSelect(List> items, Integer maxItems, Terminal terminal) { + SingleItemSelector> selector = new SingleItemSelector<>(terminal, items, "testSimple", null); selector.setResourceLoader(new DefaultResourceLoader()); selector.setTemplateExecutor(getTemplateExecutor()); diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/StringInputTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/StringInputTests.java index 06b2d708..4c50f9cb 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/StringInputTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/StringInputTests.java @@ -15,12 +15,16 @@ */ package org.springframework.shell.component; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import org.jline.terminal.impl.DumbTerminal; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -60,6 +64,34 @@ public class StringInputTests extends AbstractShellTests { service = null; } + @Test + void testNoTty() throws Exception { + ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8); + + ComponentContext empty = ComponentContext.empty(); + StringInput component1 = new StringInput(dumbTerminal, "component1", "component1ResultValue"); + component1.setPrintResults(true); + component1.setResourceLoader(new DefaultResourceLoader()); + component1.setTemplateExecutor(getTemplateExecutor()); + + service.execute(() -> { + StringInputContext run1Context = component1.run(empty); + result1.set(run1Context); + latch1.countDown(); + }); + + TestBuffer testBuffer = new TestBuffer().cr(); + write(testBuffer.getBytes()); + + latch1.await(2, TimeUnit.SECONDS); + StringInputContext run1Context = result1.get(); + + assertThat(run1Context).isNotNull(); + assertThat(run1Context.getResultValue()).isNull(); + } + @Test public void testResultBasic() throws InterruptedException { ComponentContext empty = ComponentContext.empty(); diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/flow/AbstractShellTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/AbstractShellTests.java index c54c1e85..d8d657ba 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/flow/AbstractShellTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/AbstractShellTests.java @@ -25,6 +25,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import org.jline.keymap.KeyMap; +import org.jline.terminal.Size; import org.jline.terminal.Terminal; import org.jline.terminal.impl.DumbTerminal; import org.jline.utils.AttributedString; @@ -79,6 +80,7 @@ public abstract class AbstractShellTests { pipedInputStream.connect(pipedOutputStream); terminal = new DumbTerminal("terminal", "ansi", pipedInputStream, consoleOut, StandardCharsets.UTF_8); + terminal.setSize(new Size(1, 1)); executorService.execute(() -> { try { diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java index c8939801..696c1e00 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java @@ -23,15 +23,23 @@ 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.component.flow.ComponentFlow.ComponentFlowResult; 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 { @@ -133,6 +141,117 @@ public class ComponentFlowCommands extends AbstractShellComponent { 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 multi1 = new ArrayList<>(); + if (StringUtils.hasText(asdf)) { + multi1.add(asdf); + } + + Map single1SelectItems = new HashMap<>(); + single1SelectItems.put("key1", "value1"); + single1SelectItems.put("key2", "value2"); + List 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 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 single1SelectItems = new HashMap<>();