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