001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.cli.command.shell; 018 019import java.io.IOException; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.ServiceLoader; 027import java.util.Set; 028 029import jline.console.ConsoleReader; 030import jline.console.completer.CandidateListCompletionHandler; 031import org.fusesource.jansi.AnsiRenderer.Code; 032 033import org.springframework.boot.cli.command.Command; 034import org.springframework.boot.cli.command.CommandFactory; 035import org.springframework.boot.cli.command.CommandRunner; 036import org.springframework.boot.cli.command.core.HelpCommand; 037import org.springframework.boot.cli.command.core.VersionCommand; 038import org.springframework.boot.loader.tools.SignalUtils; 039import org.springframework.util.StringUtils; 040 041/** 042 * A shell for Spring Boot. Drops the user into an event loop (REPL) where command line 043 * completion and history are available without relying on OS shell features. 044 * 045 * @author Jon Brisbin 046 * @author Dave Syer 047 * @author Phillip Webb 048 */ 049public class Shell { 050 051 private static final Set<Class<?>> NON_FORKED_COMMANDS; 052 053 static { 054 Set<Class<?>> nonForked = new HashSet<>(); 055 nonForked.add(VersionCommand.class); 056 NON_FORKED_COMMANDS = Collections.unmodifiableSet(nonForked); 057 } 058 059 private final ShellCommandRunner commandRunner; 060 061 private final ConsoleReader consoleReader; 062 063 private final EscapeAwareWhiteSpaceArgumentDelimiter argumentDelimiter = new EscapeAwareWhiteSpaceArgumentDelimiter(); 064 065 private final ShellPrompts prompts = new ShellPrompts(); 066 067 /** 068 * Create a new {@link Shell} instance. 069 * @throws IOException in case of I/O errors 070 */ 071 Shell() throws IOException { 072 attachSignalHandler(); 073 this.consoleReader = new ConsoleReader(); 074 this.commandRunner = createCommandRunner(); 075 initializeConsoleReader(); 076 } 077 078 private ShellCommandRunner createCommandRunner() { 079 ShellCommandRunner runner = new ShellCommandRunner(); 080 runner.addCommand(new HelpCommand(runner)); 081 runner.addCommands(getCommands()); 082 runner.addAliases("exit", "quit"); 083 runner.addAliases("help", "?"); 084 runner.addAliases("clear", "cls"); 085 return runner; 086 } 087 088 private Iterable<Command> getCommands() { 089 List<Command> commands = new ArrayList<>(); 090 ServiceLoader<CommandFactory> factories = ServiceLoader.load(CommandFactory.class, 091 getClass().getClassLoader()); 092 for (CommandFactory factory : factories) { 093 for (Command command : factory.getCommands()) { 094 commands.add(convertToForkCommand(command)); 095 } 096 } 097 commands.add(new PromptCommand(this.prompts)); 098 commands.add(new ClearCommand(this.consoleReader)); 099 commands.add(new ExitCommand()); 100 return commands; 101 } 102 103 private Command convertToForkCommand(Command command) { 104 for (Class<?> nonForked : NON_FORKED_COMMANDS) { 105 if (nonForked.isInstance(command)) { 106 return command; 107 } 108 } 109 return new ForkProcessCommand(command); 110 } 111 112 private void initializeConsoleReader() { 113 this.consoleReader.setHistoryEnabled(true); 114 this.consoleReader.setBellEnabled(false); 115 this.consoleReader.setExpandEvents(false); 116 this.consoleReader.addCompleter(new CommandCompleter(this.consoleReader, 117 this.argumentDelimiter, this.commandRunner)); 118 this.consoleReader.setCompletionHandler(new CandidateListCompletionHandler()); 119 } 120 121 private void attachSignalHandler() { 122 SignalUtils.attachSignalHandler(this::handleSigInt); 123 } 124 125 /** 126 * Run the shell until the user exists. 127 * @throws Exception on error 128 */ 129 public void run() throws Exception { 130 printBanner(); 131 try { 132 runInputLoop(); 133 } 134 catch (Exception ex) { 135 if (!(ex instanceof ShellExitException)) { 136 throw ex; 137 } 138 } 139 } 140 141 private void printBanner() { 142 String version = getClass().getPackage().getImplementationVersion(); 143 version = (version != null) ? " (v" + version + ")" : ""; 144 System.out.println(ansi("Spring Boot", Code.BOLD).append(version, Code.FAINT)); 145 System.out.println(ansi("Hit TAB to complete. Type 'help' and hit " 146 + "RETURN for help, and 'exit' to quit.")); 147 } 148 149 private void runInputLoop() throws Exception { 150 String line; 151 while ((line = this.consoleReader.readLine(getPrompt())) != null) { 152 while (line.endsWith("\\")) { 153 line = line.substring(0, line.length() - 1); 154 line += this.consoleReader.readLine("> "); 155 } 156 if (StringUtils.hasLength(line)) { 157 String[] args = this.argumentDelimiter.parseArguments(line); 158 this.commandRunner.runAndHandleErrors(args); 159 } 160 } 161 } 162 163 private String getPrompt() { 164 String prompt = this.prompts.getPrompt(); 165 return ansi(prompt, Code.FG_BLUE).toString(); 166 } 167 168 private AnsiString ansi(String text, Code... codes) { 169 return new AnsiString(this.consoleReader.getTerminal()).append(text, codes); 170 } 171 172 /** 173 * Final handle an interrupt signal (CTRL-C). 174 */ 175 protected void handleSigInt() { 176 if (this.commandRunner.handleSigInt()) { 177 return; 178 } 179 System.out.println(String.format("%nThanks for using Spring Boot")); 180 System.exit(1); 181 } 182 183 /** 184 * Extension of {@link CommandRunner} to deal with {@link RunProcessCommand}s and 185 * aliases. 186 */ 187 private class ShellCommandRunner extends CommandRunner { 188 189 private volatile Command lastCommand; 190 191 private final Map<String, String> aliases = new HashMap<>(); 192 193 ShellCommandRunner() { 194 super(null); 195 } 196 197 public void addAliases(String command, String... aliases) { 198 for (String alias : aliases) { 199 this.aliases.put(alias, command); 200 } 201 } 202 203 @Override 204 public Command findCommand(String name) { 205 if (name.startsWith("!")) { 206 return new RunProcessCommand(name.substring(1)); 207 } 208 if (this.aliases.containsKey(name)) { 209 name = this.aliases.get(name); 210 } 211 return super.findCommand(name); 212 } 213 214 @Override 215 protected void beforeRun(Command command) { 216 this.lastCommand = command; 217 } 218 219 @Override 220 protected void afterRun(Command command) { 221 } 222 223 public boolean handleSigInt() { 224 Command command = this.lastCommand; 225 if (command != null && command instanceof RunProcessCommand) { 226 return ((RunProcessCommand) command).handleSigInt(); 227 } 228 return false; 229 } 230 231 } 232 233}