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