/* * Copyright (c) 2014, 2017, 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 * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.internal.jshell.tool; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.io.Reader; import java.io.StringReader; import java.nio.charset.Charset; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Scanner; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.prefs.Preferences; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; import jdk.internal.jshell.debug.InternalDebugControl; import jdk.internal.jshell.tool.IOContext.InputInterruptedException; import jdk.jshell.DeclarationSnippet; import jdk.jshell.Diag; import jdk.jshell.EvalException; import jdk.jshell.ExpressionSnippet; import jdk.jshell.ImportSnippet; import jdk.jshell.JShell; import jdk.jshell.JShell.Subscription; import jdk.jshell.MethodSnippet; import jdk.jshell.Snippet; import jdk.jshell.Snippet.Status; import jdk.jshell.SnippetEvent; import jdk.jshell.SourceCodeAnalysis; import jdk.jshell.SourceCodeAnalysis.CompletionInfo; import jdk.jshell.SourceCodeAnalysis.Suggestion; import jdk.jshell.TypeDeclSnippet; import jdk.jshell.UnresolvedReferenceException; import jdk.jshell.VarSnippet; import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.nio.file.StandardOpenOption.WRITE; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.ServiceLoader; import java.util.Spliterators; import java.util.function.Function; import java.util.function.Supplier; import jdk.internal.joptsimple.*; import jdk.internal.jshell.tool.Feedback.FormatAction; import jdk.internal.jshell.tool.Feedback.FormatCase; import jdk.internal.jshell.tool.Feedback.FormatErrors; import jdk.internal.jshell.tool.Feedback.FormatResolve; import jdk.internal.jshell.tool.Feedback.FormatUnresolved; import jdk.internal.jshell.tool.Feedback.FormatWhen; import jdk.internal.editor.spi.BuildInEditorProvider; import jdk.internal.editor.external.ExternalEditor; import static java.util.Arrays.asList; import static java.util.Arrays.stream; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND; import static java.util.stream.Collectors.toMap; import static jdk.internal.jshell.debug.InternalDebugControl.DBG_COMPA; import static jdk.internal.jshell.debug.InternalDebugControl.DBG_DEP; import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT; import static jdk.internal.jshell.debug.InternalDebugControl.DBG_FMGR; import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN; import static jdk.internal.jshell.debug.InternalDebugControl.DBG_WRAP; import static jdk.internal.jshell.tool.ContinuousCompletionProvider.STARTSWITH_MATCHER; /** * Command line REPL tool for Java using the JShell API. * @author Robert Field */ public class JShellTool implements MessageHandler { private static final Pattern LINEBREAK = Pattern.compile("\\R"); private static final Pattern ID = Pattern.compile("[se]?\\d+([-\\s].*)?"); static final String RECORD_SEPARATOR = "\u241E"; private static final String RB_NAME_PREFIX = "jdk.internal.jshell.tool.resources"; private static final String VERSION_RB_NAME = RB_NAME_PREFIX + ".version"; private static final String L10N_RB_NAME = RB_NAME_PREFIX + ".l10n"; final InputStream cmdin; final PrintStream cmdout; final PrintStream cmderr; final PrintStream console; final InputStream userin; final PrintStream userout; final PrintStream usererr; final PersistentStorage prefs; final Map envvars; final Locale locale; final Feedback feedback = new Feedback(); /** * The complete constructor for the tool (used by test harnesses). * @param cmdin command line input -- snippets and commands * @param cmdout command line output, feedback including errors * @param cmderr start-up errors and debugging info * @param console console control interaction * @param userin code execution input, or null to use IOContext * @param userout code execution output -- System.out.printf("hi") * @param usererr code execution error stream -- System.err.printf("Oops") * @param prefs persistence implementation to use * @param envvars environment variable mapping to use * @param locale locale to use */ JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr, PrintStream console, InputStream userin, PrintStream userout, PrintStream usererr, PersistentStorage prefs, Map envvars, Locale locale) { this.cmdin = cmdin; this.cmdout = cmdout; this.cmderr = cmderr; this.console = console; this.userin = userin != null ? userin : new InputStream() { @Override public int read() throws IOException { return input.readUserInput(); } }; this.userout = userout; this.usererr = usererr; this.prefs = prefs; this.envvars = envvars; this.locale = locale; } private ResourceBundle versionRB = null; private ResourceBundle outputRB = null; private IOContext input = null; private boolean regenerateOnDeath = true; private boolean live = false; private Options options; SourceCodeAnalysis analysis; private JShell state = null; Subscription shutdownSubscription = null; static final EditorSetting BUILT_IN_EDITOR = new EditorSetting(null, false); private boolean debug = false; public boolean testPrompt = false; private Startup startup = null; private boolean isCurrentlyRunningStartup = false; private String executionControlSpec = null; private EditorSetting editor = BUILT_IN_EDITOR; private static final String[] EDITOR_ENV_VARS = new String[] { "JSHELLEDITOR", "VISUAL", "EDITOR"}; // Commands and snippets which can be replayed private ReplayableHistory replayableHistory; private ReplayableHistory replayableHistoryPrevious; static final String STARTUP_KEY = "STARTUP"; static final String EDITOR_KEY = "EDITOR"; static final String FEEDBACK_KEY = "FEEDBACK"; static final String MODE_KEY = "MODE"; static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE"; static final Pattern BUILTIN_FILE_PATTERN = Pattern.compile("\\w+"); static final String BUILTIN_FILE_PATH_FORMAT = "/jdk/jshell/tool/resources/%s.jsh"; // match anything followed by whitespace private static final Pattern OPTION_PRE_PATTERN = Pattern.compile("\\s*(\\S+\\s+)*?"); // match a (possibly incomplete) option flag with optional double-dash and/or internal dashes private static final Pattern OPTION_PATTERN = Pattern.compile(OPTION_PRE_PATTERN.pattern() + "(?
-??)(?-([a-z][a-z\\-]*)?)"); // match an option flag and a (possibly missing or incomplete) value private static final Pattern OPTION_VALUE_PATTERN = Pattern.compile(OPTION_PATTERN.pattern() + "\\s+(?\\S*)"); // Tool id (tid) mapping: the three name spaces NameSpace mainNamespace; NameSpace startNamespace; NameSpace errorNamespace; // Tool id (tid) mapping: the current name spaces NameSpace currentNameSpace; Map mapSnippet; // Kinds of compiler/runtime init options private enum OptionKind { CLASS_PATH("--class-path", true), MODULE_PATH("--module-path", true), ADD_MODULES("--add-modules", false), ADD_EXPORTS("--add-exports", false), TO_COMPILER("-C", false, false, true, false), TO_REMOTE_VM("-R", false, false, false, true),; final String optionFlag; final boolean onlyOne; final boolean passFlag; final boolean toCompiler; final boolean toRemoteVm; private OptionKind(String optionFlag, boolean onlyOne) { this(optionFlag, onlyOne, true, true, true); } private OptionKind(String optionFlag, boolean onlyOne, boolean passFlag, boolean toCompiler, boolean toRemoteVm) { this.optionFlag = optionFlag; this.onlyOne = onlyOne; this.passFlag = passFlag; this.toCompiler = toCompiler; this.toRemoteVm = toRemoteVm; } } // compiler/runtime init option values private static class Options { private final Map> optMap; // New blank Options Options() { optMap = new HashMap<>(); } // Options as a copy private Options(Options opts) { optMap = new HashMap<>(opts.optMap); } private String[] selectOptions(Predicate>> pred) { return optMap.entrySet().stream() .filter(pred) .flatMap(e -> e.getValue().stream()) .toArray(String[]::new); } String[] remoteVmOptions() { return selectOptions(e -> e.getKey().toRemoteVm); } String[] compilerOptions() { return selectOptions(e -> e.getKey().toCompiler); } String[] commonOptions() { return selectOptions(e -> e.getKey().passFlag); } void addAll(OptionKind kind, Collection vals) { optMap.computeIfAbsent(kind, k -> new ArrayList<>()) .addAll(vals); } // return a new Options, with parameter options overriding receiver options Options override(Options newer) { Options result = new Options(this); newer.optMap.entrySet().stream() .forEach(e -> { if (e.getKey().onlyOne) { // Only one allowed, override last result.optMap.put(e.getKey(), e.getValue()); } else { // Additive result.addAll(e.getKey(), e.getValue()); } }); return result; } } // base option parsing of /env, /reload, and /reset and command-line options private class OptionParserBase { final OptionParser parser = new OptionParser(); private final OptionSpec argClassPath = parser.accepts("class-path").withRequiredArg(); private final OptionSpec argModulePath = parser.accepts("module-path").withRequiredArg(); private final OptionSpec argAddModules = parser.accepts("add-modules").withRequiredArg(); private final OptionSpec argAddExports = parser.accepts("add-exports").withRequiredArg(); private final NonOptionArgumentSpec argNonOptions = parser.nonOptions(); private Options opts = new Options(); private List nonOptions; private boolean failed = false; List nonOptions() { return nonOptions; } void msg(String key, Object... args) { errormsg(key, args); } Options parse(String[] args) throws OptionException { try { OptionSet oset = parser.parse(args); nonOptions = oset.valuesOf(argNonOptions); return parse(oset); } catch (OptionException ex) { if (ex.options().isEmpty()) { msg("jshell.err.opt.invalid", stream(args).collect(joining(", "))); } else { boolean isKnown = parser.recognizedOptions().containsKey(ex.options().iterator().next()); msg(isKnown ? "jshell.err.opt.arg" : "jshell.err.opt.unknown", ex.options() .stream() .collect(joining(", "))); } return null; } } // check that the supplied string represent valid class/module paths // converting any ~/ to user home private Collection validPaths(Collection vals, String context, boolean isModulePath) { Stream result = vals.stream() .map(s -> Arrays.stream(s.split(File.pathSeparator)) .map(sp -> toPathResolvingUserHome(sp)) .filter(p -> checkValidPathEntry(p, context, isModulePath)) .map(p -> p.toString()) .collect(Collectors.joining(File.pathSeparator))); if (failed) { return Collections.emptyList(); } else { return result.collect(toList()); } } // Adapted from compiler method Locations.checkValidModulePathEntry private boolean checkValidPathEntry(Path p, String context, boolean isModulePath) { if (!Files.exists(p)) { msg("jshell.err.file.not.found", context, p); failed = true; return false; } if (Files.isDirectory(p)) { // if module-path, either an exploded module or a directory of modules return true; } String name = p.getFileName().toString(); int lastDot = name.lastIndexOf("."); if (lastDot > 0) { switch (name.substring(lastDot)) { case ".jar": return true; case ".jmod": if (isModulePath) { return true; } } } msg("jshell.err.arg", context, p); failed = true; return false; } Options parse(OptionSet options) { addOptions(OptionKind.CLASS_PATH, validPaths(options.valuesOf(argClassPath), "--class-path", false)); addOptions(OptionKind.MODULE_PATH, validPaths(options.valuesOf(argModulePath), "--module-path", true)); addOptions(OptionKind.ADD_MODULES, options.valuesOf(argAddModules)); addOptions(OptionKind.ADD_EXPORTS, options.valuesOf(argAddExports).stream() .map(mp -> mp.contains("=") ? mp : mp + "=ALL-UNNAMED") .collect(toList()) ); return failed ? null : opts; } void addOptions(OptionKind kind, Collection vals) { if (!vals.isEmpty()) { if (kind.onlyOne && vals.size() > 1) { msg("jshell.err.opt.one", kind.optionFlag); failed = true; return; } if (kind.passFlag) { vals = vals.stream() .flatMap(mp -> Stream.of(kind.optionFlag, mp)) .collect(toList()); } opts.addAll(kind, vals); } } } // option parsing for /reload (adds -restore -quiet) private class OptionParserReload extends OptionParserBase { private final OptionSpecBuilder argRestore = parser.accepts("restore"); private final OptionSpecBuilder argQuiet = parser.accepts("quiet"); private boolean restore = false; private boolean quiet = false; boolean restore() { return restore; } boolean quiet() { return quiet; } @Override Options parse(OptionSet options) { if (options.has(argRestore)) { restore = true; } if (options.has(argQuiet)) { quiet = true; } return super.parse(options); } } // option parsing for command-line private class OptionParserCommandLine extends OptionParserBase { private final OptionSpec argStart = parser.accepts("startup").withRequiredArg(); private final OptionSpecBuilder argNoStart = parser.acceptsAll(asList("n", "no-startup")); private final OptionSpec argFeedback = parser.accepts("feedback").withRequiredArg(); private final OptionSpec argExecution = parser.accepts("execution").withRequiredArg(); private final OptionSpecBuilder argQ = parser.accepts("q"); private final OptionSpecBuilder argS = parser.accepts("s"); private final OptionSpecBuilder argV = parser.accepts("v"); private final OptionSpec argR = parser.accepts("R").withRequiredArg(); private final OptionSpec argC = parser.accepts("C").withRequiredArg(); private final OptionSpecBuilder argHelp = parser.acceptsAll(asList("?", "h", "help")); private final OptionSpecBuilder argVersion = parser.accepts("version"); private final OptionSpecBuilder argFullVersion = parser.accepts("full-version"); private final OptionSpecBuilder argShowVersion = parser.accepts("show-version"); private final OptionSpecBuilder argHelpExtra = parser.acceptsAll(asList("X", "help-extra")); private String feedbackMode = null; private Startup initialStartup = null; String feedbackMode() { return feedbackMode; } Startup startup() { return initialStartup; } @Override void msg(String key, Object... args) { startmsg(key, args); } @Override Options parse(OptionSet options) { if (options.has(argHelp)) { printUsage(); return null; } if (options.has(argHelpExtra)) { printUsageX(); return null; } if (options.has(argVersion)) { cmdout.printf("jshell %s\n", version()); return null; } if (options.has(argFullVersion)) { cmdout.printf("jshell %s\n", fullVersion()); return null; } if (options.has(argShowVersion)) { cmdout.printf("jshell %s\n", version()); } if ((options.valuesOf(argFeedback).size() + (options.has(argQ) ? 1 : 0) + (options.has(argS) ? 1 : 0) + (options.has(argV) ? 1 : 0)) > 1) { msg("jshell.err.opt.feedback.one"); return null; } else if (options.has(argFeedback)) { feedbackMode = options.valueOf(argFeedback); } else if (options.has("q")) { feedbackMode = "concise"; } else if (options.has("s")) { feedbackMode = "silent"; } else if (options.has("v")) { feedbackMode = "verbose"; } if (options.has(argStart)) { List sts = options.valuesOf(argStart); if (options.has("no-startup")) { startmsg("jshell.err.opt.startup.conflict"); return null; } initialStartup = Startup.fromFileList(sts, "--startup", new InitMessageHandler()); if (initialStartup == null) { return null; } } else if (options.has(argNoStart)) { initialStartup = Startup.noStartup(); } else { String packedStartup = prefs.get(STARTUP_KEY); initialStartup = Startup.unpack(packedStartup, new InitMessageHandler()); } if (options.has(argExecution)) { executionControlSpec = options.valueOf(argExecution); } addOptions(OptionKind.TO_REMOTE_VM, options.valuesOf(argR)); addOptions(OptionKind.TO_COMPILER, options.valuesOf(argC)); return super.parse(options); } } /** * Encapsulate a history of snippets and commands which can be replayed. */ private static class ReplayableHistory { // the history private List hist; // the length of the history as of last save private int lastSaved; private ReplayableHistory(List hist) { this.hist = hist; this.lastSaved = 0; } // factory for empty histories static ReplayableHistory emptyHistory() { return new ReplayableHistory(new ArrayList<>()); } // factory for history stored in persistent storage static ReplayableHistory fromPrevious(PersistentStorage prefs) { // Read replay history from last jshell session String prevReplay = prefs.get(REPLAY_RESTORE_KEY); if (prevReplay == null) { return null; } else { return new ReplayableHistory(Arrays.asList(prevReplay.split(RECORD_SEPARATOR))); } } // store the history in persistent storage void storeHistory(PersistentStorage prefs) { if (hist.size() > lastSaved) { // Prevent history overflow by calculating what will fit, starting // with most recent int sepLen = RECORD_SEPARATOR.length(); int length = 0; int first = hist.size(); while (length < Preferences.MAX_VALUE_LENGTH && --first >= 0) { length += hist.get(first).length() + sepLen; } if (first >= 0) { hist = hist.subList(first + 1, hist.size()); } String shist = String.join(RECORD_SEPARATOR, hist); prefs.put(REPLAY_RESTORE_KEY, shist); markSaved(); } prefs.flush(); } // add a snippet or command to the history void add(String s) { hist.add(s); } // return history to reloaded Iterable iterable() { return hist; } // mark that persistent storage and current history are in sync void markSaved() { lastSaved = hist.size(); } } /** * Is the input/output currently interactive * * @return true if console */ boolean interactive() { return input != null && input.interactiveOutput(); } void debug(String format, Object... args) { if (debug) { cmderr.printf(format + "\n", args); } } /** * Base output for command output -- no pre- or post-fix * * @param printf format * @param printf args */ void rawout(String format, Object... args) { cmdout.printf(format, args); } /** * Must show command output * * @param format printf format * @param args printf args */ @Override public void hard(String format, Object... args) { rawout(prefix(format), args); } /** * Error command output * * @param format printf format * @param args printf args */ void error(String format, Object... args) { rawout(prefixError(format), args); } /** * Should optional informative be displayed? * @return true if they should be displayed */ @Override public boolean showFluff() { return feedback.shouldDisplayCommandFluff() && interactive(); } /** * Optional output * * @param format printf format * @param args printf args */ @Override public void fluff(String format, Object... args) { if (showFluff()) { hard(format, args); } } /** * Resource bundle look-up * * @param key the resource key */ String getResourceString(String key) { if (outputRB == null) { try { outputRB = ResourceBundle.getBundle(L10N_RB_NAME, locale); } catch (MissingResourceException mre) { error("Cannot find ResourceBundle: %s for locale: %s", L10N_RB_NAME, locale); return ""; } } String s; try { s = outputRB.getString(key); } catch (MissingResourceException mre) { error("Missing resource: %s in %s", key, L10N_RB_NAME); return ""; } return s; } /** * Add normal prefixing/postfixing to embedded newlines in a string, * bracketing with normal prefix/postfix * * @param s the string to prefix * @return the pre/post-fixed and bracketed string */ String prefix(String s) { return prefix(s, feedback.getPre(), feedback.getPost()); } /** * Add error prefixing/postfixing to embedded newlines in a string, * bracketing with error prefix/postfix * * @param s the string to prefix * @return the pre/post-fixed and bracketed string */ String prefixError(String s) { return prefix(s, feedback.getErrorPre(), feedback.getErrorPost()); } /** * Add prefixing/postfixing to embedded newlines in a string, * bracketing with prefix/postfix * * @param s the string to prefix * @param pre the string to prepend to each line * @param post the string to append to each line (replacing newline) * @return the pre/post-fixed and bracketed string */ String prefix(String s, String pre, String post) { if (s == null) { return ""; } String pp = s.replaceAll("\\R", post + pre); if (pp.endsWith(post + pre)) { // prevent an extra prefix char and blank line when the string // already terminates with newline pp = pp.substring(0, pp.length() - (post + pre).length()); } return pre + pp + post; } /** * Print using resource bundle look-up and adding prefix and postfix * * @param key the resource key */ void hardrb(String key) { hard(getResourceString(key)); } /** * Format using resource bundle look-up using MessageFormat * * @param key the resource key * @param args */ String messageFormat(String key, Object... args) { String rs = getResourceString(key); return MessageFormat.format(rs, args); } /** * Print using resource bundle look-up, MessageFormat, and add prefix and * postfix * * @param key the resource key * @param args */ @Override public void hardmsg(String key, Object... args) { hard(messageFormat(key, args)); } /** * Print error using resource bundle look-up, MessageFormat, and add prefix * and postfix * * @param key the resource key * @param args */ @Override public void errormsg(String key, Object... args) { if (isRunningInteractive()) { rawout(prefixError(messageFormat(key, args))); } else { startmsg(key, args); } } /** * Print command-line error using resource bundle look-up, MessageFormat * * @param key the resource key * @param args */ void startmsg(String key, Object... args) { cmderr.println(messageFormat(key, args)); } /** * Print (fluff) using resource bundle look-up, MessageFormat, and add * prefix and postfix * * @param key the resource key * @param args */ @Override public void fluffmsg(String key, Object... args) { if (showFluff()) { hardmsg(key, args); } } void hardPairs(Stream stream, Function a, Function b) { Map a2b = stream.collect(toMap(a, b, (m1, m2) -> m1, LinkedHashMap::new)); for (Entry e : a2b.entrySet()) { hard("%s", e.getKey()); rawout(prefix(e.getValue(), feedback.getPre() + "\t", feedback.getPost())); } } /** * Trim whitespace off end of string * * @param s * @return */ static String trimEnd(String s) { int last = s.length() - 1; int i = last; while (i >= 0 && Character.isWhitespace(s.charAt(i))) { --i; } if (i != last) { return s.substring(0, i + 1); } else { return s; } } /** * The entry point into the JShell tool. * * @param args the command-line arguments * @throws Exception catastrophic fatal exception */ public void start(String[] args) throws Exception { OptionParserCommandLine commandLineArgs = new OptionParserCommandLine(); options = commandLineArgs.parse(args); if (options == null) { // Abort return; } startup = commandLineArgs.startup(); // initialize editor settings configEditor(); // initialize JShell instance try { resetState(); } catch (IllegalStateException ex) { // Display just the cause (not a exception backtrace) cmderr.println(ex.getMessage()); //abort return; } // Read replay history from last jshell session into previous history replayableHistoryPrevious = ReplayableHistory.fromPrevious(prefs); // load snippet/command files given on command-line for (String loadFile : commandLineArgs.nonOptions()) { runFile(loadFile, "jshell"); } // if we survived that... if (regenerateOnDeath) { // initialize the predefined feedback modes initFeedback(commandLineArgs.feedbackMode()); } // check again, as feedback setting could have failed if (regenerateOnDeath) { // if we haven't died, and the feedback mode wants fluff, print welcome if (feedback.shouldDisplayCommandFluff()) { hardmsg("jshell.msg.welcome", version()); } // Be sure history is always saved so that user code isn't lost Thread shutdownHook = new Thread() { @Override public void run() { replayableHistory.storeHistory(prefs); } }; Runtime.getRuntime().addShutdownHook(shutdownHook); // execute from user input try (IOContext in = new ConsoleIOContext(this, cmdin, console)) { while (regenerateOnDeath) { if (!live) { resetState(); } run(in); } } finally { replayableHistory.storeHistory(prefs); closeState(); try { Runtime.getRuntime().removeShutdownHook(shutdownHook); } catch (Exception ex) { // ignore, this probably caused by VM aready being shutdown // and this is the last act anyhow } } } closeState(); } private EditorSetting configEditor() { // Read retained editor setting (if any) editor = EditorSetting.fromPrefs(prefs); if (editor != null) { return editor; } // Try getting editor setting from OS environment variables for (String envvar : EDITOR_ENV_VARS) { String v = envvars.get(envvar); if (v != null) { return editor = new EditorSetting(v.split("\\s+"), false); } } // Default to the built-in editor return editor = BUILT_IN_EDITOR; } private void printUsage() { cmdout.print(getResourceString("help.usage")); } private void printUsageX() { cmdout.print(getResourceString("help.usage.x")); } /** * Message handler to use during initial start-up. */ private class InitMessageHandler implements MessageHandler { @Override public void fluff(String format, Object... args) { //ignore } @Override public void fluffmsg(String messageKey, Object... args) { //ignore } @Override public void hard(String format, Object... args) { //ignore } @Override public void hardmsg(String messageKey, Object... args) { //ignore } @Override public void errormsg(String messageKey, Object... args) { startmsg(messageKey, args); } @Override public boolean showFluff() { return false; } } private void resetState() { closeState(); // Initialize tool id mapping mainNamespace = new NameSpace("main", ""); startNamespace = new NameSpace("start", "s"); errorNamespace = new NameSpace("error", "e"); mapSnippet = new LinkedHashMap<>(); currentNameSpace = startNamespace; // Reset the replayable history, saving the old for restore replayableHistoryPrevious = replayableHistory; replayableHistory = ReplayableHistory.emptyHistory(); JShell.Builder builder = JShell.builder() .in(userin) .out(userout) .err(usererr) .tempVariableNameGenerator(() -> "$" + currentNameSpace.tidNext()) .idGenerator((sn, i) -> (currentNameSpace == startNamespace || state.status(sn).isActive()) ? currentNameSpace.tid(sn) : errorNamespace.tid(sn)) .remoteVMOptions(options.remoteVmOptions()) .compilerOptions(options.compilerOptions()); if (executionControlSpec != null) { builder.executionEngine(executionControlSpec); } state = builder.build(); shutdownSubscription = state.onShutdown((JShell deadState) -> { if (deadState == state) { hardmsg("jshell.msg.terminated"); live = false; } }); analysis = state.sourceCodeAnalysis(); live = true; // Run the start-up script. // Avoid an infinite loop running start-up while running start-up. // This could, otherwise, occur when /env /reset or /reload commands are // in the start-up script. if (!isCurrentlyRunningStartup) { try { isCurrentlyRunningStartup = true; startUpRun(startup.toString()); } finally { isCurrentlyRunningStartup = false; } } // Record subsequent snippets in the main namespace. currentNameSpace = mainNamespace; } private boolean isRunningInteractive() { return currentNameSpace != null && currentNameSpace == mainNamespace; } //where -- one-time per run initialization of feedback modes private void initFeedback(String initMode) { // No fluff, no prefix, for init failures MessageHandler initmh = new InitMessageHandler(); // Execute the feedback initialization code in the resource file startUpRun(getResourceString("startup.feedback")); // These predefined modes are read-only feedback.markModesReadOnly(); // Restore user defined modes retained on previous run with /set mode -retain String encoded = prefs.get(MODE_KEY); if (encoded != null && !encoded.isEmpty()) { if (!feedback.restoreEncodedModes(initmh, encoded)) { // Catastrophic corruption -- remove the retained modes prefs.remove(MODE_KEY); } } if (initMode != null) { // The feedback mode to use was specified on the command line, use it if (!setFeedback(initmh, new ArgTokenizer("--feedback", initMode))) { regenerateOnDeath = false; } } else { String fb = prefs.get(FEEDBACK_KEY); if (fb != null) { // Restore the feedback mode to use that was retained // on a previous run with /set feedback -retain setFeedback(initmh, new ArgTokenizer("previous retain feedback", "-retain " + fb)); } } } //where private void startUpRun(String start) { try (IOContext suin = new ScannerIOContext(new StringReader(start))) { run(suin); } catch (Exception ex) { hardmsg("jshell.err.startup.unexpected.exception", ex); ex.printStackTrace(cmdout); } } private void closeState() { live = false; JShell oldState = state; if (oldState != null) { state = null; analysis = null; oldState.unsubscribe(shutdownSubscription); // No notification oldState.close(); } } /** * Main loop * @param in the line input/editing context */ private void run(IOContext in) { IOContext oldInput = input; input = in; try { String incomplete = ""; while (live) { String prompt; if (isRunningInteractive()) { prompt = testPrompt ? incomplete.isEmpty() ? "\u0005" //ENQ : "\u0006" //ACK : incomplete.isEmpty() ? feedback.getPrompt(currentNameSpace.tidNext()) : feedback.getContinuationPrompt(currentNameSpace.tidNext()) ; } else { prompt = ""; } String raw; try { raw = in.readLine(prompt, incomplete); } catch (InputInterruptedException ex) { //input interrupted - clearing current state incomplete = ""; continue; } if (raw == null) { //EOF if (in.interactiveOutput()) { // End after user ctrl-D regenerateOnDeath = false; } break; } String trimmed = trimEnd(raw); if (!trimmed.isEmpty() || !incomplete.isEmpty()) { String line = incomplete + trimmed; // No commands in the middle of unprocessed source if (incomplete.isEmpty() && line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*")) { processCommand(line.trim()); } else { incomplete = processSourceCatchingReset(line); } } } } catch (IOException ex) { errormsg("jshell.err.unexpected.exception", ex); } finally { input = oldInput; } } private void addToReplayHistory(String s) { if (isRunningInteractive()) { replayableHistory.add(s); } } private String processSourceCatchingReset(String src) { try { input.beforeUserCode(); return processSource(src); } catch (IllegalStateException ex) { hard("Resetting..."); live = false; // Make double sure return ""; } finally { input.afterUserCode(); } } /** * Process a command (as opposed to a snippet) -- things that start with * slash. * * @param input */ private void processCommand(String input) { if (input.startsWith("/-")) { try { //handle "/-[number]" cmdUseHistoryEntry(Integer.parseInt(input.substring(1))); return ; } catch (NumberFormatException ex) { //ignore } } String cmd; String arg; int idx = input.indexOf(' '); if (idx > 0) { arg = input.substring(idx + 1).trim(); cmd = input.substring(0, idx); } else { cmd = input; arg = ""; } // find the command as a "real command", not a pseudo-command or doc subject Command[] candidates = findCommand(cmd, c -> c.kind.isRealCommand); switch (candidates.length) { case 0: // not found, it is either a snippet command or an error if (ID.matcher(cmd.substring(1)).matches()) { // it is in the form of a snipppet id, see if it is a valid history reference rerunHistoryEntriesById(input); } else { errormsg("jshell.err.invalid.command", cmd); fluffmsg("jshell.msg.help.for.help"); } break; case 1: Command command = candidates[0]; // If comand was successful and is of a replayable kind, add it the replayable history if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) { addToReplayHistory((command.command + " " + arg).trim()); } break; default: // command if too short (ambigous), show the possibly matches errormsg("jshell.err.command.ambiguous", cmd, Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", "))); fluffmsg("jshell.msg.help.for.help"); break; } } private Command[] findCommand(String cmd, Predicate filter) { Command exact = commands.get(cmd); if (exact != null) return new Command[] {exact}; return commands.values() .stream() .filter(filter) .filter(command -> command.command.startsWith(cmd)) .toArray(Command[]::new); } static Path toPathResolvingUserHome(String pathString) { if (pathString.replace(File.separatorChar, '/').startsWith("~/")) return Paths.get(System.getProperty("user.home"), pathString.substring(2)); else return Paths.get(pathString); } static final class Command { public final String command; public final String helpKey; public final Function run; public final CompletionProvider completions; public final CommandKind kind; // NORMAL Commands public Command(String command, Function run, CompletionProvider completions) { this(command, run, completions, CommandKind.NORMAL); } // Special kinds of Commands public Command(String command, Function run, CompletionProvider completions, CommandKind kind) { this(command, "help." + command.substring(1), run, completions, kind); } // Documentation pseudo-commands public Command(String command, String helpKey, CommandKind kind) { this(command, helpKey, arg -> { throw new IllegalStateException(); }, EMPTY_COMPLETION_PROVIDER, kind); } public Command(String command, String helpKey, Function run, CompletionProvider completions, CommandKind kind) { this.command = command; this.helpKey = helpKey; this.run = run; this.completions = completions; this.kind = kind; } } interface CompletionProvider { List completionSuggestions(String input, int cursor, int[] anchor); } enum CommandKind { NORMAL(true, true, true), REPLAY(true, true, true), HIDDEN(true, false, false), HELP_ONLY(false, true, false), HELP_SUBJECT(false, false, false); final boolean isRealCommand; final boolean showInHelp; final boolean shouldSuggestCompletions; private CommandKind(boolean isRealCommand, boolean showInHelp, boolean shouldSuggestCompletions) { this.isRealCommand = isRealCommand; this.showInHelp = showInHelp; this.shouldSuggestCompletions = shouldSuggestCompletions; } } static final class FixedCompletionProvider implements CompletionProvider { private final String[] alternatives; public FixedCompletionProvider(String... alternatives) { this.alternatives = alternatives; } // Add more options to an existing provider public FixedCompletionProvider(FixedCompletionProvider base, String... alternatives) { List l = new ArrayList<>(Arrays.asList(base.alternatives)); l.addAll(Arrays.asList(alternatives)); this.alternatives = l.toArray(new String[l.size()]); } @Override public List completionSuggestions(String input, int cursor, int[] anchor) { List result = new ArrayList<>(); for (String alternative : alternatives) { if (alternative.startsWith(input)) { result.add(new ArgSuggestion(alternative)); } } anchor[0] = 0; return result; } } static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider(); private static final CompletionProvider SNIPPET_HISTORY_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all", "-start ", "-history"); private static final CompletionProvider SAVE_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all ", "-start ", "-history "); private static final CompletionProvider SNIPPET_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all", "-start " ); private static final FixedCompletionProvider COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider( "-class-path ", "-module-path ", "-add-modules ", "-add-exports "); private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider( COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER, "-restore ", "-quiet "); private static final CompletionProvider SET_MODE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-command", "-quiet", "-delete"); private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true); private static final Map ARG_OPTIONS = new HashMap<>(); static { ARG_OPTIONS.put("-class-path", classPathCompletion()); ARG_OPTIONS.put("-module-path", fileCompletions(Files::isDirectory)); ARG_OPTIONS.put("-add-modules", EMPTY_COMPLETION_PROVIDER); ARG_OPTIONS.put("-add-exports", EMPTY_COMPLETION_PROVIDER); } private final Map commands = new LinkedHashMap<>(); private void registerCommand(Command cmd) { commands.put(cmd.command, cmd); } private static CompletionProvider skipWordThenCompletion(CompletionProvider completionProvider) { return (input, cursor, anchor) -> { List result = Collections.emptyList(); int space = input.indexOf(' '); if (space != -1) { String rest = input.substring(space + 1); result = completionProvider.completionSuggestions(rest, cursor - space - 1, anchor); anchor[0] += space + 1; } return result; }; } private static CompletionProvider fileCompletions(Predicate accept) { return (code, cursor, anchor) -> { int lastSlash = code.lastIndexOf('/'); String path = code.substring(0, lastSlash + 1); String prefix = lastSlash != (-1) ? code.substring(lastSlash + 1) : code; Path current = toPathResolvingUserHome(path); List result = new ArrayList<>(); try (Stream dir = Files.list(current)) { dir.filter(f -> accept.test(f) && f.getFileName().toString().startsWith(prefix)) .map(f -> new ArgSuggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : ""))) .forEach(result::add); } catch (IOException ex) { //ignore... } if (path.isEmpty()) { StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false) .filter(root -> Files.exists(root)) .filter(root -> accept.test(root) && root.toString().startsWith(prefix)) .map(root -> new ArgSuggestion(root.toString())) .forEach(result::add); } anchor[0] = path.length(); return result; }; } private static CompletionProvider classPathCompletion() { return fileCompletions(p -> Files.isDirectory(p) || p.getFileName().toString().endsWith(".zip") || p.getFileName().toString().endsWith(".jar")); } // Completion based on snippet supplier private CompletionProvider snippetCompletion(Supplier> snippetsSupplier) { return (prefix, cursor, anchor) -> { anchor[0] = 0; int space = prefix.lastIndexOf(' '); Set prior = new HashSet<>(Arrays.asList(prefix.split(" "))); if (prior.contains("-all") || prior.contains("-history")) { return Collections.emptyList(); } String argPrefix = prefix.substring(space + 1); return snippetsSupplier.get() .filter(k -> !prior.contains(String.valueOf(k.id())) && (!(k instanceof DeclarationSnippet) || !prior.contains(((DeclarationSnippet) k).name()))) .flatMap(k -> (k instanceof DeclarationSnippet) ? Stream.of(String.valueOf(k.id()) + " ", ((DeclarationSnippet) k).name() + " ") : Stream.of(String.valueOf(k.id()) + " ")) .filter(k -> k.startsWith(argPrefix)) .map(ArgSuggestion::new) .collect(Collectors.toList()); }; } // Completion based on snippet supplier with -all -start (and sometimes -history) options private CompletionProvider snippetWithOptionCompletion(CompletionProvider optionProvider, Supplier> snippetsSupplier) { return (code, cursor, anchor) -> { List result = new ArrayList<>(); int pastSpace = code.lastIndexOf(' ') + 1; // zero if no space if (pastSpace == 0) { result.addAll(optionProvider.completionSuggestions(code, cursor, anchor)); } result.addAll(snippetCompletion(snippetsSupplier).completionSuggestions(code, cursor, anchor)); anchor[0] += pastSpace; return result; }; } // Completion of help, commands and subjects private CompletionProvider helpCompletion() { return (code, cursor, anchor) -> { List result; int pastSpace = code.indexOf(' ') + 1; // zero if no space if (pastSpace == 0) { // initially suggest commands (with slash) and subjects, // however, if their subject starts without slash, include // commands without slash boolean noslash = code.length() > 0 && !code.startsWith("/"); result = new FixedCompletionProvider(commands.values().stream() .filter(cmd -> cmd.kind.showInHelp || cmd.kind == CommandKind.HELP_SUBJECT) .map(c -> ((noslash && c.command.startsWith("/")) ? c.command.substring(1) : c.command) + " ") .toArray(String[]::new)) .completionSuggestions(code, cursor, anchor); } else if (code.startsWith("/se") || code.startsWith("se")) { result = new FixedCompletionProvider(SET_SUBCOMMANDS) .completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor); } else { result = Collections.emptyList(); } anchor[0] += pastSpace; return result; }; } private static CompletionProvider saveCompletion() { return (code, cursor, anchor) -> { List result = new ArrayList<>(); int space = code.indexOf(' '); if (space == (-1)) { result.addAll(SAVE_OPTION_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor)); } result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor)); anchor[0] += space + 1; return result; }; } // command-line-like option completion -- options with values private static CompletionProvider optionCompletion(CompletionProvider provider) { return (code, cursor, anchor) -> { Matcher ovm = OPTION_VALUE_PATTERN.matcher(code); if (ovm.matches()) { String flag = ovm.group("flag"); List ps = ARG_OPTIONS.entrySet().stream() .filter(es -> es.getKey().startsWith(flag)) .map(es -> es.getValue()) .collect(toList()); if (ps.size() == 1) { int pastSpace = ovm.start("val"); List result = ps.get(0).completionSuggestions( ovm.group("val"), cursor - pastSpace, anchor); anchor[0] += pastSpace; return result; } } Matcher om = OPTION_PATTERN.matcher(code); if (om.matches()) { int pastSpace = om.start("flag"); List result = provider.completionSuggestions( om.group("flag"), cursor - pastSpace, anchor); if (!om.group("dd").isEmpty()) { result = result.stream() .map(sug -> new Suggestion() { @Override public String continuation() { return "-" + sug.continuation(); } @Override public boolean matchesType() { return false; } }) .collect(toList()); --pastSpace; } anchor[0] += pastSpace; return result; } Matcher opp = OPTION_PRE_PATTERN.matcher(code); if (opp.matches()) { int pastSpace = opp.end(); List result = provider.completionSuggestions( "", cursor - pastSpace, anchor); anchor[0] += pastSpace; return result; } return Collections.emptyList(); }; } // /reload command completion private static CompletionProvider reloadCompletion() { return optionCompletion(RELOAD_OPTIONS_COMPLETION_PROVIDER); } // /env command completion private static CompletionProvider envCompletion() { return optionCompletion(COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER); } private static CompletionProvider orMostSpecificCompletion( CompletionProvider left, CompletionProvider right) { return (code, cursor, anchor) -> { int[] leftAnchor = {-1}; int[] rightAnchor = {-1}; List leftSuggestions = left.completionSuggestions(code, cursor, leftAnchor); List rightSuggestions = right.completionSuggestions(code, cursor, rightAnchor); List suggestions = new ArrayList<>(); if (leftAnchor[0] >= rightAnchor[0]) { anchor[0] = leftAnchor[0]; suggestions.addAll(leftSuggestions); } if (leftAnchor[0] <= rightAnchor[0]) { anchor[0] = rightAnchor[0]; suggestions.addAll(rightSuggestions); } return suggestions; }; } // Snippet lists Stream allSnippets() { return state.snippets(); } Stream dropableSnippets() { return state.snippets() .filter(sn -> state.status(sn).isActive()); } Stream allVarSnippets() { return state.snippets() .filter(sn -> sn.kind() == Snippet.Kind.VAR) .map(sn -> (VarSnippet) sn); } Stream allMethodSnippets() { return state.snippets() .filter(sn -> sn.kind() == Snippet.Kind.METHOD) .map(sn -> (MethodSnippet) sn); } Stream allTypeSnippets() { return state.snippets() .filter(sn -> sn.kind() == Snippet.Kind.TYPE_DECL) .map(sn -> (TypeDeclSnippet) sn); } // Table of commands -- with command forms, argument kinds, helpKey message, implementation, ... { registerCommand(new Command("/list", this::cmdList, snippetWithOptionCompletion(SNIPPET_HISTORY_OPTION_COMPLETION_PROVIDER, this::allSnippets))); registerCommand(new Command("/edit", this::cmdEdit, snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER, this::allSnippets))); registerCommand(new Command("/drop", this::cmdDrop, snippetCompletion(this::dropableSnippets), CommandKind.REPLAY)); registerCommand(new Command("/save", this::cmdSave, saveCompletion())); registerCommand(new Command("/open", this::cmdOpen, FILE_COMPLETION_PROVIDER)); registerCommand(new Command("/vars", this::cmdVars, snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER, this::allVarSnippets))); registerCommand(new Command("/methods", this::cmdMethods, snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER, this::allMethodSnippets))); registerCommand(new Command("/types", this::cmdTypes, snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER, this::allTypeSnippets))); registerCommand(new Command("/imports", arg -> cmdImports(), EMPTY_COMPLETION_PROVIDER)); registerCommand(new Command("/exit", arg -> cmdExit(), EMPTY_COMPLETION_PROVIDER)); registerCommand(new Command("/env", arg -> cmdEnv(arg), envCompletion())); registerCommand(new Command("/reset", arg -> cmdReset(arg), envCompletion())); registerCommand(new Command("/reload", this::cmdReload, reloadCompletion())); registerCommand(new Command("/history", arg -> cmdHistory(), EMPTY_COMPLETION_PROVIDER)); registerCommand(new Command("/debug", this::cmdDebug, EMPTY_COMPLETION_PROVIDER, CommandKind.HIDDEN)); registerCommand(new Command("/help", this::cmdHelp, helpCompletion())); registerCommand(new Command("/set", this::cmdSet, new ContinuousCompletionProvider(Map.of( // need more completion for format for usability "format", feedback.modeCompletions(), "truncation", feedback.modeCompletions(), "feedback", feedback.modeCompletions(), "mode", skipWordThenCompletion(orMostSpecificCompletion( feedback.modeCompletions(SET_MODE_OPTIONS_COMPLETION_PROVIDER), SET_MODE_OPTIONS_COMPLETION_PROVIDER)), "prompt", feedback.modeCompletions(), "editor", fileCompletions(Files::isExecutable), "start", FILE_COMPLETION_PROVIDER), STARTSWITH_MATCHER))); registerCommand(new Command("/?", "help.quest", this::cmdHelp, helpCompletion(), CommandKind.NORMAL)); registerCommand(new Command("/!", "help.bang", arg -> cmdUseHistoryEntry(-1), EMPTY_COMPLETION_PROVIDER, CommandKind.NORMAL)); // Documentation pseudo-commands registerCommand(new Command("/", "help.id", arg -> cmdHelp("rerun"), EMPTY_COMPLETION_PROVIDER, CommandKind.HELP_ONLY)); registerCommand(new Command("/-", "help.previous", arg -> cmdHelp("rerun"), EMPTY_COMPLETION_PROVIDER, CommandKind.HELP_ONLY)); registerCommand(new Command("intro", "help.intro", CommandKind.HELP_SUBJECT)); registerCommand(new Command("shortcuts", "help.shortcuts", CommandKind.HELP_SUBJECT)); registerCommand(new Command("context", "help.context", CommandKind.HELP_SUBJECT)); registerCommand(new Command("rerun", "help.rerun", CommandKind.HELP_SUBJECT)); commandCompletions = new ContinuousCompletionProvider( commands.values().stream() .filter(c -> c.kind.shouldSuggestCompletions) .collect(toMap(c -> c.command, c -> c.completions)), STARTSWITH_MATCHER); } private ContinuousCompletionProvider commandCompletions; public List commandCompletionSuggestions(String code, int cursor, int[] anchor) { return commandCompletions.completionSuggestions(code, cursor, anchor); } public List commandDocumentation(String code, int cursor, boolean shortDescription) { code = code.substring(0, cursor); int space = code.indexOf(' '); String prefix = space != (-1) ? code.substring(0, space) : code; List result = new ArrayList<>(); List> toShow = commands.entrySet() .stream() .filter(e -> e.getKey().startsWith(prefix)) .filter(e -> e.getValue().kind.showInHelp) .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) .collect(Collectors.toList()); if (toShow.size() == 1) { result.add(getResourceString(toShow.get(0).getValue().helpKey + (shortDescription ? ".summary" : ""))); } else { for (Entry e : toShow) { result.add(e.getKey() + "\n" +getResourceString(e.getValue().helpKey + (shortDescription ? ".summary" : ""))); } } return result; } // Attempt to stop currently running evaluation void stop() { state.stop(); } // --- Command implementations --- private static final String[] SET_SUBCOMMANDS = new String[]{ "format", "truncation", "feedback", "mode", "prompt", "editor", "start"}; final boolean cmdSet(String arg) { String cmd = "/set"; ArgTokenizer at = new ArgTokenizer(cmd, arg.trim()); String which = subCommand(cmd, at, SET_SUBCOMMANDS); if (which == null) { return false; } switch (which) { case "_retain": { errormsg("jshell.err.setting.to.retain.must.be.specified", at.whole()); return false; } case "_blank": { // show top-level settings new SetEditor().set(); showSetStart(); setFeedback(this, at); // no args so shows feedback setting hardmsg("jshell.msg.set.show.mode.settings"); return true; } case "format": return feedback.setFormat(this, at); case "truncation": return feedback.setTruncation(this, at); case "feedback": return setFeedback(this, at); case "mode": return feedback.setMode(this, at, retained -> prefs.put(MODE_KEY, retained)); case "prompt": return feedback.setPrompt(this, at); case "editor": return new SetEditor(at).set(); case "start": return setStart(at); default: errormsg("jshell.err.arg", cmd, at.val()); return false; } } boolean setFeedback(MessageHandler messageHandler, ArgTokenizer at) { return feedback.setFeedback(messageHandler, at, fb -> prefs.put(FEEDBACK_KEY, fb)); } // Find which, if any, sub-command matches. // Return null on error String subCommand(String cmd, ArgTokenizer at, String[] subs) { at.allowedOptions("-retain"); String sub = at.next(); if (sub == null) { // No sub-command was given return at.hasOption("-retain") ? "_retain" : "_blank"; } String[] matches = Arrays.stream(subs) .filter(s -> s.startsWith(sub)) .toArray(String[]::new); if (matches.length == 0) { // There are no matching sub-commands errormsg("jshell.err.arg", cmd, sub); fluffmsg("jshell.msg.use.one.of", Arrays.stream(subs) .collect(Collectors.joining(", ")) ); return null; } if (matches.length > 1) { // More than one sub-command matches the initial characters provided errormsg("jshell.err.sub.ambiguous", cmd, sub); fluffmsg("jshell.msg.use.one.of", Arrays.stream(matches) .collect(Collectors.joining(", ")) ); return null; } return matches[0]; } static class EditorSetting { static String BUILT_IN_REP = "-default"; static char WAIT_PREFIX = '-'; static char NORMAL_PREFIX = '*'; final String[] cmd; final boolean wait; EditorSetting(String[] cmd, boolean wait) { this.wait = wait; this.cmd = cmd; } // returns null if not stored in preferences static EditorSetting fromPrefs(PersistentStorage prefs) { // Read retained editor setting (if any) String editorString = prefs.get(EDITOR_KEY); if (editorString == null || editorString.isEmpty()) { return null; } else if (editorString.equals(BUILT_IN_REP)) { return BUILT_IN_EDITOR; } else { boolean wait = false; char waitMarker = editorString.charAt(0); if (waitMarker == WAIT_PREFIX || waitMarker == NORMAL_PREFIX) { wait = waitMarker == WAIT_PREFIX; editorString = editorString.substring(1); } String[] cmd = editorString.split(RECORD_SEPARATOR); return new EditorSetting(cmd, wait); } } static void removePrefs(PersistentStorage prefs) { prefs.remove(EDITOR_KEY); } void toPrefs(PersistentStorage prefs) { prefs.put(EDITOR_KEY, (this == BUILT_IN_EDITOR) ? BUILT_IN_REP : (wait ? WAIT_PREFIX : NORMAL_PREFIX) + String.join(RECORD_SEPARATOR, cmd)); } @Override public boolean equals(Object o) { if (o instanceof EditorSetting) { EditorSetting ed = (EditorSetting) o; return Arrays.equals(cmd, ed.cmd) && wait == ed.wait; } else { return false; } } @Override public int hashCode() { int hash = 7; hash = 71 * hash + Arrays.deepHashCode(this.cmd); hash = 71 * hash + (this.wait ? 1 : 0); return hash; } } class SetEditor { private final ArgTokenizer at; private final String[] command; private final boolean hasCommand; private final boolean defaultOption; private final boolean deleteOption; private final boolean waitOption; private final boolean retainOption; private final int primaryOptionCount; SetEditor(ArgTokenizer at) { at.allowedOptions("-default", "-wait", "-retain", "-delete"); String prog = at.next(); List ed = new ArrayList<>(); while (at.val() != null) { ed.add(at.val()); at.nextToken(); // so that options are not interpreted as jshell options } this.at = at; this.command = ed.toArray(new String[ed.size()]); this.hasCommand = command.length > 0; this.defaultOption = at.hasOption("-default"); this.deleteOption = at.hasOption("-delete"); this.waitOption = at.hasOption("-wait"); this.retainOption = at.hasOption("-retain"); this.primaryOptionCount = (hasCommand? 1 : 0) + (defaultOption? 1 : 0) + (deleteOption? 1 : 0); } SetEditor() { this(new ArgTokenizer("", "")); } boolean set() { if (!check()) { return false; } if (primaryOptionCount == 0 && !retainOption) { // No settings or -retain, so this is a query EditorSetting retained = EditorSetting.fromPrefs(prefs); if (retained != null) { // retained editor is set hard("/set editor -retain %s", format(retained)); } if (retained == null || !retained.equals(editor)) { // editor is not retained or retained is different from set hard("/set editor %s", format(editor)); } return true; } if (retainOption && deleteOption) { EditorSetting.removePrefs(prefs); } install(); if (retainOption && !deleteOption) { editor.toPrefs(prefs); fluffmsg("jshell.msg.set.editor.retain", format(editor)); } return true; } private boolean check() { if (!checkOptionsAndRemainingInput(at)) { return false; } if (primaryOptionCount > 1) { errormsg("jshell.err.default.option.or.program", at.whole()); return false; } if (waitOption && !hasCommand) { errormsg("jshell.err.wait.applies.to.external.editor", at.whole()); return false; } return true; } private void install() { if (hasCommand) { editor = new EditorSetting(command, waitOption); } else if (defaultOption) { editor = BUILT_IN_EDITOR; } else if (deleteOption) { configEditor(); } else { return; } fluffmsg("jshell.msg.set.editor.set", format(editor)); } private String format(EditorSetting ed) { if (ed == BUILT_IN_EDITOR) { return "-default"; } else { Stream elems = Arrays.stream(ed.cmd); if (ed.wait) { elems = Stream.concat(Stream.of("-wait"), elems); } return elems.collect(joining(" ")); } } } // The sub-command: /set start boolean setStart(ArgTokenizer at) { at.allowedOptions("-default", "-none", "-retain"); List fns = new ArrayList<>(); while (at.next() != null) { fns.add(at.val()); } if (!checkOptionsAndRemainingInput(at)) { return false; } boolean defaultOption = at.hasOption("-default"); boolean noneOption = at.hasOption("-none"); boolean retainOption = at.hasOption("-retain"); boolean hasFile = !fns.isEmpty(); int argCount = (defaultOption ? 1 : 0) + (noneOption ? 1 : 0) + (hasFile ? 1 : 0); if (argCount > 1) { errormsg("jshell.err.option.or.filename", at.whole()); return false; } if (argCount == 0 && !retainOption) { // no options or filename, show current setting showSetStart(); return true; } if (hasFile) { startup = Startup.fromFileList(fns, "/set start", this); if (startup == null) { return false; } } else if (defaultOption) { startup = Startup.defaultStartup(this); } else if (noneOption) { startup = Startup.noStartup(); } if (retainOption) { // retain startup setting prefs.put(STARTUP_KEY, startup.storedForm()); } return true; } // show the "/set start" settings (retained and, if different, current) // as commands (and file contents). All commands first, then contents. void showSetStart() { StringBuilder sb = new StringBuilder(); String retained = prefs.get(STARTUP_KEY); if (retained != null) { Startup retainedStart = Startup.unpack(retained, this); boolean currentDifferent = !startup.equals(retainedStart); sb.append(retainedStart.show(true)); if (currentDifferent) { sb.append(startup.show(false)); } sb.append(retainedStart.showDetail()); if (currentDifferent) { sb.append(startup.showDetail()); } } else { sb.append(startup.show(false)); sb.append(startup.showDetail()); } hard(sb.toString()); } boolean cmdDebug(String arg) { if (arg.isEmpty()) { debug = !debug; InternalDebugControl.setDebugFlags(state, debug ? DBG_GEN : 0); fluff("Debugging %s", debug ? "on" : "off"); } else { int flags = 0; for (char ch : arg.toCharArray()) { switch (ch) { case '0': flags = 0; debug = false; fluff("Debugging off"); break; case 'r': debug = true; fluff("REPL tool debugging on"); break; case 'g': flags |= DBG_GEN; fluff("General debugging on"); break; case 'f': flags |= DBG_FMGR; fluff("File manager debugging on"); break; case 'c': flags |= DBG_COMPA; fluff("Completion analysis debugging on"); break; case 'd': flags |= DBG_DEP; fluff("Dependency debugging on"); break; case 'e': flags |= DBG_EVNT; fluff("Event debugging on"); break; case 'w': flags |= DBG_WRAP; fluff("Wrap debugging on"); break; default: hard("Unknown debugging option: %c", ch); fluff("Use: 0 r g f c d e w"); return false; } } InternalDebugControl.setDebugFlags(state, flags); } return true; } private boolean cmdExit() { regenerateOnDeath = false; live = false; fluffmsg("jshell.msg.goodbye"); return true; } boolean cmdHelp(String arg) { ArgTokenizer at = new ArgTokenizer("/help", arg); String subject = at.next(); if (subject != null) { // check if the requested subject is a help subject or // a command, with or without slash Command[] matches = commands.values().stream() .filter(c -> c.command.startsWith(subject) || c.command.substring(1).startsWith(subject)) .toArray(Command[]::new); if (matches.length == 1) { String cmd = matches[0].command; if (cmd.equals("/set")) { // Print the help doc for the specified sub-command String which = subCommand(cmd, at, SET_SUBCOMMANDS); if (which == null) { return false; } if (!which.equals("_blank")) { hardrb("help.set." + which); return true; } } } if (matches.length > 0) { for (Command c : matches) { hard(""); hard("%s", c.command); hard(""); hardrb(c.helpKey); } return true; } else { // failing everything else, check if this is the start of // a /set sub-command name String[] subs = Arrays.stream(SET_SUBCOMMANDS) .filter(s -> s.startsWith(subject)) .toArray(String[]::new); if (subs.length > 0) { for (String sub : subs) { hardrb("help.set." + sub); hard(""); } return true; } errormsg("jshell.err.help.arg", arg); } } hardmsg("jshell.msg.help.begin"); hardPairs(commands.values().stream() .filter(cmd -> cmd.kind.showInHelp), cmd -> cmd.command + " " + getResourceString(cmd.helpKey + ".args"), cmd -> getResourceString(cmd.helpKey + ".summary") ); hardmsg("jshell.msg.help.subject"); hardPairs(commands.values().stream() .filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT), cmd -> cmd.command, cmd -> getResourceString(cmd.helpKey + ".summary") ); return true; } private boolean cmdHistory() { cmdout.println(); for (String s : input.currentSessionHistory()) { // No number prefix, confusing with snippet ids cmdout.printf("%s\n", s); } return true; } /** * Avoid parameterized varargs possible heap pollution warning. */ private interface SnippetPredicate extends Predicate { } /** * Apply filters to a stream until one that is non-empty is found. * Adapted from Stuart Marks * * @param supplier Supply the Snippet stream to filter * @param filters Filters to attempt * @return The non-empty filtered Stream, or null */ @SafeVarargs private static Stream nonEmptyStream(Supplier> supplier, SnippetPredicate... filters) { for (SnippetPredicate filt : filters) { Iterator iterator = supplier.get().filter(filt).iterator(); if (iterator.hasNext()) { return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); } } return null; } private boolean inStartUp(Snippet sn) { return mapSnippet.get(sn).space == startNamespace; } private boolean isActive(Snippet sn) { return state.status(sn).isActive(); } private boolean mainActive(Snippet sn) { return !inStartUp(sn) && isActive(sn); } private boolean matchingDeclaration(Snippet sn, String name) { return sn instanceof DeclarationSnippet && ((DeclarationSnippet) sn).name().equals(name); } /** * Convert user arguments to a Stream of snippets referenced by those * arguments (or lack of arguments). * * @param snippets the base list of possible snippets * @param defFilter the filter to apply to the arguments if no argument * @param rawargs the user's argument to the command, maybe be the empty * string * @return a Stream of referenced snippets or null if no matches are found */ private Stream argsOptionsToSnippets(Supplier> snippetSupplier, Predicate defFilter, String rawargs, String cmd) { ArgTokenizer at = new ArgTokenizer(cmd, rawargs.trim()); at.allowedOptions("-all", "-start"); return argsOptionsToSnippets(snippetSupplier, defFilter, at); } /** * Convert user arguments to a Stream of snippets referenced by those * arguments (or lack of arguments). * * @param snippets the base list of possible snippets * @param defFilter the filter to apply to the arguments if no argument * @param at the ArgTokenizer, with allowed options set * @return */ private Stream argsOptionsToSnippets(Supplier> snippetSupplier, Predicate defFilter, ArgTokenizer at) { List args = new ArrayList<>(); String s; while ((s = at.next()) != null) { args.add(s); } if (!checkOptionsAndRemainingInput(at)) { return null; } if (at.optionCount() > 0 && args.size() > 0) { errormsg("jshell.err.may.not.specify.options.and.snippets", at.whole()); return null; } if (at.optionCount() > 1) { errormsg("jshell.err.conflicting.options", at.whole()); return null; } if (at.isAllowedOption("-all") && at.hasOption("-all")) { // all snippets including start-up, failed, and overwritten return snippetSupplier.get(); } if (at.isAllowedOption("-start") && at.hasOption("-start")) { // start-up snippets return snippetSupplier.get() .filter(this::inStartUp); } if (args.isEmpty()) { // Default is all active user snippets return snippetSupplier.get() .filter(defFilter); } return new ArgToSnippets<>(snippetSupplier).argsToSnippets(args); } /** * Support for converting arguments that are definition names, snippet ids, * or snippet id ranges into a stream of snippets, * * @param the snipper subtype */ private class ArgToSnippets { // the supplier of snippet streams final Supplier> snippetSupplier; // these two are parallel, and lazily filled if a range is encountered List allSnippets; String[] allIds = null; /** * * @param snippetSupplier the base list of possible snippets */ ArgToSnippets(Supplier> snippetSupplier) { this.snippetSupplier = snippetSupplier; } /** * Convert user arguments to a Stream of snippets referenced by those * arguments. * * @param args the user's argument to the command, maybe be the empty * list * @return a Stream of referenced snippets or null if no matches to * specific arg */ Stream argsToSnippets(List args) { Stream result = null; for (String arg : args) { // Find the best match Stream st = argToSnippets(arg); if (st == null) { return null; } else { result = (result == null) ? st : Stream.concat(result, st); } } return result; } /** * Convert a user argument to a Stream of snippets referenced by the * argument. * * @param snippetSupplier the base list of possible snippets * @param arg the user's argument to the command * @return a Stream of referenced snippets or null if no matches to * specific arg */ Stream argToSnippets(String arg) { if (arg.contains("-")) { return range(arg); } // Find the best match Stream st = layeredSnippetSearch(snippetSupplier, arg); if (st == null) { badSnippetErrormsg(arg); return null; } else { return st; } } /** * Look for inappropriate snippets to give best error message * * @param arg the bad snippet arg * @param errKey the not found error key */ void badSnippetErrormsg(String arg) { Stream est = layeredSnippetSearch(state::snippets, arg); if (est == null) { if (ID.matcher(arg).matches()) { errormsg("jshell.err.no.snippet.with.id", arg); } else { errormsg("jshell.err.no.such.snippets", arg); } } else { errormsg("jshell.err.the.snippet.cannot.be.used.with.this.command", arg, est.findFirst().get().source()); } } /** * Search through the snippets for the best match to the id/name. * * @param the snippet type * @param aSnippetSupplier the supplier of snippet streams * @param arg the arg to match * @return a Stream of referenced snippets or null if no matches to * specific arg */ Stream layeredSnippetSearch(Supplier> aSnippetSupplier, String arg) { return nonEmptyStream( // the stream supplier aSnippetSupplier, // look for active user declarations matching the name sn -> isActive(sn) && matchingDeclaration(sn, arg), // else, look for any declarations matching the name sn -> matchingDeclaration(sn, arg), // else, look for an id of this name sn -> sn.id().equals(arg) ); } /** * Given an id1-id2 range specifier, return a stream of snippets within * our context * * @param arg the range arg * @return a Stream of referenced snippets or null if no matches to * specific arg */ Stream range(String arg) { int dash = arg.indexOf('-'); String iid = arg.substring(0, dash); String tid = arg.substring(dash + 1); int iidx = snippetIndex(iid); if (iidx < 0) { return null; } int tidx = snippetIndex(tid); if (tidx < 0) { return null; } if (tidx < iidx) { errormsg("jshell.err.end.snippet.range.less.than.start", iid, tid); return null; } return allSnippets.subList(iidx, tidx+1).stream(); } /** * Lazily initialize the id mapping -- needed only for id ranges. */ void initIdMapping() { if (allIds == null) { allSnippets = snippetSupplier.get() .sorted((a, b) -> order(a) - order(b)) .collect(toList()); allIds = allSnippets.stream() .map(sn -> sn.id()) .toArray(n -> new String[n]); } } /** * Return all the snippet ids -- within the context, and in order. * * @return the snippet ids */ String[] allIds() { initIdMapping(); return allIds; } /** * Establish an order on snippet ids. All startup snippets are first, * all error snippets are last -- within that is by snippet number. * * @param id the id string * @return an ordering int */ int order(String id) { try { switch (id.charAt(0)) { case 's': return Integer.parseInt(id.substring(1)); case 'e': return 0x40000000 + Integer.parseInt(id.substring(1)); default: return 0x20000000 + Integer.parseInt(id); } } catch (Exception ex) { return 0x60000000; } } /** * Establish an order on snippets, based on its snippet id. All startup * snippets are first, all error snippets are last -- within that is by * snippet number. * * @param sn the id string * @return an ordering int */ int order(Snippet sn) { return order(sn.id()); } /** * Find the index into the parallel allSnippets and allIds structures. * * @param s the snippet id name * @return the index, or, if not found, report the error and return a * negative number */ int snippetIndex(String s) { int idx = Arrays.binarySearch(allIds(), 0, allIds().length, s, (a, b) -> order(a) - order(b)); if (idx < 0) { // the id is not in the snippet domain, find the right error to report if (!ID.matcher(s).matches()) { errormsg("jshell.err.range.requires.id", s); } else { badSnippetErrormsg(s); } } return idx; } } private boolean cmdDrop(String rawargs) { ArgTokenizer at = new ArgTokenizer("/drop", rawargs.trim()); at.allowedOptions(); List args = new ArrayList<>(); String s; while ((s = at.next()) != null) { args.add(s); } if (!checkOptionsAndRemainingInput(at)) { return false; } if (args.isEmpty()) { errormsg("jshell.err.drop.arg"); return false; } Stream stream = new ArgToSnippets<>(this::dropableSnippets).argsToSnippets(args); if (stream == null) { // Snippet not found. Error already printed fluffmsg("jshell.msg.see.classes.etc"); return false; } stream.forEach(sn -> state.drop(sn).forEach(this::handleEvent)); return true; } private boolean cmdEdit(String arg) { Stream stream = argsOptionsToSnippets(state::snippets, this::mainActive, arg, "/edit"); if (stream == null) { return false; } Set srcSet = new LinkedHashSet<>(); stream.forEachOrdered(sn -> { String src = sn.source(); switch (sn.subKind()) { case VAR_VALUE_SUBKIND: break; case ASSIGNMENT_SUBKIND: case OTHER_EXPRESSION_SUBKIND: case TEMP_VAR_EXPRESSION_SUBKIND: case UNKNOWN_SUBKIND: if (!src.endsWith(";")) { src = src + ";"; } srcSet.add(src); break; case STATEMENT_SUBKIND: if (src.endsWith("}")) { // Could end with block or, for example, new Foo() {...} // so, we need deeper analysis to know if it needs a semicolon src = analysis.analyzeCompletion(src).source(); } else if (!src.endsWith(";")) { src = src + ";"; } srcSet.add(src); break; default: srcSet.add(src); break; } }); StringBuilder sb = new StringBuilder(); for (String s : srcSet) { sb.append(s); sb.append('\n'); } String src = sb.toString(); Consumer saveHandler = new SaveHandler(src, srcSet); Consumer errorHandler = s -> hard("Edit Error: %s", s); if (editor == BUILT_IN_EDITOR) { return builtInEdit(src, saveHandler, errorHandler); } else { // Changes have occurred in temp edit directory, // transfer the new sources to JShell (unless the editor is // running directly in JShell's window -- don't make a mess) String[] buffer = new String[1]; Consumer extSaveHandler = s -> { if (input.terminalEditorRunning()) { buffer[0] = s; } else { saveHandler.accept(s); } }; ExternalEditor.edit(editor.cmd, src, errorHandler, extSaveHandler, () -> input.suspend(), () -> input.resume(), editor.wait, () -> hardrb("jshell.msg.press.return.to.leave.edit.mode")); if (buffer[0] != null) { saveHandler.accept(buffer[0]); } } return true; } //where // start the built-in editor private boolean builtInEdit(String initialText, Consumer saveHandler, Consumer errorHandler) { try { ServiceLoader sl = ServiceLoader.load(BuildInEditorProvider.class); // Find the highest ranking provider BuildInEditorProvider provider = null; for (BuildInEditorProvider p : sl) { if (provider == null || p.rank() > provider.rank()) { provider = p; } } if (provider != null) { provider.edit(getResourceString("jshell.label.editpad"), initialText, saveHandler, errorHandler); return true; } else { errormsg("jshell.err.no.builtin.editor"); } } catch (RuntimeException ex) { errormsg("jshell.err.cant.launch.editor", ex); } fluffmsg("jshell.msg.try.set.editor"); return false; } //where // receives editor requests to save private class SaveHandler implements Consumer { String src; Set currSrcs; SaveHandler(String src, Set ss) { this.src = src; this.currSrcs = ss; } @Override public void accept(String s) { if (!s.equals(src)) { // quick check first src = s; try { Set nextSrcs = new LinkedHashSet<>(); boolean failed = false; while (true) { CompletionInfo an = analysis.analyzeCompletion(s); if (!an.completeness().isComplete()) { break; } String tsrc = trimNewlines(an.source()); if (!failed && !currSrcs.contains(tsrc)) { failed = processCompleteSource(tsrc); } nextSrcs.add(tsrc); if (an.remaining().isEmpty()) { break; } s = an.remaining(); } currSrcs = nextSrcs; } catch (IllegalStateException ex) { hardmsg("jshell.msg.resetting"); resetState(); currSrcs = new LinkedHashSet<>(); // re-process everything } } } private String trimNewlines(String s) { int b = 0; while (b < s.length() && s.charAt(b) == '\n') { ++b; } int e = s.length() -1; while (e >= 0 && s.charAt(e) == '\n') { --e; } return s.substring(b, e + 1); } } private boolean cmdList(String arg) { if (arg.length() >= 2 && "-history".startsWith(arg)) { return cmdHistory(); } Stream stream = argsOptionsToSnippets(state::snippets, this::mainActive, arg, "/list"); if (stream == null) { return false; } // prevent double newline on empty list boolean[] hasOutput = new boolean[1]; stream.forEachOrdered(sn -> { if (!hasOutput[0]) { cmdout.println(); hasOutput[0] = true; } cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); }); return true; } private boolean cmdOpen(String filename) { return runFile(filename, "/open"); } private boolean runFile(String filename, String context) { if (!filename.isEmpty()) { try { Path path = toPathResolvingUserHome(filename); Reader reader; String resource; if (!Files.exists(path) && (resource = getResource(filename)) != null) { // Not found as file, but found as resource reader = new StringReader(resource); } else { reader = new FileReader(path.toString()); } run(new ScannerIOContext(reader)); return true; } catch (FileNotFoundException e) { errormsg("jshell.err.file.not.found", context, filename, e.getMessage()); } catch (Exception e) { errormsg("jshell.err.file.exception", context, filename, e); } } else { errormsg("jshell.err.file.filename", context); } return false; } static String getResource(String name) { if (BUILTIN_FILE_PATTERN.matcher(name).matches()) { try { return readResource(name); } catch (Throwable t) { // Fall-through to null } } return null; } // Read a built-in file from resources static String readResource(String name) throws IOException { // Attempt to find the file as a resource String spec = String.format(BUILTIN_FILE_PATH_FORMAT, name); try (InputStream in = JShellTool.class.getResourceAsStream(spec); BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { return reader.lines().collect(Collectors.joining("\n", "", "\n")); } } private boolean cmdReset(String rawargs) { Options oldOptions = rawargs.trim().isEmpty()? null : options; if (!parseCommandLineLikeFlags(rawargs, new OptionParserBase())) { return false; } live = false; fluffmsg("jshell.msg.resetting.state"); return doReload(null, false, oldOptions); } private boolean cmdReload(String rawargs) { Options oldOptions = rawargs.trim().isEmpty()? null : options; OptionParserReload ap = new OptionParserReload(); if (!parseCommandLineLikeFlags(rawargs, ap)) { return false; } ReplayableHistory history; if (ap.restore()) { if (replayableHistoryPrevious == null) { errormsg("jshell.err.reload.no.previous"); return false; } history = replayableHistoryPrevious; fluffmsg("jshell.err.reload.restarting.previous.state"); } else { history = replayableHistory; fluffmsg("jshell.err.reload.restarting.state"); } boolean success = doReload(history, !ap.quiet(), oldOptions); if (success && ap.restore()) { // if we are restoring from previous, then if nothing was added // before time of exit, there is nothing to save replayableHistory.markSaved(); } return success; } private boolean cmdEnv(String rawargs) { if (rawargs.trim().isEmpty()) { // No arguments, display current settings (as option flags) StringBuilder sb = new StringBuilder(); for (String a : options.commonOptions()) { sb.append( a.startsWith("-") ? sb.length() > 0 ? "\n " : " " : " "); sb.append(a); } if (sb.length() > 0) { rawout(prefix(sb.toString())); } return false; } Options oldOptions = options; if (!parseCommandLineLikeFlags(rawargs, new OptionParserBase())) { return false; } fluffmsg("jshell.msg.set.restore"); return doReload(replayableHistory, false, oldOptions); } private boolean doReload(ReplayableHistory history, boolean echo, Options oldOptions) { if (oldOptions != null) { try { resetState(); } catch (IllegalStateException ex) { currentNameSpace = mainNamespace; // back out of start-up (messages) errormsg("jshell.err.restart.failed", ex.getMessage()); // attempt recovery to previous option settings options = oldOptions; resetState(); } } else { resetState(); } if (history != null) { run(new ReloadIOContext(history.iterable(), echo ? cmdout : null)); } return true; } private boolean parseCommandLineLikeFlags(String rawargs, OptionParserBase ap) { String[] args = Arrays.stream(rawargs.split("\\s+")) .filter(s -> !s.isEmpty()) .toArray(String[]::new); Options opts = ap.parse(args); if (opts == null) { return false; } if (!ap.nonOptions().isEmpty()) { errormsg("jshell.err.unexpected.at.end", ap.nonOptions(), rawargs); return false; } options = options.override(opts); return true; } private boolean cmdSave(String rawargs) { // The filename to save to is the last argument, extract it String[] args = rawargs.split("\\s"); String filename = args[args.length - 1]; if (filename.isEmpty()) { errormsg("jshell.err.file.filename", "/save"); return false; } // All the non-filename arguments are the specifier of what to save String srcSpec = Arrays.stream(args, 0, args.length - 1) .collect(Collectors.joining("\n")); // From the what to save specifier, compute the snippets (as a stream) ArgTokenizer at = new ArgTokenizer("/save", srcSpec); at.allowedOptions("-all", "-start", "-history"); Stream snippetStream = argsOptionsToSnippets(state::snippets, this::mainActive, at); if (snippetStream == null) { // error occurred, already reported return false; } try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename), Charset.defaultCharset(), CREATE, TRUNCATE_EXISTING, WRITE)) { if (at.hasOption("-history")) { // they want history (commands and snippets), ignore the snippet stream for (String s : input.currentSessionHistory()) { writer.write(s); writer.write("\n"); } } else { // write the snippet stream to the file writer.write(snippetStream .map(Snippet::source) .collect(Collectors.joining("\n"))); } } catch (FileNotFoundException e) { errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage()); return false; } catch (Exception e) { errormsg("jshell.err.file.exception", "/save", filename, e); return false; } return true; } private boolean cmdVars(String arg) { Stream stream = argsOptionsToSnippets(this::allVarSnippets, this::isActive, arg, "/vars"); if (stream == null) { return false; } stream.forEachOrdered(vk -> { String val = state.status(vk) == Status.VALID ? feedback.truncateVarValue(state.varValue(vk)) : getResourceString("jshell.msg.vars.not.active"); hard(" %s %s = %s", vk.typeName(), vk.name(), val); }); return true; } private boolean cmdMethods(String arg) { Stream stream = argsOptionsToSnippets(this::allMethodSnippets, this::isActive, arg, "/methods"); if (stream == null) { return false; } stream.forEachOrdered(meth -> { String sig = meth.signature(); int i = sig.lastIndexOf(")") + 1; if (i <= 0) { hard(" %s", meth.name()); } else { hard(" %s %s%s", sig.substring(i), meth.name(), sig.substring(0, i)); } printSnippetStatus(meth, true); }); return true; } private boolean cmdTypes(String arg) { Stream stream = argsOptionsToSnippets(this::allTypeSnippets, this::isActive, arg, "/types"); if (stream == null) { return false; } stream.forEachOrdered(ck -> { String kind; switch (ck.subKind()) { case INTERFACE_SUBKIND: kind = "interface"; break; case CLASS_SUBKIND: kind = "class"; break; case ENUM_SUBKIND: kind = "enum"; break; case ANNOTATION_TYPE_SUBKIND: kind = "@interface"; break; default: assert false : "Wrong kind" + ck.subKind(); kind = "class"; break; } hard(" %s %s", kind, ck.name()); printSnippetStatus(ck, true); }); return true; } private boolean cmdImports() { state.imports().forEach(ik -> { hard(" import %s%s", ik.isStatic() ? "static " : "", ik.fullname()); }); return true; } private boolean cmdUseHistoryEntry(int index) { List keys = state.snippets().collect(toList()); if (index < 0) index += keys.size(); else index--; if (index >= 0 && index < keys.size()) { rerunSnippet(keys.get(index)); } else { errormsg("jshell.err.out.of.range"); return false; } return true; } boolean checkOptionsAndRemainingInput(ArgTokenizer at) { String junk = at.remainder(); if (!junk.isEmpty()) { errormsg("jshell.err.unexpected.at.end", junk, at.whole()); return false; } else { String bad = at.badOptions(); if (!bad.isEmpty()) { errormsg("jshell.err.unknown.option", bad, at.whole()); return false; } } return true; } /** * Handle snippet reevaluation commands: {@code /}. These commands are a * sequence of ids and id ranges (names are permitted, though not in the * first position. Support for names is purposely not documented). * * @param rawargs the whole command including arguments */ private void rerunHistoryEntriesById(String rawargs) { ArgTokenizer at = new ArgTokenizer("/", rawargs.trim().substring(1)); at.allowedOptions(); Stream stream = argsOptionsToSnippets(state::snippets, sn -> true, at); if (stream != null) { // successfully parsed, rerun snippets stream.forEach(sn -> rerunSnippet(sn)); } } private void rerunSnippet(Snippet snippet) { String source = snippet.source(); cmdout.printf("%s\n", source); input.replaceLastHistoryEntry(source); processSourceCatchingReset(source); } /** * Filter diagnostics for only errors (no warnings, ...) * @param diagnostics input list * @return filtered list */ List errorsOnly(List diagnostics) { return diagnostics.stream() .filter(Diag::isError) .collect(toList()); } void displayDiagnostics(String source, Diag diag, List toDisplay) { for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize if (!line.trim().startsWith("location:")) { toDisplay.add(line); } } int pstart = (int) diag.getStartPosition(); int pend = (int) diag.getEndPosition(); Matcher m = LINEBREAK.matcher(source); int pstartl = 0; int pendl = -2; while (m.find(pstartl)) { pendl = m.start(); if (pendl >= pstart) { break; } else { pstartl = m.end(); } } if (pendl < pstart) { pendl = source.length(); } toDisplay.add(source.substring(pstartl, pendl)); StringBuilder sb = new StringBuilder(); int start = pstart - pstartl; for (int i = 0; i < start; ++i) { sb.append(' '); } sb.append('^'); boolean multiline = pend > pendl; int end = (multiline ? pendl : pend) - pstartl - 1; if (end > start) { for (int i = start + 1; i < end; ++i) { sb.append('-'); } if (multiline) { sb.append("-..."); } else { sb.append('^'); } } toDisplay.add(sb.toString()); debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this); debug("Code: %s", diag.getCode()); debug("Pos: %d (%d - %d)", diag.getPosition(), diag.getStartPosition(), diag.getEndPosition()); } private String processSource(String srcInput) throws IllegalStateException { while (true) { CompletionInfo an = analysis.analyzeCompletion(srcInput); if (!an.completeness().isComplete()) { return an.remaining(); } boolean failed = processCompleteSource(an.source()); if (failed || an.remaining().isEmpty()) { return ""; } srcInput = an.remaining(); } } //where boolean processCompleteSource(String source) throws IllegalStateException { debug("Compiling: %s", source); boolean failed = false; boolean isActive = false; List events = state.eval(source); for (SnippetEvent e : events) { // Report the event, recording failure failed |= handleEvent(e); // If any main snippet is active, this should be replayable // also ignore var value queries isActive |= e.causeSnippet() == null && e.status().isActive() && e.snippet().subKind() != VAR_VALUE_SUBKIND; } // If this is an active snippet and it didn't cause the backend to die, // add it to the replayable history if (isActive && live) { addToReplayHistory(source); } return failed; } // Handle incoming snippet events -- return true on failure private boolean handleEvent(SnippetEvent ste) { Snippet sn = ste.snippet(); if (sn == null) { debug("Event with null key: %s", ste); return false; } List diagnostics = state.diagnostics(sn).collect(toList()); String source = sn.source(); if (ste.causeSnippet() == null) { // main event for (Diag d : diagnostics) { hardmsg(d.isError()? "jshell.msg.error" : "jshell.msg.warning"); List disp = new ArrayList<>(); displayDiagnostics(source, d, disp); disp.stream() .forEach(l -> hard("%s", l)); } if (ste.status() != Status.REJECTED) { if (ste.exception() != null) { if (ste.exception() instanceof EvalException) { printEvalException((EvalException) ste.exception()); return true; } else if (ste.exception() instanceof UnresolvedReferenceException) { printUnresolvedException((UnresolvedReferenceException) ste.exception()); } else { hard("Unexpected execution exception: %s", ste.exception()); return true; } } else { new DisplayEvent(ste, FormatWhen.PRIMARY, ste.value(), diagnostics) .displayDeclarationAndValue(); } } else { if (diagnostics.isEmpty()) { errormsg("jshell.err.failed"); } return true; } } else { // Update if (sn instanceof DeclarationSnippet) { List other = errorsOnly(diagnostics); // display update information new DisplayEvent(ste, FormatWhen.UPDATE, ste.value(), other) .displayDeclarationAndValue(); } } return false; } //where void printStackTrace(StackTraceElement[] stes) { for (StackTraceElement ste : stes) { StringBuilder sb = new StringBuilder(); String cn = ste.getClassName(); if (!cn.isEmpty()) { int dot = cn.lastIndexOf('.'); if (dot > 0) { sb.append(cn.substring(dot + 1)); } else { sb.append(cn); } sb.append("."); } if (!ste.getMethodName().isEmpty()) { sb.append(ste.getMethodName()); sb.append(" "); } String fileName = ste.getFileName(); int lineNumber = ste.getLineNumber(); String loc = ste.isNativeMethod() ? getResourceString("jshell.msg.native.method") : fileName == null ? getResourceString("jshell.msg.unknown.source") : lineNumber >= 0 ? fileName + ":" + lineNumber : fileName; hard(" at %s(%s)", sb, loc); } } //where void printUnresolvedException(UnresolvedReferenceException ex) { printSnippetStatus(ex.getSnippet(), false); } //where void printEvalException(EvalException ex) { if (ex.getMessage() == null) { hard("%s thrown", ex.getExceptionClassName()); } else { hard("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage()); } printStackTrace(ex.getStackTrace()); } private FormatAction toAction(Status status, Status previousStatus, boolean isSignatureChange) { FormatAction act; switch (status) { case VALID: case RECOVERABLE_DEFINED: case RECOVERABLE_NOT_DEFINED: if (previousStatus.isActive()) { act = isSignatureChange ? FormatAction.REPLACED : FormatAction.MODIFIED; } else { act = FormatAction.ADDED; } break; case OVERWRITTEN: act = FormatAction.OVERWROTE; break; case DROPPED: act = FormatAction.DROPPED; break; case REJECTED: case NONEXISTENT: default: // Should not occur error("Unexpected status: " + previousStatus.toString() + "=>" + status.toString()); act = FormatAction.DROPPED; } return act; } void printSnippetStatus(DeclarationSnippet sn, boolean resolve) { List otherErrors = errorsOnly(state.diagnostics(sn).collect(toList())); new DisplayEvent(sn, state.status(sn), resolve, otherErrors) .displayDeclarationAndValue(); } class DisplayEvent { private final Snippet sn; private final FormatAction action; private final FormatWhen update; private final String value; private final List errorLines; private final FormatResolve resolution; private final String unresolved; private final FormatUnresolved unrcnt; private final FormatErrors errcnt; private final boolean resolve; DisplayEvent(SnippetEvent ste, FormatWhen update, String value, List errors) { this(ste.snippet(), ste.status(), false, toAction(ste.status(), ste.previousStatus(), ste.isSignatureChange()), update, value, errors); } DisplayEvent(Snippet sn, Status status, boolean resolve, List errors) { this(sn, status, resolve, FormatAction.USED, FormatWhen.UPDATE, null, errors); } private DisplayEvent(Snippet sn, Status status, boolean resolve, FormatAction action, FormatWhen update, String value, List errors) { this.sn = sn; this.resolve =resolve; this.action = action; this.update = update; this.value = value; this.errorLines = new ArrayList<>(); for (Diag d : errors) { displayDiagnostics(sn.source(), d, errorLines); } if (resolve) { // resolve needs error lines indented for (int i = 0; i < errorLines.size(); ++i) { errorLines.set(i, " " + errorLines.get(i)); } } long unresolvedCount; if (sn instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) { resolution = (status == Status.RECOVERABLE_NOT_DEFINED) ? FormatResolve.NOTDEFINED : FormatResolve.DEFINED; unresolved = unresolved((DeclarationSnippet) sn); unresolvedCount = state.unresolvedDependencies((DeclarationSnippet) sn).count(); } else { resolution = FormatResolve.OK; unresolved = ""; unresolvedCount = 0; } unrcnt = unresolvedCount == 0 ? FormatUnresolved.UNRESOLVED0 : unresolvedCount == 1 ? FormatUnresolved.UNRESOLVED1 : FormatUnresolved.UNRESOLVED2; errcnt = errors.isEmpty() ? FormatErrors.ERROR0 : errors.size() == 1 ? FormatErrors.ERROR1 : FormatErrors.ERROR2; } private String unresolved(DeclarationSnippet key) { List unr = state.unresolvedDependencies(key).collect(toList()); StringBuilder sb = new StringBuilder(); int fromLast = unr.size(); if (fromLast > 0) { sb.append(" "); } for (String u : unr) { --fromLast; sb.append(u); switch (fromLast) { // No suffix case 0: break; case 1: sb.append(", and "); break; default: sb.append(", "); break; } } return sb.toString(); } private void custom(FormatCase fcase, String name) { custom(fcase, name, null); } private void custom(FormatCase fcase, String name, String type) { if (resolve) { String resolutionErrors = feedback.format("resolve", fcase, action, update, resolution, unrcnt, errcnt, name, type, value, unresolved, errorLines); if (!resolutionErrors.trim().isEmpty()) { hard(" %s", resolutionErrors); } } else if (interactive()) { String display = feedback.format(fcase, action, update, resolution, unrcnt, errcnt, name, type, value, unresolved, errorLines); cmdout.print(display); } } @SuppressWarnings("fallthrough") private void displayDeclarationAndValue() { switch (sn.subKind()) { case CLASS_SUBKIND: custom(FormatCase.CLASS, ((TypeDeclSnippet) sn).name()); break; case INTERFACE_SUBKIND: custom(FormatCase.INTERFACE, ((TypeDeclSnippet) sn).name()); break; case ENUM_SUBKIND: custom(FormatCase.ENUM, ((TypeDeclSnippet) sn).name()); break; case ANNOTATION_TYPE_SUBKIND: custom(FormatCase.ANNOTATION, ((TypeDeclSnippet) sn).name()); break; case METHOD_SUBKIND: custom(FormatCase.METHOD, ((MethodSnippet) sn).name(), ((MethodSnippet) sn).parameterTypes()); break; case VAR_DECLARATION_SUBKIND: { VarSnippet vk = (VarSnippet) sn; custom(FormatCase.VARDECL, vk.name(), vk.typeName()); break; } case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: { VarSnippet vk = (VarSnippet) sn; custom(FormatCase.VARINIT, vk.name(), vk.typeName()); break; } case TEMP_VAR_EXPRESSION_SUBKIND: { VarSnippet vk = (VarSnippet) sn; custom(FormatCase.EXPRESSION, vk.name(), vk.typeName()); break; } case OTHER_EXPRESSION_SUBKIND: error("Unexpected expression form -- value is: %s", (value)); break; case VAR_VALUE_SUBKIND: { ExpressionSnippet ek = (ExpressionSnippet) sn; custom(FormatCase.VARVALUE, ek.name(), ek.typeName()); break; } case ASSIGNMENT_SUBKIND: { ExpressionSnippet ek = (ExpressionSnippet) sn; custom(FormatCase.ASSIGNMENT, ek.name(), ek.typeName()); break; } case SINGLE_TYPE_IMPORT_SUBKIND: case TYPE_IMPORT_ON_DEMAND_SUBKIND: case SINGLE_STATIC_IMPORT_SUBKIND: case STATIC_IMPORT_ON_DEMAND_SUBKIND: custom(FormatCase.IMPORT, ((ImportSnippet) sn).name()); break; case STATEMENT_SUBKIND: custom(FormatCase.STATEMENT, null); break; } } } /** The current version number as a string. */ String version() { return version("release"); // mm.nn.oo[-milestone] } /** The current full version number as a string. */ String fullVersion() { return version("full"); // mm.mm.oo[-milestone]-build } private String version(String key) { if (versionRB == null) { try { versionRB = ResourceBundle.getBundle(VERSION_RB_NAME, locale); } catch (MissingResourceException e) { return "(version info not available)"; } } try { return versionRB.getString(key); } catch (MissingResourceException e) { return "(version info not available)"; } } class NameSpace { final String spaceName; final String prefix; private int nextNum; NameSpace(String spaceName, String prefix) { this.spaceName = spaceName; this.prefix = prefix; this.nextNum = 1; } String tid(Snippet sn) { String tid = prefix + nextNum++; mapSnippet.put(sn, new SnippetInfo(sn, this, tid)); return tid; } String tidNext() { return prefix + nextNum; } } static class SnippetInfo { final Snippet snippet; final NameSpace space; final String tid; SnippetInfo(Snippet snippet, NameSpace space, String tid) { this.snippet = snippet; this.space = space; this.tid = tid; } } static class ArgSuggestion implements Suggestion { private final String continuation; /** * Create a {@code Suggestion} instance. * * @param continuation a candidate continuation of the user's input */ public ArgSuggestion(String continuation) { this.continuation = continuation; } /** * The candidate continuation of the given user's input. * * @return the continuation string */ @Override public String continuation() { return continuation; } /** * Indicates whether input continuation matches the target type and is thus * more likely to be the desired continuation. A matching continuation is * preferred. * * @return {@code false}, non-types analysis */ @Override public boolean matchesType() { return false; } } } abstract class NonInteractiveIOContext extends IOContext { @Override public boolean interactiveOutput() { return false; } @Override public Iterable currentSessionHistory() { return Collections.emptyList(); } @Override public boolean terminalEditorRunning() { return false; } @Override public void suspend() { } @Override public void resume() { } @Override public void beforeUserCode() { } @Override public void afterUserCode() { } @Override public void replaceLastHistoryEntry(String source) { } } class ScannerIOContext extends NonInteractiveIOContext { private final Scanner scannerIn; ScannerIOContext(Scanner scannerIn) { this.scannerIn = scannerIn; } ScannerIOContext(Reader rdr) throws FileNotFoundException { this(new Scanner(rdr)); } @Override public String readLine(String prompt, String prefix) { if (scannerIn.hasNextLine()) { return scannerIn.nextLine(); } else { return null; } } @Override public void close() { scannerIn.close(); } @Override public int readUserInput() { return -1; } } class ReloadIOContext extends NonInteractiveIOContext { private final Iterator it; private final PrintStream echoStream; ReloadIOContext(Iterable history, PrintStream echoStream) { this.it = history.iterator(); this.echoStream = echoStream; } @Override public String readLine(String prompt, String prefix) { String s = it.hasNext() ? it.next() : null; if (echoStream != null && s != null) { String p = "-: "; String p2 = "\n "; echoStream.printf("%s%s\n", p, s.replace("\n", p2)); } return s; } @Override public void close() { } @Override public int readUserInput() { return -1; } }