1 /*
   2  * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.internal.jshell.tool;
  27 
  28 import java.io.BufferedReader;
  29 import java.io.BufferedWriter;
  30 import java.io.EOFException;
  31 import java.io.File;
  32 import java.io.FileNotFoundException;
  33 import java.io.FileReader;
  34 import java.io.IOException;
  35 import java.io.InputStream;
  36 import java.io.InputStreamReader;
  37 import java.io.PrintStream;
  38 import java.io.Reader;
  39 import java.io.StringReader;
  40 import java.lang.module.ModuleDescriptor;
  41 import java.lang.module.ModuleFinder;
  42 import java.lang.module.ModuleReference;
  43 import java.nio.charset.Charset;
  44 import java.nio.file.FileSystems;
  45 import java.nio.file.Files;
  46 import java.nio.file.InvalidPathException;
  47 import java.nio.file.Path;
  48 import java.nio.file.Paths;
  49 import java.text.MessageFormat;
  50 import java.util.ArrayList;
  51 import java.util.Arrays;
  52 import java.util.Collection;
  53 import java.util.Collections;
  54 import java.util.HashMap;
  55 import java.util.HashSet;
  56 import java.util.Iterator;
  57 import java.util.LinkedHashMap;
  58 import java.util.LinkedHashSet;
  59 import java.util.List;
  60 import java.util.Locale;
  61 import java.util.Map;
  62 import java.util.Map.Entry;
  63 import java.util.Optional;
  64 import java.util.Scanner;
  65 import java.util.Set;
  66 import java.util.function.Consumer;
  67 import java.util.function.Predicate;
  68 import java.util.prefs.Preferences;
  69 import java.util.regex.Matcher;
  70 import java.util.regex.Pattern;
  71 import java.util.stream.Collectors;
  72 import java.util.stream.Stream;
  73 import java.util.stream.StreamSupport;
  74 
  75 import jdk.internal.jshell.debug.InternalDebugControl;
  76 import jdk.internal.jshell.tool.IOContext.InputInterruptedException;
  77 import jdk.jshell.DeclarationSnippet;
  78 import jdk.jshell.Diag;
  79 import jdk.jshell.EvalException;
  80 import jdk.jshell.ExpressionSnippet;
  81 import jdk.jshell.ImportSnippet;
  82 import jdk.jshell.JShell;
  83 import jdk.jshell.JShell.Subscription;
  84 import jdk.jshell.MethodSnippet;
  85 import jdk.jshell.Snippet;
  86 import jdk.jshell.Snippet.Kind;
  87 import jdk.jshell.Snippet.Status;
  88 import jdk.jshell.SnippetEvent;
  89 import jdk.jshell.SourceCodeAnalysis;
  90 import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
  91 import jdk.jshell.SourceCodeAnalysis.Completeness;
  92 import jdk.jshell.SourceCodeAnalysis.Suggestion;
  93 import jdk.jshell.TypeDeclSnippet;
  94 import jdk.jshell.UnresolvedReferenceException;
  95 import jdk.jshell.VarSnippet;
  96 
  97 import static java.nio.file.StandardOpenOption.CREATE;
  98 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
  99 import static java.nio.file.StandardOpenOption.WRITE;
 100 import java.util.AbstractMap.SimpleEntry;
 101 import java.util.MissingResourceException;
 102 import java.util.ResourceBundle;
 103 import java.util.ServiceLoader;
 104 import java.util.Spliterators;
 105 import java.util.function.Function;
 106 import java.util.function.Supplier;
 107 import jdk.internal.joptsimple.*;
 108 import jdk.internal.jshell.tool.Feedback.FormatAction;
 109 import jdk.internal.jshell.tool.Feedback.FormatCase;
 110 import jdk.internal.jshell.tool.Feedback.FormatErrors;
 111 import jdk.internal.jshell.tool.Feedback.FormatResolve;
 112 import jdk.internal.jshell.tool.Feedback.FormatUnresolved;
 113 import jdk.internal.jshell.tool.Feedback.FormatWhen;
 114 import jdk.internal.editor.spi.BuildInEditorProvider;
 115 import jdk.internal.editor.external.ExternalEditor;
 116 import static java.util.Arrays.asList;
 117 import static java.util.Arrays.stream;
 118 import static java.util.Collections.singletonList;
 119 import static java.util.stream.Collectors.joining;
 120 import static java.util.stream.Collectors.toList;
 121 import static jdk.jshell.Snippet.SubKind.TEMP_VAR_EXPRESSION_SUBKIND;
 122 import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND;
 123 import static java.util.stream.Collectors.toMap;
 124 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_COMPA;
 125 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_DEP;
 126 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT;
 127 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_FMGR;
 128 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN;
 129 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_WRAP;
 130 import static jdk.internal.jshell.tool.ContinuousCompletionProvider.STARTSWITH_MATCHER;
 131 
 132 /**
 133  * Command line REPL tool for Java using the JShell API.
 134  * @author Robert Field
 135  */
 136 public class JShellTool implements MessageHandler {
 137 
 138     private static final Pattern LINEBREAK = Pattern.compile("\\R");
 139     private static final Pattern ID = Pattern.compile("[se]?\\d+([-\\s].*)?");
 140     private static final Pattern RERUN_ID = Pattern.compile("/" + ID.pattern());
 141     private static final Pattern RERUN_PREVIOUS = Pattern.compile("/\\-\\d+( .*)?");
 142     private static final Pattern SET_SUB = Pattern.compile("/?set .*");
 143             static final String RECORD_SEPARATOR = "\u241E";
 144     private static final String RB_NAME_PREFIX  = "jdk.internal.jshell.tool.resources";
 145     private static final String VERSION_RB_NAME = RB_NAME_PREFIX + ".version";
 146     private static final String L10N_RB_NAME    = RB_NAME_PREFIX + ".l10n";
 147 
 148     final InputStream cmdin;
 149     final PrintStream cmdout;
 150     final PrintStream cmderr;
 151     final PrintStream console;
 152     final InputStream userin;
 153     final PrintStream userout;
 154     final PrintStream usererr;
 155     final PersistentStorage prefs;
 156     final Map<String, String> envvars;
 157     final Locale locale;
 158 
 159     final Feedback feedback = new Feedback();
 160 
 161     /**
 162      * The complete constructor for the tool (used by test harnesses).
 163      * @param cmdin command line input -- snippets and commands
 164      * @param cmdout command line output, feedback including errors
 165      * @param cmderr start-up errors and debugging info
 166      * @param console console control interaction
 167      * @param userin code execution input, or null to use IOContext
 168      * @param userout code execution output  -- System.out.printf("hi")
 169      * @param usererr code execution error stream  -- System.err.printf("Oops")
 170      * @param prefs persistence implementation to use
 171      * @param envvars environment variable mapping to use
 172      * @param locale locale to use
 173      */
 174     JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr,
 175             PrintStream console,
 176             InputStream userin, PrintStream userout, PrintStream usererr,
 177             PersistentStorage prefs, Map<String, String> envvars, Locale locale) {
 178         this.cmdin = cmdin;
 179         this.cmdout = cmdout;
 180         this.cmderr = cmderr;
 181         this.console = console;
 182         this.userin = userin != null ? userin : new InputStream() {
 183             @Override
 184             public int read() throws IOException {
 185                 return input.readUserInput();
 186             }
 187         };
 188         this.userout = userout;
 189         this.usererr = usererr;
 190         this.prefs = prefs;
 191         this.envvars = envvars;
 192         this.locale = locale;
 193     }
 194 
 195     private ResourceBundle versionRB = null;
 196     private ResourceBundle outputRB  = null;
 197 
 198     private IOContext input = null;
 199     private boolean regenerateOnDeath = true;
 200     private boolean live = false;
 201     private boolean interactiveModeBegun = false;
 202     private Options options;
 203 
 204     SourceCodeAnalysis analysis;
 205     private JShell state = null;
 206     Subscription shutdownSubscription = null;
 207 
 208     static final EditorSetting BUILT_IN_EDITOR = new EditorSetting(null, false);
 209 
 210     private boolean debug = false;
 211     public boolean testPrompt = false;
 212     private Startup startup = null;
 213     private boolean isCurrentlyRunningStartup = false;
 214     private String executionControlSpec = null;
 215     private EditorSetting editor = BUILT_IN_EDITOR;
 216     private int exitCode = 0;
 217 
 218     private static final String[] EDITOR_ENV_VARS = new String[] {
 219         "JSHELLEDITOR", "VISUAL", "EDITOR"};
 220 
 221     // Commands and snippets which can be replayed
 222     private ReplayableHistory replayableHistory;
 223     private ReplayableHistory replayableHistoryPrevious;
 224 
 225     static final String STARTUP_KEY  = "STARTUP";
 226     static final String EDITOR_KEY   = "EDITOR";
 227     static final String FEEDBACK_KEY = "FEEDBACK";
 228     static final String MODE_KEY     = "MODE";
 229     static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE";
 230 
 231     static final Pattern BUILTIN_FILE_PATTERN = Pattern.compile("\\w+");
 232     static final String BUILTIN_FILE_PATH_FORMAT = "/jdk/jshell/tool/resources/%s.jsh";
 233     static final String INT_PREFIX = "int $$exit$$ = ";
 234 
 235     static final int OUTPUT_WIDTH = 72;
 236 
 237     // match anything followed by whitespace
 238     private static final Pattern OPTION_PRE_PATTERN =
 239             Pattern.compile("\\s*(\\S+\\s+)*?");
 240     // match a (possibly incomplete) option flag with optional double-dash and/or internal dashes
 241     private static final Pattern OPTION_PATTERN =
 242             Pattern.compile(OPTION_PRE_PATTERN.pattern() + "(?<dd>-??)(?<flag>-([a-z][a-z\\-]*)?)");
 243     // match an option flag and a (possibly missing or incomplete) value
 244     private static final Pattern OPTION_VALUE_PATTERN =
 245             Pattern.compile(OPTION_PATTERN.pattern() + "\\s+(?<val>\\S*)");
 246 
 247     // Tool id (tid) mapping: the three name spaces
 248     NameSpace mainNamespace;
 249     NameSpace startNamespace;
 250     NameSpace errorNamespace;
 251 
 252     // Tool id (tid) mapping: the current name spaces
 253     NameSpace currentNameSpace;
 254 
 255     Map<Snippet, SnippetInfo> mapSnippet;
 256 
 257     // Kinds of compiler/runtime init options
 258     private enum OptionKind {
 259         CLASS_PATH("--class-path", true),
 260         MODULE_PATH("--module-path", true),
 261         ADD_MODULES("--add-modules", false),
 262         ADD_EXPORTS("--add-exports", false),
 263         TO_COMPILER("-C", false, false, true, false),
 264         TO_REMOTE_VM("-R", false, false, false, true),;
 265         final String optionFlag;
 266         final boolean onlyOne;
 267         final boolean passFlag;
 268         final boolean toCompiler;
 269         final boolean toRemoteVm;
 270 
 271         private OptionKind(String optionFlag, boolean onlyOne) {
 272             this(optionFlag, onlyOne, true, true, true);
 273         }
 274 
 275         private OptionKind(String optionFlag, boolean onlyOne, boolean passFlag,
 276                 boolean toCompiler, boolean toRemoteVm) {
 277             this.optionFlag = optionFlag;
 278             this.onlyOne = onlyOne;
 279             this.passFlag = passFlag;
 280             this.toCompiler = toCompiler;
 281             this.toRemoteVm = toRemoteVm;
 282         }
 283 
 284     }
 285 
 286     // compiler/runtime init option values
 287     private static class Options {
 288 
 289         private final Map<OptionKind, List<String>> optMap;
 290 
 291         // New blank Options
 292         Options() {
 293             optMap = new HashMap<>();
 294         }
 295 
 296         // Options as a copy
 297         private Options(Options opts) {
 298             optMap = new HashMap<>(opts.optMap);
 299         }
 300 
 301         private String[] selectOptions(Predicate<Entry<OptionKind, List<String>>> pred) {
 302             return optMap.entrySet().stream()
 303                     .filter(pred)
 304                     .flatMap(e -> e.getValue().stream())
 305                     .toArray(String[]::new);
 306         }
 307 
 308         String[] remoteVmOptions() {
 309             return selectOptions(e -> e.getKey().toRemoteVm);
 310         }
 311 
 312         String[] compilerOptions() {
 313             return selectOptions(e -> e.getKey().toCompiler);
 314         }
 315 
 316         String[] commonOptions() {
 317             return selectOptions(e -> e.getKey().passFlag);
 318         }
 319 
 320         void addAll(OptionKind kind, Collection<String> vals) {
 321             optMap.computeIfAbsent(kind, k -> new ArrayList<>())
 322                     .addAll(vals);
 323         }
 324 
 325         // return a new Options, with parameter options overriding receiver options
 326         Options override(Options newer) {
 327             Options result = new Options(this);
 328             newer.optMap.entrySet().stream()
 329                     .forEach(e -> {
 330                         if (e.getKey().onlyOne) {
 331                             // Only one allowed, override last
 332                             result.optMap.put(e.getKey(), e.getValue());
 333                         } else {
 334                             // Additive
 335                             result.addAll(e.getKey(), e.getValue());
 336                         }
 337                     });
 338             return result;
 339         }
 340     }
 341 
 342     // base option parsing of /env, /reload, and /reset and command-line options
 343     private class OptionParserBase {
 344 
 345         final OptionParser parser = new OptionParser();
 346         private final OptionSpec<String> argClassPath = parser.accepts("class-path").withRequiredArg();
 347         private final OptionSpec<String> argModulePath = parser.accepts("module-path").withRequiredArg();
 348         private final OptionSpec<String> argAddModules = parser.accepts("add-modules").withRequiredArg();
 349         private final OptionSpec<String> argAddExports = parser.accepts("add-exports").withRequiredArg();
 350         private final NonOptionArgumentSpec<String> argNonOptions = parser.nonOptions();
 351 
 352         private Options opts = new Options();
 353         private List<String> nonOptions;
 354         private boolean failed = false;
 355 
 356         List<String> nonOptions() {
 357             return nonOptions;
 358         }
 359 
 360         void msg(String key, Object... args) {
 361             errormsg(key, args);
 362         }
 363 
 364         Options parse(String[] args) throws OptionException {
 365             try {
 366                 OptionSet oset = parser.parse(args);
 367                 nonOptions = oset.valuesOf(argNonOptions);
 368                 return parse(oset);
 369             } catch (OptionException ex) {
 370                 if (ex.options().isEmpty()) {
 371                     msg("jshell.err.opt.invalid", stream(args).collect(joining(", ")));
 372                 } else {
 373                     boolean isKnown = parser.recognizedOptions().containsKey(ex.options().iterator().next());
 374                     msg(isKnown
 375                             ? "jshell.err.opt.arg"
 376                             : "jshell.err.opt.unknown",
 377                             ex.options()
 378                             .stream()
 379                             .collect(joining(", ")));
 380                 }
 381                 exitCode = 1;
 382                 return null;
 383             }
 384         }
 385 
 386         // check that the supplied string represent valid class/module paths
 387         // converting any ~/ to user home
 388         private Collection<String> validPaths(Collection<String> vals, String context, boolean isModulePath) {
 389             Stream<String> result = vals.stream()
 390                     .map(s -> Arrays.stream(s.split(File.pathSeparator))
 391                         .flatMap(sp -> toPathImpl(sp, context))
 392                         .filter(p -> checkValidPathEntry(p, context, isModulePath))
 393                         .map(p -> p.toString())
 394                         .collect(Collectors.joining(File.pathSeparator)));
 395             if (failed) {
 396                 return Collections.emptyList();
 397             } else {
 398                 return result.collect(toList());
 399             }
 400         }
 401 
 402         // Adapted from compiler method Locations.checkValidModulePathEntry
 403         private boolean checkValidPathEntry(Path p, String context, boolean isModulePath) {
 404             if (!Files.exists(p)) {
 405                 msg("jshell.err.file.not.found", context, p);
 406                 failed = true;
 407                 return false;
 408             }
 409             if (Files.isDirectory(p)) {
 410                 // if module-path, either an exploded module or a directory of modules
 411                 return true;
 412             }
 413 
 414             String name = p.getFileName().toString();
 415             int lastDot = name.lastIndexOf(".");
 416             if (lastDot > 0) {
 417                 switch (name.substring(lastDot)) {
 418                     case ".jar":
 419                         return true;
 420                     case ".jmod":
 421                         if (isModulePath) {
 422                             return true;
 423                         }
 424                 }
 425             }
 426             msg("jshell.err.arg", context, p);
 427             failed = true;
 428             return false;
 429         }
 430 
 431         private Stream<Path> toPathImpl(String path, String context) {
 432             try {
 433                 return Stream.of(toPathResolvingUserHome(path));
 434             } catch (InvalidPathException ex) {
 435                 msg("jshell.err.file.not.found", context, path);
 436                 failed = true;
 437                 return Stream.empty();
 438             }
 439         }
 440 
 441         Options parse(OptionSet options) {
 442             addOptions(OptionKind.CLASS_PATH,
 443                     validPaths(options.valuesOf(argClassPath), "--class-path", false));
 444             addOptions(OptionKind.MODULE_PATH,
 445                     validPaths(options.valuesOf(argModulePath), "--module-path", true));
 446             addOptions(OptionKind.ADD_MODULES, options.valuesOf(argAddModules));
 447             addOptions(OptionKind.ADD_EXPORTS, options.valuesOf(argAddExports).stream()
 448                     .map(mp -> mp.contains("=") ? mp : mp + "=ALL-UNNAMED")
 449                     .collect(toList())
 450             );
 451 
 452             if (failed) {
 453                 exitCode = 1;
 454                 return null;
 455             } else {
 456                 return opts;
 457             }
 458         }
 459 
 460         void addOptions(OptionKind kind, Collection<String> vals) {
 461             if (!vals.isEmpty()) {
 462                 if (kind.onlyOne && vals.size() > 1) {
 463                     msg("jshell.err.opt.one", kind.optionFlag);
 464                     failed = true;
 465                     return;
 466                 }
 467                 if (kind.passFlag) {
 468                     vals = vals.stream()
 469                             .flatMap(mp -> Stream.of(kind.optionFlag, mp))
 470                             .collect(toList());
 471                 }
 472                 opts.addAll(kind, vals);
 473             }
 474         }
 475     }
 476 
 477     // option parsing for /reload (adds -restore -quiet)
 478     private class OptionParserReload extends OptionParserBase {
 479 
 480         private final OptionSpecBuilder argRestore = parser.accepts("restore");
 481         private final OptionSpecBuilder argQuiet   = parser.accepts("quiet");
 482 
 483         private boolean restore = false;
 484         private boolean quiet = false;
 485 
 486         boolean restore() {
 487             return restore;
 488         }
 489 
 490         boolean quiet() {
 491             return quiet;
 492         }
 493 
 494         @Override
 495         Options parse(OptionSet options) {
 496             if (options.has(argRestore)) {
 497                 restore = true;
 498             }
 499             if (options.has(argQuiet)) {
 500                 quiet = true;
 501             }
 502             return super.parse(options);
 503         }
 504     }
 505 
 506     // option parsing for command-line
 507     private class OptionParserCommandLine extends OptionParserBase {
 508 
 509         private final OptionSpec<String> argStart = parser.accepts("startup").withRequiredArg();
 510         private final OptionSpecBuilder argNoStart = parser.acceptsAll(asList("n", "no-startup"));
 511         private final OptionSpec<String> argFeedback = parser.accepts("feedback").withRequiredArg();
 512         private final OptionSpec<String> argExecution = parser.accepts("execution").withRequiredArg();
 513         private final OptionSpecBuilder argQ = parser.accepts("q");
 514         private final OptionSpecBuilder argS = parser.accepts("s");
 515         private final OptionSpecBuilder argV = parser.accepts("v");
 516         private final OptionSpec<String> argR = parser.accepts("R").withRequiredArg();
 517         private final OptionSpec<String> argC = parser.accepts("C").withRequiredArg();
 518         private final OptionSpecBuilder argHelp = parser.acceptsAll(asList("?", "h", "help"));
 519         private final OptionSpecBuilder argVersion = parser.accepts("version");
 520         private final OptionSpecBuilder argFullVersion = parser.accepts("full-version");
 521         private final OptionSpecBuilder argShowVersion = parser.accepts("show-version");
 522         private final OptionSpecBuilder argHelpExtra = parser.acceptsAll(asList("X", "help-extra"));
 523 
 524         private String feedbackMode = null;
 525         private Startup initialStartup = null;
 526 
 527         String feedbackMode() {
 528             return feedbackMode;
 529         }
 530 
 531         Startup startup() {
 532             return initialStartup;
 533         }
 534 
 535         @Override
 536         void msg(String key, Object... args) {
 537             errormsg(key, args);
 538         }
 539 
 540         /**
 541          * Parse the command line options.
 542          * @return the options as an Options object, or null if error
 543          */
 544         @Override
 545         Options parse(OptionSet options) {
 546             if (options.has(argHelp)) {
 547                 printUsage();
 548                 return null;
 549             }
 550             if (options.has(argHelpExtra)) {
 551                 printUsageX();
 552                 return null;
 553             }
 554             if (options.has(argVersion)) {
 555                 cmdout.printf("jshell %s\n", version());
 556                 return null;
 557             }
 558             if (options.has(argFullVersion)) {
 559                 cmdout.printf("jshell %s\n", fullVersion());
 560                 return null;
 561             }
 562             if (options.has(argShowVersion)) {
 563                 cmdout.printf("jshell %s\n", version());
 564             }
 565             if ((options.valuesOf(argFeedback).size() +
 566                     (options.has(argQ) ? 1 : 0) +
 567                     (options.has(argS) ? 1 : 0) +
 568                     (options.has(argV) ? 1 : 0)) > 1) {
 569                 msg("jshell.err.opt.feedback.one");
 570                 exitCode = 1;
 571                 return null;
 572             } else if (options.has(argFeedback)) {
 573                 feedbackMode = options.valueOf(argFeedback);
 574             } else if (options.has("q")) {
 575                 feedbackMode = "concise";
 576             } else if (options.has("s")) {
 577                 feedbackMode = "silent";
 578             } else if (options.has("v")) {
 579                 feedbackMode = "verbose";
 580             }
 581             if (options.has(argStart)) {
 582                 List<String> sts = options.valuesOf(argStart);
 583                 if (options.has("no-startup")) {
 584                     msg("jshell.err.opt.startup.conflict");
 585                     exitCode = 1;
 586                     return null;
 587                 }
 588                 initialStartup = Startup.fromFileList(sts, "--startup", new InitMessageHandler());
 589                 if (initialStartup == null) {
 590                     exitCode = 1;
 591                     return null;
 592                 }
 593             } else if (options.has(argNoStart)) {
 594                 initialStartup = Startup.noStartup();
 595             } else {
 596                 String packedStartup = prefs.get(STARTUP_KEY);
 597                 initialStartup = Startup.unpack(packedStartup, new InitMessageHandler());
 598             }
 599             if (options.has(argExecution)) {
 600                 executionControlSpec = options.valueOf(argExecution);
 601             }
 602             addOptions(OptionKind.TO_REMOTE_VM, options.valuesOf(argR));
 603             addOptions(OptionKind.TO_COMPILER, options.valuesOf(argC));
 604             return super.parse(options);
 605         }
 606     }
 607 
 608     /**
 609      * Encapsulate a history of snippets and commands which can be replayed.
 610      */
 611     private static class ReplayableHistory {
 612 
 613         // the history
 614         private List<String> hist;
 615 
 616         // the length of the history as of last save
 617         private int lastSaved;
 618 
 619         private ReplayableHistory(List<String> hist) {
 620             this.hist = hist;
 621             this.lastSaved = 0;
 622         }
 623 
 624         // factory for empty histories
 625         static ReplayableHistory emptyHistory() {
 626             return new ReplayableHistory(new ArrayList<>());
 627         }
 628 
 629         // factory for history stored in persistent storage
 630         static ReplayableHistory fromPrevious(PersistentStorage prefs) {
 631             // Read replay history from last jshell session
 632             String prevReplay = prefs.get(REPLAY_RESTORE_KEY);
 633             if (prevReplay == null) {
 634                 return null;
 635             } else {
 636                 return new ReplayableHistory(Arrays.asList(prevReplay.split(RECORD_SEPARATOR)));
 637             }
 638 
 639         }
 640 
 641         // store the history in persistent storage
 642         void storeHistory(PersistentStorage prefs) {
 643             if (hist.size() > lastSaved) {
 644                 // Prevent history overflow by calculating what will fit, starting
 645                 // with most recent
 646                 int sepLen = RECORD_SEPARATOR.length();
 647                 int length = 0;
 648                 int first = hist.size();
 649                 while (length < Preferences.MAX_VALUE_LENGTH && --first >= 0) {
 650                     length += hist.get(first).length() + sepLen;
 651                 }
 652                 if (first >= 0) {
 653                     hist = hist.subList(first + 1, hist.size());
 654                 }
 655                 String shist = String.join(RECORD_SEPARATOR, hist);
 656                 prefs.put(REPLAY_RESTORE_KEY, shist);
 657                 markSaved();
 658             }
 659             prefs.flush();
 660         }
 661 
 662         // add a snippet or command to the history
 663         void add(String s) {
 664             hist.add(s);
 665         }
 666 
 667         // return history to reloaded
 668         Iterable<String> iterable() {
 669             return hist;
 670         }
 671 
 672         // mark that persistent storage and current history are in sync
 673         void markSaved() {
 674             lastSaved = hist.size();
 675         }
 676     }
 677 
 678     /**
 679      * Is the input/output currently interactive
 680      *
 681      * @return true if console
 682      */
 683     boolean interactive() {
 684         return input != null && input.interactiveOutput();
 685     }
 686 
 687     void debug(String format, Object... args) {
 688         if (debug) {
 689             cmderr.printf(format + "\n", args);
 690         }
 691     }
 692 
 693     /**
 694      * Must show command output
 695      *
 696      * @param format printf format
 697      * @param args printf args
 698      */
 699     @Override
 700     public void hard(String format, Object... args) {
 701         cmdout.printf(prefix(format), args);
 702     }
 703 
 704    /**
 705      * Error command output
 706      *
 707      * @param format printf format
 708      * @param args printf args
 709      */
 710     void error(String format, Object... args) {
 711         (interactiveModeBegun? cmdout : cmderr).printf(prefixError(format), args);
 712     }
 713 
 714     /**
 715      * Should optional informative be displayed?
 716      * @return true if they should be displayed
 717      */
 718     @Override
 719     public boolean showFluff() {
 720         return feedback.shouldDisplayCommandFluff() && interactive();
 721     }
 722 
 723     /**
 724      * Optional output
 725      *
 726      * @param format printf format
 727      * @param args printf args
 728      */
 729     @Override
 730     public void fluff(String format, Object... args) {
 731         if (showFluff()) {
 732             hard(format, args);
 733         }
 734     }
 735 
 736     /**
 737      * Resource bundle look-up
 738      *
 739      * @param key the resource key
 740      */
 741     String getResourceString(String key) {
 742         if (outputRB == null) {
 743             try {
 744                 outputRB = ResourceBundle.getBundle(L10N_RB_NAME, locale);
 745             } catch (MissingResourceException mre) {
 746                 error("Cannot find ResourceBundle: %s for locale: %s", L10N_RB_NAME, locale);
 747                 return "";
 748             }
 749         }
 750         String s;
 751         try {
 752             s = outputRB.getString(key);
 753         } catch (MissingResourceException mre) {
 754             error("Missing resource: %s in %s", key, L10N_RB_NAME);
 755             return "";
 756         }
 757         return s;
 758     }
 759 
 760     /**
 761      * Add normal prefixing/postfixing to embedded newlines in a string,
 762      * bracketing with normal prefix/postfix
 763      *
 764      * @param s the string to prefix
 765      * @return the pre/post-fixed and bracketed string
 766      */
 767     String prefix(String s) {
 768          return prefix(s, feedback.getPre(), feedback.getPost());
 769     }
 770 
 771     /**
 772      * Add error prefixing/postfixing to embedded newlines in a string,
 773      * bracketing with error prefix/postfix
 774      *
 775      * @param s the string to prefix
 776      * @return the pre/post-fixed and bracketed string
 777      */
 778     String prefixError(String s) {
 779          return prefix(s, feedback.getErrorPre(), feedback.getErrorPost());
 780     }
 781 
 782     /**
 783      * Add prefixing/postfixing to embedded newlines in a string,
 784      * bracketing with prefix/postfix.  No prefixing when non-interactive.
 785      * Result is expected to be the format for a printf.
 786      *
 787      * @param s the string to prefix
 788      * @param pre the string to prepend to each line
 789      * @param post the string to append to each line (replacing newline)
 790      * @return the pre/post-fixed and bracketed string
 791      */
 792     String prefix(String s, String pre, String post) {
 793         if (s == null) {
 794             return "";
 795         }
 796         if (!interactiveModeBegun) {
 797             // messages expect to be new-line terminated (even when not prefixed)
 798             return s + "%n";
 799         }
 800         String pp = s.replaceAll("\\R", post + pre);
 801         if (pp.endsWith(post + pre)) {
 802             // prevent an extra prefix char and blank line when the string
 803             // already terminates with newline
 804             pp = pp.substring(0, pp.length() - (post + pre).length());
 805         }
 806         return pre + pp + post;
 807     }
 808 
 809     /**
 810      * Print using resource bundle look-up and adding prefix and postfix
 811      *
 812      * @param key the resource key
 813      */
 814     void hardrb(String key) {
 815         hard(getResourceString(key));
 816     }
 817 
 818     /**
 819      * Format using resource bundle look-up using MessageFormat
 820      *
 821      * @param key the resource key
 822      * @param args
 823      */
 824     String messageFormat(String key, Object... args) {
 825         String rs = getResourceString(key);
 826         return MessageFormat.format(rs, args);
 827     }
 828 
 829     /**
 830      * Print using resource bundle look-up, MessageFormat, and add prefix and
 831      * postfix
 832      *
 833      * @param key the resource key
 834      * @param args
 835      */
 836     @Override
 837     public void hardmsg(String key, Object... args) {
 838         hard(messageFormat(key, args));
 839     }
 840 
 841     /**
 842      * Print error using resource bundle look-up, MessageFormat, and add prefix
 843      * and postfix
 844      *
 845      * @param key the resource key
 846      * @param args
 847      */
 848     @Override
 849     public void errormsg(String key, Object... args) {
 850         error(messageFormat(key, args));
 851     }
 852 
 853     /**
 854      * Print (fluff) using resource bundle look-up, MessageFormat, and add
 855      * prefix and postfix
 856      *
 857      * @param key the resource key
 858      * @param args
 859      */
 860     @Override
 861     public void fluffmsg(String key, Object... args) {
 862         if (showFluff()) {
 863             hardmsg(key, args);
 864         }
 865     }
 866 
 867     <T> void hardPairs(Stream<T> stream, Function<T, String> a, Function<T, String> b) {
 868         Map<String, String> a2b = stream.collect(toMap(a, b,
 869                 (m1, m2) -> m1,
 870                 LinkedHashMap::new));
 871         for (Entry<String, String> e : a2b.entrySet()) {
 872             hard("%s", e.getKey());
 873             cmdout.printf(prefix(e.getValue(), feedback.getPre() + "\t", feedback.getPost()));
 874         }
 875     }
 876 
 877     /**
 878      * Trim whitespace off end of string
 879      *
 880      * @param s
 881      * @return
 882      */
 883     static String trimEnd(String s) {
 884         int last = s.length() - 1;
 885         int i = last;
 886         while (i >= 0 && Character.isWhitespace(s.charAt(i))) {
 887             --i;
 888         }
 889         if (i != last) {
 890             return s.substring(0, i + 1);
 891         } else {
 892             return s;
 893         }
 894     }
 895 
 896     /**
 897      * The entry point into the JShell tool.
 898      *
 899      * @param args the command-line arguments
 900      * @throws Exception catastrophic fatal exception
 901      * @return the exit code
 902      */
 903     public int start(String[] args) throws Exception {
 904         OptionParserCommandLine commandLineArgs = new OptionParserCommandLine();
 905         options = commandLineArgs.parse(args);
 906         if (options == null) {
 907             // A null means end immediately, this may be an error or because
 908             // of options like --version.  Exit code has been set.
 909             return exitCode;
 910         }
 911         startup = commandLineArgs.startup();
 912         // initialize editor settings
 913         configEditor();
 914         // initialize JShell instance
 915         try {
 916             resetState();
 917         } catch (IllegalStateException ex) {
 918             // Display just the cause (not a exception backtrace)
 919             cmderr.println(ex.getMessage());
 920             //abort
 921             return 1;
 922         }
 923         // Read replay history from last jshell session into previous history
 924         replayableHistoryPrevious = ReplayableHistory.fromPrevious(prefs);
 925         // load snippet/command files given on command-line
 926         for (String loadFile : commandLineArgs.nonOptions()) {
 927             if (!runFile(loadFile, "jshell")) {
 928                 // Load file failed -- abort
 929                 return 1;
 930             }
 931         }
 932         // if we survived that...
 933         if (regenerateOnDeath) {
 934             // initialize the predefined feedback modes
 935             initFeedback(commandLineArgs.feedbackMode());
 936         }
 937         // check again, as feedback setting could have failed
 938         if (regenerateOnDeath) {
 939             // if we haven't died, and the feedback mode wants fluff, print welcome
 940             interactiveModeBegun = true;
 941             if (feedback.shouldDisplayCommandFluff()) {
 942                 hardmsg("jshell.msg.welcome", version());
 943             }
 944             // Be sure history is always saved so that user code isn't lost
 945             Thread shutdownHook = new Thread() {
 946                 @Override
 947                 public void run() {
 948                     replayableHistory.storeHistory(prefs);
 949                 }
 950             };
 951             Runtime.getRuntime().addShutdownHook(shutdownHook);
 952             // execute from user input
 953             try (IOContext in = new ConsoleIOContext(this, cmdin, console)) {
 954                 while (regenerateOnDeath) {
 955                     if (!live) {
 956                         resetState();
 957                     }
 958                     run(in);
 959                 }
 960             } finally {
 961                 replayableHistory.storeHistory(prefs);
 962                 closeState();
 963                 try {
 964                     Runtime.getRuntime().removeShutdownHook(shutdownHook);
 965                 } catch (Exception ex) {
 966                     // ignore, this probably caused by VM aready being shutdown
 967                     // and this is the last act anyhow
 968                 }
 969             }
 970         }
 971         closeState();
 972         return exitCode;
 973     }
 974 
 975     private EditorSetting configEditor() {
 976         // Read retained editor setting (if any)
 977         editor = EditorSetting.fromPrefs(prefs);
 978         if (editor != null) {
 979             return editor;
 980         }
 981         // Try getting editor setting from OS environment variables
 982         for (String envvar : EDITOR_ENV_VARS) {
 983             String v = envvars.get(envvar);
 984             if (v != null) {
 985                 return editor = new EditorSetting(v.split("\\s+"), false);
 986             }
 987         }
 988         // Default to the built-in editor
 989         return editor = BUILT_IN_EDITOR;
 990     }
 991 
 992     private void printUsage() {
 993         cmdout.print(getResourceString("help.usage"));
 994     }
 995 
 996     private void printUsageX() {
 997         cmdout.print(getResourceString("help.usage.x"));
 998     }
 999 
