JShellTool.java revision 3717:2a3e23ee1b65
1/* 2 * Copyright (c) 2014, 2016, 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 26package jdk.internal.jshell.tool; 27 28import java.io.BufferedWriter; 29import java.io.File; 30import java.io.FileNotFoundException; 31import java.io.FileReader; 32import java.io.IOException; 33import java.io.InputStream; 34import java.io.PrintStream; 35import java.io.Reader; 36import java.io.StringReader; 37import java.nio.charset.Charset; 38import java.nio.file.AccessDeniedException; 39import java.nio.file.FileSystems; 40import java.nio.file.Files; 41import java.nio.file.NoSuchFileException; 42import java.nio.file.Path; 43import java.nio.file.Paths; 44import java.text.MessageFormat; 45import java.util.ArrayList; 46import java.util.Arrays; 47import java.util.Collections; 48import java.util.Iterator; 49import java.util.LinkedHashMap; 50import java.util.LinkedHashSet; 51import java.util.List; 52import java.util.Locale; 53import java.util.Map; 54import java.util.Map.Entry; 55import java.util.Scanner; 56import java.util.Set; 57import java.util.function.Consumer; 58import java.util.function.Predicate; 59import java.util.prefs.Preferences; 60import java.util.regex.Matcher; 61import java.util.regex.Pattern; 62import java.util.stream.Collectors; 63import java.util.stream.Stream; 64import java.util.stream.StreamSupport; 65 66import jdk.internal.jshell.debug.InternalDebugControl; 67import jdk.internal.jshell.tool.IOContext.InputInterruptedException; 68import jdk.jshell.DeclarationSnippet; 69import jdk.jshell.Diag; 70import jdk.jshell.EvalException; 71import jdk.jshell.ExpressionSnippet; 72import jdk.jshell.ImportSnippet; 73import jdk.jshell.JShell; 74import jdk.jshell.JShell.Subscription; 75import jdk.jshell.MethodSnippet; 76import jdk.jshell.Snippet; 77import jdk.jshell.Snippet.Status; 78import jdk.jshell.SnippetEvent; 79import jdk.jshell.SourceCodeAnalysis; 80import jdk.jshell.SourceCodeAnalysis.CompletionInfo; 81import jdk.jshell.SourceCodeAnalysis.Suggestion; 82import jdk.jshell.TypeDeclSnippet; 83import jdk.jshell.UnresolvedReferenceException; 84import jdk.jshell.VarSnippet; 85 86import static java.nio.file.StandardOpenOption.CREATE; 87import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; 88import static java.nio.file.StandardOpenOption.WRITE; 89import java.util.MissingResourceException; 90import java.util.Optional; 91import java.util.ResourceBundle; 92import java.util.Spliterators; 93import java.util.function.Function; 94import java.util.function.Supplier; 95import jdk.internal.joptsimple.*; 96import jdk.internal.jshell.tool.Feedback.FormatAction; 97import jdk.internal.jshell.tool.Feedback.FormatCase; 98import jdk.internal.jshell.tool.Feedback.FormatErrors; 99import jdk.internal.jshell.tool.Feedback.FormatResolve; 100import jdk.internal.jshell.tool.Feedback.FormatUnresolved; 101import jdk.internal.jshell.tool.Feedback.FormatWhen; 102import static java.util.Arrays.asList; 103import static java.util.Arrays.stream; 104import static java.util.stream.Collectors.joining; 105import static java.util.stream.Collectors.toList; 106import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND; 107import static java.util.stream.Collectors.toMap; 108import static jdk.internal.jshell.debug.InternalDebugControl.DBG_COMPA; 109import static jdk.internal.jshell.debug.InternalDebugControl.DBG_DEP; 110import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT; 111import static jdk.internal.jshell.debug.InternalDebugControl.DBG_FMGR; 112import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN; 113import static jdk.internal.jshell.tool.ContinuousCompletionProvider.STARTSWITH_MATCHER; 114 115/** 116 * Command line REPL tool for Java using the JShell API. 117 * @author Robert Field 118 */ 119public class JShellTool implements MessageHandler { 120 121 private static final String LINE_SEP = System.getProperty("line.separator"); 122 private static final Pattern LINEBREAK = Pattern.compile("\\R"); 123 private static final String RECORD_SEPARATOR = "\u241E"; 124 private static final String RB_NAME_PREFIX = "jdk.internal.jshell.tool.resources"; 125 private static final String VERSION_RB_NAME = RB_NAME_PREFIX + ".version"; 126 private static final String L10N_RB_NAME = RB_NAME_PREFIX + ".l10n"; 127 128 final InputStream cmdin; 129 final PrintStream cmdout; 130 final PrintStream cmderr; 131 final PrintStream console; 132 final InputStream userin; 133 final PrintStream userout; 134 final PrintStream usererr; 135 final Preferences prefs; 136 final Locale locale; 137 138 final Feedback feedback = new Feedback(); 139 140 /** 141 * The constructor for the tool (used by tool launch via main and by test 142 * harnesses to capture ins and outs. 143 * @param in command line input -- snippets, commands and user input 144 * @param cmdout command line output, feedback including errors 145 * @param cmderr start-up errors and debugging info 146 * @param console console control interaction 147 * @param userout code execution output -- System.out.printf("hi") 148 * @param usererr code execution error stream -- System.err.printf("Oops") 149 * @param prefs preferences to use 150 * @param locale locale to use 151 */ 152 public JShellTool(InputStream in, PrintStream cmdout, PrintStream cmderr, 153 PrintStream console, 154 PrintStream userout, PrintStream usererr, 155 Preferences prefs, Locale locale) { 156 this(in, cmdout, cmderr, console, null, userout, usererr, prefs, locale); 157 } 158 159 /** 160 * The constructor for the tool (used by tool launch via main and by test 161 * harnesses to capture ins and outs. 162 * @param cmdin command line input -- snippets and commands 163 * @param cmdout command line output, feedback including errors 164 * @param cmderr start-up errors and debugging info 165 * @param console console control interaction 166 * @param userin code execution input, or null to use IOContext 167 * @param userout code execution output -- System.out.printf("hi") 168 * @param usererr code execution error stream -- System.err.printf("Oops") 169 * @param prefs preferences to use 170 * @param locale locale to use 171 */ 172 public JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr, 173 PrintStream console, 174 InputStream userin, PrintStream userout, PrintStream usererr, 175 Preferences prefs, Locale locale) { 176 this.cmdin = cmdin; 177 this.cmdout = cmdout; 178 this.cmderr = cmderr; 179 this.console = console; 180 this.userin = userin != null ? userin : new InputStream() { 181 @Override 182 public int read() throws IOException { 183 return input.readUserInput(); 184 } 185 }; 186 this.userout = userout; 187 this.usererr = usererr; 188 this.prefs = prefs; 189 this.locale = locale; 190 } 191 192 private ResourceBundle versionRB = null; 193 private ResourceBundle outputRB = null; 194 195 private IOContext input = null; 196 private boolean regenerateOnDeath = true; 197 private boolean live = false; 198 private boolean feedbackInitialized = false; 199 private String commandLineFeedbackMode = null; 200 private List<String> remoteVMOptions = new ArrayList<>(); 201 private List<String> compilerOptions = new ArrayList<>(); 202 203 SourceCodeAnalysis analysis; 204 JShell state = null; 205 Subscription shutdownSubscription = null; 206 207 static final EditorSetting BUILT_IN_EDITOR = new EditorSetting(null, false); 208 209 private boolean debug = false; 210 public boolean testPrompt = false; 211 private String cmdlineClasspath = null; 212 private String startup = null; 213 private EditorSetting editor = BUILT_IN_EDITOR; 214 215 // Commands and snippets which should be replayed 216 private List<String> replayableHistory; 217 private List<String> replayableHistoryPrevious; 218 219 static final String STARTUP_KEY = "STARTUP"; 220 static final String EDITOR_KEY = "EDITOR"; 221 static final String FEEDBACK_KEY = "FEEDBACK"; 222 static final String MODE_KEY = "MODE"; 223 static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE"; 224 225 static final String DEFAULT_STARTUP = 226 "\n" + 227 "import java.util.*;\n" + 228 "import java.io.*;\n" + 229 "import java.math.*;\n" + 230 "import java.net.*;\n" + 231 "import java.util.concurrent.*;\n" + 232 "import java.util.prefs.*;\n" + 233 "import java.util.regex.*;\n" + 234 "void printf(String format, Object... args) { System.out.printf(format, args); }\n"; 235 236 // Tool id (tid) mapping: the three name spaces 237 NameSpace mainNamespace; 238 NameSpace startNamespace; 239 NameSpace errorNamespace; 240 241 // Tool id (tid) mapping: the current name spaces 242 NameSpace currentNameSpace; 243 244 Map<Snippet,SnippetInfo> mapSnippet; 245 246 /** 247 * Is the input/output currently interactive 248 * 249 * @return true if console 250 */ 251 boolean interactive() { 252 return input != null && input.interactiveOutput(); 253 } 254 255 void debug(String format, Object... args) { 256 if (debug) { 257 cmderr.printf(format + "\n", args); 258 } 259 } 260 261 /** 262 * Base output for command output -- no pre- or post-fix 263 * 264 * @param printf format 265 * @param printf args 266 */ 267 void rawout(String format, Object... args) { 268 cmdout.printf(format, args); 269 } 270 271 /** 272 * Must show command output 273 * 274 * @param format printf format 275 * @param args printf args 276 */ 277 @Override 278 public void hard(String format, Object... args) { 279 rawout(feedback.getPre() + format + feedback.getPost(), args); 280 } 281 282 /** 283 * Error command output 284 * 285 * @param format printf format 286 * @param args printf args 287 */ 288 void error(String format, Object... args) { 289 rawout(feedback.getErrorPre() + format + feedback.getErrorPost(), args); 290 } 291 292 /** 293 * Should optional informative be displayed? 294 * @return true if they should be displayed 295 */ 296 @Override 297 public boolean showFluff() { 298 return feedback.shouldDisplayCommandFluff() && interactive(); 299 } 300 301 /** 302 * Optional output 303 * 304 * @param format printf format 305 * @param args printf args 306 */ 307 @Override 308 public void fluff(String format, Object... args) { 309 if (showFluff()) { 310 hard(format, args); 311 } 312 } 313 314 /** 315 * Optional output -- with embedded per- and post-fix 316 * 317 * @param format printf format 318 * @param args printf args 319 */ 320 void fluffRaw(String format, Object... args) { 321 if (showFluff()) { 322 rawout(format, args); 323 } 324 } 325 326 /** 327 * Print using resource bundle look-up and adding prefix and postfix 328 * 329 * @param key the resource key 330 */ 331 String getResourceString(String key) { 332 if (outputRB == null) { 333 try { 334 outputRB = ResourceBundle.getBundle(L10N_RB_NAME, locale); 335 } catch (MissingResourceException mre) { 336 error("Cannot find ResourceBundle: %s for locale: %s", L10N_RB_NAME, locale); 337 return ""; 338 } 339 } 340 String s; 341 try { 342 s = outputRB.getString(key); 343 } catch (MissingResourceException mre) { 344 error("Missing resource: %s in %s", key, L10N_RB_NAME); 345 return ""; 346 } 347 return s; 348 } 349 350 /** 351 * Add prefixing to embedded newlines in a string, leading with the normal 352 * prefix 353 * 354 * @param s the string to prefix 355 */ 356 String prefix(String s) { 357 return prefix(s, feedback.getPre()); 358 } 359 360 /** 361 * Add prefixing to embedded newlines in a string 362 * 363 * @param s the string to prefix 364 * @param leading the string to prepend 365 */ 366 String prefix(String s, String leading) { 367 if (s == null || s.isEmpty()) { 368 return ""; 369 } 370 return leading 371 + s.substring(0, s.length() - 1).replaceAll("\\R", System.getProperty("line.separator") + feedback.getPre()) 372 + s.substring(s.length() - 1, s.length()); 373 } 374 375 /** 376 * Print using resource bundle look-up and adding prefix and postfix 377 * 378 * @param key the resource key 379 */ 380 void hardrb(String key) { 381 String s = prefix(getResourceString(key)); 382 cmdout.println(s); 383 } 384 385 /** 386 * Format using resource bundle look-up using MessageFormat 387 * 388 * @param key the resource key 389 * @param args 390 */ 391 String messageFormat(String key, Object... args) { 392 String rs = getResourceString(key); 393 return MessageFormat.format(rs, args); 394 } 395 396 /** 397 * Print using resource bundle look-up, MessageFormat, and add prefix and 398 * postfix 399 * 400 * @param key the resource key 401 * @param args 402 */ 403 @Override 404 public void hardmsg(String key, Object... args) { 405 cmdout.println(prefix(messageFormat(key, args))); 406 } 407 408 /** 409 * Print error using resource bundle look-up, MessageFormat, and add prefix 410 * and postfix 411 * 412 * @param key the resource key 413 * @param args 414 */ 415 @Override 416 public void errormsg(String key, Object... args) { 417 if (isRunningInteractive()) { 418 cmdout.println(prefix(messageFormat(key, args), feedback.getErrorPre())); 419 } else { 420 startmsg(key, args); 421 } 422 } 423 424 /** 425 * Print command-line error using resource bundle look-up, MessageFormat 426 * 427 * @param key the resource key 428 * @param args 429 */ 430 void startmsg(String key, Object... args) { 431 cmderr.println(prefix(messageFormat(key, args), "")); 432 } 433 434 /** 435 * Print (fluff) using resource bundle look-up, MessageFormat, and add 436 * prefix and postfix 437 * 438 * @param key the resource key 439 * @param args 440 */ 441 @Override 442 public void fluffmsg(String key, Object... args) { 443 if (showFluff()) { 444 hardmsg(key, args); 445 } 446 } 447 448 <T> void hardPairs(Stream<T> stream, Function<T, String> a, Function<T, String> b) { 449 Map<String, String> a2b = stream.collect(toMap(a, b, 450 (m1, m2) -> m1, 451 () -> new LinkedHashMap<>())); 452 int aLen = 0; 453 for (String av : a2b.keySet()) { 454 aLen = Math.max(aLen, av.length()); 455 } 456 String format = " %-" + aLen + "s -- %s"; 457 String indentedNewLine = LINE_SEP + feedback.getPre() 458 + String.format(" %-" + (aLen + 4) + "s", ""); 459 for (Entry<String, String> e : a2b.entrySet()) { 460 hard(format, e.getKey(), e.getValue().replaceAll("\n", indentedNewLine)); 461 } 462 } 463 464 /** 465 * Trim whitespace off end of string 466 * 467 * @param s 468 * @return 469 */ 470 static String trimEnd(String s) { 471 int last = s.length() - 1; 472 int i = last; 473 while (i >= 0 && Character.isWhitespace(s.charAt(i))) { 474 --i; 475 } 476 if (i != last) { 477 return s.substring(0, i + 1); 478 } else { 479 return s; 480 } 481 } 482 483 /** 484 * Normal start entry point 485 * @param args 486 * @throws Exception 487 */ 488 public static void main(String[] args) throws Exception { 489 new JShellTool(System.in, System.out, System.err, System.out, 490 System.out, System.err, 491 Preferences.userRoot().node("tool/JShell"), 492 Locale.getDefault()) 493 .start(args); 494 } 495 496 public void start(String[] args) throws Exception { 497 List<String> loadList = processCommandArgs(args); 498 if (loadList == null) { 499 // Abort 500 return; 501 } 502 try (IOContext in = new ConsoleIOContext(this, cmdin, console)) { 503 start(in, loadList); 504 } 505 } 506 507 private void start(IOContext in, List<String> loadList) { 508 // If startup hasn't been set by command line, set from retained/default 509 if (startup == null) { 510 startup = prefs.get(STARTUP_KEY, null); 511 if (startup == null) { 512 startup = DEFAULT_STARTUP; 513 } 514 } 515 516 // Read retained editor setting (if any) 517 editor = EditorSetting.fromPrefs(prefs); 518 if (editor == null) { 519 editor = BUILT_IN_EDITOR; 520 } 521 522 resetState(); // Initialize 523 524 // Read replay history from last jshell session into previous history 525 String prevReplay = prefs.get(REPLAY_RESTORE_KEY, null); 526 if (prevReplay != null) { 527 replayableHistoryPrevious = Arrays.asList(prevReplay.split(RECORD_SEPARATOR)); 528 } 529 530 for (String loadFile : loadList) { 531 runFile(loadFile, "jshell"); 532 } 533 534 if (regenerateOnDeath) { 535 hardmsg("jshell.msg.welcome", version()); 536 } 537 538 try { 539 while (regenerateOnDeath) { 540 if (!live) { 541 resetState(); 542 } 543 run(in); 544 } 545 } finally { 546 closeState(); 547 } 548 } 549 550 /** 551 * Process the command line arguments. 552 * Set options. 553 * @param args the command line arguments 554 * @return the list of files to be loaded 555 */ 556 private List<String> processCommandArgs(String[] args) { 557 OptionParser parser = new OptionParser(); 558 OptionSpec<String> cp = parser.accepts("class-path").withRequiredArg(); 559 OptionSpec<String> st = parser.accepts("startup").withRequiredArg(); 560 parser.acceptsAll(asList("n", "no-startup")); 561 OptionSpec<String> fb = parser.accepts("feedback").withRequiredArg(); 562 parser.accepts("q"); 563 parser.accepts("s"); 564 parser.accepts("v"); 565 OptionSpec<String> r = parser.accepts("R").withRequiredArg(); 566 OptionSpec<String> c = parser.accepts("C").withRequiredArg(); 567 parser.acceptsAll(asList("h", "help")); 568 parser.accepts("version"); 569 parser.accepts("full-version"); 570 571 parser.accepts("X"); 572 OptionSpec<String> addExports = parser.accepts("add-exports").withRequiredArg(); 573 574 NonOptionArgumentSpec<String> loadFileSpec = parser.nonOptions(); 575 576 OptionSet options; 577 try { 578 options = parser.parse(args); 579 } catch (OptionException ex) { 580 if (ex.options().isEmpty()) { 581 startmsg("jshell.err.opt.invalid", stream(args).collect(joining(", "))); 582 } else { 583 boolean isKnown = parser.recognizedOptions().containsKey(ex.options().iterator().next()); 584 startmsg(isKnown 585 ? "jshell.err.opt.arg" 586 : "jshell.err.opt.unknown", 587 ex.options() 588 .stream() 589 .collect(joining(", "))); 590 } 591 return null; 592 } 593 594 if (options.has("help")) { 595 printUsage(); 596 return null; 597 } 598 if (options.has("X")) { 599 printUsageX(); 600 return null; 601 } 602 if (options.has("version")) { 603 cmdout.printf("jshell %s\n", version()); 604 return null; 605 } 606 if (options.has("full-version")) { 607 cmdout.printf("jshell %s\n", fullVersion()); 608 return null; 609 } 610 if (options.has(cp)) { 611 List<String> cps = options.valuesOf(cp); 612 if (cps.size() > 1) { 613 startmsg("jshell.err.opt.one", "--class-path"); 614 return null; 615 } 616 cmdlineClasspath = cps.get(0); 617 } 618 if (options.has(st)) { 619 List<String> sts = options.valuesOf(st); 620 if (sts.size() != 1 || options.has("no-startup")) { 621 startmsg("jshell.err.opt.startup.one"); 622 return null; 623 } 624 startup = readFile(sts.get(0), "--startup"); 625 if (startup == null) { 626 return null; 627 } 628 } else if (options.has("no-startup")) { 629 startup = ""; 630 } 631 if ((options.valuesOf(fb).size() + 632 (options.has("q") ? 1 : 0) + 633 (options.has("s") ? 1 : 0) + 634 (options.has("v") ? 1 : 0)) > 1) { 635 startmsg("jshell.err.opt.feedback.one"); 636 return null; 637 } else if (options.has(fb)) { 638 commandLineFeedbackMode = options.valueOf(fb); 639 } else if (options.has("q")) { 640 commandLineFeedbackMode = "concise"; 641 } else if (options.has("s")) { 642 commandLineFeedbackMode = "silent"; 643 } else if (options.has("v")) { 644 commandLineFeedbackMode = "verbose"; 645 } 646 if (options.has(r)) { 647 remoteVMOptions.addAll(options.valuesOf(r)); 648 } 649 if (options.has(c)) { 650 compilerOptions.addAll(options.valuesOf(c)); 651 } 652 653 if (options.has(addExports)) { 654 List<String> exports = options.valuesOf(addExports).stream() 655 .map(mp -> mp + "=ALL-UNNAMED") 656 .flatMap(mp -> Stream.of("--add-exports", mp)) 657 .collect(toList()); 658 remoteVMOptions.addAll(exports); 659 compilerOptions.addAll(exports); 660 } 661 662 return options.valuesOf(loadFileSpec); 663 } 664 665 private void printUsage() { 666 cmdout.print(getResourceString("help.usage")); 667 } 668 669 private void printUsageX() { 670 cmdout.print(getResourceString("help.usage.x")); 671 } 672 673 /** 674 * Message handler to use during initial start-up. 675 */ 676 private class InitMessageHandler implements MessageHandler { 677 678 @Override 679 public void fluff(String format, Object... args) { 680 //ignore 681 } 682 683 @Override 684 public void fluffmsg(String messageKey, Object... args) { 685 //ignore 686 } 687 688 @Override 689 public void hard(String format, Object... args) { 690 //ignore 691 } 692 693 @Override 694 public void hardmsg(String messageKey, Object... args) { 695 //ignore 696 } 697 698 @Override 699 public void errormsg(String messageKey, Object... args) { 700 startmsg(messageKey, args); 701 } 702 703 @Override 704 public boolean showFluff() { 705 return false; 706 } 707 } 708 709 private void resetState() { 710 closeState(); 711 712 // Initialize tool id mapping 713 mainNamespace = new NameSpace("main", ""); 714 startNamespace = new NameSpace("start", "s"); 715 errorNamespace = new NameSpace("error", "e"); 716 mapSnippet = new LinkedHashMap<>(); 717 currentNameSpace = startNamespace; 718 719 // Reset the replayable history, saving the old for restore 720 replayableHistoryPrevious = replayableHistory; 721 replayableHistory = new ArrayList<>(); 722 723 state = JShell.builder() 724 .in(userin) 725 .out(userout) 726 .err(usererr) 727 .tempVariableNameGenerator(()-> "$" + currentNameSpace.tidNext()) 728 .idGenerator((sn, i) -> (currentNameSpace == startNamespace || state.status(sn).isActive()) 729 ? currentNameSpace.tid(sn) 730 : errorNamespace.tid(sn)) 731 .remoteVMOptions(remoteVMOptions.stream().toArray(String[]::new)) 732 .compilerOptions(compilerOptions.stream().toArray(String[]::new)) 733 .build(); 734 shutdownSubscription = state.onShutdown((JShell deadState) -> { 735 if (deadState == state) { 736 hardmsg("jshell.msg.terminated"); 737 live = false; 738 } 739 }); 740 analysis = state.sourceCodeAnalysis(); 741 live = true; 742 if (!feedbackInitialized) { 743 // One time per run feedback initialization 744 feedbackInitialized = true; 745 initFeedback(); 746 } 747 748 if (cmdlineClasspath != null) { 749 state.addToClasspath(cmdlineClasspath); 750 } 751 752 startUpRun(startup); 753 currentNameSpace = mainNamespace; 754 } 755 756 private boolean isRunningInteractive() { 757 return currentNameSpace != null && currentNameSpace == mainNamespace; 758 } 759 760 //where -- one-time per run initialization of feedback modes 761 private void initFeedback() { 762 // No fluff, no prefix, for init failures 763 MessageHandler initmh = new InitMessageHandler(); 764 // Execute the feedback initialization code in the resource file 765 startUpRun(getResourceString("startup.feedback")); 766 // These predefined modes are read-only 767 feedback.markModesReadOnly(); 768 // Restore user defined modes retained on previous run with /set mode -retain 769 String encoded = prefs.get(MODE_KEY, null); 770 if (encoded != null && !encoded.isEmpty()) { 771 if (!feedback.restoreEncodedModes(initmh, encoded)) { 772 // Catastrophic corruption -- remove the retained modes 773 prefs.remove(MODE_KEY); 774 } 775 } 776 if (commandLineFeedbackMode != null) { 777 // The feedback mode to use was specified on the command line, use it 778 if (!setFeedback(initmh, new ArgTokenizer("--feedback", commandLineFeedbackMode))) { 779 regenerateOnDeath = false; 780 } 781 commandLineFeedbackMode = null; 782 } else { 783 String fb = prefs.get(FEEDBACK_KEY, null); 784 if (fb != null) { 785 // Restore the feedback mode to use that was retained 786 // on a previous run with /set feedback -retain 787 setFeedback(initmh, new ArgTokenizer("previous retain feedback", "-retain " + fb)); 788 } 789 } 790 } 791 792 //where 793 private void startUpRun(String start) { 794 try (IOContext suin = new FileScannerIOContext(new StringReader(start))) { 795 run(suin); 796 } catch (Exception ex) { 797 hardmsg("jshell.err.startup.unexpected.exception", ex); 798 ex.printStackTrace(cmdout); 799 } 800 } 801 802 private void closeState() { 803 live = false; 804 JShell oldState = state; 805 if (oldState != null) { 806 oldState.unsubscribe(shutdownSubscription); // No notification 807 oldState.close(); 808 } 809 } 810 811 /** 812 * Main loop 813 * @param in the line input/editing context 814 */ 815 private void run(IOContext in) { 816 IOContext oldInput = input; 817 input = in; 818 try { 819 String incomplete = ""; 820 while (live) { 821 String prompt; 822 if (isRunningInteractive()) { 823 prompt = testPrompt 824 ? incomplete.isEmpty() 825 ? "\u0005" //ENQ 826 : "\u0006" //ACK 827 : incomplete.isEmpty() 828 ? feedback.getPrompt(currentNameSpace.tidNext()) 829 : feedback.getContinuationPrompt(currentNameSpace.tidNext()) 830 ; 831 } else { 832 prompt = ""; 833 } 834 String raw; 835 try { 836 raw = in.readLine(prompt, incomplete); 837 } catch (InputInterruptedException ex) { 838 //input interrupted - clearing current state 839 incomplete = ""; 840 continue; 841 } 842 if (raw == null) { 843 //EOF 844 if (in.interactiveOutput()) { 845 // End after user ctrl-D 846 regenerateOnDeath = false; 847 } 848 break; 849 } 850 String trimmed = trimEnd(raw); 851 if (!trimmed.isEmpty()) { 852 String line = incomplete + trimmed; 853 854 // No commands in the middle of unprocessed source 855 if (incomplete.isEmpty() && line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*")) { 856 processCommand(line.trim()); 857 } else { 858 incomplete = processSourceCatchingReset(line); 859 } 860 } 861 } 862 } catch (IOException ex) { 863 errormsg("jshell.err.unexpected.exception", ex); 864 } finally { 865 input = oldInput; 866 } 867 } 868 869 private void addToReplayHistory(String s) { 870 if (isRunningInteractive()) { 871 replayableHistory.add(s); 872 } 873 } 874 875 private String processSourceCatchingReset(String src) { 876 try { 877 input.beforeUserCode(); 878 return processSource(src); 879 } catch (IllegalStateException ex) { 880 hard("Resetting..."); 881 live = false; // Make double sure 882 return ""; 883 } finally { 884 input.afterUserCode(); 885 } 886 } 887 888 private void processCommand(String cmd) { 889 if (cmd.startsWith("/-")) { 890 try { 891 //handle "/-[number]" 892 cmdUseHistoryEntry(Integer.parseInt(cmd.substring(1))); 893 return ; 894 } catch (NumberFormatException ex) { 895 //ignore 896 } 897 } 898 String arg = ""; 899 int idx = cmd.indexOf(' '); 900 if (idx > 0) { 901 arg = cmd.substring(idx + 1).trim(); 902 cmd = cmd.substring(0, idx); 903 } 904 Command[] candidates = findCommand(cmd, c -> c.kind.isRealCommand); 905 switch (candidates.length) { 906 case 0: 907 if (!rerunHistoryEntryById(cmd.substring(1))) { 908 errormsg("jshell.err.no.such.command.or.snippet.id", cmd); 909 fluffmsg("jshell.msg.help.for.help"); 910 } break; 911 case 1: 912 Command command = candidates[0]; 913 // If comand was successful and is of a replayable kind, add it the replayable history 914 if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) { 915 addToReplayHistory((command.command + " " + arg).trim()); 916 } break; 917 default: 918 errormsg("jshell.err.command.ambiguous", cmd, 919 Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", "))); 920 fluffmsg("jshell.msg.help.for.help"); 921 break; 922 } 923 } 924 925 private Command[] findCommand(String cmd, Predicate<Command> filter) { 926 Command exact = commands.get(cmd); 927 if (exact != null) 928 return new Command[] {exact}; 929 930 return commands.values() 931 .stream() 932 .filter(filter) 933 .filter(command -> command.command.startsWith(cmd)) 934 .toArray(size -> new Command[size]); 935 } 936 937 private static Path toPathResolvingUserHome(String pathString) { 938 if (pathString.replace(File.separatorChar, '/').startsWith("~/")) 939 return Paths.get(System.getProperty("user.home"), pathString.substring(2)); 940 else 941 return Paths.get(pathString); 942 } 943 944 static final class Command { 945 public final String command; 946 public final String helpKey; 947 public final Function<String,Boolean> run; 948 public final CompletionProvider completions; 949 public final CommandKind kind; 950 951 // NORMAL Commands 952 public Command(String command, Function<String,Boolean> run, CompletionProvider completions) { 953 this(command, run, completions, CommandKind.NORMAL); 954 } 955 956 // Special kinds of Commands 957 public Command(String command, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) { 958 this(command, "help." + command.substring(1), 959 run, completions, kind); 960 } 961 962 // Documentation pseudo-commands 963 public Command(String command, String helpKey, CommandKind kind) { 964 this(command, helpKey, 965 arg -> { throw new IllegalStateException(); }, 966 EMPTY_COMPLETION_PROVIDER, 967 kind); 968 } 969 970 public Command(String command, String helpKey, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) { 971 this.command = command; 972 this.helpKey = helpKey; 973 this.run = run; 974 this.completions = completions; 975 this.kind = kind; 976 } 977 978 } 979 980 interface CompletionProvider { 981 List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor); 982 983 } 984 985 enum CommandKind { 986 NORMAL(true, true, true), 987 REPLAY(true, true, true), 988 HIDDEN(true, false, false), 989 HELP_ONLY(false, true, false), 990 HELP_SUBJECT(false, false, false); 991 992 final boolean isRealCommand; 993 final boolean showInHelp; 994 final boolean shouldSuggestCompletions; 995 private CommandKind(boolean isRealCommand, boolean showInHelp, boolean shouldSuggestCompletions) { 996 this.isRealCommand = isRealCommand; 997 this.showInHelp = showInHelp; 998 this.shouldSuggestCompletions = shouldSuggestCompletions; 999 } 1000 } 1001 1002 static final class FixedCompletionProvider implements CompletionProvider { 1003 1004 private final String[] alternatives; 1005 1006 public FixedCompletionProvider(String... alternatives) { 1007 this.alternatives = alternatives; 1008 } 1009 1010 @Override 1011 public List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor) { 1012 List<Suggestion> result = new ArrayList<>(); 1013 1014 for (String alternative : alternatives) { 1015 if (alternative.startsWith(input)) { 1016 result.add(new ArgSuggestion(alternative)); 1017 } 1018 } 1019 1020 anchor[0] = 0; 1021 1022 return result; 1023 } 1024 1025 } 1026 1027 static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider(); 1028 private static final CompletionProvider KEYWORD_COMPLETION_PROVIDER = new FixedCompletionProvider("-all ", "-start ", "-history "); 1029 private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-restore", "-quiet"); 1030 private static final CompletionProvider SET_MODE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-command", "-quiet", "-delete"); 1031 private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true); 1032 private final Map<String, Command> commands = new LinkedHashMap<>(); 1033 private void registerCommand(Command cmd) { 1034 commands.put(cmd.command, cmd); 1035 } 1036 1037 private static CompletionProvider skipWordThenCompletion(CompletionProvider completionProvider) { 1038 return (input, cursor, anchor) -> { 1039 List<Suggestion> result = Collections.emptyList(); 1040 1041 int space = input.indexOf(' '); 1042 if (space != -1) { 1043 String rest = input.substring(space + 1); 1044 result = completionProvider.completionSuggestions(rest, cursor - space - 1, anchor); 1045 anchor[0] += space + 1; 1046 } 1047 1048 return result; 1049 }; 1050 } 1051 1052 private static CompletionProvider fileCompletions(Predicate<Path> accept) { 1053 return (code, cursor, anchor) -> { 1054 int lastSlash = code.lastIndexOf('/'); 1055 String path = code.substring(0, lastSlash + 1); 1056 String prefix = lastSlash != (-1) ? code.substring(lastSlash + 1) : code; 1057 Path current = toPathResolvingUserHome(path); 1058 List<Suggestion> result = new ArrayList<>(); 1059 try (Stream<Path> dir = Files.list(current)) { 1060 dir.filter(f -> accept.test(f) && f.getFileName().toString().startsWith(prefix)) 1061 .map(f -> new ArgSuggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : ""))) 1062 .forEach(result::add); 1063 } catch (IOException ex) { 1064 //ignore... 1065 } 1066 if (path.isEmpty()) { 1067 StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false) 1068 .filter(root -> accept.test(root) && root.toString().startsWith(prefix)) 1069 .map(root -> new ArgSuggestion(root.toString())) 1070 .forEach(result::add); 1071 } 1072 anchor[0] = path.length(); 1073 return result; 1074 }; 1075 } 1076 1077 private static CompletionProvider classPathCompletion() { 1078 return fileCompletions(p -> Files.isDirectory(p) || 1079 p.getFileName().toString().endsWith(".zip") || 1080 p.getFileName().toString().endsWith(".jar")); 1081 } 1082 1083 private CompletionProvider snippetCompletion(Supplier<Stream<? extends Snippet>> snippetsSupplier) { 1084 return (prefix, cursor, anchor) -> { 1085 anchor[0] = 0; 1086 return snippetsSupplier.get() 1087 .flatMap(k -> (k instanceof DeclarationSnippet) 1088 ? Stream.of(String.valueOf(k.id()), ((DeclarationSnippet) k).name()) 1089 : Stream.of(String.valueOf(k.id()))) 1090 .filter(k -> k.startsWith(prefix)) 1091 .map(k -> new ArgSuggestion(k)) 1092 .collect(Collectors.toList()); 1093 }; 1094 } 1095 1096 private CompletionProvider snippetKeywordCompletion(Supplier<Stream<? extends Snippet>> snippetsSupplier) { 1097 return (code, cursor, anchor) -> { 1098 List<Suggestion> result = new ArrayList<>(); 1099 result.addAll(KEYWORD_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor)); 1100 result.addAll(snippetCompletion(snippetsSupplier).completionSuggestions(code, cursor, anchor)); 1101 return result; 1102 }; 1103 } 1104 1105 private static CompletionProvider saveCompletion() { 1106 return (code, cursor, anchor) -> { 1107 List<Suggestion> result = new ArrayList<>(); 1108 int space = code.indexOf(' '); 1109 if (space == (-1)) { 1110 result.addAll(KEYWORD_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor)); 1111 } 1112 result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor)); 1113 anchor[0] += space + 1; 1114 return result; 1115 }; 1116 } 1117 1118 private static CompletionProvider reloadCompletion() { 1119 return (code, cursor, anchor) -> { 1120 List<Suggestion> result = new ArrayList<>(); 1121 int pastSpace = code.indexOf(' ') + 1; // zero if no space 1122 result.addAll(RELOAD_OPTIONS_COMPLETION_PROVIDER.completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor)); 1123 anchor[0] += pastSpace; 1124 return result; 1125 }; 1126 } 1127 1128 private static CompletionProvider orMostSpecificCompletion( 1129 CompletionProvider left, CompletionProvider right) { 1130 return (code, cursor, anchor) -> { 1131 int[] leftAnchor = {-1}; 1132 int[] rightAnchor = {-1}; 1133 1134 List<Suggestion> leftSuggestions = left.completionSuggestions(code, cursor, leftAnchor); 1135 List<Suggestion> rightSuggestions = right.completionSuggestions(code, cursor, rightAnchor); 1136 1137 List<Suggestion> suggestions = new ArrayList<>(); 1138 1139 if (leftAnchor[0] >= rightAnchor[0]) { 1140 anchor[0] = leftAnchor[0]; 1141 suggestions.addAll(leftSuggestions); 1142 } 1143 1144 if (leftAnchor[0] <= rightAnchor[0]) { 1145 anchor[0] = rightAnchor[0]; 1146 suggestions.addAll(rightSuggestions); 1147 } 1148 1149 return suggestions; 1150 }; 1151 } 1152 1153 // Snippet lists 1154 1155 Stream<Snippet> allSnippets() { 1156 return state.snippets(); 1157 } 1158 1159 Stream<Snippet> dropableSnippets() { 1160 return state.snippets() 1161 .filter(sn -> state.status(sn).isActive()); 1162 } 1163 1164 Stream<VarSnippet> allVarSnippets() { 1165 return state.snippets() 1166 .filter(sn -> sn.kind() == Snippet.Kind.VAR) 1167 .map(sn -> (VarSnippet) sn); 1168 } 1169 1170 Stream<MethodSnippet> allMethodSnippets() { 1171 return state.snippets() 1172 .filter(sn -> sn.kind() == Snippet.Kind.METHOD) 1173 .map(sn -> (MethodSnippet) sn); 1174 } 1175 1176 Stream<TypeDeclSnippet> allTypeSnippets() { 1177 return state.snippets() 1178 .filter(sn -> sn.kind() == Snippet.Kind.TYPE_DECL) 1179 .map(sn -> (TypeDeclSnippet) sn); 1180 } 1181 1182 // Table of commands -- with command forms, argument kinds, helpKey message, implementation, ... 1183 1184 { 1185 registerCommand(new Command("/list", 1186 arg -> cmdList(arg), 1187 snippetKeywordCompletion(this::allSnippets))); 1188 registerCommand(new Command("/edit", 1189 arg -> cmdEdit(arg), 1190 snippetCompletion(this::allSnippets))); 1191 registerCommand(new Command("/drop", 1192 arg -> cmdDrop(arg), 1193 snippetCompletion(this::dropableSnippets), 1194 CommandKind.REPLAY)); 1195 registerCommand(new Command("/save", 1196 arg -> cmdSave(arg), 1197 saveCompletion())); 1198 registerCommand(new Command("/open", 1199 arg -> cmdOpen(arg), 1200 FILE_COMPLETION_PROVIDER)); 1201 registerCommand(new Command("/vars", 1202 arg -> cmdVars(arg), 1203 snippetKeywordCompletion(this::allVarSnippets))); 1204 registerCommand(new Command("/methods", 1205 arg -> cmdMethods(arg), 1206 snippetKeywordCompletion(this::allMethodSnippets))); 1207 registerCommand(new Command("/types", 1208 arg -> cmdTypes(arg), 1209 snippetKeywordCompletion(this::allTypeSnippets))); 1210 registerCommand(new Command("/imports", 1211 arg -> cmdImports(), 1212 EMPTY_COMPLETION_PROVIDER)); 1213 registerCommand(new Command("/exit", 1214 arg -> cmdExit(), 1215 EMPTY_COMPLETION_PROVIDER)); 1216 registerCommand(new Command("/reset", 1217 arg -> cmdReset(), 1218 EMPTY_COMPLETION_PROVIDER)); 1219 registerCommand(new Command("/reload", 1220 arg -> cmdReload(arg), 1221 reloadCompletion())); 1222 registerCommand(new Command("/classpath", 1223 arg -> cmdClasspath(arg), 1224 classPathCompletion(), 1225 CommandKind.REPLAY)); 1226 registerCommand(new Command("/history", 1227 arg -> cmdHistory(), 1228 EMPTY_COMPLETION_PROVIDER)); 1229 registerCommand(new Command("/debug", 1230 arg -> cmdDebug(arg), 1231 EMPTY_COMPLETION_PROVIDER, 1232 CommandKind.HIDDEN)); 1233 registerCommand(new Command("/help", 1234 arg -> cmdHelp(arg), 1235 EMPTY_COMPLETION_PROVIDER)); 1236 registerCommand(new Command("/set", 1237 arg -> cmdSet(arg), 1238 new ContinuousCompletionProvider(Map.of( 1239 // need more completion for format for usability 1240 "format", feedback.modeCompletions(), 1241 "truncation", feedback.modeCompletions(), 1242 "feedback", feedback.modeCompletions(), 1243 "mode", skipWordThenCompletion(orMostSpecificCompletion( 1244 feedback.modeCompletions(SET_MODE_OPTIONS_COMPLETION_PROVIDER), 1245 SET_MODE_OPTIONS_COMPLETION_PROVIDER)), 1246 "prompt", feedback.modeCompletions(), 1247 "editor", fileCompletions(Files::isExecutable), 1248 "start", FILE_COMPLETION_PROVIDER), 1249 STARTSWITH_MATCHER))); 1250 registerCommand(new Command("/?", 1251 "help.quest", 1252 arg -> cmdHelp(arg), 1253 EMPTY_COMPLETION_PROVIDER, 1254 CommandKind.NORMAL)); 1255 registerCommand(new Command("/!", 1256 "help.bang", 1257 arg -> cmdUseHistoryEntry(-1), 1258 EMPTY_COMPLETION_PROVIDER, 1259 CommandKind.NORMAL)); 1260 1261 // Documentation pseudo-commands 1262 registerCommand(new Command("/<id>", 1263 "help.id", 1264 CommandKind.HELP_ONLY)); 1265 registerCommand(new Command("/-<n>", 1266 "help.previous", 1267 CommandKind.HELP_ONLY)); 1268 registerCommand(new Command("intro", 1269 "help.intro", 1270 CommandKind.HELP_SUBJECT)); 1271 registerCommand(new Command("shortcuts", 1272 "help.shortcuts", 1273 CommandKind.HELP_SUBJECT)); 1274 1275 commandCompletions = new ContinuousCompletionProvider( 1276 commands.values().stream() 1277 .filter(c -> c.kind.shouldSuggestCompletions) 1278 .collect(toMap(c -> c.command, c -> c.completions)), 1279 STARTSWITH_MATCHER); 1280 } 1281 1282 private ContinuousCompletionProvider commandCompletions; 1283 1284 public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) { 1285 return commandCompletions.completionSuggestions(code, cursor, anchor); 1286 } 1287 1288 public String commandDocumentation(String code, int cursor) { 1289 code = code.substring(0, cursor); 1290 int space = code.indexOf(' '); 1291 1292 if (space != (-1)) { 1293 String cmd = code.substring(0, space); 1294 Command command = commands.get(cmd); 1295 if (command != null) { 1296 return getResourceString(command.helpKey + ".summary"); 1297 } 1298 } 1299 1300 return null; 1301 } 1302 1303 // --- Command implementations --- 1304 1305 private static final String[] SET_SUBCOMMANDS = new String[]{ 1306 "format", "truncation", "feedback", "mode", "prompt", "editor", "start"}; 1307 1308 final boolean cmdSet(String arg) { 1309 String cmd = "/set"; 1310 ArgTokenizer at = new ArgTokenizer(cmd, arg.trim()); 1311 String which = subCommand(cmd, at, SET_SUBCOMMANDS); 1312 if (which == null) { 1313 return false; 1314 } 1315 switch (which) { 1316 case "_retain": { 1317 errormsg("jshell.err.setting.to.retain.must.be.specified", at.whole()); 1318 return false; 1319 } 1320 case "_blank": { 1321 // show top-level settings 1322 new SetEditor().set(); 1323 showSetStart(); 1324 setFeedback(this, at); // no args so shows feedback setting 1325 hardmsg("jshell.msg.set.show.mode.settings"); 1326 return true; 1327 } 1328 case "format": 1329 return feedback.setFormat(this, at); 1330 case "truncation": 1331 return feedback.setTruncation(this, at); 1332 case "feedback": 1333 return setFeedback(this, at); 1334 case "mode": 1335 return feedback.setMode(this, at, 1336 retained -> prefs.put(MODE_KEY, retained)); 1337 case "prompt": 1338 return feedback.setPrompt(this, at); 1339 case "editor": 1340 return new SetEditor(at).set(); 1341 case "start": 1342 return setStart(at); 1343 default: 1344 errormsg("jshell.err.arg", cmd, at.val()); 1345 return false; 1346 } 1347 } 1348 1349 boolean setFeedback(MessageHandler messageHandler, ArgTokenizer at) { 1350 return feedback.setFeedback(messageHandler, at, 1351 fb -> prefs.put(FEEDBACK_KEY, fb)); 1352 } 1353 1354 // Find which, if any, sub-command matches. 1355 // Return null on error 1356 String subCommand(String cmd, ArgTokenizer at, String[] subs) { 1357 at.allowedOptions("-retain"); 1358 String sub = at.next(); 1359 if (sub == null) { 1360 // No sub-command was given 1361 return at.hasOption("-retain") 1362 ? "_retain" 1363 : "_blank"; 1364 } 1365 String[] matches = Arrays.stream(subs) 1366 .filter(s -> s.startsWith(sub)) 1367 .toArray(size -> new String[size]); 1368 if (matches.length == 0) { 1369 // There are no matching sub-commands 1370 errormsg("jshell.err.arg", cmd, sub); 1371 fluffmsg("jshell.msg.use.one.of", Arrays.stream(subs) 1372 .collect(Collectors.joining(", ")) 1373 ); 1374 return null; 1375 } 1376 if (matches.length > 1) { 1377 // More than one sub-command matches the initial characters provided 1378 errormsg("jshell.err.sub.ambiguous", cmd, sub); 1379 fluffmsg("jshell.msg.use.one.of", Arrays.stream(matches) 1380 .collect(Collectors.joining(", ")) 1381 ); 1382 return null; 1383 } 1384 return matches[0]; 1385 } 1386 1387 static class EditorSetting { 1388 1389 static String BUILT_IN_REP = "-default"; 1390 static char WAIT_PREFIX = '-'; 1391 static char NORMAL_PREFIX = '*'; 1392 1393 final String[] cmd; 1394 final boolean wait; 1395 1396 EditorSetting(String[] cmd, boolean wait) { 1397 this.wait = wait; 1398 this.cmd = cmd; 1399 } 1400 1401 // returns null if not stored in preferences 1402 static EditorSetting fromPrefs(Preferences prefs) { 1403 // Read retained editor setting (if any) 1404 String editorString = prefs.get(EDITOR_KEY, ""); 1405 if (editorString == null || editorString.isEmpty()) { 1406 return null; 1407 } else if (editorString.equals(BUILT_IN_REP)) { 1408 return BUILT_IN_EDITOR; 1409 } else { 1410 boolean wait = false; 1411 char waitMarker = editorString.charAt(0); 1412 if (waitMarker == WAIT_PREFIX || waitMarker == NORMAL_PREFIX) { 1413 wait = waitMarker == WAIT_PREFIX; 1414 editorString = editorString.substring(1); 1415 } 1416 String[] cmd = editorString.split(RECORD_SEPARATOR); 1417 return new EditorSetting(cmd, wait); 1418 } 1419 } 1420 1421 void toPrefs(Preferences prefs) { 1422 prefs.put(EDITOR_KEY, (this == BUILT_IN_EDITOR) 1423 ? BUILT_IN_REP 1424 : (wait ? WAIT_PREFIX : NORMAL_PREFIX) + String.join(RECORD_SEPARATOR, cmd)); 1425 } 1426 1427 @Override 1428 public boolean equals(Object o) { 1429 if (o instanceof EditorSetting) { 1430 EditorSetting ed = (EditorSetting) o; 1431 return Arrays.equals(cmd, ed.cmd) && wait == ed.wait; 1432 } else { 1433 return false; 1434 } 1435 } 1436 1437 @Override 1438 public int hashCode() { 1439 int hash = 7; 1440 hash = 71 * hash + Arrays.deepHashCode(this.cmd); 1441 hash = 71 * hash + (this.wait ? 1 : 0); 1442 return hash; 1443 } 1444 } 1445 1446 class SetEditor { 1447 1448 private final ArgTokenizer at; 1449 private final String[] command; 1450 private final boolean hasCommand; 1451 private final boolean defaultOption; 1452 private final boolean waitOption; 1453 private final boolean retainOption; 1454 1455 SetEditor(ArgTokenizer at) { 1456 at.allowedOptions("-default", "-wait", "-retain"); 1457 String prog = at.next(); 1458 List<String> ed = new ArrayList<>(); 1459 while (at.val() != null) { 1460 ed.add(at.val()); 1461 at.nextToken(); // so that options are not interpreted as jshell options 1462 } 1463 this.at = at; 1464 this.command = ed.toArray(new String[ed.size()]); 1465 this.hasCommand = command.length > 0; 1466 this.defaultOption = at.hasOption("-default"); 1467 this.waitOption = at.hasOption("-wait"); 1468 this.retainOption = at.hasOption("-retain"); 1469 } 1470 1471 SetEditor() { 1472 this(new ArgTokenizer("", "")); 1473 } 1474 1475 boolean set() { 1476 if (!check()) { 1477 return false; 1478 } 1479 if (!hasCommand && !defaultOption && !retainOption) { 1480 // No settings or -retain, so this is a query 1481 EditorSetting retained = EditorSetting.fromPrefs(prefs); 1482 if (retained != null) { 1483 // retained editor is set 1484 hard("/set editor -retain %s", format(retained)); 1485 } 1486 if (retained == null || !retained.equals(editor)) { 1487 // editor is not retained or retained is different from set 1488 hard("/set editor %s", format(editor)); 1489 } 1490 return true; 1491 } 1492 install(); 1493 if (retainOption) { 1494 editor.toPrefs(prefs); 1495 fluffmsg("jshell.msg.set.editor.retain", format(editor)); 1496 } 1497 return true; 1498 } 1499 1500 private boolean check() { 1501 if (!checkOptionsAndRemainingInput(at)) { 1502 return false; 1503 } 1504 if (hasCommand && defaultOption) { 1505 errormsg("jshell.err.default.option.or.program", at.whole()); 1506 return false; 1507 } 1508 if (waitOption && !hasCommand) { 1509 errormsg("jshell.err.wait.applies.to.external.editor", at.whole()); 1510 return false; 1511 } 1512 return true; 1513 } 1514 1515 private void install() { 1516 if (hasCommand) { 1517 editor = new EditorSetting(command, waitOption); 1518 } else if (defaultOption) { 1519 editor = BUILT_IN_EDITOR; 1520 } else { 1521 return; 1522 } 1523 fluffmsg("jshell.msg.set.editor.set", format(editor)); 1524 } 1525 1526 private String format(EditorSetting ed) { 1527 if (ed == BUILT_IN_EDITOR) { 1528 return "-default"; 1529 } else { 1530 Stream<String> elems = Arrays.stream(ed.cmd); 1531 if (ed.wait) { 1532 elems = Stream.concat(Stream.of("-wait"), elems); 1533 } 1534 return elems.collect(joining(" ")); 1535 } 1536 } 1537 } 1538 1539 // The sub-command: /set start <start-file> 1540 boolean setStart(ArgTokenizer at) { 1541 at.allowedOptions("-default", "-none", "-retain"); 1542 String fn = at.next(); 1543 if (!checkOptionsAndRemainingInput(at)) { 1544 return false; 1545 } 1546 boolean defaultOption = at.hasOption("-default"); 1547 boolean noneOption = at.hasOption("-none"); 1548 boolean retainOption = at.hasOption("-retain"); 1549 boolean hasFile = fn != null; 1550 1551 int argCount = (defaultOption ? 1 : 0) + (noneOption ? 1 : 0) + (hasFile ? 1 : 0); 1552 if (argCount > 1) { 1553 errormsg("jshell.err.option.or.filename", at.whole()); 1554 return false; 1555 } 1556 if (argCount == 0 && !retainOption) { 1557 // no options or filename, show current setting 1558 showSetStart(); 1559 return true; 1560 } 1561 if (hasFile) { 1562 String init = readFile(fn, "/set start"); 1563 if (init == null) { 1564 return false; 1565 } 1566 startup = init; 1567 } else if (defaultOption) { 1568 startup = DEFAULT_STARTUP; 1569 } else if (noneOption) { 1570 startup = ""; 1571 } 1572 if (retainOption) { 1573 // retain startup setting 1574 prefs.put(STARTUP_KEY, startup); 1575 } 1576 return true; 1577 } 1578 1579 void showSetStart() { 1580 String retained = prefs.get(STARTUP_KEY, null); 1581 if (retained != null) { 1582 showSetStart(true, retained); 1583 } 1584 if (retained == null || !startup.equals(retained)) { 1585 showSetStart(false, startup); 1586 } 1587 } 1588 1589 void showSetStart(boolean isRetained, String start) { 1590 String cmd = "/set start" + (isRetained ? " -retain " : " "); 1591 String stset; 1592 if (start.equals(DEFAULT_STARTUP)) { 1593 stset = cmd + "-default"; 1594 } else if (start.isEmpty()) { 1595 stset = cmd + "-none"; 1596 } else { 1597 stset = prefix("startup.jsh:\n" + start + "\n" + cmd + "startup.jsh", ""); 1598 } 1599 hard(stset); 1600 } 1601 1602 boolean cmdClasspath(String arg) { 1603 if (arg.isEmpty()) { 1604 errormsg("jshell.err.classpath.arg"); 1605 return false; 1606 } else { 1607 state.addToClasspath(toPathResolvingUserHome(arg).toString()); 1608 fluffmsg("jshell.msg.classpath", arg); 1609 return true; 1610 } 1611 } 1612 1613 boolean cmdDebug(String arg) { 1614 if (arg.isEmpty()) { 1615 debug = !debug; 1616 InternalDebugControl.setDebugFlags(state, debug ? DBG_GEN : 0); 1617 fluff("Debugging %s", debug ? "on" : "off"); 1618 } else { 1619 int flags = 0; 1620 for (char ch : arg.toCharArray()) { 1621 switch (ch) { 1622 case '0': 1623 flags = 0; 1624 debug = false; 1625 fluff("Debugging off"); 1626 break; 1627 case 'r': 1628 debug = true; 1629 fluff("REPL tool debugging on"); 1630 break; 1631 case 'g': 1632 flags |= DBG_GEN; 1633 fluff("General debugging on"); 1634 break; 1635 case 'f': 1636 flags |= DBG_FMGR; 1637 fluff("File manager debugging on"); 1638 break; 1639 case 'c': 1640 flags |= DBG_COMPA; 1641 fluff("Completion analysis debugging on"); 1642 break; 1643 case 'd': 1644 flags |= DBG_DEP; 1645 fluff("Dependency debugging on"); 1646 break; 1647 case 'e': 1648 flags |= DBG_EVNT; 1649 fluff("Event debugging on"); 1650 break; 1651 default: 1652 hard("Unknown debugging option: %c", ch); 1653 fluff("Use: 0 r g f c d"); 1654 return false; 1655 } 1656 } 1657 InternalDebugControl.setDebugFlags(state, flags); 1658 } 1659 return true; 1660 } 1661 1662 private boolean cmdExit() { 1663 regenerateOnDeath = false; 1664 live = false; 1665 if (!replayableHistory.isEmpty()) { 1666 // Prevent history overflow by calculating what will fit, starting 1667 // with most recent 1668 int sepLen = RECORD_SEPARATOR.length(); 1669 int length = 0; 1670 int first = replayableHistory.size(); 1671 while(length < Preferences.MAX_VALUE_LENGTH && --first >= 0) { 1672 length += replayableHistory.get(first).length() + sepLen; 1673 } 1674 String hist = String.join(RECORD_SEPARATOR, 1675 replayableHistory.subList(first + 1, replayableHistory.size())); 1676 prefs.put(REPLAY_RESTORE_KEY, hist); 1677 } 1678 fluffmsg("jshell.msg.goodbye"); 1679 return true; 1680 } 1681 1682 boolean cmdHelp(String arg) { 1683 ArgTokenizer at = new ArgTokenizer("/help", arg); 1684 String subject = at.next(); 1685 if (subject != null) { 1686 Command[] matches = commands.values().stream() 1687 .filter(c -> c.command.startsWith(subject)) 1688 .toArray(size -> new Command[size]); 1689 if (matches.length == 1) { 1690 String cmd = matches[0].command; 1691 if (cmd.equals("/set")) { 1692 // Print the help doc for the specified sub-command 1693 String which = subCommand(cmd, at, SET_SUBCOMMANDS); 1694 if (which == null) { 1695 return false; 1696 } 1697 if (!which.equals("_blank")) { 1698 hardrb("help.set." + which); 1699 return true; 1700 } 1701 } 1702 } 1703 if (matches.length > 0) { 1704 for (Command c : matches) { 1705 hard(""); 1706 hard("%s", c.command); 1707 hard(""); 1708 hardrb(c.helpKey); 1709 } 1710 return true; 1711 } else { 1712 errormsg("jshell.err.help.arg", arg); 1713 } 1714 } 1715 hardmsg("jshell.msg.help.begin"); 1716 hardPairs(commands.values().stream() 1717 .filter(cmd -> cmd.kind.showInHelp), 1718 cmd -> cmd.command + " " + getResourceString(cmd.helpKey + ".args"), 1719 cmd -> getResourceString(cmd.helpKey + ".summary") 1720 ); 1721 hardmsg("jshell.msg.help.subject"); 1722 hardPairs(commands.values().stream() 1723 .filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT), 1724 cmd -> cmd.command, 1725 cmd -> getResourceString(cmd.helpKey + ".summary") 1726 ); 1727 return true; 1728 } 1729 1730 private boolean cmdHistory() { 1731 cmdout.println(); 1732 for (String s : input.currentSessionHistory()) { 1733 // No number prefix, confusing with snippet ids 1734 cmdout.printf("%s\n", s); 1735 } 1736 return true; 1737 } 1738 1739 /** 1740 * Avoid parameterized varargs possible heap pollution warning. 1741 */ 1742 private interface SnippetPredicate<T extends Snippet> extends Predicate<T> { } 1743 1744 /** 1745 * Apply filters to a stream until one that is non-empty is found. 1746 * Adapted from Stuart Marks 1747 * 1748 * @param supplier Supply the Snippet stream to filter 1749 * @param filters Filters to attempt 1750 * @return The non-empty filtered Stream, or null 1751 */ 1752 @SafeVarargs 1753 private static <T extends Snippet> Stream<T> nonEmptyStream(Supplier<Stream<T>> supplier, 1754 SnippetPredicate<T>... filters) { 1755 for (SnippetPredicate<T> filt : filters) { 1756 Iterator<T> iterator = supplier.get().filter(filt).iterator(); 1757 if (iterator.hasNext()) { 1758 return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); 1759 } 1760 } 1761 return null; 1762 } 1763 1764 private boolean inStartUp(Snippet sn) { 1765 return mapSnippet.get(sn).space == startNamespace; 1766 } 1767 1768 private boolean isActive(Snippet sn) { 1769 return state.status(sn).isActive(); 1770 } 1771 1772 private boolean mainActive(Snippet sn) { 1773 return !inStartUp(sn) && isActive(sn); 1774 } 1775 1776 private boolean matchingDeclaration(Snippet sn, String name) { 1777 return sn instanceof DeclarationSnippet 1778 && ((DeclarationSnippet) sn).name().equals(name); 1779 } 1780 1781 /** 1782 * Convert user arguments to a Stream of snippets referenced by those 1783 * arguments (or lack of arguments). 1784 * 1785 * @param snippets the base list of possible snippets 1786 * @param defFilter the filter to apply to the arguments if no argument 1787 * @param rawargs the user's argument to the command, maybe be the empty 1788 * string 1789 * @return a Stream of referenced snippets or null if no matches are found 1790 */ 1791 private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier, 1792 Predicate<Snippet> defFilter, String rawargs, String cmd) { 1793 ArgTokenizer at = new ArgTokenizer(cmd, rawargs.trim()); 1794 at.allowedOptions("-all", "-start"); 1795 List<String> args = new ArrayList<>(); 1796 String s; 1797 while ((s = at.next()) != null) { 1798 args.add(s); 1799 } 1800 if (!checkOptionsAndRemainingInput(at)) { 1801 return null; 1802 } 1803 if (at.optionCount() > 0 && args.size() > 0) { 1804 errormsg("jshell.err.may.not.specify.options.and.snippets", at.whole()); 1805 return null; 1806 } 1807 if (at.optionCount() > 1) { 1808 errormsg("jshell.err.conflicting.options", at.whole()); 1809 return null; 1810 } 1811 if (at.hasOption("-all")) { 1812 // all snippets including start-up, failed, and overwritten 1813 return snippetSupplier.get(); 1814 } 1815 if (at.hasOption("-start")) { 1816 // start-up snippets 1817 return snippetSupplier.get() 1818 .filter(this::inStartUp); 1819 } 1820 if (args.isEmpty()) { 1821 // Default is all active user snippets 1822 return snippetSupplier.get() 1823 .filter(defFilter); 1824 } 1825 return argsToSnippets(snippetSupplier, args); 1826 } 1827 1828 /** 1829 * Convert user arguments to a Stream of snippets referenced by those 1830 * arguments. 1831 * 1832 * @param snippetSupplier the base list of possible snippets 1833 * @param args the user's argument to the command, maybe be the empty list 1834 * @return a Stream of referenced snippets or null if no matches to specific 1835 * arg 1836 */ 1837 private <T extends Snippet> Stream<T> argsToSnippets(Supplier<Stream<T>> snippetSupplier, 1838 List<String> args) { 1839 Stream<T> result = null; 1840 for (String arg : args) { 1841 // Find the best match 1842 Stream<T> st = layeredSnippetSearch(snippetSupplier, arg); 1843 if (st == null) { 1844 Stream<Snippet> est = layeredSnippetSearch(state::snippets, arg); 1845 if (est == null) { 1846 errormsg("jshell.err.no.such.snippets", arg); 1847 } else { 1848 errormsg("jshell.err.the.snippet.cannot.be.used.with.this.command", 1849 arg, est.findFirst().get().source()); 1850 } 1851 return null; 1852 } 1853 if (result == null) { 1854 result = st; 1855 } else { 1856 result = Stream.concat(result, st); 1857 } 1858 } 1859 return result; 1860 } 1861 1862 private <T extends Snippet> Stream<T> layeredSnippetSearch(Supplier<Stream<T>> snippetSupplier, String arg) { 1863 return nonEmptyStream( 1864 // the stream supplier 1865 snippetSupplier, 1866 // look for active user declarations matching the name 1867 sn -> isActive(sn) && matchingDeclaration(sn, arg), 1868 // else, look for any declarations matching the name 1869 sn -> matchingDeclaration(sn, arg), 1870 // else, look for an id of this name 1871 sn -> sn.id().equals(arg) 1872 ); 1873 } 1874 1875 private boolean cmdDrop(String rawargs) { 1876 ArgTokenizer at = new ArgTokenizer("/drop", rawargs.trim()); 1877 at.allowedOptions(); 1878 List<String> args = new ArrayList<>(); 1879 String s; 1880 while ((s = at.next()) != null) { 1881 args.add(s); 1882 } 1883 if (!checkOptionsAndRemainingInput(at)) { 1884 return false; 1885 } 1886 if (args.isEmpty()) { 1887 errormsg("jshell.err.drop.arg"); 1888 return false; 1889 } 1890 Stream<Snippet> stream = argsToSnippets(this::dropableSnippets, args); 1891 if (stream == null) { 1892 // Snippet not found. Error already printed 1893 fluffmsg("jshell.msg.see.classes.etc"); 1894 return false; 1895 } 1896 List<Snippet> snippets = stream.collect(toList()); 1897 if (snippets.size() > args.size()) { 1898 // One of the args references more thean one snippet 1899 errormsg("jshell.err.drop.ambiguous"); 1900 fluffmsg("jshell.msg.use.one.of", snippets.stream() 1901 .map(sn -> String.format("\n/drop %-5s : %s", sn.id(), sn.source().replace("\n", "\n "))) 1902 .collect(Collectors.joining(", ")) 1903 ); 1904 return false; 1905 } 1906 snippets.stream() 1907 .forEach(sn -> state.drop(sn).forEach(this::handleEvent)); 1908 return true; 1909 } 1910 1911 private boolean cmdEdit(String arg) { 1912 Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, 1913 this::mainActive, arg, "/edit"); 1914 if (stream == null) { 1915 return false; 1916 } 1917 Set<String> srcSet = new LinkedHashSet<>(); 1918 stream.forEachOrdered(sn -> { 1919 String src = sn.source(); 1920 switch (sn.subKind()) { 1921 case VAR_VALUE_SUBKIND: 1922 break; 1923 case ASSIGNMENT_SUBKIND: 1924 case OTHER_EXPRESSION_SUBKIND: 1925 case TEMP_VAR_EXPRESSION_SUBKIND: 1926 if (!src.endsWith(";")) { 1927 src = src + ";"; 1928 } 1929 srcSet.add(src); 1930 break; 1931 default: 1932 srcSet.add(src); 1933 break; 1934 } 1935 }); 1936 StringBuilder sb = new StringBuilder(); 1937 for (String s : srcSet) { 1938 sb.append(s); 1939 sb.append('\n'); 1940 } 1941 String src = sb.toString(); 1942 Consumer<String> saveHandler = new SaveHandler(src, srcSet); 1943 Consumer<String> errorHandler = s -> hard("Edit Error: %s", s); 1944 if (editor == BUILT_IN_EDITOR) { 1945 try { 1946 EditPad.edit(errorHandler, src, saveHandler); 1947 } catch (RuntimeException ex) { 1948 errormsg("jshell.err.cant.launch.editor", ex); 1949 fluffmsg("jshell.msg.try.set.editor"); 1950 return false; 1951 } 1952 } else { 1953 ExternalEditor.edit(editor.cmd, errorHandler, src, saveHandler, input, 1954 editor.wait, this::hardrb); 1955 } 1956 return true; 1957 } 1958 //where 1959 // receives editor requests to save 1960 private class SaveHandler implements Consumer<String> { 1961 1962 String src; 1963 Set<String> currSrcs; 1964 1965 SaveHandler(String src, Set<String> ss) { 1966 this.src = src; 1967 this.currSrcs = ss; 1968 } 1969 1970 @Override 1971 public void accept(String s) { 1972 if (!s.equals(src)) { // quick check first 1973 src = s; 1974 try { 1975 Set<String> nextSrcs = new LinkedHashSet<>(); 1976 boolean failed = false; 1977 while (true) { 1978 CompletionInfo an = analysis.analyzeCompletion(s); 1979 if (!an.completeness().isComplete()) { 1980 break; 1981 } 1982 String tsrc = trimNewlines(an.source()); 1983 if (!failed && !currSrcs.contains(tsrc)) { 1984 failed = processCompleteSource(tsrc); 1985 } 1986 nextSrcs.add(tsrc); 1987 if (an.remaining().isEmpty()) { 1988 break; 1989 } 1990 s = an.remaining(); 1991 } 1992 currSrcs = nextSrcs; 1993 } catch (IllegalStateException ex) { 1994 hardmsg("jshell.msg.resetting"); 1995 resetState(); 1996 currSrcs = new LinkedHashSet<>(); // re-process everything 1997 } 1998 } 1999 } 2000 2001 private String trimNewlines(String s) { 2002 int b = 0; 2003 while (b < s.length() && s.charAt(b) == '\n') { 2004 ++b; 2005 } 2006 int e = s.length() -1; 2007 while (e >= 0 && s.charAt(e) == '\n') { 2008 --e; 2009 } 2010 return s.substring(b, e + 1); 2011 } 2012 } 2013 2014 private boolean cmdList(String arg) { 2015 if (arg.length() >= 2 && "-history".startsWith(arg)) { 2016 return cmdHistory(); 2017 } 2018 Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, 2019 this::mainActive, arg, "/list"); 2020 if (stream == null) { 2021 return false; 2022 } 2023 2024 // prevent double newline on empty list 2025 boolean[] hasOutput = new boolean[1]; 2026 stream.forEachOrdered(sn -> { 2027 if (!hasOutput[0]) { 2028 cmdout.println(); 2029 hasOutput[0] = true; 2030 } 2031 cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); 2032 }); 2033 return true; 2034 } 2035 2036 private boolean cmdOpen(String filename) { 2037 return runFile(filename, "/open"); 2038 } 2039 2040 private boolean runFile(String filename, String context) { 2041 if (!filename.isEmpty()) { 2042 try { 2043 run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString())); 2044 return true; 2045 } catch (FileNotFoundException e) { 2046 errormsg("jshell.err.file.not.found", context, filename, e.getMessage()); 2047 } catch (Exception e) { 2048 errormsg("jshell.err.file.exception", context, filename, e); 2049 } 2050 } else { 2051 errormsg("jshell.err.file.filename", context); 2052 } 2053 return false; 2054 } 2055 2056 /** 2057 * Read an external file. Error messages accessed via keyPrefix 2058 * 2059 * @param filename file to access or null 2060 * @param context printable non-natural language context for errors 2061 * @return contents of file as string 2062 */ 2063 String readFile(String filename, String context) { 2064 if (filename != null) { 2065 try { 2066 byte[] encoded = Files.readAllBytes(Paths.get(filename)); 2067 return new String(encoded); 2068 } catch (AccessDeniedException e) { 2069 errormsg("jshell.err.file.not.accessible", context, filename, e.getMessage()); 2070 } catch (NoSuchFileException e) { 2071 errormsg("jshell.err.file.not.found", context, filename); 2072 } catch (Exception e) { 2073 errormsg("jshell.err.file.exception", context, filename, e); 2074 } 2075 } else { 2076 errormsg("jshell.err.file.filename", context); 2077 } 2078 return null; 2079 2080 } 2081 2082 private boolean cmdReset() { 2083 live = false; 2084 fluffmsg("jshell.msg.resetting.state"); 2085 return true; 2086 } 2087 2088 private boolean cmdReload(String rawargs) { 2089 ArgTokenizer at = new ArgTokenizer("/reload", rawargs.trim()); 2090 at.allowedOptions("-restore", "-quiet"); 2091 if (!checkOptionsAndRemainingInput(at)) { 2092 return false; 2093 } 2094 Iterable<String> history; 2095 if (at.hasOption("-restore")) { 2096 if (replayableHistoryPrevious == null) { 2097 errormsg("jshell.err.reload.no.previous"); 2098 return false; 2099 } 2100 history = replayableHistoryPrevious; 2101 fluffmsg("jshell.err.reload.restarting.previous.state"); 2102 } else { 2103 history = replayableHistory; 2104 fluffmsg("jshell.err.reload.restarting.state"); 2105 } 2106 boolean echo = !at.hasOption("-quiet"); 2107 resetState(); 2108 run(new ReloadIOContext(history, 2109 echo ? cmdout : null)); 2110 return true; 2111 } 2112 2113 private boolean cmdSave(String rawargs) { 2114 ArgTokenizer at = new ArgTokenizer("/save", rawargs.trim()); 2115 at.allowedOptions("-all", "-start", "-history"); 2116 String filename = at.next(); 2117 if (filename == null) { 2118 errormsg("jshell.err.file.filename", "/save"); 2119 return false; 2120 } 2121 if (!checkOptionsAndRemainingInput(at)) { 2122 return false; 2123 } 2124 if (at.optionCount() > 1) { 2125 errormsg("jshell.err.conflicting.options", at.whole()); 2126 return false; 2127 } 2128 try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename), 2129 Charset.defaultCharset(), 2130 CREATE, TRUNCATE_EXISTING, WRITE)) { 2131 if (at.hasOption("-history")) { 2132 for (String s : input.currentSessionHistory()) { 2133 writer.write(s); 2134 writer.write("\n"); 2135 } 2136 } else if (at.hasOption("-start")) { 2137 writer.append(startup); 2138 } else { 2139 String sources = (at.hasOption("-all") 2140 ? state.snippets() 2141 : state.snippets().filter(this::mainActive)) 2142 .map(Snippet::source) 2143 .collect(Collectors.joining("\n")); 2144 writer.write(sources); 2145 } 2146 } catch (FileNotFoundException e) { 2147 errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage()); 2148 return false; 2149 } catch (Exception e) { 2150 errormsg("jshell.err.file.exception", "/save", filename, e); 2151 return false; 2152 } 2153 return true; 2154 } 2155 2156 private boolean cmdVars(String arg) { 2157 Stream<VarSnippet> stream = argsOptionsToSnippets(this::allVarSnippets, 2158 this::isActive, arg, "/vars"); 2159 if (stream == null) { 2160 return false; 2161 } 2162 stream.forEachOrdered(vk -> 2163 { 2164 String val = state.status(vk) == Status.VALID 2165 ? state.varValue(vk) 2166 : getResourceString("jshell.msg.vars.not.active"); 2167 hard(" %s %s = %s", vk.typeName(), vk.name(), val); 2168 }); 2169 return true; 2170 } 2171 2172 private boolean cmdMethods(String arg) { 2173 Stream<MethodSnippet> stream = argsOptionsToSnippets(this::allMethodSnippets, 2174 this::isActive, arg, "/methods"); 2175 if (stream == null) { 2176 return false; 2177 } 2178 stream.forEachOrdered(mk 2179 -> hard(" %s %s", mk.name(), mk.signature()) 2180 ); 2181 return true; 2182 } 2183 2184 private boolean cmdTypes(String arg) { 2185 Stream<TypeDeclSnippet> stream = argsOptionsToSnippets(this::allTypeSnippets, 2186 this::isActive, arg, "/types"); 2187 if (stream == null) { 2188 return false; 2189 } 2190 stream.forEachOrdered(ck 2191 -> { 2192 String kind; 2193 switch (ck.subKind()) { 2194 case INTERFACE_SUBKIND: 2195 kind = "interface"; 2196 break; 2197 case CLASS_SUBKIND: 2198 kind = "class"; 2199 break; 2200 case ENUM_SUBKIND: 2201 kind = "enum"; 2202 break; 2203 case ANNOTATION_TYPE_SUBKIND: 2204 kind = "@interface"; 2205 break; 2206 default: 2207 assert false : "Wrong kind" + ck.subKind(); 2208 kind = "class"; 2209 break; 2210 } 2211 hard(" %s %s", kind, ck.name()); 2212 }); 2213 return true; 2214 } 2215 2216 private boolean cmdImports() { 2217 state.imports().forEach(ik -> { 2218 hard(" import %s%s", ik.isStatic() ? "static " : "", ik.fullname()); 2219 }); 2220 return true; 2221 } 2222 2223 private boolean cmdUseHistoryEntry(int index) { 2224 List<Snippet> keys = state.snippets().collect(toList()); 2225 if (index < 0) 2226 index += keys.size(); 2227 else 2228 index--; 2229 if (index >= 0 && index < keys.size()) { 2230 rerunSnippet(keys.get(index)); 2231 } else { 2232 errormsg("jshell.err.out.of.range"); 2233 return false; 2234 } 2235 return true; 2236 } 2237 2238 boolean checkOptionsAndRemainingInput(ArgTokenizer at) { 2239 String junk = at.remainder(); 2240 if (!junk.isEmpty()) { 2241 errormsg("jshell.err.unexpected.at.end", junk, at.whole()); 2242 return false; 2243 } else { 2244 String bad = at.badOptions(); 2245 if (!bad.isEmpty()) { 2246 errormsg("jshell.err.unknown.option", bad, at.whole()); 2247 return false; 2248 } 2249 } 2250 return true; 2251 } 2252 2253 private boolean rerunHistoryEntryById(String id) { 2254 Optional<Snippet> snippet = state.snippets() 2255 .filter(s -> s.id().equals(id)) 2256 .findFirst(); 2257 return snippet.map(s -> { 2258 rerunSnippet(s); 2259 return true; 2260 }).orElse(false); 2261 } 2262 2263 private void rerunSnippet(Snippet snippet) { 2264 String source = snippet.source(); 2265 cmdout.printf("%s\n", source); 2266 input.replaceLastHistoryEntry(source); 2267 processSourceCatchingReset(source); 2268 } 2269 2270 /** 2271 * Filter diagnostics for only errors (no warnings, ...) 2272 * @param diagnostics input list 2273 * @return filtered list 2274 */ 2275 List<Diag> errorsOnly(List<Diag> diagnostics) { 2276 return diagnostics.stream() 2277 .filter(d -> d.isError()) 2278 .collect(toList()); 2279 } 2280 2281 void displayDiagnostics(String source, Diag diag, List<String> toDisplay) { 2282 for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize 2283 if (!line.trim().startsWith("location:")) { 2284 toDisplay.add(line); 2285 } 2286 } 2287 2288 int pstart = (int) diag.getStartPosition(); 2289 int pend = (int) diag.getEndPosition(); 2290 Matcher m = LINEBREAK.matcher(source); 2291 int pstartl = 0; 2292 int pendl = -2; 2293 while (m.find(pstartl)) { 2294 pendl = m.start(); 2295 if (pendl >= pstart) { 2296 break; 2297 } else { 2298 pstartl = m.end(); 2299 } 2300 } 2301 if (pendl < pstart) { 2302 pendl = source.length(); 2303 } 2304 toDisplay.add(source.substring(pstartl, pendl)); 2305 2306 StringBuilder sb = new StringBuilder(); 2307 int start = pstart - pstartl; 2308 for (int i = 0; i < start; ++i) { 2309 sb.append(' '); 2310 } 2311 sb.append('^'); 2312 boolean multiline = pend > pendl; 2313 int end = (multiline ? pendl : pend) - pstartl - 1; 2314 if (end > start) { 2315 for (int i = start + 1; i < end; ++i) { 2316 sb.append('-'); 2317 } 2318 if (multiline) { 2319 sb.append("-..."); 2320 } else { 2321 sb.append('^'); 2322 } 2323 } 2324 toDisplay.add(sb.toString()); 2325 2326 debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this); 2327 debug("Code: %s", diag.getCode()); 2328 debug("Pos: %d (%d - %d)", diag.getPosition(), 2329 diag.getStartPosition(), diag.getEndPosition()); 2330 } 2331 2332 private String processSource(String srcInput) throws IllegalStateException { 2333 while (true) { 2334 CompletionInfo an = analysis.analyzeCompletion(srcInput); 2335 if (!an.completeness().isComplete()) { 2336 return an.remaining(); 2337 } 2338 boolean failed = processCompleteSource(an.source()); 2339 if (failed || an.remaining().isEmpty()) { 2340 return ""; 2341 } 2342 srcInput = an.remaining(); 2343 } 2344 } 2345 //where 2346 private boolean processCompleteSource(String source) throws IllegalStateException { 2347 debug("Compiling: %s", source); 2348 boolean failed = false; 2349 boolean isActive = false; 2350 List<SnippetEvent> events = state.eval(source); 2351 for (SnippetEvent e : events) { 2352 // Report the event, recording failure 2353 failed |= handleEvent(e); 2354 2355 // If any main snippet is active, this should be replayable 2356 // also ignore var value queries 2357 isActive |= e.causeSnippet() == null && 2358 e.status().isActive() && 2359 e.snippet().subKind() != VAR_VALUE_SUBKIND; 2360 } 2361 // If this is an active snippet and it didn't cause the backend to die, 2362 // add it to the replayable history 2363 if (isActive && live) { 2364 addToReplayHistory(source); 2365 } 2366 2367 return failed; 2368 } 2369 2370 // Handle incoming snippet events -- return true on failure 2371 private boolean handleEvent(SnippetEvent ste) { 2372 Snippet sn = ste.snippet(); 2373 if (sn == null) { 2374 debug("Event with null key: %s", ste); 2375 return false; 2376 } 2377 List<Diag> diagnostics = state.diagnostics(sn).collect(toList()); 2378 String source = sn.source(); 2379 if (ste.causeSnippet() == null) { 2380 // main event 2381 for (Diag d : diagnostics) { 2382 hardmsg(d.isError()? "jshell.msg.error" : "jshell.msg.warning"); 2383 List<String> disp = new ArrayList<>(); 2384 displayDiagnostics(source, d, disp); 2385 disp.stream() 2386 .forEach(l -> hard("%s", l)); 2387 } 2388 2389 if (ste.status() != Status.REJECTED) { 2390 if (ste.exception() != null) { 2391 if (ste.exception() instanceof EvalException) { 2392 printEvalException((EvalException) ste.exception()); 2393 return true; 2394 } else if (ste.exception() instanceof UnresolvedReferenceException) { 2395 printUnresolvedException((UnresolvedReferenceException) ste.exception()); 2396 } else { 2397 hard("Unexpected execution exception: %s", ste.exception()); 2398 return true; 2399 } 2400 } else { 2401 new DisplayEvent(ste, false, ste.value(), diagnostics).displayDeclarationAndValue(); 2402 } 2403 } else { 2404 if (diagnostics.isEmpty()) { 2405 errormsg("jshell.err.failed"); 2406 } 2407 return true; 2408 } 2409 } else { 2410 // Update 2411 if (sn instanceof DeclarationSnippet) { 2412 List<Diag> other = errorsOnly(diagnostics); 2413 2414 // display update information 2415 new DisplayEvent(ste, true, ste.value(), other).displayDeclarationAndValue(); 2416 } 2417 } 2418 return false; 2419 } 2420 //where 2421 void printStackTrace(StackTraceElement[] stes) { 2422 for (StackTraceElement ste : stes) { 2423 StringBuilder sb = new StringBuilder(); 2424 String cn = ste.getClassName(); 2425 if (!cn.isEmpty()) { 2426 int dot = cn.lastIndexOf('.'); 2427 if (dot > 0) { 2428 sb.append(cn.substring(dot + 1)); 2429 } else { 2430 sb.append(cn); 2431 } 2432 sb.append("."); 2433 } 2434 if (!ste.getMethodName().isEmpty()) { 2435 sb.append(ste.getMethodName()); 2436 sb.append(" "); 2437 } 2438 String fileName = ste.getFileName(); 2439 int lineNumber = ste.getLineNumber(); 2440 String loc = ste.isNativeMethod() 2441 ? getResourceString("jshell.msg.native.method") 2442 : fileName == null 2443 ? getResourceString("jshell.msg.unknown.source") 2444 : lineNumber >= 0 2445 ? fileName + ":" + lineNumber 2446 : fileName; 2447 hard(" at %s(%s)", sb, loc); 2448 2449 } 2450 } 2451 //where 2452 void printUnresolvedException(UnresolvedReferenceException ex) { 2453 DeclarationSnippet corralled = ex.getSnippet(); 2454 List<Diag> otherErrors = errorsOnly(state.diagnostics(corralled).collect(toList())); 2455 new DisplayEvent(corralled, state.status(corralled), FormatAction.USED, true, null, otherErrors) 2456 .displayDeclarationAndValue(); 2457 } 2458 //where 2459 void printEvalException(EvalException ex) { 2460 if (ex.getMessage() == null) { 2461 hard("%s thrown", ex.getExceptionClassName()); 2462 } else { 2463 hard("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage()); 2464 } 2465 printStackTrace(ex.getStackTrace()); 2466 } 2467 2468 private FormatAction toAction(Status status, Status previousStatus, boolean isSignatureChange) { 2469 FormatAction act; 2470 switch (status) { 2471 case VALID: 2472 case RECOVERABLE_DEFINED: 2473 case RECOVERABLE_NOT_DEFINED: 2474 if (previousStatus.isActive()) { 2475 act = isSignatureChange 2476 ? FormatAction.REPLACED 2477 : FormatAction.MODIFIED; 2478 } else { 2479 act = FormatAction.ADDED; 2480 } 2481 break; 2482 case OVERWRITTEN: 2483 act = FormatAction.OVERWROTE; 2484 break; 2485 case DROPPED: 2486 act = FormatAction.DROPPED; 2487 break; 2488 case REJECTED: 2489 case NONEXISTENT: 2490 default: 2491 // Should not occur 2492 error("Unexpected status: " + previousStatus.toString() + "=>" + status.toString()); 2493 act = FormatAction.DROPPED; 2494 } 2495 return act; 2496 } 2497 2498 class DisplayEvent { 2499 private final Snippet sn; 2500 private final FormatAction action; 2501 private final boolean update; 2502 private final String value; 2503 private final List<String> errorLines; 2504 private final FormatResolve resolution; 2505 private final String unresolved; 2506 private final FormatUnresolved unrcnt; 2507 private final FormatErrors errcnt; 2508 2509 DisplayEvent(SnippetEvent ste, boolean update, String value, List<Diag> errors) { 2510 this(ste.snippet(), ste.status(), toAction(ste.status(), ste.previousStatus(), ste.isSignatureChange()), update, value, errors); 2511 } 2512 2513 DisplayEvent(Snippet sn, Status status, FormatAction action, boolean update, String value, List<Diag> errors) { 2514 this.sn = sn; 2515 this.action = action; 2516 this.update = update; 2517 this.value = value; 2518 this.errorLines = new ArrayList<>(); 2519 for (Diag d : errors) { 2520 displayDiagnostics(sn.source(), d, errorLines); 2521 } 2522 long unresolvedCount; 2523 if (sn instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) { 2524 resolution = (status == Status.RECOVERABLE_NOT_DEFINED) 2525 ? FormatResolve.NOTDEFINED 2526 : FormatResolve.DEFINED; 2527 unresolved = unresolved((DeclarationSnippet) sn); 2528 unresolvedCount = state.unresolvedDependencies((DeclarationSnippet) sn).count(); 2529 } else { 2530 resolution = FormatResolve.OK; 2531 unresolved = ""; 2532 unresolvedCount = 0; 2533 } 2534 unrcnt = unresolvedCount == 0 2535 ? FormatUnresolved.UNRESOLVED0 2536 : unresolvedCount == 1 2537 ? FormatUnresolved.UNRESOLVED1 2538 : FormatUnresolved.UNRESOLVED2; 2539 errcnt = errors.isEmpty() 2540 ? FormatErrors.ERROR0 2541 : errors.size() == 1 2542 ? FormatErrors.ERROR1 2543 : FormatErrors.ERROR2; 2544 } 2545 2546 private String unresolved(DeclarationSnippet key) { 2547 List<String> unr = state.unresolvedDependencies(key).collect(toList()); 2548 StringBuilder sb = new StringBuilder(); 2549 int fromLast = unr.size(); 2550 if (fromLast > 0) { 2551 sb.append(" "); 2552 } 2553 for (String u : unr) { 2554 --fromLast; 2555 sb.append(u); 2556 switch (fromLast) { 2557 // No suffix 2558 case 0: 2559 break; 2560 case 1: 2561 sb.append(", and "); 2562 break; 2563 default: 2564 sb.append(", "); 2565 break; 2566 } 2567 } 2568 return sb.toString(); 2569 } 2570 2571 private void custom(FormatCase fcase, String name) { 2572 custom(fcase, name, null); 2573 } 2574 2575 private void custom(FormatCase fcase, String name, String type) { 2576 String display = feedback.format(fcase, action, (update ? FormatWhen.UPDATE : FormatWhen.PRIMARY), 2577 resolution, unrcnt, errcnt, 2578 name, type, value, unresolved, errorLines); 2579 if (interactive()) { 2580 cmdout.print(display); 2581 } 2582 } 2583 2584 @SuppressWarnings("fallthrough") 2585 private void displayDeclarationAndValue() { 2586 switch (sn.subKind()) { 2587 case CLASS_SUBKIND: 2588 custom(FormatCase.CLASS, ((TypeDeclSnippet) sn).name()); 2589 break; 2590 case INTERFACE_SUBKIND: 2591 custom(FormatCase.INTERFACE, ((TypeDeclSnippet) sn).name()); 2592 break; 2593 case ENUM_SUBKIND: 2594 custom(FormatCase.ENUM, ((TypeDeclSnippet) sn).name()); 2595 break; 2596 case ANNOTATION_TYPE_SUBKIND: 2597 custom(FormatCase.ANNOTATION, ((TypeDeclSnippet) sn).name()); 2598 break; 2599 case METHOD_SUBKIND: 2600 custom(FormatCase.METHOD, ((MethodSnippet) sn).name(), ((MethodSnippet) sn).parameterTypes()); 2601 break; 2602 case VAR_DECLARATION_SUBKIND: { 2603 VarSnippet vk = (VarSnippet) sn; 2604 custom(FormatCase.VARDECL, vk.name(), vk.typeName()); 2605 break; 2606 } 2607 case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: { 2608 VarSnippet vk = (VarSnippet) sn; 2609 custom(FormatCase.VARINIT, vk.name(), vk.typeName()); 2610 break; 2611 } 2612 case TEMP_VAR_EXPRESSION_SUBKIND: { 2613 VarSnippet vk = (VarSnippet) sn; 2614 custom(FormatCase.EXPRESSION, vk.name(), vk.typeName()); 2615 break; 2616 } 2617 case OTHER_EXPRESSION_SUBKIND: 2618 error("Unexpected expression form -- value is: %s", (value)); 2619 break; 2620 case VAR_VALUE_SUBKIND: { 2621 ExpressionSnippet ek = (ExpressionSnippet) sn; 2622 custom(FormatCase.VARVALUE, ek.name(), ek.typeName()); 2623 break; 2624 } 2625 case ASSIGNMENT_SUBKIND: { 2626 ExpressionSnippet ek = (ExpressionSnippet) sn; 2627 custom(FormatCase.ASSIGNMENT, ek.name(), ek.typeName()); 2628 break; 2629 } 2630 case SINGLE_TYPE_IMPORT_SUBKIND: 2631 case TYPE_IMPORT_ON_DEMAND_SUBKIND: 2632 case SINGLE_STATIC_IMPORT_SUBKIND: 2633 case STATIC_IMPORT_ON_DEMAND_SUBKIND: 2634 custom(FormatCase.IMPORT, ((ImportSnippet) sn).name()); 2635 break; 2636 case STATEMENT_SUBKIND: 2637 custom(FormatCase.STATEMENT, null); 2638 break; 2639 } 2640 } 2641 } 2642 2643 /** The current version number as a string. 2644 */ 2645 String version() { 2646 return version("release"); // mm.nn.oo[-milestone] 2647 } 2648 2649 /** The current full version number as a string. 2650 */ 2651 String fullVersion() { 2652 return version("full"); // mm.mm.oo[-milestone]-build 2653 } 2654 2655 private String version(String key) { 2656 if (versionRB == null) { 2657 try { 2658 versionRB = ResourceBundle.getBundle(VERSION_RB_NAME, locale); 2659 } catch (MissingResourceException e) { 2660 return "(version info not available)"; 2661 } 2662 } 2663 try { 2664 return versionRB.getString(key); 2665 } 2666 catch (MissingResourceException e) { 2667 return "(version info not available)"; 2668 } 2669 } 2670 2671 class NameSpace { 2672 final String spaceName; 2673 final String prefix; 2674 private int nextNum; 2675 2676 NameSpace(String spaceName, String prefix) { 2677 this.spaceName = spaceName; 2678 this.prefix = prefix; 2679 this.nextNum = 1; 2680 } 2681 2682 String tid(Snippet sn) { 2683 String tid = prefix + nextNum++; 2684 mapSnippet.put(sn, new SnippetInfo(sn, this, tid)); 2685 return tid; 2686 } 2687 2688 String tidNext() { 2689 return prefix + nextNum; 2690 } 2691 } 2692 2693 static class SnippetInfo { 2694 final Snippet snippet; 2695 final NameSpace space; 2696 final String tid; 2697 2698 SnippetInfo(Snippet snippet, NameSpace space, String tid) { 2699 this.snippet = snippet; 2700 this.space = space; 2701 this.tid = tid; 2702 } 2703 } 2704 2705 static class ArgSuggestion implements Suggestion { 2706 2707 private final String continuation; 2708 2709 /** 2710 * Create a {@code Suggestion} instance. 2711 * 2712 * @param continuation a candidate continuation of the user's input 2713 */ 2714 public ArgSuggestion(String continuation) { 2715 this.continuation = continuation; 2716 } 2717 2718 /** 2719 * The candidate continuation of the given user's input. 2720 * 2721 * @return the continuation string 2722 */ 2723 @Override 2724 public String continuation() { 2725 return continuation; 2726 } 2727 2728 /** 2729 * Indicates whether input continuation matches the target type and is thus 2730 * more likely to be the desired continuation. A matching continuation is 2731 * preferred. 2732 * 2733 * @return {@code false}, non-types analysis 2734 */ 2735 @Override 2736 public boolean matchesType() { 2737 return false; 2738 } 2739 } 2740} 2741 2742abstract class NonInteractiveIOContext extends IOContext { 2743 2744 @Override 2745 public boolean interactiveOutput() { 2746 return false; 2747 } 2748 2749 @Override 2750 public Iterable<String> currentSessionHistory() { 2751 return Collections.emptyList(); 2752 } 2753 2754 @Override 2755 public boolean terminalEditorRunning() { 2756 return false; 2757 } 2758 2759 @Override 2760 public void suspend() { 2761 } 2762 2763 @Override 2764 public void resume() { 2765 } 2766 2767 @Override 2768 public void beforeUserCode() { 2769 } 2770 2771 @Override 2772 public void afterUserCode() { 2773 } 2774 2775 @Override 2776 public void replaceLastHistoryEntry(String source) { 2777 } 2778} 2779 2780class ScannerIOContext extends NonInteractiveIOContext { 2781 private final Scanner scannerIn; 2782 2783 ScannerIOContext(Scanner scannerIn) { 2784 this.scannerIn = scannerIn; 2785 } 2786 2787 @Override 2788 public String readLine(String prompt, String prefix) { 2789 if (scannerIn.hasNextLine()) { 2790 return scannerIn.nextLine(); 2791 } else { 2792 return null; 2793 } 2794 } 2795 2796 @Override 2797 public void close() { 2798 scannerIn.close(); 2799 } 2800 2801 @Override 2802 public int readUserInput() { 2803 return -1; 2804 } 2805} 2806 2807class FileScannerIOContext extends ScannerIOContext { 2808 2809 FileScannerIOContext(String fn) throws FileNotFoundException { 2810 this(new FileReader(fn)); 2811 } 2812 2813 FileScannerIOContext(Reader rdr) throws FileNotFoundException { 2814 super(new Scanner(rdr)); 2815 } 2816} 2817 2818class ReloadIOContext extends NonInteractiveIOContext { 2819 private final Iterator<String> it; 2820 private final PrintStream echoStream; 2821 2822 ReloadIOContext(Iterable<String> history, PrintStream echoStream) { 2823 this.it = history.iterator(); 2824 this.echoStream = echoStream; 2825 } 2826 2827 @Override 2828 public String readLine(String prompt, String prefix) { 2829 String s = it.hasNext() 2830 ? it.next() 2831 : null; 2832 if (echoStream != null && s != null) { 2833 String p = "-: "; 2834 String p2 = "\n "; 2835 echoStream.printf("%s%s\n", p, s.replace("\n", p2)); 2836 } 2837 return s; 2838 } 2839 2840 @Override 2841 public void close() { 2842 } 2843 2844 @Override 2845 public int readUserInput() { 2846 return -1; 2847 } 2848} 2849