Skip to content

Commit

Permalink
8247403: JShell: No custom input (e.g. from GUI) possible with JavaSh…
Browse files Browse the repository at this point in the history
…ellToolBuilder

Reviewed-by: vromero
  • Loading branch information
lahodaj committed May 31, 2021
1 parent 64f0f68 commit 2c8e94f
Show file tree
Hide file tree
Showing 14 changed files with 247 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4157,7 +4157,7 @@ private AttributedString expandPromptPattern(String pattern, int padToWidth,
} else
sb.append(ch);
}
if (padToWidth > cols) {
if (padToWidth > cols && padToWidth > 0) {
int padCharCols = WCWidth.wcwidth(padChar);
int padCount = (padToWidth - cols) / padCharCols;
sb = padPartString;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -47,6 +47,7 @@
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -103,8 +104,8 @@ class ConsoleIOContext extends IOContext {

String prefix = "";

ConsoleIOContext(JShellTool repl, InputStream cmdin, PrintStream cmdout) throws Exception {
this.allowIncompleteInputs = Boolean.getBoolean("jshell.test.allow.incomplete.inputs");
ConsoleIOContext(JShellTool repl, InputStream cmdin, PrintStream cmdout,
boolean interactive) throws Exception {
this.repl = repl;
Map<String, Object> variables = new HashMap<>();
this.input = new StopDetectingInputStream(() -> repl.stop(),
Expand All @@ -116,15 +117,28 @@ public int readBuffered(byte[] b) throws IOException {
}
};
Terminal terminal;
if (System.getProperty("test.jdk") != null) {
terminal = new TestTerminal(nonBlockingInput, cmdout);
boolean allowIncompleteInputs = Boolean.getBoolean("jshell.test.allow.incomplete.inputs");
Consumer<LineReaderImpl> setupReader = r -> {};
if (cmdin != System.in) {
if (System.getProperty("test.jdk") != null) {
terminal = new TestTerminal(nonBlockingInput, cmdout);
} else {
Size size = null;
terminal = new ProgrammaticInTerminal(nonBlockingInput, cmdout, interactive,
size);
if (!interactive) {
setupReader = r -> r.unsetOpt(Option.BRACKETED_PASTE);
allowIncompleteInputs = true;
}
}
input.setInputStream(cmdin);
} else {
terminal = TerminalBuilder.builder().inputStreamWrapper(in -> {
input.setInputStream(in);
return nonBlockingInput;
}).build();
}
this.allowIncompleteInputs = allowIncompleteInputs;
originalAttributes = terminal.getAttributes();
Attributes noIntr = new Attributes(originalAttributes);
noIntr.setControlChar(ControlChar.VINTR, 0);
Expand Down Expand Up @@ -179,10 +193,11 @@ protected boolean insertCloseSquare() {
}
};

setupReader.accept(reader);
reader.setOpt(Option.DISABLE_EVENT_EXPANSION);

reader.setParser((line, cursor, context) -> {
if (!allowIncompleteInputs && !repl.isComplete(line)) {
if (!ConsoleIOContext.this.allowIncompleteInputs && !repl.isComplete(line)) {
int pendingBraces = countPendingOpenBraces(line);
throw new EOFError(cursor, cursor, line, null, pendingBraces, null);
}
Expand Down Expand Up @@ -230,7 +245,7 @@ public String readLine(String firstLinePrompt, String continuationPrompt,
this.prefix = prefix;
try {
in.setVariable(LineReader.SECONDARY_PROMPT_PATTERN, continuationPrompt);
return in.readLine(firstLinePrompt);
return in.readLine(firstLine ? firstLinePrompt : continuationPrompt);
} catch (UserInterruptException ex) {
throw (InputInterruptedException) new InputInterruptedException().initCause(ex);
} catch (EndOfFileException ex) {
Expand Down Expand Up @@ -1276,28 +1291,31 @@ private History getHistory() {
return in.getHistory();
}

private static final class TestTerminal extends LineDisciplineTerminal {
private static class ProgrammaticInTerminal extends LineDisciplineTerminal {

private static final int DEFAULT_HEIGHT = 24;
protected static final int DEFAULT_HEIGHT = 24;

private final NonBlockingReader inputReader;
private final Size bufferSize;

public ProgrammaticInTerminal(InputStream input, OutputStream output,
boolean interactive, Size size) throws Exception {
this(input, output, interactive ? "ansi" : "dumb",
size != null ? size : new Size(80, DEFAULT_HEIGHT),
size != null ? size
: interactive ? new Size(80, DEFAULT_HEIGHT)
: new Size(Integer.MAX_VALUE - 1, DEFAULT_HEIGHT));
}

public TestTerminal(InputStream input, OutputStream output) throws Exception {
super("test", "ansi", output, Charset.forName("UTF-8"));
protected ProgrammaticInTerminal(InputStream input, OutputStream output,
String terminal, Size size, Size bufferSize) throws Exception {
super("non-system-in", terminal, output, Charset.forName("UTF-8"));
this.inputReader = NonBlocking.nonBlocking(getName(), input, encoding());
Attributes a = new Attributes(getAttributes());
a.setLocalFlag(LocalFlag.ECHO, false);
setAttributes(attributes);
int h = DEFAULT_HEIGHT;
try {
String hp = System.getProperty("test.terminal.height");
if (hp != null && !hp.isEmpty()) {
h = Integer.parseInt(hp);
}
} catch (Throwable ex) {
// ignore
}
setSize(new Size(80, h));
setSize(size);
this.bufferSize = bufferSize;
}

@Override
Expand All @@ -1312,6 +1330,31 @@ protected void doClose() throws IOException {
inputReader.close();
}

@Override
public Size getBufferSize() {
return bufferSize;
}
}

private static final class TestTerminal extends ProgrammaticInTerminal {
private static Size computeSize() {
int h = DEFAULT_HEIGHT;
try {
String hp = System.getProperty("test.terminal.height");
if (hp != null && !hp.isEmpty() && System.getProperty("test.jdk") != null) {
h = Integer.parseInt(hp);
}
} catch (Throwable ex) {
// ignore
}
return new Size(80, h);
}
public TestTerminal(InputStream input, OutputStream output) throws Exception {
this(input, output, computeSize());
}
private TestTerminal(InputStream input, OutputStream output, Size size) throws Exception {
super(input, output, "ansi", size, size);
}
}

private static final class CompletionState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@
*/
public class JShellTool implements MessageHandler {

private static String PROMPT = "\u0005";
private static String CONTINUATION_PROMPT = "\u0006";
private static final Pattern LINEBREAK = Pattern.compile("\\R");
private static final Pattern ID = Pattern.compile("[se]?\\d+([-\\s].*)?");
private static final Pattern RERUN_ID = Pattern.compile("/" + ID.pattern());
Expand All @@ -158,6 +160,7 @@ public class JShellTool implements MessageHandler {
final PersistentStorage prefs;
final Map<String, String> envvars;
final Locale locale;
final boolean interactiveTerminal;

final Feedback feedback = new Feedback();

Expand All @@ -177,7 +180,8 @@ public class JShellTool implements MessageHandler {
JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr,
PrintStream console,
InputStream userin, PrintStream userout, PrintStream usererr,
PersistentStorage prefs, Map<String, String> envvars, Locale locale) {
PersistentStorage prefs, Map<String, String> envvars, Locale locale,
boolean interactiveTerminal) {
this.cmdin = cmdin;
this.cmdout = cmdout;
this.cmderr = cmderr;
Expand All @@ -193,6 +197,7 @@ public int read() throws IOException {
this.prefs = prefs;
this.envvars = envvars;
this.locale = locale;
this.interactiveTerminal = interactiveTerminal;
}

private ResourceBundle versionRB = null;
Expand Down Expand Up @@ -974,7 +979,7 @@ public void run() {
};
Runtime.getRuntime().addShutdownHook(shutdownHook);
// execute from user input
try (IOContext in = new ConsoleIOContext(this, cmdin, console)) {
try (IOContext in = new ConsoleIOContext(this, cmdin, console, interactiveTerminal)) {
int indent;
try {
String indentValue = indent();
Expand Down Expand Up @@ -1256,12 +1261,12 @@ private String getInput(String initial) throws IOException{
return src;
}
String firstLinePrompt = interactive()
? testPrompt ? " \005"
? testPrompt ? PROMPT
: feedback.getPrompt(currentNameSpace.tidNext())
: "" // Non-interactive -- no prompt
;
String continuationPrompt = interactive()
? testPrompt ? " \006"
? testPrompt ? CONTINUATION_PROMPT
: feedback.getContinuationPrompt(currentNameSpace.tidNext())
: "" // Non-interactive -- no prompt
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class JShellToolBuilder implements JavaShellToolBuilder {
private PersistentStorage prefs = null;
private Map<String, String> vars = null;
private Locale locale = Locale.getDefault();
private boolean interactiveTerminal;
private boolean capturePrompt = false;

/**
Expand Down Expand Up @@ -208,6 +209,12 @@ public JavaShellToolBuilder promptCapture(boolean capture) {
return this;
}

@Override
public JavaShellToolBuilder interactiveTerminal(boolean terminal) {
this.interactiveTerminal = terminal;
return this;
}

/**
* Create a tool instance for testing. Not in JavaShellToolBuilder.
*
Expand All @@ -221,7 +228,7 @@ public JShellTool rawTool() {
vars = System.getenv();
}
JShellTool sh = new JShellTool(cmdIn, cmdOut, cmdErr, console, userIn,
userOut, userErr, prefs, vars, locale);
userOut, userErr, prefs, vars, locale, interactiveTerminal);
sh.testPrompt = capturePrompt;
return sh;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,32 @@ static JavaShellToolBuilder builder() {
*/
JavaShellToolBuilder promptCapture(boolean capture);

/**
* Set to true to specify the inputs and outputs are connected to an interactive terminal
* that can interpret the ANSI escape codes. The characters sent to the output streams are
* assumed to be interpreted by a terminal and shown to the user, and the exact order and nature
* of characters sent to the outputs are unspecified.
*
* Set to false to specify a legacy simpler behavior whose output can be parsed by automatic
* tools.
*
* When the input stream for this Java Shell is {@code System.in}, this value is ignored,
* and the behavior is similar to specifying {@code true} in this method, but is more closely
* following the specific terminal connected to {@code System.in}.
*
* @implSpec If this method is not called, the behavior should be
* equivalent to calling {@code interactiveTerminal(false)}. The default implementation of
* this method returns {@code this}.
*
* @param terminal if {@code true}, an terminal that can interpret the ANSI escape codes is
* assumed to interpret the output. If {@code false}, a simpler output is selected.
* @return the {@code JavaShellToolBuilder} instance
* @since 17
*/
default JavaShellToolBuilder interactiveTerminal(boolean terminal) {
return this;
}

/**
* Run an instance of the Java shell tool as configured by the other methods
* in this interface. This call is not destructive, more than one call of
Expand Down
Loading

1 comment on commit 2c8e94f

@openjdk-notifier
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.