1000     /**
1001      * Message handler to use during initial start-up.
1002      */
1003     private class InitMessageHandler implements MessageHandler {
1004 
1005         @Override
1006         public void fluff(String format, Object... args) {
1007             //ignore
1008         }
1009 
1010         @Override
1011         public void fluffmsg(String messageKey, Object... args) {
1012             //ignore
1013         }
1014 
1015         @Override
1016         public void hard(String format, Object... args) {
1017             //ignore
1018         }
1019 
1020         @Override
1021         public void hardmsg(String messageKey, Object... args) {
1022             //ignore
1023         }
1024 
1025         @Override
1026         public void errormsg(String messageKey, Object... args) {
1027             JShellTool.this.errormsg(messageKey, args);
1028         }
1029 
1030         @Override
1031         public boolean showFluff() {
1032             return false;
1033         }
1034     }
1035 
1036     private void resetState() {
1037         closeState();
1038 
1039         // Initialize tool id mapping
1040         mainNamespace = new NameSpace("main", "");
1041         startNamespace = new NameSpace("start", "s");
1042         errorNamespace = new NameSpace("error", "e");
1043         mapSnippet = new LinkedHashMap<>();
1044         currentNameSpace = startNamespace;
1045 
1046         // Reset the replayable history, saving the old for restore
1047         replayableHistoryPrevious = replayableHistory;
1048         replayableHistory = ReplayableHistory.emptyHistory();
1049         JShell.Builder builder =
1050                JShell.builder()
1051                 .in(userin)
1052                 .out(userout)
1053                 .err(usererr)
1054                 .tempVariableNameGenerator(() -> "$" + currentNameSpace.tidNext())
1055                 .idGenerator((sn, i) -> (currentNameSpace == startNamespace || state.status(sn).isActive())
1056                         ? currentNameSpace.tid(sn)
1057                         : errorNamespace.tid(sn))
1058                 .remoteVMOptions(options.remoteVmOptions())
1059                 .compilerOptions(options.compilerOptions());
1060         if (executionControlSpec != null) {
1061             builder.executionEngine(executionControlSpec);
1062         }
1063         state = builder.build();
1064         shutdownSubscription = state.onShutdown((JShell deadState) -> {
1065             if (deadState == state) {
1066                 hardmsg("jshell.msg.terminated");
1067                 fluffmsg("jshell.msg.terminated.restore");
1068                 live = false;
1069             }
1070         });
1071         analysis = state.sourceCodeAnalysis();
1072         live = true;
1073 
1074         // Run the start-up script.
1075         // Avoid an infinite loop running start-up while running start-up.
1076         // This could, otherwise, occur when /env /reset or /reload commands are
1077         // in the start-up script.
1078         if (!isCurrentlyRunningStartup) {
1079             try {
1080                 isCurrentlyRunningStartup = true;
1081                 startUpRun(startup.toString());
1082             } finally {
1083                 isCurrentlyRunningStartup = false;
1084             }
1085         }
1086         // Record subsequent snippets in the main namespace.
1087         currentNameSpace = mainNamespace;
1088     }
1089 
1090     //where -- one-time per run initialization of feedback modes
1091     private void initFeedback(String initMode) {
1092         // No fluff, no prefix, for init failures
1093         MessageHandler initmh = new InitMessageHandler();
1094         // Execute the feedback initialization code in the resource file
1095         startUpRun(getResourceString("startup.feedback"));
1096         // These predefined modes are read-only
1097         feedback.markModesReadOnly();
1098         // Restore user defined modes retained on previous run with /set mode -retain
1099         String encoded = prefs.get(MODE_KEY);
1100         if (encoded != null && !encoded.isEmpty()) {
1101             if (!feedback.restoreEncodedModes(initmh, encoded)) {
1102                 // Catastrophic corruption -- remove the retained modes
1103                 prefs.remove(MODE_KEY);
1104             }
1105         }
1106         if (initMode != null) {
1107             // The feedback mode to use was specified on the command line, use it
1108             if (!setFeedback(initmh, new ArgTokenizer("--feedback", initMode))) {
1109                 regenerateOnDeath = false;
1110                 exitCode = 1;
1111             }
1112         } else {
1113             String fb = prefs.get(FEEDBACK_KEY);
1114             if (fb != null) {
1115                 // Restore the feedback mode to use that was retained
1116                 // on a previous run with /set feedback -retain
1117                 setFeedback(initmh, new ArgTokenizer("previous retain feedback", "-retain " + fb));
1118             }
1119         }
1120     }
1121 
1122     //where
1123     private void startUpRun(String start) {
1124         try (IOContext suin = new ScannerIOContext(new StringReader(start))) {
1125             run(suin);
1126         } catch (Exception ex) {
1127             errormsg("jshell.err.startup.unexpected.exception", ex);
1128             ex.printStackTrace(cmderr);
1129         }
1130     }
1131 
1132     private void closeState() {
1133         live = false;
1134         JShell oldState = state;
1135         if (oldState != null) {
1136             state = null;
1137             analysis = null;
1138             oldState.unsubscribe(shutdownSubscription); // No notification
1139             oldState.close();
1140         }
1141     }
1142 
1143     /**
1144      * Main loop
1145      *
1146      * @param in the line input/editing context
1147      */
1148     private void run(IOContext in) {
1149         IOContext oldInput = input;
1150         input = in;
1151         try {
1152             // remaining is the source left after one snippet is evaluated
1153             String remaining = "";
1154             while (live) {
1155                 // Get a line(s) of input
1156                 String src = getInput(remaining);
1157                 // Process the snippet or command, returning the remaining source
1158                 remaining = processInput(src);
1159             }
1160         } catch (EOFException ex) {
1161             // Just exit loop
1162         } catch (IOException ex) {
1163             errormsg("jshell.err.unexpected.exception", ex);
1164         } finally {
1165             input = oldInput;
1166         }
1167     }
1168 
1169     /**
1170      * Process an input command or snippet.
1171      *
1172      * @param src the source to process
1173      * @return any remaining input to processed
1174      */
1175     private String processInput(String src) {
1176         if (isCommand(src)) {
1177             // It is a command
1178             processCommand(src.trim());
1179             // No remaining input after a command
1180             return "";
1181         } else {
1182             // It is a snipet. Separate the source from the remaining. Evaluate
1183             // the source
1184             CompletionInfo an = analysis.analyzeCompletion(src);
1185             if (processSourceCatchingReset(trimEnd(an.source()))) {
1186                 // Snippet was successful use any leftover source
1187                 return an.remaining();
1188             } else {
1189                 // Snippet failed, throw away any remaining source
1190                 return "";
1191             }
1192         }
1193     }
1194 
1195     /**
1196      * Get the input line (or, if incomplete, lines).
1197      *
1198      * @param initial leading input (left over after last snippet)
1199      * @return the complete input snippet or command
1200      * @throws IOException on unexpected I/O error
1201      */
1202     private String getInput(String initial) throws IOException{
1203         String src = initial;
1204         while (live) { // loop while incomplete (and live)
1205             if (!src.isEmpty()) {
1206                 // We have some source, see if it is complete, if so, use it
1207                 String check;
1208 
1209                 if (isCommand(src)) {
1210                     // A command can only be incomplete if it is a /exit with
1211                     // an argument
1212                     int sp = src.indexOf(" ");
1213                     if (sp < 0) return src;
1214                     check = src.substring(sp).trim();
1215                     if (check.isEmpty()) return src;
1216                     String cmd = src.substring(0, sp);
1217                     Command[] match = findCommand(cmd, c -> c.kind.isRealCommand);
1218                     if (match.length != 1 || !match[0].command.equals("/exit")) {
1219                         // A command with no snippet arg, so no multi-line input
1220                         return src;
1221                     }
1222                 } else {
1223                     // For a snippet check the whole source
1224                     check = src;
1225                 }
1226                 Completeness comp = analysis.analyzeCompletion(check).completeness();
1227                 if (comp.isComplete() || comp == Completeness.EMPTY) {
1228                     return src;
1229                 }
1230             }
1231             String prompt = interactive()
1232                     ? testPrompt
1233                             ? src.isEmpty()
1234                                     ? "\u0005" //ENQ -- test prompt
1235                                     : "\u0006" //ACK -- test continuation prompt
1236                             : src.isEmpty()
1237                                     ? feedback.getPrompt(currentNameSpace.tidNext())
1238                                     : feedback.getContinuationPrompt(currentNameSpace.tidNext())
1239                     : "" // Non-interactive -- no prompt
1240                     ;
1241             String line;
1242             try {
1243                 line = input.readLine(prompt, src);
1244             } catch (InputInterruptedException ex) {
1245                 //input interrupted - clearing current state
1246                 src = "";
1247                 continue;
1248             }
1249             if (line == null) {
1250                 //EOF
1251                 if (input.interactiveOutput()) {
1252                     // End after user ctrl-D
1253                     regenerateOnDeath = false;
1254                 }
1255                 throw new EOFException(); // no more input
1256             }
1257             src = src.isEmpty()
1258                     ? line
1259                     : src + "\n" + line;
1260         }
1261         throw new EOFException(); // not longer live
1262     }
1263 
1264     private boolean isCommand(String line) {
1265         return line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*");
1266     }
1267 
1268     private void addToReplayHistory(String s) {
1269         if (!isCurrentlyRunningStartup) {
1270             replayableHistory.add(s);
1271         }
1272     }
1273 
1274     /**
1275      * Process a source snippet.
1276      *
1277      * @param src the snippet source to process
1278      * @return true on success, false on failure
1279      */
1280     private boolean processSourceCatchingReset(String src) {
1281         try {
1282             input.beforeUserCode();
1283             return processSource(src);
1284         } catch (IllegalStateException ex) {
1285             hard("Resetting...");
1286             live = false; // Make double sure
1287             return false;
1288         } finally {
1289             input.afterUserCode();
1290         }
1291     }
1292 
1293     /**
1294      * Process a command (as opposed to a snippet) -- things that start with
1295      * slash.
1296      *
1297      * @param input
1298      */
1299     private void processCommand(String input) {
1300         if (input.startsWith("/-")) {
1301             try {
1302                 //handle "/-[number]"
1303                 cmdUseHistoryEntry(Integer.parseInt(input.substring(1)));
1304                 return ;
1305             } catch (NumberFormatException ex) {
1306                 //ignore
1307             }
1308         }
1309         String cmd;
1310         String arg;
1311         int idx = input.indexOf(' ');
1312         if (idx > 0) {
1313             arg = input.substring(idx + 1).trim();
1314             cmd = input.substring(0, idx);
1315         } else {
1316             cmd = input;
1317             arg = "";
1318         }
1319         // find the command as a "real command", not a pseudo-command or doc subject
1320         Command[] candidates = findCommand(cmd, c -> c.kind.isRealCommand);
1321         switch (candidates.length) {
1322             case 0:
1323                 // not found, it is either a rerun-ID command or an error
1324                 if (RERUN_ID.matcher(cmd).matches()) {
1325                     // it is in the form of a snipppet id, see if it is a valid history reference
1326                     rerunHistoryEntriesById(input);
1327                 } else {
1328                     errormsg("jshell.err.invalid.command", cmd);
1329                     fluffmsg("jshell.msg.help.for.help");
1330                 }
1331                 break;
1332             case 1:
1333                 Command command = candidates[0];
1334                 // If comand was successful and is of a replayable kind, add it the replayable history
1335                 if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) {
1336                     addToReplayHistory((command.command + " " + arg).trim());
1337                 }
1338                 break;
1339             default:
1340                 // command if too short (ambigous), show the possibly matches
1341                 errormsg("jshell.err.command.ambiguous", cmd,
1342                         Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", ")));
1343                 fluffmsg("jshell.msg.help.for.help");
1344                 break;
1345         }
1346     }
1347 
1348     private Command[] findCommand(String cmd, Predicate<Command> filter) {
1349         Command exact = commands.get(cmd);
1350         if (exact != null)
1351             return new Command[] {exact};
1352 
1353         return commands.values()
1354                        .stream()
1355                        .filter(filter)
1356                        .filter(command -> command.command.startsWith(cmd))
1357                        .toArray(Command[]::new);
1358     }
1359 
1360     static Path toPathResolvingUserHome(String pathString) {
1361         if (pathString.replace(File.separatorChar, '/').startsWith("~/"))
1362             return Paths.get(System.getProperty("user.home"), pathString.substring(2));
1363         else
1364             return Paths.get(pathString);
1365     }
1366 
1367     static final class Command {
1368         public final String command;
1369         public final String helpKey;
1370         public final Function<String,Boolean> run;
1371         public final CompletionProvider completions;
1372         public final CommandKind kind;
1373 
1374         // NORMAL Commands
1375         public Command(String command, Function<String,Boolean> run, CompletionProvider completions) {
1376             this(command, run, completions, CommandKind.NORMAL);
1377         }
1378 
1379         // Special kinds of Commands
1380         public Command(String command, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) {
1381             this(command, "help." + command.substring(1),
1382                     run, completions, kind);
1383         }
1384 
1385         // Documentation pseudo-commands
1386         public Command(String command, String helpKey, CommandKind kind) {
1387             this(command, helpKey,
1388                     arg -> { throw new IllegalStateException(); },
1389                     EMPTY_COMPLETION_PROVIDER,
1390                     kind);
1391         }
1392 
1393         public Command(String command, String helpKey, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) {
1394             this.command = command;
1395             this.helpKey = helpKey;
1396             this.run = run;
1397             this.completions = completions;
1398             this.kind = kind;
1399         }
1400 
1401     }
1402 
1403     interface CompletionProvider {
1404         List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);
1405 
1406     }
1407 
1408     enum CommandKind {
1409         NORMAL(true, true, true),
1410         REPLAY(true, true, true),
1411         HIDDEN(true, false, false),
1412         HELP_ONLY(false, true, false),
1413         HELP_SUBJECT(false, false, false);
1414 
1415         final boolean isRealCommand;
1416         final boolean showInHelp;
1417         final boolean shouldSuggestCompletions;
1418         private CommandKind(boolean isRealCommand, boolean showInHelp, boolean shouldSuggestCompletions) {
1419             this.isRealCommand = isRealCommand;
1420             this.showInHelp = showInHelp;
1421             this.shouldSuggestCompletions = shouldSuggestCompletions;
1422         }
1423     }
1424 
1425     static final class FixedCompletionProvider implements CompletionProvider {
1426 
1427         private final String[] alternatives;
1428 
1429         public FixedCompletionProvider(String... alternatives) {
1430             this.alternatives = alternatives;
1431         }
1432 
1433         // Add more options to an existing provider
1434         public FixedCompletionProvider(FixedCompletionProvider base, String... alternatives) {
1435             List<String> l = new ArrayList<>(Arrays.asList(base.alternatives));
1436             l.addAll(Arrays.asList(alternatives));
1437             this.alternatives = l.toArray(new String[l.size()]);
1438         }
1439 
1440         @Override
1441         public List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor) {
1442             List<Suggestion> result = new ArrayList<>();
1443 
1444             for (String alternative : alternatives) {
1445                 if (alternative.startsWith(input)) {
1446                     result.add(new ArgSuggestion(alternative));
1447                 }
1448             }
1449 
1450             anchor[0] = 0;
1451 
1452             return result;
1453         }
1454 
1455     }
1456 
1457     static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider();
1458     private static final CompletionProvider SNIPPET_HISTORY_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all", "-start ", "-history");
1459     private static final CompletionProvider SAVE_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all ", "-start ", "-history ");
1460     private static final CompletionProvider SNIPPET_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all", "-start " );
1461     private static final FixedCompletionProvider COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider(
1462             "-class-path ", "-module-path ", "-add-modules ", "-add-exports ");
1463     private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider(
1464             COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER,
1465             "-restore ", "-quiet ");
1466     private static final CompletionProvider SET_MODE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-command", "-quiet", "-delete");
1467     private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true);
1468     private static final Map<String, CompletionProvider> ARG_OPTIONS = new HashMap<>();
1469     static {
1470         ARG_OPTIONS.put("-class-path", classPathCompletion());
1471         ARG_OPTIONS.put("-module-path", fileCompletions(Files::isDirectory));
1472         ARG_OPTIONS.put("-add-modules", EMPTY_COMPLETION_PROVIDER);
1473         ARG_OPTIONS.put("-add-exports", EMPTY_COMPLETION_PROVIDER);
1474     }
1475     private final Map<String, Command> commands = new LinkedHashMap<>();
1476     private void registerCommand(Command cmd) {
1477         commands.put(cmd.command, cmd);
1478     }
1479 
1480     private static CompletionProvider skipWordThenCompletion(CompletionProvider completionProvider) {
1481         return (input, cursor, anchor) -> {
1482             List<Suggestion> result = Collections.emptyList();
1483 
1484             int space = input.indexOf(' ');
1485             if (space != -1) {
1486                 String rest = input.substring(space + 1);
1487                 result = completionProvider.completionSuggestions(rest, cursor - space - 1, anchor);
1488                 anchor[0] += space + 1;
1489             }
1490 
1491             return result;
1492         };
1493     }
1494 
1495     private static CompletionProvider fileCompletions(Predicate<Path> accept) {
1496         return (code, cursor, anchor) -> {
1497             int lastSlash = code.lastIndexOf('/');
1498             String path = code.substring(0, lastSlash + 1);
1499             String prefix = lastSlash != (-1) ? code.substring(lastSlash + 1) : code;
1500             Path current = toPathResolvingUserHome(path);
1501             List<Suggestion> result = new ArrayList<>();
1502             try (Stream<Path> dir = Files.list(current)) {
1503                 dir.filter(f -> accept.test(f) && f.getFileName().toString().startsWith(prefix))
1504                    .map(f -> new ArgSuggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : "")))
1505                    .forEach(result::add);
1506             } catch (IOException ex) {
1507                 //ignore...
1508             }
1509             if (path.isEmpty()) {
1510                 StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false)
1511                              .filter(root -> Files.exists(root))
1512                              .filter(root -> accept.test(root) && root.toString().startsWith(prefix))
1513                              .map(root -> new ArgSuggestion(root.toString()))
1514                              .forEach(result::add);
1515             }
1516             anchor[0] = path.length();
1517             return result;
1518         };
1519     }
1520 
1521     private static CompletionProvider classPathCompletion() {
1522         return fileCompletions(p -> Files.isDirectory(p) ||
1523                                     p.getFileName().toString().endsWith(".zip") ||
1524                                     p.getFileName().toString().endsWith(".jar"));
1525     }
1526 
1527     // Completion based on snippet supplier
1528     private CompletionProvider snippetCompletion(Supplier<Stream<? extends Snippet>> snippetsSupplier) {
1529         return (prefix, cursor, anchor) -> {
1530             anchor[0] = 0;
1531             int space = prefix.lastIndexOf(' ');
1532             Set<String> prior = new HashSet<>(Arrays.asList(prefix.split(" ")));
1533             if (prior.contains("-all") || prior.contains("-history")) {
1534                 return Collections.emptyList();
1535             }
1536             String argPrefix = prefix.substring(space + 1);
1537             return snippetsSupplier.get()
1538                         .filter(k -> !prior.contains(String.valueOf(k.id()))
1539                                 && (!(k instanceof DeclarationSnippet)
1540                                      || !prior.contains(((DeclarationSnippet) k).name())))
1541                         .flatMap(k -> (k instanceof DeclarationSnippet)
1542                                 ? Stream.of(String.valueOf(k.id()) + " ", ((DeclarationSnippet) k).name() + " ")
1543                                 : Stream.of(String.valueOf(k.id()) + " "))
1544                         .filter(k -> k.startsWith(argPrefix))
1545                         .map(ArgSuggestion::new)
1546                         .collect(Collectors.toList());
1547         };
1548     }
1549 
1550     // Completion based on snippet supplier with -all -start (and sometimes -history) options
1551     private CompletionProvider snippetWithOptionCompletion(CompletionProvider optionProvider,
1552             Supplier<Stream<? extends Snippet>> snippetsSupplier) {
1553         return (code, cursor, anchor) -> {
1554             List<Suggestion> result = new ArrayList<>();
1555             int pastSpace = code.lastIndexOf(' ') + 1; // zero if no space
1556             if (pastSpace == 0) {
1557                 result.addAll(optionProvider.completionSuggestions(code, cursor, anchor));
1558             }
1559             result.addAll(snippetCompletion(snippetsSupplier).completionSuggestions(code, cursor, anchor));
1560             anchor[0] += pastSpace;
1561             return result;
1562         };
1563     }
1564 
1565     // Completion of help, commands and subjects
1566     private CompletionProvider helpCompletion() {
1567         return (code, cursor, anchor) -> {
1568             List<Suggestion> result;
1569             int pastSpace = code.indexOf(' ') + 1; // zero if no space
1570             if (pastSpace == 0) {
1571                 // initially suggest commands (with slash) and subjects,
1572                 // however, if their subject starts without slash, include
1573                 // commands without slash
1574                 boolean noslash = code.length() > 0 && !code.startsWith("/");
1575                 result = new FixedCompletionProvider(commands.values().stream()
1576                         .filter(cmd -> cmd.kind.showInHelp || cmd.kind == CommandKind.HELP_SUBJECT)
1577                         .map(c -> ((noslash && c.command.startsWith("/"))
1578                                 ? c.command.substring(1)
1579                                 : c.command) + " ")
1580                         .toArray(String[]::new))
1581                         .completionSuggestions(code, cursor, anchor);
1582             } else if (code.startsWith("/se") || code.startsWith("se")) {
1583                 result = new FixedCompletionProvider(SET_SUBCOMMANDS)
1584                         .completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor);
1585             } else {
1586                 result = Collections.emptyList();
1587             }
1588             anchor[0] += pastSpace;
1589             return result;
1590         };
1591     }
1592 
1593     private static CompletionProvider saveCompletion() {
1594         return (code, cursor, anchor) -> {
1595             List<Suggestion> result = new ArrayList<>();
1596             int space = code.indexOf(' ');
1597             if (space == (-1)) {
1598                 result.addAll(SAVE_OPTION_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor));
1599             }
1600             result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor));
1601             anchor[0] += space + 1;
1602             return result;
1603         };
1604     }
1605 
1606     // command-line-like option completion -- options with values
1607     private static CompletionProvider optionCompletion(CompletionProvider provider) {
1608         return (code, cursor, anchor) -> {
1609             Matcher ovm = OPTION_VALUE_PATTERN.matcher(code);
1610             if (ovm.matches()) {
1611                 String flag = ovm.group("flag");
1612                 List<CompletionProvider> ps = ARG_OPTIONS.entrySet().stream()
1613                         .filter(es -> es.getKey().startsWith(flag))
1614                         .map(es -> es.getValue())
1615                         .collect(toList());
1616                 if (ps.size() == 1) {
1617                     int pastSpace = ovm.start("val");
1618                     List<Suggestion> result = ps.get(0).completionSuggestions(
1619                             ovm.group("val"), cursor - pastSpace, anchor);
1620                     anchor[0] += pastSpace;
1621                     return result;
1622                 }
1623             }
1624             Matcher om = OPTION_PATTERN.matcher(code);
1625             if (om.matches()) {
1626                 int pastSpace = om.start("flag");
1627                 List<Suggestion> result = provider.completionSuggestions(
1628                         om.group("flag"), cursor - pastSpace, anchor);
1629                 if (!om.group("dd").isEmpty()) {
1630                     result = result.stream()
1631                             .map(sug -> new Suggestion() {
1632                                 @Override
1633                                 public String continuation() {
1634                                     return "-" + sug.continuation();
1635                                 }
1636 
1637                                 @Override
1638                                 public boolean matchesType() {
1639                                     return false;
1640                                 }
1641                             })
1642                             .collect(toList());
1643                     --pastSpace;
1644                 }
1645                 anchor[0] += pastSpace;
1646                 return result;
1647             }
1648             Matcher opp = OPTION_PRE_PATTERN.matcher(code);
1649             if (opp.matches()) {
1650                 int pastSpace = opp.end();
1651                 List<Suggestion> result = provider.completionSuggestions(
1652                         "", cursor - pastSpace, anchor);
1653                 anchor[0] += pastSpace;
1654                 return result;
1655             }
1656             return Collections.emptyList();
1657         };
1658     }
1659 
1660     // /reload command completion
1661     private static CompletionProvider reloadCompletion() {
1662         return optionCompletion(RELOAD_OPTIONS_COMPLETION_PROVIDER);
1663     }
1664 
1665     // /env command completion
1666     private static CompletionProvider envCompletion() {
1667         return optionCompletion(COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER);
1668     }
1669 
1670     private static CompletionProvider orMostSpecificCompletion(
1671             CompletionProvider left, CompletionProvider right) {
1672         return (code, cursor, anchor) -> {
1673             int[] leftAnchor = {-1};
1674             int[] rightAnchor = {-1};
1675 
1676             List<Suggestion> leftSuggestions = left.completionSuggestions(code, cursor, leftAnchor);
1677             List<Suggestion> rightSuggestions = right.completionSuggestions(code, cursor, rightAnchor);
1678 
1679             List<Suggestion> suggestions = new ArrayList<>();
1680 
1681             if (leftAnchor[0] >= rightAnchor[0]) {
1682                 anchor[0] = leftAnchor[0];
1683                 suggestions.addAll(leftSuggestions);
1684             }
1685 
1686             if (leftAnchor[0] <= rightAnchor[0]) {
1687                 anchor[0] = rightAnchor[0];
1688                 suggestions.addAll(rightSuggestions);
1689             }
1690 
1691             return suggestions;
1692         };
1693     }
1694 
1695     // Snippet lists
1696 
1697     Stream<Snippet> allSnippets() {
1698         return state.snippets();
1699     }
1700 
1701     Stream<Snippet> dropableSnippets() {
1702         return state.snippets()
1703                 .filter(sn -> state.status(sn).isActive());
1704     }
1705 
1706     Stream<VarSnippet> allVarSnippets() {
1707         return state.snippets()
1708                 .filter(sn -> sn.kind() == Snippet.Kind.VAR)
1709                 .map(sn -> (VarSnippet) sn);
1710     }
1711 
1712     Stream<MethodSnippet> allMethodSnippets() {
1713         return state.snippets()
1714                 .filter(sn -> sn.kind() == Snippet.Kind.METHOD)
1715                 .map(sn -> (MethodSnippet) sn);
1716     }
1717 
1718     Stream<TypeDeclSnippet> allTypeSnippets() {
1719         return state.snippets()
1720                 .filter(sn -> sn.kind() == Snippet.Kind.TYPE_DECL)
1721                 .map(sn -> (TypeDeclSnippet) sn);
1722     }
1723 
1724     // Table of commands -- with command forms, argument kinds, helpKey message, implementation, ...
1725 
1726     {
1727         registerCommand(new Command("/list",
1728                 this::cmdList,
1729                 snippetWithOptionCompletion(SNIPPET_HISTORY_OPTION_COMPLETION_PROVIDER,
1730                         this::allSnippets)));
1731         registerCommand(new Command("/edit",
1732                 this::cmdEdit,
1733                 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
1734                         this::allSnippets)));
1735         registerCommand(new Command("/drop",
1736                 this::cmdDrop,
1737                 snippetCompletion(this::dropableSnippets),
1738                 CommandKind.REPLAY));
1739         registerCommand(new Command("/save",
1740                 this::cmdSave,
1741                 saveCompletion()));
1742         registerCommand(new Command("/open",
1743                 this::cmdOpen,
1744                 FILE_COMPLETION_PROVIDER));
1745         registerCommand(new Command("/vars",
1746                 this::cmdVars,
1747                 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
1748                         this::allVarSnippets)));
1749         registerCommand(new Command("/methods",
1750                 this::cmdMethods,
1751                 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
1752                         this::allMethodSnippets)));
1753         registerCommand(new Command("/types",
1754                 this::cmdTypes,
1755                 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER,
1756                         this::allTypeSnippets)));
1757         registerCommand(new Command("/imports",
1758                 arg -> cmdImports(),
1759                 EMPTY_COMPLETION_PROVIDER));
1760         registerCommand(new Command("/exit",
1761                 arg -> cmdExit(arg),
1762                 (sn, c, a) -> {
1763                     if (analysis == null || sn.isEmpty()) {
1764                         // No completions if uninitialized or snippet not started
1765                         return Collections.emptyList();
1766                     } else {
1767                         // Give exit code an int context by prefixing the arg
1768                         List<Suggestion> suggestions = analysis.completionSuggestions(INT_PREFIX + sn,
1769                                 INT_PREFIX.length() + c, a);
1770                         a[0] -= INT_PREFIX.length();
1771                         return suggestions;
1772                     }
1773                 }));
1774         registerCommand(new Command("/env",
1775                 arg -> cmdEnv(arg),
1776                 envCompletion()));
1777         registerCommand(new Command("/reset",
1778                 arg -> cmdReset(arg),
1779                 envCompletion()));
1780         registerCommand(new Command("/reload",
1781                 this::cmdReload,
1782                 reloadCompletion()));
1783         registerCommand(new Command("/history",
1784                 arg -> cmdHistory(),
1785                 EMPTY_COMPLETION_PROVIDER));
1786         registerCommand(new Command("/debug",
1787                 this::cmdDebug,
1788                 EMPTY_COMPLETION_PROVIDER,
1789                 CommandKind.HIDDEN));
1790         registerCommand(new Command("/help",
1791                 this::cmdHelp,
1792                 helpCompletion()));
1793         registerCommand(new Command("/set",
1794                 this::cmdSet,
1795                 new ContinuousCompletionProvider(Map.of(
1796                         // need more completion for format for usability
1797                         "format", feedback.modeCompletions(),
1798                         "truncation", feedback.modeCompletions(),
1799                         "feedback", feedback.modeCompletions(),
1800                         "mode", skipWordThenCompletion(orMostSpecificCompletion(
1801                                 feedback.modeCompletions(SET_MODE_OPTIONS_COMPLETION_PROVIDER),
1802                                 SET_MODE_OPTIONS_COMPLETION_PROVIDER)),
1803                         "prompt", feedback.modeCompletions(),
1804                         "editor", fileCompletions(Files::isExecutable),
1805                         "start", FILE_COMPLETION_PROVIDER),
1806                         STARTSWITH_MATCHER)));
1807         registerCommand(new Command("/?",
1808                 "help.quest",
1809                 this::cmdHelp,
1810                 helpCompletion(),
1811                 CommandKind.NORMAL));
1812         registerCommand(new Command("/!",
1813                 "help.bang",
1814                 arg -> cmdUseHistoryEntry(-1),
1815                 EMPTY_COMPLETION_PROVIDER,
1816                 CommandKind.NORMAL));
1817 
1818         // Documentation pseudo-commands
1819         registerCommand(new Command("/<id>",
1820                 "help.slashID",
1821                 arg -> cmdHelp("rerun"),
1822                 EMPTY_COMPLETION_PROVIDER,
1823                 CommandKind.HELP_ONLY));
1824         registerCommand(new Command("/-<n>",
1825                 "help.previous",
1826                 arg -> cmdHelp("rerun"),
1827                 EMPTY_COMPLETION_PROVIDER,
1828                 CommandKind.HELP_ONLY));
1829         registerCommand(new Command("intro",
1830                 "help.intro",
1831                 CommandKind.HELP_SUBJECT));
1832         registerCommand(new Command("id",
1833                 "help.id",
1834                 CommandKind.HELP_SUBJECT));
1835         registerCommand(new Command("shortcuts",
1836                 "help.shortcuts",
1837                 CommandKind.HELP_SUBJECT));
1838         registerCommand(new Command("context",
1839                 "help.context",
1840                 CommandKind.HELP_SUBJECT));
1841         registerCommand(new Command("rerun",
1842                 "help.rerun",
1843                 CommandKind.HELP_SUBJECT));
1844 
1845         commandCompletions = new ContinuousCompletionProvider(
1846                 commands.values().stream()
1847                         .filter(c -> c.kind.shouldSuggestCompletions)
1848                         .collect(toMap(c -> c.command, c -> c.completions)),
1849                 STARTSWITH_MATCHER);
1850     }
1851 
1852     private ContinuousCompletionProvider commandCompletions;
1853 
1854     public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) {
1855         return commandCompletions.completionSuggestions(code, cursor, anchor);
1856     }
1857 
1858     public List<String> commandDocumentation(String code, int cursor, boolean shortDescription) {
1859         code = code.substring(0, cursor).replaceAll("\\h+", " ");
1860         String stripped = code.replaceFirst("/(he(lp?)?|\\?) ", "");
1861         boolean inHelp = !code.equals(stripped);
1862         int space = stripped.indexOf(' ');
1863         String prefix = space != (-1) ? stripped.substring(0, space) : stripped;
1864         List<String> result = new ArrayList<>();
1865 
1866         List<Entry<String, String>> toShow;
1867 
1868         if (SET_SUB.matcher(stripped).matches()) {
1869             String setSubcommand = stripped.replaceFirst("/?set ([^ ]*)($| .*)", "$1");
1870             toShow =
1871                 Arrays.stream(SET_SUBCOMMANDS)
1872                        .filter(s -> s.startsWith(setSubcommand))
1873                         .map(s -> new SimpleEntry<>("/set " + s, "help.set." + s))
1874                         .collect(toList());
1875         } else if (RERUN_ID.matcher(stripped).matches()) {
1876             toShow =
1877                 singletonList(new SimpleEntry<>("/<id>", "help.rerun"));
1878         } else if (RERUN_PREVIOUS.matcher(stripped).matches()) {
1879             toShow =
1880                 singletonList(new SimpleEntry<>("/-<n>", "help.rerun"));
1881         } else {
1882             toShow =
1883                 commands.values()
1884                         .stream()
1885                         .filter(c -> c.command.startsWith(prefix)
1886                                   || c.command.substring(1).startsWith(prefix))
1887                         .filter(c -> c.kind.showInHelp
1888                                   || (inHelp && c.kind == CommandKind.HELP_SUBJECT))
1889                         .sorted((c1, c2) -> c1.command.compareTo(c2.command))
1890                         .map(c -> new SimpleEntry<>(c.command, c.helpKey))
1891                         .collect(toList());
1892         }
1893 
1894         if (toShow.size() == 1 && !inHelp) {
1895             result.add(getResourceString(toShow.get(0).getValue() + (shortDescription ? ".summary" : "")));
1896         } else {
1897             for (Entry<String, String> e : toShow) {
1898                 result.add(e.getKey() + "\n" + getResourceString(e.getValue() + (shortDescription ? ".summary" : "")));
1899             }
1900         }
1901 
1902         return result;
1903     }
1904 
1905     // Attempt to stop currently running evaluation
1906     void stop() {
1907         state.stop();
1908     }
1909 
1910     // --- Command implementations ---
1911 
1912     private static final String[] SET_SUBCOMMANDS = new String[]{
1913         "format", "truncation", "feedback", "mode", "prompt", "editor", "start"};
1914 
1915     final boolean cmdSet(String arg) {
1916         String cmd = "/set";
1917         ArgTokenizer at = new ArgTokenizer(cmd, arg.trim());
1918         String which = subCommand(cmd, at, SET_SUBCOMMANDS);
1919         if (which == null) {
1920             return false;
1921         }
1922         switch (which) {
1923             case "_retain": {
1924                 errormsg("jshell.err.setting.to.retain.must.be.specified", at.whole());
1925                 return false;
1926             }
1927             case "_blank": {
1928                 // show top-level settings
1929                 new SetEditor().set();
1930                 showSetStart();
1931                 setFeedback(this, at); // no args so shows feedback setting
1932                 hardmsg("jshell.msg.set.show.mode.settings");
1933                 return true;
1934             }
1935             case "format":
1936                 return feedback.setFormat(this, at);
1937             case "truncation":
1938                 return feedback.setTruncation(this, at);
1939             case "feedback":
1940                 return setFeedback(this, at);
1941             case "mode":
1942                 return feedback.setMode(this, at,
1943                         retained -> prefs.put(MODE_KEY, retained));
1944             case "prompt":
1945                 return feedback.setPrompt(this, at);
1946             case "editor":
1947                 return new SetEditor(at).set();
1948             case "start":
1949                 return setStart(at);
1950             default:
1951                 errormsg("jshell.err.arg", cmd, at.val());
1952                 return false;
1953         }
1954     }
1955 
1956     boolean setFeedback(MessageHandler messageHandler, ArgTokenizer at) {
1957         return feedback.setFeedback(messageHandler, at,
1958                 fb -> prefs.put(FEEDBACK_KEY, fb));
1959     }
1960 
1961     // Find which, if any, sub-command matches.
1962     // Return null on error
1963     String subCommand(String cmd, ArgTokenizer at, String[] subs) {
1964         at.allowedOptions("-retain");
1965         String sub = at.next();
1966         if (sub == null) {
1967             // No sub-command was given
1968             return at.hasOption("-retain")
1969                     ? "_retain"
1970                     : "_blank";
1971         }
1972         String[] matches = Arrays.stream(subs)
1973                 .filter(s -> s.startsWith(sub))
1974                 .toArray(String[]::new);
1975         if (matches.length == 0) {
1976             // There are no matching sub-commands
1977             errormsg("jshell.err.arg", cmd, sub);
1978             fluffmsg("jshell.msg.use.one.of", Arrays.stream(subs)
1979                     .collect(Collectors.joining(", "))
1980             );
1981             return null;
1982         }
1983         if (matches.length > 1) {
1984             // More than one sub-command matches the initial characters provided
1985             errormsg("jshell.err.sub.ambiguous", cmd, sub);
1986             fluffmsg("jshell.msg.use.one.of", Arrays.stream(matches)
1987                     .collect(Collectors.joining(", "))
1988             );
1989             return null;
1990         }
1991         return matches[0];
1992     }
1993 
1994     static class EditorSetting {
1995 
1996         static String BUILT_IN_REP = "-default";
1997         static char WAIT_PREFIX = '-';
1998         static char NORMAL_PREFIX = '*';
1999 
2000         final String[] cmd;
2001         final boolean wait;
2002 
2003         EditorSetting(String[] cmd, boolean wait) {
2004             this.wait = wait;
2005             this.cmd = cmd;
2006         }
2007 
2008         // returns null if not stored in preferences
2009         static EditorSetting fromPrefs(PersistentStorage prefs) {
2010             // Read retained editor setting (if any)
2011             String editorString = prefs.get(EDITOR_KEY);
2012             if (editorString == null || editorString.isEmpty()) {
2013                 return null;
2014             } else if (editorString.equals(BUILT_IN_REP)) {
2015                 return BUILT_IN_EDITOR;
2016             } else {
2017                 boolean wait = false;
2018                 char waitMarker = editorString.charAt(0);
2019                 if (waitMarker == WAIT_PREFIX || waitMarker == NORMAL_PREFIX) {
2020                     wait = waitMarker == WAIT_PREFIX;
2021                     editorString = editorString.substring(1);
2022                 }
2023                 String[] cmd = editorString.split(RECORD_SEPARATOR);
2024                 return new EditorSetting(cmd, wait);
2025             }
2026         }
2027 
2028         static void removePrefs(PersistentStorage prefs) {
2029             prefs.remove(EDITOR_KEY);
2030         }
2031 
2032         void toPrefs(PersistentStorage prefs) {
2033             prefs.put(EDITOR_KEY, (this == BUILT_IN_EDITOR)
2034                     ? BUILT_IN_REP
2035                     : (wait ? WAIT_PREFIX : NORMAL_PREFIX) + String.join(RECORD_SEPARATOR, cmd));
2036         }
2037 
2038         @Override
2039         public boolean equals(Object o) {
2040             if (o instanceof EditorSetting) {
2041                 EditorSetting ed = (EditorSetting) o;
2042                 return Arrays.equals(cmd, ed.cmd) && wait == ed.wait;
2043             } else {
2044                 return false;
2045             }
2046         }
2047 
2048         @Override
2049         public int hashCode() {
2050             int hash = 7;
2051             hash = 71 * hash + Arrays.deepHashCode(this.cmd);
2052             hash = 71 * hash + (this.wait ? 1 : 0);
2053             return hash;
2054         }
2055     }
2056 
2057     class SetEditor {
2058 
2059         private final ArgTokenizer at;
2060         private final String[] command;
2061         private final boolean hasCommand;
2062         private final boolean defaultOption;
2063         private final boolean deleteOption;
2064         private final boolean waitOption;
2065         private final boolean retainOption;
2066         private final int primaryOptionCount;
2067 
2068         SetEditor(ArgTokenizer at) {
2069             at.allowedOptions("-default", "-wait", "-retain", "-delete");
2070             String prog = at.next();
2071             List<String> ed = new ArrayList<>();
2072             while (at.val() != null) {
2073                 ed.add(at.val());
2074                 at.nextToken();  // so that options are not interpreted as jshell options
2075             }
2076             this.at = at;
2077             this.command = ed.toArray(new String[ed.size()]);
2078             this.hasCommand = command.length > 0;
2079             this.defaultOption = at.hasOption("-default");
2080             this.deleteOption = at.hasOption("-delete");
2081             this.waitOption = at.hasOption("-wait");
2082             this.retainOption = at.hasOption("-retain");
2083             this.primaryOptionCount = (hasCommand? 1 : 0) + (defaultOption? 1 : 0) + (deleteOption? 1 : 0);
2084         }
2085 
2086         SetEditor() {
2087             this(new ArgTokenizer("", ""));
2088         }
2089 
2090         boolean set() {
2091             if (!check()) {
2092                 return false;
2093             }
2094             if (primaryOptionCount == 0 && !retainOption) {
2095                 // No settings or -retain, so this is a query
2096                 EditorSetting retained = EditorSetting.fromPrefs(prefs);
2097                 if (retained != null) {
2098                     // retained editor is set
2099                     hard("/set editor -retain %s", format(retained));
2100                 }
2101                 if (retained == null || !retained.equals(editor)) {
2102                     // editor is not retained or retained is different from set
2103                     hard("/set editor %s", format(editor));
2104                 }
2105                 return true;
2106             }
2107             if (retainOption && deleteOption) {
2108                 EditorSetting.removePrefs(prefs);
2109             }
2110             install();
2111             if (retainOption && !deleteOption) {
2112                 editor.toPrefs(prefs);
2113                 fluffmsg("jshell.msg.set.editor.retain", format(editor));
2114             }
2115             return true;
2116         }
2117 
2118         private boolean check() {
2119             if (!checkOptionsAndRemainingInput(at)) {
2120                 return false;
2121             }
2122             if (primaryOptionCount > 1) {
2123                 errormsg("jshell.err.default.option.or.program", at.whole());
2124                 return false;
2125             }
2126             if (waitOption && !hasCommand) {
2127                 errormsg("jshell.err.wait.applies.to.external.editor", at.whole());
2128                 return false;
2129             }
2130             return true;
2131         }
2132 
2133         private void install() {
2134             if (hasCommand) {
2135                 editor = new EditorSetting(command, waitOption);
2136             } else if (defaultOption) {
2137                 editor = BUILT_IN_EDITOR;
2138             } else if (deleteOption) {
2139                 configEditor();
2140             } else {
2141                 return;
2142             }
2143             fluffmsg("jshell.msg.set.editor.set", format(editor));
2144         }
2145 
2146         private String format(EditorSetting ed) {
2147             if (ed == BUILT_IN_EDITOR) {
2148                 return "-default";
2149             } else {
2150                 Stream<String> elems = Arrays.stream(ed.cmd);
2151                 if (ed.wait) {
2152                     elems = Stream.concat(Stream.of("-wait"), elems);
2153                 }
2154                 return elems.collect(joining(" "));
2155             }
2156         }
2157     }
2158 
2159     // The sub-command:  /set start <start-file>
2160     boolean setStart(ArgTokenizer at) {
2161         at.allowedOptions("-default", "-none", "-retain");
2162         List<String> fns = new ArrayList<>();
2163         while (at.next() != null) {
2164             fns.add(at.val());
2165         }
2166         if (!checkOptionsAndRemainingInput(at)) {
2167             return false;
2168         }
2169         boolean defaultOption = at.hasOption("-default");
2170         boolean noneOption = at.hasOption("-none");
2171         boolean retainOption = at.hasOption("-retain");
2172         boolean hasFile = !fns.isEmpty();
2173 
2174         int argCount = (defaultOption ? 1 : 0) + (noneOption ? 1 : 0) + (hasFile ? 1 : 0);
2175         if (argCount > 1) {
2176             errormsg("jshell.err.option.or.filename", at.whole());
2177             return false;
2178         }
2179         if (argCount == 0 && !retainOption) {
2180             // no options or filename, show current setting
2181             showSetStart();
2182             return true;
2183         }
2184         if (hasFile) {
2185             startup = Startup.fromFileList(fns, "/set start", this);
2186             if (startup == null) {
2187                 return false;
2188             }
2189         } else if (defaultOption) {
2190             startup = Startup.defaultStartup(this);
2191         } else if (noneOption) {
2192             startup = Startup.noStartup();
2193         }
2194         if (retainOption) {
2195             // retain startup setting
2196             prefs.put(STARTUP_KEY, startup.storedForm());
2197         }
2198         return true;
2199     }
2200 
2201     // show the "/set start" settings (retained and, if different, current)
2202     // as commands (and file contents).  All commands first, then contents.
2203     void showSetStart() {
2204         StringBuilder sb = new StringBuilder();
2205         String retained = prefs.get(STARTUP_KEY);
2206         if (retained != null) {
2207             Startup retainedStart = Startup.unpack(retained, this);
2208             boolean currentDifferent = !startup.equals(retainedStart);
2209             sb.append(retainedStart.show(true));
2210             if (currentDifferent) {
2211                 sb.append(startup.show(false));
2212             }
2213             sb.append(retainedStart.showDetail());
2214             if (currentDifferent) {
2215                 sb.append(startup.showDetail());
2216             }
2217         } else {
2218             sb.append(startup.show(false));
2219             sb.append(startup.showDetail());
2220         }
2221         hard(sb.toString());
2222     }
2223 
2224     boolean cmdDebug(String arg) {
2225         if (arg.isEmpty()) {
2226             debug = !debug;
2227             InternalDebugControl.setDebugFlags(state, debug ? DBG_GEN : 0);
2228             fluff("Debugging %s", debug ? "on" : "off");
2229         } else {
2230             int flags = 0;
2231             for (char ch : arg.toCharArray()) {
2232                 switch (ch) {
2233                     case '0':
2234                         flags = 0;
2235                         debug = false;
2236                         fluff("Debugging off");
2237                         break;
2238                     case 'r':
2239                         debug = true;
2240                         fluff("REPL tool debugging on");
2241                         break;
2242                     case 'g':
2243                         flags |= DBG_GEN;
2244                         fluff("General debugging on");
2245                         break;
2246                     case 'f':
2247                         flags |= DBG_FMGR;
2248                         fluff("File manager debugging on");
2249                         break;
2250                     case 'c':
2251                         flags |= DBG_COMPA;
2252                         fluff("Completion analysis debugging on");
2253                         break;
2254                     case 'd':
2255                         flags |= DBG_DEP;
2256                         fluff("Dependency debugging on");
2257                         break;
2258                     case 'e':
2259                         flags |= DBG_EVNT;
2260                         fluff("Event debugging on");
2261                         break;
2262                     case 'w':
2263                         flags |= DBG_WRAP;
2264                         fluff("Wrap debugging on");
2265                         break;
2266                     default:
2267                         error("Unknown debugging option: %c", ch);
2268                         fluff("Use: 0 r g f c d e w");
2269                         return false;
2270                 }
2271             }
2272             InternalDebugControl.setDebugFlags(state, flags);
2273         }
2274         return true;
2275     }
2276 
2277     private boolean cmdExit(String arg) {
2278         if (!arg.trim().isEmpty()) {
2279             debug("Compiling exit: %s", arg);
2280             List<SnippetEvent> events = state.eval(arg);
2281             for (SnippetEvent e : events) {
2282                 // Only care about main snippet
2283                 if (e.causeSnippet() == null) {
2284                     Snippet sn = e.snippet();
2285 
2286                     // Show any diagnostics
2287                     List<Diag> diagnostics = state.diagnostics(sn).collect(toList());
2288                     String source = sn.source();
2289                     displayDiagnostics(source, diagnostics);
2290 
2291                     // Show any exceptions
2292                     if (e.exception() != null && e.status() != Status.REJECTED) {
2293                         if (displayException(e.exception())) {
2294                             // Abort: an exception occurred (reported)
2295                             return false;
2296                         }
2297                     }
2298 
2299                     if (e.status() != Status.VALID) {
2300                         // Abort: can only use valid snippets, diagnostics have been reported (above)
2301                         return false;
2302                     }
2303                     String typeName;
2304                     if (sn.kind() == Kind.EXPRESSION) {
2305                         typeName = ((ExpressionSnippet) sn).typeName();
2306                     } else if (sn.subKind() == TEMP_VAR_EXPRESSION_SUBKIND) {
2307                         typeName = ((VarSnippet) sn).typeName();
2308                     } else {
2309                         // Abort: not an expression
2310                         errormsg("jshell.err.exit.not.expression", arg);
2311                         return false;
2312                     }
2313                     switch (typeName) {
2314                         case "int":
2315                         case "Integer":
2316                         case "byte":
2317                         case "Byte":
2318                         case "short":
2319                         case "Short":
2320                             try {
2321                                 int i = Integer.parseInt(e.value());
2322                                 /**
2323                                 addToReplayHistory("/exit " + arg);
2324                                 replayableHistory.storeHistory(prefs);
2325                                 closeState();
2326                                 try {
2327                                     input.close();
2328                                 } catch (Exception exc) {
2329                                     // ignore
2330                                 }
2331                                 * **/
2332                                 exitCode = i;
2333                                 break;
2334                             } catch (NumberFormatException exc) {
2335                                 // Abort: bad value
2336                                 errormsg("jshell.err.exit.bad.value", arg, e.value());
2337                                 return false;
2338                             }
2339                         default:
2340                             // Abort: bad type
2341                             errormsg("jshell.err.exit.bad.type", arg, typeName);
2342                             return false;
2343                     }
2344                 }
2345             }
2346         }
2347         regenerateOnDeath = false;
2348         live = false;
2349         if (exitCode == 0) {
2350             fluffmsg("jshell.msg.goodbye");
2351         } else {
2352             fluffmsg("jshell.msg.goodbye.value", exitCode);
2353         }
2354         return true;
2355     }
2356 
2357     boolean cmdHelp(String arg) {
2358         ArgTokenizer at = new ArgTokenizer("/help", arg);
2359         String subject = at.next();
2360         if (subject != null) {
2361             // check if the requested subject is a help subject or
2362             // a command, with or without slash
2363             Command[] matches = commands.values().stream()
2364                     .filter(c -> c.command.startsWith(subject)
2365                               || c.command.substring(1).startsWith(subject))
2366                     .toArray(Command[]::new);
2367             if (matches.length == 1) {
2368                 String cmd = matches[0].command;
2369                 if (cmd.equals("/set")) {
2370                     // Print the help doc for the specified sub-command
2371                     String which = subCommand(cmd, at, SET_SUBCOMMANDS);
2372                     if (which == null) {
2373                         return false;
2374                     }
2375                     if (!which.equals("_blank")) {
2376                         printHelp("/set " + which, "help.set." + which);
2377                         return true;
2378                     }
2379                 }
2380             }
2381             if (matches.length > 0) {
2382                 for (Command c : matches) {
2383                     printHelp(c.command, c.helpKey);
2384                 }
2385                 return true;
2386             } else {
2387                 // failing everything else, check if this is the start of
2388                 // a /set sub-command name
2389                 String[] subs = Arrays.stream(SET_SUBCOMMANDS)
2390                         .filter(s -> s.startsWith(subject))
2391                         .toArray(String[]::new);
2392                 if (subs.length > 0) {
2393                     for (String sub : subs) {
2394                         printHelp("/set " + sub, "help.set." + sub);
2395                     }
2396                     return true;
2397                 }
2398                 errormsg("jshell.err.help.arg", arg);
2399             }
2400         }
2401         hardmsg("jshell.msg.help.begin");
2402         hardPairs(commands.values().stream()
2403                 .filter(cmd -> cmd.kind.showInHelp),
2404                 cmd -> cmd.command + " " + getResourceString(cmd.helpKey + ".args"),
2405                 cmd -> getResourceString(cmd.helpKey + ".summary")
2406         );
2407         hardmsg("jshell.msg.help.subject");
2408         hardPairs(commands.values().stream()
2409                 .filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT),
2410                 cmd -> cmd.command,
2411                 cmd -> getResourceString(cmd.helpKey + ".summary")
2412         );
2413         return true;
2414     }
2415 
2416     private void printHelp(String name, String key) {
2417         int len = name.length();
2418         String centered = "%" + ((OUTPUT_WIDTH + len) / 2) + "s";
2419         hard("");
2420         hard(centered, name);
2421         hard(centered, Stream.generate(() -> "=").limit(len).collect(Collectors.joining()));
2422         hard("");
2423         hardrb(key);
2424     }
2425 
2426     private boolean cmdHistory() {
2427         cmdout.println();
2428         for (String s : input.currentSessionHistory()) {
2429             // No number prefix, confusing with snippet ids
2430             cmdout.printf("%s\n", s);
2431         }
2432         return true;
2433     }
2434 
2435     /**
2436      * Avoid parameterized varargs possible heap pollution warning.
2437      */
2438     private interface SnippetPredicate<T extends Snippet> extends Predicate<T> { }
2439 
2440     /**
2441      * Apply filters to a stream until one that is non-empty is found.
2442      * Adapted from Stuart Marks
2443      *
2444      * @param supplier Supply the Snippet stream to filter
2445      * @param filters Filters to attempt
2446      * @return The non-empty filtered Stream, or null
2447      */
2448     @SafeVarargs
2449     private static <T extends Snippet> Stream<T> nonEmptyStream(Supplier<Stream<T>> supplier,
2450             SnippetPredicate<T>... filters) {
2451         for (SnippetPredicate<T> filt : filters) {
2452             Iterator<T> iterator = supplier.get().filter(filt).iterator();
2453             if (iterator.hasNext()) {
2454                 return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
2455             }
2456         }
2457         return null;
2458     }
2459 
2460     private boolean inStartUp(Snippet sn) {
2461         return mapSnippet.get(sn).space == startNamespace;
2462     }
2463 
2464     private boolean isActive(Snippet sn) {
2465         return state.status(sn).isActive();
2466     }
2467 
2468     private boolean mainActive(Snippet sn) {
2469         return !inStartUp(sn) && isActive(sn);
2470     }
2471 
2472     private boolean matchingDeclaration(Snippet sn, String name) {
2473         return sn instanceof DeclarationSnippet
2474                 && ((DeclarationSnippet) sn).name().equals(name);
2475     }
2476 
2477     /**
2478      * Convert user arguments to a Stream of snippets referenced by those
2479      * arguments (or lack of arguments).
2480      *
2481      * @param snippets the base list of possible snippets
2482      * @param defFilter the filter to apply to the arguments if no argument
2483      * @param rawargs the user's argument to the command, maybe be the empty
2484      * string
2485      * @return a Stream of referenced snippets or null if no matches are found
2486      */
2487     private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier,
2488             Predicate<Snippet> defFilter, String rawargs, String cmd) {
2489         ArgTokenizer at = new ArgTokenizer(cmd, rawargs.trim());
2490         at.allowedOptions("-all", "-start");
2491         return argsOptionsToSnippets(snippetSupplier, defFilter, at);
2492     }
2493 
2494     /**
2495      * Convert user arguments to a Stream of snippets referenced by those
2496      * arguments (or lack of arguments).
2497      *
2498      * @param snippets the base list of possible snippets
2499      * @param defFilter the filter to apply to the arguments if no argument
2500      * @param at the ArgTokenizer, with allowed options set
2501      * @return
2502      */
2503     private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier,
2504             Predicate<Snippet> defFilter, ArgTokenizer at) {
2505         List<String> args = new ArrayList<>();
2506         String s;
2507         while ((s = at.next()) != null) {
2508             args.add(s);
2509         }
2510         if (!checkOptionsAndRemainingInput(at)) {
2511             return null;
2512         }
2513         if (at.optionCount() > 0 && args.size() > 0) {
2514             errormsg("jshell.err.may.not.specify.options.and.snippets", at.whole());
2515             return null;
2516         }
2517         if (at.optionCount() > 1) {
2518             errormsg("jshell.err.conflicting.options", at.whole());
2519             return null;
2520         }
2521         if (at.isAllowedOption("-all") && at.hasOption("-all")) {
2522             // all snippets including start-up, failed, and overwritten
2523             return snippetSupplier.get();
2524         }
2525         if (at.isAllowedOption("-start") && at.hasOption("-start")) {
2526             // start-up snippets
2527             return snippetSupplier.get()
2528                     .filter(this::inStartUp);
2529         }
2530         if (args.isEmpty()) {
2531             // Default is all active user snippets
2532             return snippetSupplier.get()
2533                     .filter(defFilter);
2534         }
2535         return new ArgToSnippets<>(snippetSupplier).argsToSnippets(args);
2536     }
2537 
2538     /**
2539      * Support for converting arguments that are definition names, snippet ids,
2540      * or snippet id ranges into a stream of snippets,
2541      *
2542      * @param <T> the snipper subtype
2543      */
2544     private class ArgToSnippets<T extends Snippet> {
2545 
2546         // the supplier of snippet streams
2547         final Supplier<Stream<T>> snippetSupplier;
2548         // these two are parallel, and lazily filled if a range is encountered
2549         List<T> allSnippets;
2550         String[] allIds = null;
2551 
2552         /**
2553          *
2554          * @param snippetSupplier the base list of possible snippets
2555         */
2556         ArgToSnippets(Supplier<Stream<T>> snippetSupplier) {
2557             this.snippetSupplier = snippetSupplier;
2558         }
2559 
2560         /**
2561          * Convert user arguments to a Stream of snippets referenced by those
2562          * arguments.
2563          *
2564          * @param args the user's argument to the command, maybe be the empty
2565          * list
2566          * @return a Stream of referenced snippets or null if no matches to
2567          * specific arg
2568          */
2569         Stream<T> argsToSnippets(List<String> args) {
2570             Stream<T> result = null;
2571             for (String arg : args) {
2572                 // Find the best match
2573                 Stream<T> st = argToSnippets(arg);
2574                 if (st == null) {
2575                     return null;
2576                 } else {
2577                     result = (result == null)
2578                             ? st
2579                             : Stream.concat(result, st);
2580                 }
2581             }
2582             return result;
2583         }
2584 
2585         /**
2586          * Convert a user argument to a Stream of snippets referenced by the
2587          * argument.
2588          *
2589          * @param snippetSupplier the base list of possible snippets
2590          * @param arg the user's argument to the command
2591          * @return a Stream of referenced snippets or null if no matches to
2592          * specific arg
2593          */
2594         Stream<T> argToSnippets(String arg) {
2595             if (arg.contains("-")) {
2596                 return range(arg);
2597             }
2598             // Find the best match
2599             Stream<T> st = layeredSnippetSearch(snippetSupplier, arg);
2600             if (st == null) {
2601                 badSnippetErrormsg(arg);
2602                 return null;
2603             } else {
2604                 return st;
2605             }
2606         }
2607 
2608         /**
2609          * Look for inappropriate snippets to give best error message
2610          *
2611          * @param arg the bad snippet arg
2612          * @param errKey the not found error key
2613          */
2614         void badSnippetErrormsg(String arg) {
2615             Stream<Snippet> est = layeredSnippetSearch(state::snippets, arg);
2616             if (est == null) {
2617                 if (ID.matcher(arg).matches()) {
2618                     errormsg("jshell.err.no.snippet.with.id", arg);
2619                 } else {
2620                     errormsg("jshell.err.no.such.snippets", arg);
2621                 }
2622             } else {
2623                 errormsg("jshell.err.the.snippet.cannot.be.used.with.this.command",
2624                         arg, est.findFirst().get().source());
2625             }
2626         }
2627 
2628         /**
2629          * Search through the snippets for the best match to the id/name.
2630          *
2631          * @param <R> the snippet type
2632          * @param aSnippetSupplier the supplier of snippet streams
2633          * @param arg the arg to match
2634          * @return a Stream of referenced snippets or null if no matches to
2635          * specific arg
2636          */
2637         <R extends Snippet> Stream<R> layeredSnippetSearch(Supplier<Stream<R>> aSnippetSupplier, String arg) {
2638             return nonEmptyStream(
2639                     // the stream supplier
2640                     aSnippetSupplier,
2641                     // look for active user declarations matching the name
2642                     sn -> isActive(sn) && matchingDeclaration(sn, arg),
2643                     // else, look for any declarations matching the name
2644                     sn -> matchingDeclaration(sn, arg),
2645                     // else, look for an id of this name
2646                     sn -> sn.id().equals(arg)
2647             );
2648         }
2649 
2650         /**
2651          * Given an id1-id2 range specifier, return a stream of snippets within
2652          * our context
2653          *
2654          * @param arg the range arg
2655          * @return a Stream of referenced snippets or null if no matches to
2656          * specific arg
2657          */
2658         Stream<T> range(String arg) {
2659             int dash = arg.indexOf('-');
2660             String iid = arg.substring(0, dash);
2661             String tid = arg.substring(dash + 1);
2662             int iidx = snippetIndex(iid);
2663             if (iidx < 0) {
2664                 return null;
2665             }
2666             int tidx = snippetIndex(tid);
2667             if (tidx < 0) {
2668                 return null;
2669             }
2670             if (tidx < iidx) {
2671                 errormsg("jshell.err.end.snippet.range.less.than.start", iid, tid);
2672                 return null;
2673             }
2674             return allSnippets.subList(iidx, tidx+1).stream();
2675         }
2676 
2677         /**
2678          * Lazily initialize the id mapping -- needed only for id ranges.
2679          */
2680         void initIdMapping() {
2681             if (allIds == null) {
2682                 allSnippets = snippetSupplier.get()
2683                         .sorted((a, b) -> order(a) - order(b))
2684                         .collect(toList());
2685                 allIds = allSnippets.stream()
2686                         .map(sn -> sn.id())
2687                         .toArray(n -> new String[n]);
2688             }
2689         }
2690 
2691         /**
2692          * Return all the snippet ids -- within the context, and in order.
2693          *
2694          * @return the snippet ids
2695          */
2696         String[] allIds() {
2697             initIdMapping();
2698             return allIds;
2699         }
2700 
2701         /**
2702          * Establish an order on snippet ids.  All startup snippets are first,
2703          * all error snippets are last -- within that is by snippet number.
2704          *
2705          * @param id the id string
2706          * @return an ordering int
2707          */
2708         int order(String id) {
2709             try {
2710                 switch (id.charAt(0)) {
2711                     case 's':
2712                         return Integer.parseInt(id.substring(1));
2713                     case 'e':
2714                         return 0x40000000 + Integer.parseInt(id.substring(1));
2715                     default:
2716                         return 0x20000000 + Integer.parseInt(id);
2717                 }
2718             } catch (Exception ex) {
2719                 return 0x60000000;
2720             }
2721         }
2722 
2723         /**
2724          * Establish an order on snippets, based on its snippet id. All startup
2725          * snippets are first, all error snippets are last -- within that is by
2726          * snippet number.
2727          *
2728          * @param sn the id string
2729          * @return an ordering int
2730          */
2731         int order(Snippet sn) {
2732             return order(sn.id());
2733         }
2734 
2735         /**
2736          * Find the index into the parallel allSnippets and allIds structures.
2737          *
2738          * @param s the snippet id name
2739          * @return the index, or, if not found, report the error and return a
2740          * negative number
2741          */
2742         int snippetIndex(String s) {
2743             int idx = Arrays.binarySearch(allIds(), 0, allIds().length, s,
2744                     (a, b) -> order(a) - order(b));
2745             if (idx < 0) {
2746                 // the id is not in the snippet domain, find the right error to report
2747                 if (!ID.matcher(s).matches()) {
2748                     errormsg("jshell.err.range.requires.id", s);
2749                 } else {
2750                     badSnippetErrormsg(s);
2751                 }
2752             }
2753             return idx;
2754         }
2755 
2756     }
2757 
2758     private boolean cmdDrop(String rawargs) {
2759         ArgTokenizer at = new ArgTokenizer("/drop", rawargs.trim());
2760         at.allowedOptions();
2761         List<String> args = new ArrayList<>();
2762         String s;
2763         while ((s = at.next()) != null) {
2764             args.add(s);
2765         }
2766         if (!checkOptionsAndRemainingInput(at)) {
2767             return false;
2768         }
2769         if (args.isEmpty()) {
2770             errormsg("jshell.err.drop.arg");
2771             return false;
2772         }
2773         Stream<Snippet> stream = new ArgToSnippets<>(this::dropableSnippets).argsToSnippets(args);
2774         if (stream == null) {
2775             // Snippet not found. Error already printed
2776             fluffmsg("jshell.msg.see.classes.etc");
2777             return false;
2778         }
2779         stream.forEach(sn -> state.drop(sn).forEach(this::handleEvent));
2780         return true;
2781     }
2782 
2783     private boolean cmdEdit(String arg) {
2784         Stream<Snippet> stream = argsOptionsToSnippets(state::snippets,
2785                 this::mainActive, arg, "/edit");
2786         if (stream == null) {
2787             return false;
2788         }
2789         Set<String> srcSet = new LinkedHashSet<>();
2790         stream.forEachOrdered(sn -> {
2791             String src = sn.source();
2792             switch (sn.subKind()) {
2793                 case VAR_VALUE_SUBKIND:
2794                     break;
2795                 case ASSIGNMENT_SUBKIND:
2796                 case OTHER_EXPRESSION_SUBKIND:
2797                 case TEMP_VAR_EXPRESSION_SUBKIND:
2798                 case UNKNOWN_SUBKIND:
2799                     if (!src.endsWith(";")) {
2800                         src = src + ";";
2801                     }
2802                     srcSet.add(src);
2803                     break;
2804                 case STATEMENT_SUBKIND:
2805                     if (src.endsWith("}")) {
2806                         // Could end with block or, for example, new Foo() {...}
2807                         // so, we need deeper analysis to know if it needs a semicolon
2808                         src = analysis.analyzeCompletion(src).source();
2809                     } else if (!src.endsWith(";")) {
2810                         src = src + ";";
2811                     }
2812                     srcSet.add(src);
2813                     break;
2814                 default:
2815                     srcSet.add(src);
2816                     break;
2817             }
2818         });
2819         StringBuilder sb = new StringBuilder();
2820         for (String s : srcSet) {
2821             sb.append(s);
2822             sb.append('\n');
2823         }
2824         String src = sb.toString();
2825         Consumer<String> saveHandler = new SaveHandler(src, srcSet);
2826         Consumer<String> errorHandler = s -> hard("Edit Error: %s", s);
2827         if (editor == BUILT_IN_EDITOR) {
2828             return builtInEdit(src, saveHandler, errorHandler);
2829         } else {
2830             // Changes have occurred in temp edit directory,
2831             // transfer the new sources to JShell (unless the editor is
2832             // running directly in JShell's window -- don't make a mess)
2833             String[] buffer = new String[1];
2834             Consumer<String> extSaveHandler = s -> {
2835                 if (input.terminalEditorRunning()) {
2836                     buffer[0] = s;
2837                 } else {
2838                     saveHandler.accept(s);
2839                 }
2840             };
2841             ExternalEditor.edit(editor.cmd, src,
2842                     errorHandler, extSaveHandler,
2843                     () -> input.suspend(),
2844                     () -> input.resume(),
2845                     editor.wait,
2846                     () -> hardrb("jshell.msg.press.return.to.leave.edit.mode"));
2847             if (buffer[0] != null) {
2848                 saveHandler.accept(buffer[0]);
2849             }
2850         }
2851         return true;
2852     }
2853     //where
2854     // start the built-in editor
2855     private boolean builtInEdit(String initialText,
2856             Consumer<String> saveHandler, Consumer<String> errorHandler) {
2857         try {
2858             ServiceLoader<BuildInEditorProvider> sl
2859                     = ServiceLoader.load(BuildInEditorProvider.class);
2860             // Find the highest ranking provider
2861             BuildInEditorProvider provider = null;
2862             for (BuildInEditorProvider p : sl) {
2863                 if (provider == null || p.rank() > provider.rank()) {
2864                     provider = p;
2865                 }
2866             }
2867             if (provider != null) {
2868                 provider.edit(getResourceString("jshell.label.editpad"),
2869                         initialText, saveHandler, errorHandler);
2870                 return true;
2871             } else {
2872                 errormsg("jshell.err.no.builtin.editor");
2873             }
2874         } catch (RuntimeException ex) {
2875             errormsg("jshell.err.cant.launch.editor", ex);
2876         }
2877         fluffmsg("jshell.msg.try.set.editor");
2878         return false;
2879     }
2880     //where
2881     // receives editor requests to save
2882     private class SaveHandler implements Consumer<String> {
2883 
2884         String src;
2885         Set<String> currSrcs;
2886 
2887         SaveHandler(String src, Set<String> ss) {
2888             this.src = src;
2889             this.currSrcs = ss;
2890         }
2891 
2892         @Override
2893         public void accept(String s) {
2894             if (!s.equals(src)) { // quick check first
2895                 src = s;
2896                 try {
2897                     Set<String> nextSrcs = new LinkedHashSet<>();
2898                     boolean failed = false;
2899                     while (true) {
2900                         CompletionInfo an = analysis.analyzeCompletion(s);
2901                         if (!an.completeness().isComplete()) {
2902                             break;
2903                         }
2904                         String tsrc = trimNewlines(an.source());
2905                         if (!failed && !currSrcs.contains(tsrc)) {
2906                             failed = processSource(tsrc);
2907                         }
2908                         nextSrcs.add(tsrc);
2909                         if (an.remaining().isEmpty()) {
2910                             break;
2911                         }
2912                         s = an.remaining();
2913                     }
2914                     currSrcs = nextSrcs;
2915                 } catch (IllegalStateException ex) {
2916                     errormsg("jshell.msg.resetting");
2917                     resetState();
2918                     currSrcs = new LinkedHashSet<>(); // re-process everything
2919                 }
2920             }
2921         }
2922 
2923         private String trimNewlines(String s) {
2924             int b = 0;
2925             while (b < s.length() && s.charAt(b) == '\n') {
2926                 ++b;
2927             }
2928             int e = s.length() -1;
2929             while (e >= 0 && s.charAt(e) == '\n') {
2930                 --e;
2931             }
2932             return s.substring(b, e + 1);
2933         }
2934     }
2935 
2936     private boolean cmdList(String arg) {
2937         if (arg.length() >= 2 && "-history".startsWith(arg)) {
2938             return cmdHistory();
2939         }
2940         Stream<Snippet> stream = argsOptionsToSnippets(state::snippets,
2941                 this::mainActive, arg, "/list");
2942         if (stream == null) {
2943             return false;
2944         }
2945 
2946         // prevent double newline on empty list
2947         boolean[] hasOutput = new boolean[1];
2948         stream.forEachOrdered(sn -> {
2949             if (!hasOutput[0]) {
2950                 cmdout.println();
2951                 hasOutput[0] = true;
2952             }
2953             cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n       "));
2954         });
2955         return true;
2956     }
2957 
2958     private boolean cmdOpen(String filename) {
2959         return runFile(filename, "/open");
2960     }
2961 
2962     private boolean runFile(String filename, String context) {
2963         if (!filename.isEmpty()) {
2964             try {
2965                 Scanner scanner;
2966                 if (!interactiveModeBegun && filename.equals("-")) {
2967                     // - on command line: no interactive later, read from input
2968                     regenerateOnDeath = false;
2969                     scanner = new Scanner(cmdin);
2970                 } else {
2971                     Path path = toPathResolvingUserHome(filename);
2972                     String resource;
2973                     scanner = new Scanner(
2974                             (!Files.exists(path) && (resource = getResource(filename)) != null)
2975                             ? new StringReader(resource) // Not found as file, but found as resource
2976                             : new FileReader(path.toString())
2977                     );
2978                 }
2979                 run(new ScannerIOContext(scanner));
2980                 return true;
2981             } catch (FileNotFoundException e) {
2982                 errormsg("jshell.err.file.not.found", context, filename, e.getMessage());
2983             } catch (Exception e) {
2984                 errormsg("jshell.err.file.exception", context, filename, e);
2985             }
2986         } else {
2987             errormsg("jshell.err.file.filename", context);
2988         }
2989         return false;
2990     }
2991 
2992     static String getResource(String name) {
2993         if (BUILTIN_FILE_PATTERN.matcher(name).matches()) {
2994             try {
2995                 return readResource(name);
2996             } catch (Throwable t) {
2997                 // Fall-through to null
2998             }
2999         }
3000         return null;
3001     }
3002 
3003     // Read a built-in file from resources or compute it
3004     static String readResource(String name) throws Exception {
3005         // Class to compute imports by following requires for a module
3006         class ComputeImports {
3007             final String base;
3008             ModuleFinder finder = ModuleFinder.ofSystem();
3009 
3010             ComputeImports(String base) {
3011                 this.base = base;
3012             }
3013 
3014             Set<ModuleDescriptor> modules() {
3015                 Set<ModuleDescriptor> closure = new HashSet<>();
3016                 moduleClosure(finder.find(base), closure);
3017                 return closure;
3018             }
3019 
3020             void moduleClosure(Optional<ModuleReference> omr, Set<ModuleDescriptor> closure) {
3021                 if (omr.isPresent()) {
3022                     ModuleDescriptor mdesc = omr.get().descriptor();
3023                     if (closure.add(mdesc)) {
3024                         for (ModuleDescriptor.Requires req : mdesc.requires()) {
3025                             if (!req.modifiers().contains(ModuleDescriptor.Requires.Modifier.STATIC)) {
3026                                 moduleClosure(finder.find(req.name()), closure);
3027                             }
3028                         }
3029                     }
3030                 }
3031             }
3032 
3033             Set<String> packages() {
3034                 return modules().stream().flatMap(md -> md.exports().stream())
3035                         .filter(e -> !e.isQualified()).map(Object::toString).collect(Collectors.toSet());
3036             }
3037 
3038             String imports() {
3039                 Set<String> si = packages();
3040                 String[] ai = si.toArray(new String[si.size()]);
3041                 Arrays.sort(ai);
3042                 return Arrays.stream(ai)
3043                         .map(p -> String.format("import %s.*;\n", p))
3044                         .collect(Collectors.joining());
3045             }
3046         }
3047 
3048         if (name.equals("JAVASE")) {
3049             // The built-in JAVASE is computed as the imports of all the packages in Java SE
3050             return new ComputeImports("java.se").imports();
3051         }
3052 
3053         // Attempt to find the file as a resource
3054         String spec = String.format(BUILTIN_FILE_PATH_FORMAT, name);
3055 
3056         try (InputStream in = JShellTool.class.getResourceAsStream(spec);
3057                 BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
3058             return reader.lines().collect(Collectors.joining("\n", "", "\n"));
3059         }
3060     }
3061 
3062     private boolean cmdReset(String rawargs) {
3063         Options oldOptions = rawargs.trim().isEmpty()? null : options;
3064         if (!parseCommandLineLikeFlags(rawargs, new OptionParserBase())) {
3065             return false;
3066         }
3067         live = false;
3068         fluffmsg("jshell.msg.resetting.state");
3069         return doReload(null, false, oldOptions);
3070     }
3071 
3072     private boolean cmdReload(String rawargs) {
3073         Options oldOptions = rawargs.trim().isEmpty()? null : options;
3074         OptionParserReload ap = new OptionParserReload();
3075         if (!parseCommandLineLikeFlags(rawargs, ap)) {
3076             return false;
3077         }
3078         ReplayableHistory history;
3079         if (ap.restore()) {
3080             if (replayableHistoryPrevious == null) {
3081                 errormsg("jshell.err.reload.no.previous");
3082                 return false;
3083             }
3084             history = replayableHistoryPrevious;
3085             fluffmsg("jshell.err.reload.restarting.previous.state");
3086         } else {
3087             history = replayableHistory;
3088             fluffmsg("jshell.err.reload.restarting.state");
3089         }
3090         boolean success = doReload(history, !ap.quiet(), oldOptions);
3091         if (success && ap.restore()) {
3092             // if we are restoring from previous, then if nothing was added
3093             // before time of exit, there is nothing to save
3094             replayableHistory.markSaved();
3095         }
3096         return success;
3097     }
3098 
3099     private boolean cmdEnv(String rawargs) {
3100         if (rawargs.trim().isEmpty()) {
3101             // No arguments, display current settings (as option flags)
3102             StringBuilder sb = new StringBuilder();
3103             for (String a : options.commonOptions()) {
3104                 sb.append(
3105                         a.startsWith("-")
3106                             ? sb.length() > 0
3107                                     ? "\n   "
3108                                     :   "   "
3109                             : " ");
3110                 sb.append(a);
3111             }
3112             if (sb.length() > 0) {
3113                 hard(sb.toString());
3114             }
3115             return false;
3116         }
3117         Options oldOptions = options;
3118         if (!parseCommandLineLikeFlags(rawargs, new OptionParserBase())) {
3119             return false;
3120         }
3121         fluffmsg("jshell.msg.set.restore");
3122         return doReload(replayableHistory, false, oldOptions);
3123     }
3124 
3125     private boolean doReload(ReplayableHistory history, boolean echo, Options oldOptions) {
3126         if (oldOptions != null) {
3127             try {
3128                 resetState();
3129             } catch (IllegalStateException ex) {
3130                 currentNameSpace = mainNamespace; // back out of start-up (messages)
3131                 errormsg("jshell.err.restart.failed", ex.getMessage());
3132                 // attempt recovery to previous option settings
3133                 options = oldOptions;
3134                 resetState();
3135             }
3136         } else {
3137             resetState();
3138         }
3139         if (history != null) {
3140             run(new ReloadIOContext(history.iterable(),
3141                     echo ? cmdout : null));
3142         }
3143         return true;
3144     }
3145 
3146     private boolean parseCommandLineLikeFlags(String rawargs, OptionParserBase ap) {
3147         String[] args = Arrays.stream(rawargs.split("\\s+"))
3148                 .filter(s -> !s.isEmpty())
3149                 .toArray(String[]::new);
3150         Options opts = ap.parse(args);
3151         if (opts == null) {
3152             return false;
3153         }
3154         if (!ap.nonOptions().isEmpty()) {
3155             errormsg("jshell.err.unexpected.at.end", ap.nonOptions(), rawargs);
3156             return false;
3157         }
3158         options = options.override(opts);
3159         return true;
3160     }
3161 
3162     private boolean cmdSave(String rawargs) {
3163         // The filename to save to is the last argument, extract it
3164         String[] args = rawargs.split("\\s");
3165         String filename = args[args.length - 1];
3166         if (filename.isEmpty()) {
3167             errormsg("jshell.err.file.filename", "/save");
3168             return false;
3169         }
3170         // All the non-filename arguments are the specifier of what to save
3171         String srcSpec = Arrays.stream(args, 0, args.length - 1)
3172                 .collect(Collectors.joining("\n"));
3173         // From the what to save specifier, compute the snippets (as a stream)
3174         ArgTokenizer at = new ArgTokenizer("/save", srcSpec);
3175         at.allowedOptions("-all", "-start", "-history");
3176         Stream<Snippet> snippetStream = argsOptionsToSnippets(state::snippets, this::mainActive, at);
3177         if (snippetStream == null) {
3178             // error occurred, already reported
3179             return false;
3180         }
3181         try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename),
3182                 Charset.defaultCharset(),
3183                 CREATE, TRUNCATE_EXISTING, WRITE)) {
3184             if (at.hasOption("-history")) {
3185                 // they want history (commands and snippets), ignore the snippet stream
3186                 for (String s : input.currentSessionHistory()) {
3187                     writer.write(s);
3188                     writer.write("\n");
3189                 }
3190             } else {
3191                 // write the snippet stream to the file
3192                 writer.write(snippetStream
3193                         .map(Snippet::source)
3194                         .collect(Collectors.joining("\n")));
3195             }
3196         } catch (FileNotFoundException e) {
3197             errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage());
3198             return false;
3199         } catch (Exception e) {
3200             errormsg("jshell.err.file.exception", "/save", filename, e);
3201             return false;
3202         }
3203         return true;
3204     }
3205 
3206     private boolean cmdVars(String arg) {
3207         Stream<VarSnippet> stream = argsOptionsToSnippets(this::allVarSnippets,
3208                 this::isActive, arg, "/vars");
3209         if (stream == null) {
3210             return false;
3211         }
3212         stream.forEachOrdered(vk ->
3213         {
3214             String val = state.status(vk) == Status.VALID
3215                     ? feedback.truncateVarValue(state.varValue(vk))
3216                     : getResourceString("jshell.msg.vars.not.active");
3217             hard("  %s %s = %s", vk.typeName(), vk.name(), val);
3218         });
3219         return true;
3220     }
3221 
3222     private boolean cmdMethods(String arg) {
3223         Stream<MethodSnippet> stream = argsOptionsToSnippets(this::allMethodSnippets,
3224                 this::isActive, arg, "/methods");
3225         if (stream == null) {
3226             return false;
3227         }
3228         stream.forEachOrdered(meth -> {
3229             String sig = meth.signature();
3230             int i = sig.lastIndexOf(")") + 1;
3231             if (i <= 0) {
3232                 hard("  %s", meth.name());
3233             } else {
3234                 hard("  %s %s%s", sig.substring(i), meth.name(), sig.substring(0, i));
3235             }
3236             printSnippetStatus(meth, true);
3237         });
3238         return true;
3239     }
3240 
3241     private boolean cmdTypes(String arg) {
3242         Stream<TypeDeclSnippet> stream = argsOptionsToSnippets(this::allTypeSnippets,
3243                 this::isActive, arg, "/types");
3244         if (stream == null) {
3245             return false;
3246         }
3247         stream.forEachOrdered(ck
3248         -> {
3249             String kind;
3250             switch (ck.subKind()) {
3251                 case INTERFACE_SUBKIND:
3252                     kind = "interface";
3253                     break;
3254                 case CLASS_SUBKIND:
3255                     kind = "class";
3256                     break;
3257                 case ENUM_SUBKIND:
3258                     kind = "enum";
3259                     break;
3260                 case ANNOTATION_TYPE_SUBKIND:
3261                     kind = "@interface";
3262                     break;
3263                 default:
3264                     assert false : "Wrong kind" + ck.subKind();
3265                     kind = "class";
3266                     break;
3267             }
3268             hard("  %s %s", kind, ck.name());
3269             printSnippetStatus(ck, true);
3270         });
3271         return true;
3272     }
3273 
3274     private boolean cmdImports() {
3275         state.imports().forEach(ik -> {
3276             hard("  import %s%s", ik.isStatic() ? "static " : "", ik.fullname());
3277         });
3278         return true;
3279     }
3280 
3281     private boolean cmdUseHistoryEntry(int index) {
3282         List<Snippet> keys = state.snippets().collect(toList());
3283         if (index < 0)
3284             index += keys.size();
3285         else
3286             index--;
3287         if (index >= 0 && index < keys.size()) {
3288             rerunSnippet(keys.get(index));
3289         } else {
3290             errormsg("jshell.err.out.of.range");
3291             return false;
3292         }
3293         return true;
3294     }
3295 
3296     boolean checkOptionsAndRemainingInput(ArgTokenizer at) {
3297         String junk = at.remainder();
3298         if (!junk.isEmpty()) {
3299             errormsg("jshell.err.unexpected.at.end", junk, at.whole());
3300             return false;
3301         } else {
3302             String bad = at.badOptions();
3303             if (!bad.isEmpty()) {
3304                 errormsg("jshell.err.unknown.option", bad, at.whole());
3305                 return false;
3306             }
3307         }
3308         return true;
3309     }
3310 
3311     /**
3312      * Handle snippet reevaluation commands: {@code /<id>}. These commands are a
3313      * sequence of ids and id ranges (names are permitted, though not in the
3314      * first position. Support for names is purposely not documented).
3315      *
3316      * @param rawargs the whole command including arguments
3317      */
3318     private void rerunHistoryEntriesById(String rawargs) {
3319         ArgTokenizer at = new ArgTokenizer("/<id>", rawargs.trim().substring(1));
3320         at.allowedOptions();
3321         Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, sn -> true, at);
3322         if (stream != null) {
3323             // successfully parsed, rerun snippets
3324             stream.forEach(sn -> rerunSnippet(sn));
3325         }
3326     }
3327 
3328     private void rerunSnippet(Snippet snippet) {
3329         String source = snippet.source();
3330         cmdout.printf("%s\n", source);
3331         input.replaceLastHistoryEntry(source);
3332         processSourceCatchingReset(source);
3333     }
3334 
3335     /**
3336      * Filter diagnostics for only errors (no warnings, ...)
3337      * @param diagnostics input list
3338      * @return filtered list
3339      */
3340     List<Diag> errorsOnly(List<Diag> diagnostics) {
3341         return diagnostics.stream()
3342                 .filter(Diag::isError)
3343                 .collect(toList());
3344     }
3345 
3346     /**
3347      * Print out a snippet exception.
3348      *
3349      * @param exception the exception to print
3350      * @return true on fatal exception
3351      */
3352     private boolean displayException(Exception exception) {
3353         if (exception instanceof EvalException) {
3354             printEvalException((EvalException) exception);
3355             return true;
3356         } else if (exception instanceof UnresolvedReferenceException) {
3357             printUnresolvedException((UnresolvedReferenceException) exception);
3358             return false;
3359         } else {
3360             error("Unexpected execution exception: %s", exception);
3361             return true;
3362         }
3363     }
3364 
3365     /**
3366      * Display a list of diagnostics.
3367      *
3368      * @param source the source line with the error/warning
3369      * @param diagnostics the diagnostics to display
3370      */
3371     private void displayDiagnostics(String source, List<Diag> diagnostics) {
3372         for (Diag d : diagnostics) {
3373             errormsg(d.isError() ? "jshell.msg.error" : "jshell.msg.warning");
3374             List<String> disp = new ArrayList<>();
3375             displayableDiagnostic(source, d, disp);
3376             disp.stream()
3377                     .forEach(l -> error("%s", l));
3378         }
3379     }
3380 
3381     /**
3382      * Convert a diagnostic into a list of pretty displayable strings with
3383      * source context.
3384      *
3385      * @param source the source line for the error/warning
3386      * @param diag the diagnostic to convert
3387      * @param toDisplay a list that the displayable strings are added to
3388      */
3389     private void displayableDiagnostic(String source, Diag diag, List<String> toDisplay) {
3390         for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize
3391             if (!line.trim().startsWith("location:")) {
3392                 toDisplay.add(line);
3393             }
3394         }
3395 
3396         int pstart = (int) diag.getStartPosition();
3397         int pend = (int) diag.getEndPosition();
3398         Matcher m = LINEBREAK.matcher(source);
3399         int pstartl = 0;
3400         int pendl = -2;
3401         while (m.find(pstartl)) {
3402             pendl = m.start();
3403             if (pendl >= pstart) {
3404                 break;
3405             } else {
3406                 pstartl = m.end();
3407             }
3408         }
3409         if (pendl < pstart) {
3410             pendl = source.length();
3411         }
3412         toDisplay.add(source.substring(pstartl, pendl));
3413 
3414         StringBuilder sb = new StringBuilder();
3415         int start = pstart - pstartl;
3416         for (int i = 0; i < start; ++i) {
3417             sb.append(' ');
3418         }
3419         sb.append('^');
3420         boolean multiline = pend > pendl;
3421         int end = (multiline ? pendl : pend) - pstartl - 1;
3422         if (end > start) {
3423             for (int i = start + 1; i < end; ++i) {
3424                 sb.append('-');
3425             }
3426             if (multiline) {
3427                 sb.append("-...");
3428             } else {
3429                 sb.append('^');
3430             }
3431         }
3432         toDisplay.add(sb.toString());
3433 
3434         debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this);
3435         debug("Code: %s", diag.getCode());
3436         debug("Pos: %d (%d - %d)", diag.getPosition(),
3437                 diag.getStartPosition(), diag.getEndPosition());
3438     }
3439 
3440     /**
3441      * Process a source snippet.
3442      *
3443      * @param source the input source
3444      * @return true if the snippet succeeded
3445      */
3446     boolean processSource(String source) {
3447         debug("Compiling: %s", source);
3448         boolean failed = false;
3449         boolean isActive = false;
3450         List<SnippetEvent> events = state.eval(source);
3451         for (SnippetEvent e : events) {
3452             // Report the event, recording failure
3453             failed |= handleEvent(e);
3454 
3455             // If any main snippet is active, this should be replayable
3456             // also ignore var value queries
3457             isActive |= e.causeSnippet() == null &&
3458                     e.status().isActive() &&
3459                     e.snippet().subKind() != VAR_VALUE_SUBKIND;
3460         }
3461         // If this is an active snippet and it didn't cause the backend to die,
3462         // add it to the replayable history
3463         if (isActive && live) {
3464             addToReplayHistory(source);
3465         }
3466 
3467         return !failed;
3468     }
3469 
3470     // Handle incoming snippet events -- return true on failure
3471     private boolean handleEvent(SnippetEvent ste) {
3472         Snippet sn = ste.snippet();
3473         if (sn == null) {
3474             debug("Event with null key: %s", ste);
3475             return false;
3476         }
3477         List<Diag> diagnostics = state.diagnostics(sn).collect(toList());
3478         String source = sn.source();
3479         if (ste.causeSnippet() == null) {
3480             // main event
3481             displayDiagnostics(source, diagnostics);
3482 
3483             if (ste.status() != Status.REJECTED) {
3484                 if (ste.exception() != null) {
3485                     if (displayException(ste.exception())) {
3486                         return true;
3487                     }
3488                 } else {
3489                     new DisplayEvent(ste, FormatWhen.PRIMARY, ste.value(), diagnostics)
3490                             .displayDeclarationAndValue();
3491                 }
3492             } else {
3493                 if (diagnostics.isEmpty()) {
3494                     errormsg("jshell.err.failed");
3495                 }
3496                 return true;
3497             }
3498         } else {
3499             // Update
3500             if (sn instanceof DeclarationSnippet) {
3501                 List<Diag> other = errorsOnly(diagnostics);
3502 
3503                 // display update information
3504                 new DisplayEvent(ste, FormatWhen.UPDATE, ste.value(), other)
3505                         .displayDeclarationAndValue();
3506             }
3507         }
3508         return false;
3509     }
3510     //where
3511     void printStackTrace(StackTraceElement[] stes) {
3512         for (StackTraceElement ste : stes) {
3513             StringBuilder sb = new StringBuilder();
3514             String cn = ste.getClassName();
3515             if (!cn.isEmpty()) {
3516                 int dot = cn.lastIndexOf('.');
3517                 if (dot > 0) {
3518                     sb.append(cn.substring(dot + 1));
3519                 } else {
3520                     sb.append(cn);
3521                 }
3522                 sb.append(".");
3523             }
3524             if (!ste.getMethodName().isEmpty()) {
3525                 sb.append(ste.getMethodName());
3526                 sb.append(" ");
3527             }
3528             String fileName = ste.getFileName();
3529             int lineNumber = ste.getLineNumber();
3530             String loc = ste.isNativeMethod()
3531                     ? getResourceString("jshell.msg.native.method")
3532                     : fileName == null
3533                             ? getResourceString("jshell.msg.unknown.source")
3534                             : lineNumber >= 0
3535                                     ? fileName + ":" + lineNumber
3536                                     : fileName;
3537             error("      at %s(%s)", sb, loc);
3538 
3539         }
3540     }
3541     //where
3542     void printUnresolvedException(UnresolvedReferenceException ex) {
3543         printSnippetStatus(ex.getSnippet(), false);
3544     }
3545     //where
3546     void printEvalException(EvalException ex) {
3547         if (ex.getMessage() == null) {
3548             error("%s thrown", ex.getExceptionClassName());
3549         } else {
3550             error("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage());
3551         }
3552         printStackTrace(ex.getStackTrace());
3553     }
3554 
3555     private FormatAction toAction(Status status, Status previousStatus, boolean isSignatureChange) {
3556         FormatAction act;
3557         switch (status) {
3558             case VALID:
3559             case RECOVERABLE_DEFINED:
3560             case RECOVERABLE_NOT_DEFINED:
3561                 if (previousStatus.isActive()) {
3562                     act = isSignatureChange
3563                             ? FormatAction.REPLACED
3564                             : FormatAction.MODIFIED;
3565                 } else {
3566                     act = FormatAction.ADDED;
3567                 }
3568                 break;
3569             case OVERWRITTEN:
3570                 act = FormatAction.OVERWROTE;
3571                 break;
3572             case DROPPED:
3573                 act = FormatAction.DROPPED;
3574                 break;
3575             case REJECTED:
3576             case NONEXISTENT:
3577             default:
3578                 // Should not occur
3579                 error("Unexpected status: " + previousStatus.toString() + "=>" + status.toString());
3580                 act = FormatAction.DROPPED;
3581         }
3582         return act;
3583     }
3584 
3585     void printSnippetStatus(DeclarationSnippet sn, boolean resolve) {
3586         List<Diag> otherErrors = errorsOnly(state.diagnostics(sn).collect(toList()));
3587         new DisplayEvent(sn, state.status(sn), resolve, otherErrors)
3588                 .displayDeclarationAndValue();
3589     }
3590 
3591     class DisplayEvent {
3592         private final Snippet sn;
3593         private final FormatAction action;
3594         private final FormatWhen update;
3595         private final String value;
3596         private final List<String> errorLines;
3597         private final FormatResolve resolution;
3598         private final String unresolved;
3599         private final FormatUnresolved unrcnt;
3600         private final FormatErrors errcnt;
3601         private final boolean resolve;
3602 
3603         DisplayEvent(SnippetEvent ste, FormatWhen update, String value, List<Diag> errors) {
3604             this(ste.snippet(), ste.status(), false,
3605                     toAction(ste.status(), ste.previousStatus(), ste.isSignatureChange()),
3606                     update, value, errors);
3607         }
3608 
3609         DisplayEvent(Snippet sn, Status status, boolean resolve, List<Diag> errors) {
3610             this(sn, status, resolve, FormatAction.USED, FormatWhen.UPDATE, null, errors);
3611         }
3612 
3613         private DisplayEvent(Snippet sn, Status status, boolean resolve,
3614                 FormatAction action, FormatWhen update, String value, List<Diag> errors) {
3615             this.sn = sn;
3616             this.resolve =resolve;
3617             this.action = action;
3618             this.update = update;
3619             this.value = value;
3620             this.errorLines = new ArrayList<>();
3621             for (Diag d : errors) {
3622                 displayableDiagnostic(sn.source(), d, errorLines);
3623             }
3624             if (resolve) {
3625                 // resolve needs error lines indented
3626                 for (int i = 0; i < errorLines.size(); ++i) {
3627                     errorLines.set(i, "    " + errorLines.get(i));
3628                 }
3629             }
3630             long unresolvedCount;
3631             if (sn instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) {
3632                 resolution = (status == Status.RECOVERABLE_NOT_DEFINED)
3633                         ? FormatResolve.NOTDEFINED
3634                         : FormatResolve.DEFINED;
3635                 unresolved = unresolved((DeclarationSnippet) sn);
3636                 unresolvedCount = state.unresolvedDependencies((DeclarationSnippet) sn).count();
3637             } else {
3638                 resolution = FormatResolve.OK;
3639                 unresolved = "";
3640                 unresolvedCount = 0;
3641             }
3642             unrcnt = unresolvedCount == 0
3643                     ? FormatUnresolved.UNRESOLVED0
3644                     : unresolvedCount == 1
3645                         ? FormatUnresolved.UNRESOLVED1
3646                         : FormatUnresolved.UNRESOLVED2;
3647             errcnt = errors.isEmpty()
3648                     ? FormatErrors.ERROR0
3649                     : errors.size() == 1
3650                         ? FormatErrors.ERROR1
3651                         : FormatErrors.ERROR2;
3652         }
3653 
3654         private String unresolved(DeclarationSnippet key) {
3655             List<String> unr = state.unresolvedDependencies(key).collect(toList());
3656             StringBuilder sb = new StringBuilder();
3657             int fromLast = unr.size();
3658             if (fromLast > 0) {
3659                 sb.append(" ");
3660             }
3661             for (String u : unr) {
3662                 --fromLast;
3663                 sb.append(u);
3664                 switch (fromLast) {
3665                     // No suffix
3666                     case 0:
3667                         break;
3668                     case 1:
3669                         sb.append(", and ");
3670                         break;
3671                     default:
3672                         sb.append(", ");
3673                         break;
3674                 }
3675             }
3676             return sb.toString();
3677         }
3678 
3679         private void custom(FormatCase fcase, String name) {
3680             custom(fcase, name, null);
3681         }
3682 
3683         private void custom(FormatCase fcase, String name, String type) {
3684             if (resolve) {
3685                 String resolutionErrors = feedback.format("resolve", fcase, action, update,
3686                         resolution, unrcnt, errcnt,
3687                         name, type, value, unresolved, errorLines);
3688                 if (!resolutionErrors.trim().isEmpty()) {
3689                     error("    %s", resolutionErrors);
3690                 }
3691             } else if (interactive()) {
3692                 String display = feedback.format(fcase, action, update,
3693                         resolution, unrcnt, errcnt,
3694                         name, type, value, unresolved, errorLines);
3695                 cmdout.print(display);
3696             }
3697         }
3698 
3699         @SuppressWarnings("fallthrough")
3700         private void displayDeclarationAndValue() {
3701             switch (sn.subKind()) {
3702                 case CLASS_SUBKIND:
3703                     custom(FormatCase.CLASS, ((TypeDeclSnippet) sn).name());
3704                     break;
3705                 case INTERFACE_SUBKIND:
3706                     custom(FormatCase.INTERFACE, ((TypeDeclSnippet) sn).name());
3707                     break;
3708                 case ENUM_SUBKIND:
3709                     custom(FormatCase.ENUM, ((TypeDeclSnippet) sn).name());
3710                     break;
3711                 case ANNOTATION_TYPE_SUBKIND:
3712                     custom(FormatCase.ANNOTATION, ((TypeDeclSnippet) sn).name());
3713                     break;
3714                 case METHOD_SUBKIND:
3715                     custom(FormatCase.METHOD, ((MethodSnippet) sn).name(), ((MethodSnippet) sn).parameterTypes());
3716                     break;
3717                 case VAR_DECLARATION_SUBKIND: {
3718                     VarSnippet vk = (VarSnippet) sn;
3719                     custom(FormatCase.VARDECL, vk.name(), vk.typeName());
3720                     break;
3721                 }
3722                 case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: {
3723                     VarSnippet vk = (VarSnippet) sn;
3724                     custom(FormatCase.VARINIT, vk.name(), vk.typeName());
3725                     break;
3726                 }
3727                 case TEMP_VAR_EXPRESSION_SUBKIND: {
3728                     VarSnippet vk = (VarSnippet) sn;
3729                     custom(FormatCase.EXPRESSION, vk.name(), vk.typeName());
3730                     break;
3731                 }
3732                 case OTHER_EXPRESSION_SUBKIND:
3733                     error("Unexpected expression form -- value is: %s", (value));
3734                     break;
3735                 case VAR_VALUE_SUBKIND: {
3736                     ExpressionSnippet ek = (ExpressionSnippet) sn;
3737                     custom(FormatCase.VARVALUE, ek.name(), ek.typeName());
3738                     break;
3739                 }
3740                 case ASSIGNMENT_SUBKIND: {
3741                     ExpressionSnippet ek = (ExpressionSnippet) sn;
3742                     custom(FormatCase.ASSIGNMENT, ek.name(), ek.typeName());
3743                     break;
3744                 }
3745                 case SINGLE_TYPE_IMPORT_SUBKIND:
3746                 case TYPE_IMPORT_ON_DEMAND_SUBKIND:
3747                 case SINGLE_STATIC_IMPORT_SUBKIND:
3748                 case STATIC_IMPORT_ON_DEMAND_SUBKIND:
3749                     custom(FormatCase.IMPORT, ((ImportSnippet) sn).name());
3750                     break;
3751                 case STATEMENT_SUBKIND:
3752                     custom(FormatCase.STATEMENT, null);
3753                     break;
3754             }
3755         }
3756     }
3757 
3758     /** The current version number as a string.
3759      */
3760     String version() {
3761         return version("release");  // mm.nn.oo[-milestone]
3762     }
3763 
3764     /** The current full version number as a string.
3765      */
3766     String fullVersion() {
3767         return version("full"); // mm.mm.oo[-milestone]-build
3768     }
3769 
3770     private String version(String key) {
3771         if (versionRB == null) {
3772             try {
3773                 versionRB = ResourceBundle.getBundle(VERSION_RB_NAME, locale);
3774             } catch (MissingResourceException e) {
3775                 return "(version info not available)";
3776             }
3777         }
3778         try {
3779             return versionRB.getString(key);
3780         }
3781         catch (MissingResourceException e) {
3782             return "(version info not available)";
3783         }
3784     }
3785 
3786     class NameSpace {
3787         final String spaceName;
3788         final String prefix;
3789         private int nextNum;
3790 
3791         NameSpace(String spaceName, String prefix) {
3792             this.spaceName = spaceName;
3793             this.prefix = prefix;
3794             this.nextNum = 1;
3795         }
3796 
3797         String tid(Snippet sn) {
3798             String tid = prefix + nextNum++;
3799             mapSnippet.put(sn, new SnippetInfo(sn, this, tid));
3800             return tid;
3801         }
3802 
3803         String tidNext() {
3804             return prefix + nextNum;
3805         }
3806     }
3807 
3808     static class SnippetInfo {
3809         final Snippet snippet;
3810         final NameSpace space;
3811         final String tid;
3812 
3813         SnippetInfo(Snippet snippet, NameSpace space, String tid) {
3814             this.snippet = snippet;
3815             this.space = space;
3816             this.tid = tid;
3817         }
3818     }
3819 
3820     static class ArgSuggestion implements Suggestion {
3821 
3822         private final String continuation;
3823 
3824         /**
3825          * Create a {@code Suggestion} instance.
3826          *
3827          * @param continuation a candidate continuation of the user's input
3828          */
3829         public ArgSuggestion(String continuation) {
3830             this.continuation = continuation;
3831         }
3832 
3833         /**
3834          * The candidate continuation of the given user's input.
3835          *
3836          * @return the continuation string
3837          */
3838         @Override
3839         public String continuation() {
3840             return continuation;
3841         }
3842 
3843         /**
3844          * Indicates whether input continuation matches the target type and is thus
3845          * more likely to be the desired continuation. A matching continuation is
3846          * preferred.
3847          *
3848          * @return {@code false}, non-types analysis
3849          */
3850         @Override
3851         public boolean matchesType() {
3852             return false;
3853         }
3854     }
3855 }
3856 
3857 abstract class NonInteractiveIOContext extends IOContext {
3858 
3859     @Override
3860     public boolean interactiveOutput() {
3861         return false;
3862     }
3863 
3864     @Override
3865     public Iterable<String> currentSessionHistory() {
3866         return Collections.emptyList();
3867     }
3868 
3869     @Override
3870     public boolean terminalEditorRunning() {
3871         return false;
3872     }
3873 
3874     @Override
3875     public void suspend() {
3876     }
3877 
3878     @Override
3879     public void resume() {
3880     }
3881 
3882     @Override
3883     public void beforeUserCode() {
3884     }
3885 
3886     @Override
3887     public void afterUserCode() {
3888     }
3889 
3890     @Override
3891     public void replaceLastHistoryEntry(String source) {
3892     }
3893 }
3894 
3895 class ScannerIOContext extends NonInteractiveIOContext {
3896     private final Scanner scannerIn;
3897 
3898     ScannerIOContext(Scanner scannerIn) {
3899         this.scannerIn = scannerIn;
3900     }
3901 
3902     ScannerIOContext(Reader rdr) throws FileNotFoundException {
3903         this(new Scanner(rdr));
3904     }
3905 
3906     @Override
3907     public String readLine(String prompt, String prefix) {
3908         if (scannerIn.hasNextLine()) {
3909             return scannerIn.nextLine();
3910         } else {
3911             return null;
3912         }
3913     }
3914 
3915     @Override
3916     public void close() {
3917         scannerIn.close();
3918     }
3919 
3920     @Override
3921     public int readUserInput() {
3922         return -1;
3923     }
3924 }
3925 
3926 class ReloadIOContext extends NonInteractiveIOContext {
3927     private final Iterator<String> it;
3928     private final PrintStream echoStream;
3929 
3930     ReloadIOContext(Iterable<String> history, PrintStream echoStream) {
3931         this.it = history.iterator();
3932         this.echoStream = echoStream;
3933     }
3934 
3935     @Override
3936     public String readLine(String prompt, String prefix) {
3937         String s = it.hasNext()
3938                 ? it.next()
3939                 : null;
3940         if (echoStream != null && s != null) {
3941             String p = "-: ";
3942             String p2 = "\n   ";
3943             echoStream.printf("%s%s\n", p, s.replace("\n", p2));
3944         }
3945         return s;
3946     }
3947 
3948     @Override
3949     public void close() {
3950     }
3951 
3952     @Override
3953     public int readUserInput() {
3954         return -1;
3955     }
3956 }