Components can't use interactive mode without tty

- Adding a concept of no-tty which in this commit simply
  tracks DumbTerminal as jline creates that if there nothing
  better.
- For components without tty don't go to interaction loop.
- For new sample show that we can at least manually handle
  required option with a flow while command option is not
  required.
- Fixes #444
This commit is contained in:
Janne Valkealahti
2022-06-15 07:10:56 +01:00
parent a019934169
commit 195d1d00ab
18 changed files with 349 additions and 9 deletions

View File

@@ -210,6 +210,10 @@ public interface CommandExecution {
this.parserExceptions = parserExceptions;
}
public static CommandParserExceptionsException of(String message, List<CommandParserException> parserExceptions) {
return new CommandParserExceptionsException(message, parserExceptions);
}
public List<CommandParserException> getParserExceptions() {
return parserExceptions;
}

View File

@@ -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<Boolean, ConfirmationInputContext> {
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<Boolean, Confirmati
@Override
protected boolean read(BindingReader bindingReader, KeyMap<String> 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:

View File

@@ -70,7 +70,10 @@ public class MultiItemSelector<T, I extends Nameable & Matchable & Enableable &
@Override
protected MultiItemSelectorContext<T, I> runInternal(MultiItemSelectorContext<T, I> 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;
}

View File

@@ -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<Path, PathInputContext> {
private final static Logger log = LoggerFactory.getLogger(PathInput.class);
private PathInputContext currentContext;
private Function<String, Path> pathProvider = (path) -> Paths.get(path);
@@ -75,6 +78,10 @@ public class PathInput extends AbstractTextComponent<Path, PathInputContext> {
@Override
protected boolean read(BindingReader bindingReader, KeyMap<String> 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:

View File

@@ -70,7 +70,10 @@ public class SingleItemSelector<T, I extends Nameable & Matchable & Enableable &
@Override
protected SingleItemSelectorContext<T, I> runInternal(SingleItemSelectorContext<T, I> 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;
}

View File

@@ -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<String, StringInputContext> {
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<String, StringInputContex
@Override
protected boolean read(BindingReader bindingReader, KeyMap<String> 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:

View File

@@ -56,6 +56,14 @@ public interface ComponentContext<C extends ComponentContext<C>> {
*/
<T> T get(Object key, Class<T> 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.
*

View File

@@ -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<T extends ComponentContext<T>> 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<T extends ComponentContext<T>> 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.
*

View File

@@ -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<T, C extends SelectorComponentContext<T, I, C>, I extends Nameable & Matchable & Enableable & Itemable<T>>
extends AbstractComponent<C> {
private final static Logger log = LoggerFactory.getLogger(AbstractSelectorComponent.class);
protected final String name;
private final List<I> items;
private Comparator<I> comparator = (o1, o2) -> 0;
@@ -155,6 +158,10 @@ public abstract class AbstractSelectorComponent<T, C extends SelectorComponentCo
ItemStateViewProjection buildItemStateView = buildItemStateView(start.get(), thisContext);
List<ItemState<I>> 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:

View File

@@ -66,7 +66,10 @@ public abstract class AbstractTextComponent<T, C extends TextComponentContext<T,
@Override
protected C runInternal(C 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;
}

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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<SelectorItem<SimplePojo>> 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<SelectorItem<SimplePojo>> items) {
scheduleSelect(items, null);
}
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems) {
MultiItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new MultiItemSelector<>(getTerminal(),
scheduleSelect(items, maxItems, getTerminal());
}
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems, Terminal terminal) {
MultiItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new MultiItemSelector<>(terminal,
items, "testSimple", null);
selector.setResourceLoader(new DefaultResourceLoader());
selector.setTemplateExecutor(getTemplateExecutor());

View File

@@ -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");

View File

@@ -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<SelectorItem<SimplePojo>> 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<SelectorItem<SimplePojo>> items) {
@@ -166,7 +188,11 @@ public class SingleItemSelectorTests extends AbstractShellTests {
}
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems) {
SingleItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new SingleItemSelector<>(getTerminal(),
scheduleSelect(items, maxItems, getTerminal());
}
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems, Terminal terminal) {
SingleItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new SingleItemSelector<>(terminal,
items, "testSimple", null);
selector.setResourceLoader(new DefaultResourceLoader());
selector.setTemplateExecutor(getTemplateExecutor());

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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<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<>();