Skip to content

Commit 195d1d0

Browse files
committed
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
1 parent a019934 commit 195d1d0

18 files changed

+349
-9
lines changed

spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ public CommandParserExceptionsException(String message, List<CommandParserExcept
210210
this.parserExceptions = parserExceptions;
211211
}
212212

213+
public static CommandParserExceptionsException of(String message, List<CommandParserException> parserExceptions) {
214+
return new CommandParserExceptionsException(message, parserExceptions);
215+
}
216+
213217
public List<CommandParserException> getParserExceptions() {
214218
return parserExceptions;
215219
}

spring-shell-core/src/main/java/org/springframework/shell/component/ConfirmationInput.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.jline.keymap.KeyMap;
2525
import org.jline.terminal.Terminal;
2626
import org.jline.utils.AttributedString;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
2729

2830
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
2931
import org.springframework.shell.component.context.ComponentContext;
@@ -39,6 +41,7 @@
3941
*/
4042
public class ConfirmationInput extends AbstractTextComponent<Boolean, ConfirmationInputContext> {
4143

44+
private final static Logger log = LoggerFactory.getLogger(ConfirmationInput.class);
4245
private final boolean defaultValue;
4346
private ConfirmationInputContext currentContext;
4447

@@ -78,6 +81,10 @@ public ConfirmationInputContext getThisContext(ComponentContext<?> context) {
7881
@Override
7982
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, ConfirmationInputContext context) {
8083
String operation = bindingReader.readBinding(keyMap);
84+
log.debug("Binding read result {}", operation);
85+
if (operation == null) {
86+
return true;
87+
}
8188
String input;
8289
switch (operation) {
8390
case OPERATION_CHAR:

spring-shell-core/src/main/java/org/springframework/shell/component/MultiItemSelector.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ public MultiItemSelectorContext<T, I> getThisContext(ComponentContext<?> context
7070
@Override
7171
protected MultiItemSelectorContext<T, I> runInternal(MultiItemSelectorContext<T, I> context) {
7272
super.runInternal(context);
73-
loop(context);
73+
// if there's no tty don't try to loop as it would then cause user interaction
74+
if (hasTty()) {
75+
loop(context);
76+
}
7477
return context;
7578
}
7679

spring-shell-core/src/main/java/org/springframework/shell/component/PathInput.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.jline.keymap.KeyMap;
2828
import org.jline.terminal.Terminal;
2929
import org.jline.utils.AttributedString;
30+
import org.slf4j.Logger;
31+
import org.slf4j.LoggerFactory;
3032

3133
import org.springframework.shell.component.PathInput.PathInputContext;
3234
import org.springframework.shell.component.context.ComponentContext;
@@ -42,6 +44,7 @@
4244
*/
4345
public class PathInput extends AbstractTextComponent<Path, PathInputContext> {
4446

47+
private final static Logger log = LoggerFactory.getLogger(PathInput.class);
4548
private PathInputContext currentContext;
4649
private Function<String, Path> pathProvider = (path) -> Paths.get(path);
4750

@@ -75,6 +78,10 @@ public PathInputContext getThisContext(ComponentContext<?> context) {
7578
@Override
7679
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, PathInputContext context) {
7780
String operation = bindingReader.readBinding(keyMap);
81+
log.debug("Binding read result {}", operation);
82+
if (operation == null) {
83+
return true;
84+
}
7885
String input;
7986
switch (operation) {
8087
case OPERATION_CHAR:

spring-shell-core/src/main/java/org/springframework/shell/component/SingleItemSelector.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ public SingleItemSelectorContext<T, I> getThisContext(ComponentContext<?> contex
7070
@Override
7171
protected SingleItemSelectorContext<T, I> runInternal(SingleItemSelectorContext<T, I> context) {
7272
super.runInternal(context);
73-
loop(context);
73+
// if there's no tty don't try to loop as it would then cause user interaction
74+
if (hasTty()) {
75+
loop(context);
76+
}
7477
return context;
7578
}
7679

spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.jline.keymap.KeyMap;
2525
import org.jline.terminal.Terminal;
2626
import org.jline.utils.AttributedString;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
2729

2830
import org.springframework.shell.component.StringInput.StringInputContext;
2931
import org.springframework.shell.component.context.ComponentContext;
@@ -38,6 +40,7 @@
3840
*/
3941
public class StringInput extends AbstractTextComponent<String, StringInputContext> {
4042

43+
private final static Logger log = LoggerFactory.getLogger(StringInput.class);
4144
private final String defaultValue;
4245
private StringInputContext currentContext;
4346
private Character maskCharacter;
@@ -83,6 +86,10 @@ public StringInputContext getThisContext(ComponentContext<?> context) {
8386
@Override
8487
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, StringInputContext context) {
8588
String operation = bindingReader.readBinding(keyMap);
89+
log.debug("Binding read result {}", operation);
90+
if (operation == null) {
91+
return true;
92+
}
8693
String input;
8794
switch (operation) {
8895
case OPERATION_CHAR:

spring-shell-core/src/main/java/org/springframework/shell/component/context/ComponentContext.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ static <C extends ComponentContext<C>> ComponentContext<C> empty() {
5656
*/
5757
<T> T get(Object key, Class<T> type);
5858

59+
/**
60+
* Check if a context contains a key.
61+
*
62+
* @param key the key
63+
* @return true if context contains key
64+
*/
65+
boolean containsKey(Object key);
66+
5967
/**
6068
* Put an entry into a context.
6169
*

spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractComponent.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.jline.terminal.Attributes;
3434
import org.jline.terminal.Size;
3535
import org.jline.terminal.Terminal;
36+
import org.jline.terminal.impl.DumbTerminal;
3637
import org.jline.utils.AttributedString;
3738
import org.jline.utils.Display;
3839
import org.jline.utils.InfoCmp.Capability;
@@ -153,7 +154,8 @@ public final T run(ComponentContext<?> context) {
153154
context = runPreRunHandlers(getThisContext(context));
154155
T run = runInternal(getThisContext(context));
155156
context = runPostRunHandlers(getThisContext(context));
156-
if (printResults) {
157+
// if there's no tty don't try to print results as it'd be pointless
158+
if (printResults && hasTty()) {
157159
printResults(context);
158160
}
159161
return run;
@@ -186,6 +188,22 @@ public void setTemplateLocation(String templateLocation) {
186188
this.templateLocation = templateLocation;
187189
}
188190

191+
/**
192+
* Checks if this component has an existing {@code tty}.
193+
*
194+
* @return true if component has tty
195+
*/
196+
protected boolean hasTty() {
197+
boolean hasTty = true;
198+
if (this.terminal instanceof DumbTerminal) {
199+
if (this.terminal.getSize().getRows() == 0) {
200+
hasTty = false;
201+
}
202+
}
203+
log.debug("Terminal is {} with size {}, marking hasTty as {}", this.terminal, this.terminal.getSize(), hasTty);
204+
return hasTty;
205+
}
206+
189207
/**
190208
* Render a given template with attributes.
191209
*

spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractSelectorComponent.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import org.jline.keymap.KeyMap;
2727
import org.jline.terminal.Terminal;
2828
import org.jline.utils.InfoCmp.Capability;
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
2931

3032
import org.springframework.shell.component.context.BaseComponentContext;
3133
import org.springframework.shell.component.context.ComponentContext;
@@ -46,6 +48,7 @@
4648
public abstract class AbstractSelectorComponent<T, C extends SelectorComponentContext<T, I, C>, I extends Nameable & Matchable & Enableable & Itemable<T>>
4749
extends AbstractComponent<C> {
4850

51+
private final static Logger log = LoggerFactory.getLogger(AbstractSelectorComponent.class);
4952
protected final String name;
5053
private final List<I> items;
5154
private Comparator<I> comparator = (o1, o2) -> 0;
@@ -155,6 +158,10 @@ protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, C con
155158
ItemStateViewProjection buildItemStateView = buildItemStateView(start.get(), thisContext);
156159
List<ItemState<I>> itemStateView = buildItemStateView.items;
157160
String operation = bindingReader.readBinding(keyMap);
161+
log.debug("Binding read result {}", operation);
162+
if (operation == null) {
163+
return true;
164+
}
158165
String input;
159166
switch (operation) {
160167
case OPERATION_SELECT:

spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractTextComponent.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ protected void bindKeyMap(KeyMap<String> keyMap) {
6666

6767
@Override
6868
protected C runInternal(C context) {
69-
loop(context);
69+
// if there's no tty don't try to loop as it would then cause user interaction
70+
if (hasTty()) {
71+
loop(context);
72+
}
7073
return context;
7174
}
7275

spring-shell-core/src/test/java/org/springframework/shell/component/AbstractShellTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.concurrent.LinkedBlockingQueue;
2626

2727
import org.jline.keymap.KeyMap;
28+
import org.jline.terminal.Size;
2829
import org.jline.terminal.Terminal;
2930
import org.jline.terminal.impl.DumbTerminal;
3031
import org.jline.utils.AttributedString;
@@ -79,6 +80,7 @@ public ThemeSettings getSettings() {
7980

8081
pipedInputStream.connect(pipedOutputStream);
8182
terminal = new DumbTerminal("terminal", "ansi", pipedInputStream, consoleOut, StandardCharsets.UTF_8);
83+
terminal.setSize(new Size(1, 1));
8284

8385
executorService.execute(() -> {
8486
try {

spring-shell-core/src/test/java/org/springframework/shell/component/ConfirmationInputTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@
1515
*/
1616
package org.springframework.shell.component;
1717

18+
import java.io.ByteArrayInputStream;
19+
import java.io.ByteArrayOutputStream;
1820
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
1922
import java.util.concurrent.CountDownLatch;
2023
import java.util.concurrent.ExecutorService;
2124
import java.util.concurrent.Executors;
2225
import java.util.concurrent.TimeUnit;
2326
import java.util.concurrent.atomic.AtomicReference;
2427

28+
import org.jline.terminal.impl.DumbTerminal;
2529
import org.junit.jupiter.api.AfterEach;
2630
import org.junit.jupiter.api.BeforeEach;
2731
import org.junit.jupiter.api.Test;
@@ -55,6 +59,33 @@ public void cleanupTests() throws IOException {
5559
service = null;
5660
}
5761

62+
@Test
63+
void testNoTty() throws Exception {
64+
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
65+
ByteArrayOutputStream out = new ByteArrayOutputStream();
66+
DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8);
67+
68+
ComponentContext<?> empty = ComponentContext.empty();
69+
ConfirmationInput component1 = new ConfirmationInput(dumbTerminal, "component1");
70+
component1.setResourceLoader(new DefaultResourceLoader());
71+
component1.setTemplateExecutor(getTemplateExecutor());
72+
73+
service.execute(() -> {
74+
ConfirmationInputContext run1Context = component1.run(empty);
75+
result1.set(run1Context);
76+
latch1.countDown();
77+
});
78+
79+
TestBuffer testBuffer = new TestBuffer().cr();
80+
write(testBuffer.getBytes());
81+
82+
latch1.await(2, TimeUnit.SECONDS);
83+
ConfirmationInputContext run1Context = result1.get();
84+
85+
assertThat(run1Context).isNotNull();
86+
assertThat(run1Context.getResultValue()).isNull();
87+
}
88+
5889
@Test
5990
public void testResultUserInputEnterDefaultYes() throws InterruptedException, IOException {
6091
ComponentContext<?> empty = ComponentContext.empty();

spring-shell-core/src/test/java/org/springframework/shell/component/MultiItemSelectorTests.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package org.springframework.shell.component;
1717

18+
import java.io.ByteArrayInputStream;
19+
import java.io.ByteArrayOutputStream;
20+
import java.nio.charset.StandardCharsets;
1821
import java.time.Duration;
1922
import java.util.Arrays;
2023
import java.util.List;
@@ -25,6 +28,8 @@
2528
import java.util.concurrent.atomic.AtomicReference;
2629
import java.util.stream.Stream;
2730

31+
import org.jline.terminal.Terminal;
32+
import org.jline.terminal.impl.DumbTerminal;
2833
import org.junit.jupiter.api.AfterEach;
2934
import org.junit.jupiter.api.BeforeEach;
3035
import org.junit.jupiter.api.Test;
@@ -75,6 +80,19 @@ public void cleanupMulti() {
7580
service = null;
7681
}
7782

83+
@Test
84+
void testNoTty() throws Exception {
85+
ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
86+
ByteArrayOutputStream out = new ByteArrayOutputStream();
87+
DumbTerminal dumbTerminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8);
88+
89+
scheduleSelect(dumbTerminal);
90+
awaitLatch();
91+
92+
List<SelectorItem<SimplePojo>> selected = result.get();
93+
assertThat(selected).isNull();
94+
}
95+
7896
@Test
7997
public void testItemsShown() {
8098
scheduleSelect();
@@ -192,12 +210,21 @@ private void scheduleSelect() {
192210
SELECTOR_ITEM_4));
193211
}
194212

213+
private void scheduleSelect(Terminal terminal) {
214+
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3, SELECTOR_ITEM_4), null,
215+
terminal);
216+
}
217+
195218
private void scheduleSelect(List<SelectorItem<SimplePojo>> items) {
196219
scheduleSelect(items, null);
197220
}
198221

199222
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems) {
200-
MultiItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new MultiItemSelector<>(getTerminal(),
223+
scheduleSelect(items, maxItems, getTerminal());
224+
}
225+
226+
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems, Terminal terminal) {
227+
MultiItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new MultiItemSelector<>(terminal,
201228
items, "testSimple", null);
202229
selector.setResourceLoader(new DefaultResourceLoader());
203230
selector.setTemplateExecutor(getTemplateExecutor());

0 commit comments

Comments
 (0)