/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.felix.gogo.jline; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.PrintWriter; import java.io.Reader; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.nio.CharBuffer; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.felix.gogo.runtime.Closure; import org.apache.felix.gogo.runtime.CommandProxy; import org.apache.felix.gogo.runtime.CommandSessionImpl; import org.apache.felix.service.command.Job; import org.apache.felix.service.command.Job.Status; import org.apache.felix.gogo.runtime.Reflective; import org.apache.felix.service.command.CommandProcessor; import org.apache.felix.service.command.CommandSession; import org.apache.felix.service.command.Converter; import org.apache.felix.service.command.Descriptor; import org.apache.felix.service.command.Function; import org.apache.felix.service.command.Parameter; import org.apache.felix.service.threadio.ThreadIO; import org.jline.builtins.Completers.CompletionData; import org.jline.builtins.Completers.CompletionEnvironment; import org.jline.builtins.Options; import org.jline.reader.EndOfFileException; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; import org.jline.reader.ParsedLine; import org.jline.reader.UserInterruptException; import org.jline.terminal.Terminal; import org.jline.terminal.Terminal.Signal; import org.jline.terminal.Terminal.SignalHandler; import org.jline.utils.AttributedStringBuilder; import org.jline.utils.AttributedStyle; public class Shell { public static final String VAR_COMPLETIONS = ".completions"; public static final String VAR_COMMAND_LINE = ".commandLine"; public static final String VAR_READER = ".reader"; public static final String VAR_SESSION = ".session"; public static final String VAR_PROCESSOR = ".processor"; public static final String VAR_TERMINAL = ".terminal"; public static final String VAR_EXCEPTION = "exception"; public static final String VAR_RESULT = "_"; public static final String VAR_LOCATION = ".location"; public static final String VAR_PROMPT = "prompt"; public static final String VAR_RPROMPT = "rprompt"; public static final String VAR_SCOPE = "SCOPE"; public static final String VAR_CONTEXT = org.apache.felix.gogo.runtime.activator.Activator.CONTEXT; static final String[] functions = {"gosh", "sh", "source", "help"}; private final URI baseURI; private final String profile; private final Context context; private final CommandProcessor processor; private final ThreadIO tio; private AtomicBoolean stopping = new AtomicBoolean(); public Shell(Context context, CommandProcessor processor) { this(context, processor, null); } public Shell(Context context, CommandProcessor processor, String profile) { this(context, processor, null, profile); } public Shell(Context context, CommandProcessor processor, ThreadIO tio, String profile) { this.context = context; this.processor = processor; this.tio = tio != null ? tio : getThreadIO(processor); String baseDir = context.getProperty("gosh.home"); baseDir = (baseDir == null) ? context.getProperty("user.dir") : baseDir; this.baseURI = new File(baseDir).toURI(); this.profile = profile != null ? profile : "gosh_profile"; } private ThreadIO getThreadIO(CommandProcessor processor) { try { Field field = processor.getClass().getDeclaredField("threadIO"); field.setAccessible(true); return (ThreadIO) field.get(processor); } catch (Throwable t) { return null; } } public Context getContext() { return context; } public static Terminal getTerminal(CommandSession session) { return (Terminal) session.get(VAR_TERMINAL); } public static LineReader getReader(CommandSession session) { return (LineReader) session.get(VAR_READER); } public static CommandProcessor getProcessor(CommandSession session) { return (CommandProcessor) session.get(VAR_PROCESSOR); } @SuppressWarnings("unchecked") public static Map<String, List<CompletionData>> getCompletions(CommandSession session) { return (Map) session.get(VAR_COMPLETIONS); } @SuppressWarnings("unchecked") public static Set<String> getCommands(CommandSession session) { return (Set<String>) session.get(CommandSessionImpl.COMMANDS); } public static ParsedLine getParsedLine(CommandSession session) { return (ParsedLine) session.get(VAR_COMMAND_LINE); } public static String getPrompt(CommandSession session) { return expand(session, VAR_PROMPT, "gl! "); } public static String getRPrompt(CommandSession session) { return expand(session, VAR_RPROMPT, null); } public static String expand(CommandSession session, String name, String def) { Object prompt = session.get(name); if (prompt != null) { try { Object o = org.apache.felix.gogo.runtime.Expander.expand( prompt.toString(), new Closure((CommandSessionImpl) session, null, null)); if (o != null) { return o.toString(); } } catch (Exception e) { // ignore } } return def; } public static String resolve(CommandSession session, String command) { String resolved = command; if (command.indexOf(':') < 0) { Set<String> commands = getCommands(session); Object path = session.get(VAR_SCOPE); String scopePath = (null == path ? "*" : path.toString()); for (String scope : scopePath.split(":")) { for (String entry : commands) { if ("*".equals(scope) && entry.endsWith(":" + command) || entry.equals(scope + ":" + command)) { resolved = entry; break; } } } } return resolved; } public static CharSequence readScript(URI script) throws Exception { URLConnection conn = script.toURL().openConnection(); int length = conn.getContentLength(); if (length == -1) { System.err.println("eek! unknown Contentlength for: " + script); length = 10240; } InputStream in = conn.getInputStream(); CharBuffer cbuf = CharBuffer.allocate(length); Reader reader = new InputStreamReader(in); reader.read(cbuf); in.close(); cbuf.rewind(); return cbuf; } @SuppressWarnings("unchecked") static Set<String> getVariables(CommandSession session) { return (Set<String>) session.get(".variables"); } private static <T extends Annotation> T findAnnotation(Annotation[] anns, Class<T> clazz) { for (int i = 0; (anns != null) && (i < anns.length); i++) { if (clazz.isInstance(anns[i])) { return clazz.cast(anns[i]); } } return null; } public void stop() { stopping.set(true); } public Object gosh(CommandSession currentSession, String[] argv) throws Exception { final String[] usage = { "gosh - execute script with arguments in a new session", " args are available as session variables $1..$9 and $args.", "Usage: gosh [OPTIONS] [script-file [args..]]", " -c --command pass all remaining args to sub-shell", " --nointeractive don't start interactive session", " --login login shell (same session, reads etc/gosh_profile)", " -s --noshutdown don't shutdown framework when script completes", " -x --xtrace echo commands before execution", " -? --help show help", "If no script-file, an interactive shell is started, type $D to exit."}; Options opt = Options.compile(usage).setOptionsFirst(true).parse(argv); List<String> args = opt.args(); boolean login = opt.isSet("login"); boolean interactive = !opt.isSet("nointeractive"); if (opt.isSet("help")) { opt.usage(System.err); if (login && !opt.isSet("noshutdown")) { shutdown(); } return null; } if (opt.isSet("command") && args.isEmpty()) { throw opt.usageError("option --command requires argument(s)"); } CommandSession session; if (login) { session = currentSession; } else { session = createChildSession(currentSession); } if (opt.isSet("xtrace")) { session.put("echo", true); } Terminal terminal = getTerminal(session); session.put(Shell.VAR_CONTEXT, context); session.put(Shell.VAR_PROCESSOR, processor); session.put(Shell.VAR_SESSION, session); session.put("#TERM", (Function) (s, arguments) -> terminal.getType()); session.put("#COLUMNS", (Function) (s, arguments) -> terminal.getWidth()); session.put("#LINES", (Function) (s, arguments) -> terminal.getHeight()); session.put("#PWD", (Function) (s, arguments) -> s.currentDir().toString()); session.put(LineReader.HISTORY_FILE, Paths.get(System.getProperty("user.home"), ".gogo.history")); if (tio != null) { PrintWriter writer = terminal.writer(); PrintStream out = new PrintStream(new OutputStream() { @Override public void write(int b) throws IOException { write(new byte[]{(byte) b}, 0, 1); } public void write(byte b[], int off, int len) throws IOException { writer.write(new String(b, off, len)); } public void flush() throws IOException { writer.flush(); } public void close() throws IOException { writer.close(); } }); tio.setStreams(terminal.input(), out, out); } try { LineReader reader; if (args.isEmpty() && interactive) { CompletionEnvironment completionEnvironment = new CompletionEnvironment() { @Override public Map<String, List<CompletionData>> getCompletions() { return Shell.getCompletions(session); } @Override public Set<String> getCommands() { return Shell.getCommands(session); } @Override public String resolveCommand(String command) { return Shell.resolve(session, command); } @Override public String commandName(String command) { int idx = command.indexOf(':'); return idx >= 0 ? command.substring(idx + 1) : command; } @Override public Object evaluate(LineReader reader, ParsedLine line, String func) throws Exception { session.put(Shell.VAR_COMMAND_LINE, line); return session.execute(func); } }; reader = LineReaderBuilder.builder() .terminal(terminal) .variables(((CommandSessionImpl) session).getVariables()) .completer(new org.jline.builtins.Completers.Completer(completionEnvironment)) .highlighter(new Highlighter(session)) .parser(new Parser()) .expander(new Expander(session)) .build(); reader.setOpt(LineReader.Option.AUTO_FRESH_LINE); session.put(Shell.VAR_READER, reader); session.put(Shell.VAR_COMPLETIONS, new HashMap()); } else { reader = null; } if (login || interactive) { URI uri = baseURI.resolve("etc/" + profile); if (!new File(uri).exists()) { URL url = getClass().getResource("/ext/" + profile); if (url == null) { url = getClass().getResource("/" + profile); } uri = (url == null) ? null : url.toURI(); } if (uri != null) { source(session, uri.toString()); } } Object result = null; if (args.isEmpty()) { if (interactive) { result = runShell(session, terminal, reader); } } else { CharSequence program; if (opt.isSet("command")) { StringBuilder buf = new StringBuilder(); for (String arg : args) { if (buf.length() > 0) { buf.append(' '); } buf.append(arg); } program = buf; } else { URI script = session.currentDir().toUri().resolve(args.remove(0)); // set script arguments session.put("0", script); session.put("args", args); for (int i = 0; i < args.size(); ++i) { session.put(String.valueOf(i + 1), args.get(i)); } program = readScript(script); } result = session.execute(program); } if (login && interactive && !opt.isSet("noshutdown")) { if (terminal != null) { terminal.writer().println("gosh: stopping framework"); terminal.flush(); } shutdown(); } return result; } finally { if (tio != null) { tio.close(); } } } private CommandSession createChildSession(CommandSession parent) { CommandSession session = processor.createSession(parent); getVariables(parent).stream() .filter(key -> key.matches("[.]?[A-Z].*")) .forEach(key -> session.put(key, parent.get(key))); session.put(Shell.VAR_TERMINAL, getTerminal(parent)); return session; } private Object runShell(final CommandSession session, Terminal terminal, LineReader reader) throws InterruptedException { AtomicBoolean reading = new AtomicBoolean(); session.setJobListener((job, previous, current) -> { if (previous == Status.Background || current == Status.Background || previous == Status.Suspended || current == Status.Suspended) { int width = terminal.getWidth(); String status = current.name().toLowerCase(); terminal.writer().write(getStatusLine(job, width, status)); terminal.flush(); if (reading.get() && !stopping.get()) { reader.callWidget(LineReader.REDRAW_LINE); reader.callWidget(LineReader.REDISPLAY); } } }); SignalHandler intHandler = terminal.handle(Signal.INT, s -> { Job current = session.foregroundJob(); if (current != null) { current.interrupt(); } }); SignalHandler suspHandler = terminal.handle(Signal.TSTP, s -> { Job current = session.foregroundJob(); if (current != null) { current.suspend(); } }); Object result = null; try { while (!stopping.get()) { try { reading.set(true); try { String prompt = Shell.getPrompt(session); String rprompt = Shell.getRPrompt(session); if (stopping.get()) { break; } reader.readLine(prompt, rprompt, null, null); } finally { reading.set(false); } ParsedLine parsedLine = reader.getParsedLine(); if (parsedLine == null) { throw new EndOfFileException(); } try { result = session.execute(((ParsedLineImpl) parsedLine).program()); session.put(Shell.VAR_RESULT, result); // set $_ to last result if (result != null && !Boolean.FALSE.equals(session.get(".Gogo.format"))) { System.out.println(session.format(result, Converter.INSPECT)); } } catch (Exception e) { AttributedStringBuilder sb = new AttributedStringBuilder(); sb.style(sb.style().foreground(AttributedStyle.RED)); sb.append(e.toString()); sb.style(sb.style().foregroundDefault()); terminal.writer().println(sb.toAnsi(terminal)); terminal.flush(); session.put(Shell.VAR_EXCEPTION, e); } waitJobCompletion(session); } catch (UserInterruptException e) { // continue; } catch (EndOfFileException e) { reader.getHistory().save(); break; } } } finally { terminal.handle(Signal.INT, intHandler); terminal.handle(Signal.TSTP, suspHandler); } return result; } private void waitJobCompletion(final CommandSession session) throws InterruptedException { while (true) { Job job = session.foregroundJob(); if (job != null) { //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (job) { if (job.status() == Status.Foreground) { job.wait(); } } } else { break; } } } private String getStatusLine(Job job, int width, String status) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < width - 1; i++) { sb.append(' '); } sb.append('\r'); sb.append("[").append(job.id()).append("] "); sb.append(status); for (int i = status.length(); i < "background".length(); i++) { sb.append(' '); } sb.append(" ").append(job.command()).append("\n"); return sb.toString(); } @Descriptor("start a new shell") public Object sh(final CommandSession session, String[] argv) throws Exception { return gosh(session, argv); } private void shutdown() throws Exception { context.exit(); } @Descriptor("Evaluates contents of file") public Object source(CommandSession session, String script) throws Exception { URI uri = session.currentDir().toUri().resolve(script); session.put("0", uri); try { return session.execute(readScript(uri)); } finally { session.put("0", null); // API doesn't support remove } } private Map<String, List<Method>> getReflectionCommands(CommandSession session) { Map<String, List<Method>> commands = new TreeMap<>(); Set<String> names = getCommands(session); for (String name : names) { Function function = (Function) session.get(name); if (function instanceof CommandProxy) { Object target = ((CommandProxy) function).getTarget(); List<Method> methods = new ArrayList<>(); String func = name.substring(name.indexOf(':') + 1).toLowerCase(); List<String> funcs = new ArrayList<>(); funcs.add("is" + func); funcs.add("get" + func); funcs.add("set" + func); if (Reflective.KEYWORDS.contains(func)) { funcs.add("_" + func); } else { funcs.add(func); } for (Method method : target.getClass().getMethods()) { if (funcs.contains(method.getName().toLowerCase())) { methods.add(method); } } commands.put(name, methods); ((CommandProxy) function).ungetTarget(); } } return commands; } @Descriptor("displays available commands") public void help(CommandSession session) { Map<String, List<Method>> commands = getReflectionCommands(session); commands.keySet().forEach(System.out::println); } @Descriptor("displays information about a specific command") public void help(CommandSession session, @Descriptor("target command") String name) { Map<String, List<Method>> commands = getReflectionCommands(session); List<Method> methods = null; // If the specified command doesn't have a scope, then // search for matching methods by ignoring the scope. int scopeIdx = name.indexOf(':'); if (scopeIdx < 0) { for (Entry<String, List<Method>> entry : commands.entrySet()) { String k = entry.getKey().substring(entry.getKey().indexOf(':') + 1); if (name.equals(k)) { name = entry.getKey(); methods = entry.getValue(); break; } } } // Otherwise directly look up matching methods. else { methods = commands.get(name); } if ((methods != null) && (methods.size() > 0)) { for (Method m : methods) { Descriptor d = m.getAnnotation(Descriptor.class); if (d == null) { System.out.println("\n" + m.getName()); } else { System.out.println("\n" + m.getName() + " - " + d.value()); } System.out.println(" scope: " + name.substring(0, name.indexOf(':'))); // Get flags and options. Class<?>[] paramTypes = m.getParameterTypes(); Map<String, Parameter> flags = new TreeMap<>(); Map<String, String> flagDescs = new TreeMap<>(); Map<String, Parameter> options = new TreeMap<>(); Map<String, String> optionDescs = new TreeMap<>(); List<String> params = new ArrayList<>(); Annotation[][] anns = m.getParameterAnnotations(); for (int paramIdx = 0; paramIdx < anns.length; paramIdx++) { Class<?> paramType = m.getParameterTypes()[paramIdx]; if (paramType == CommandSession.class) { /* Do not bother the user with a CommandSession. */ continue; } Parameter p = findAnnotation(anns[paramIdx], Parameter.class); d = findAnnotation(anns[paramIdx], Descriptor.class); if (p != null) { if (p.presentValue().equals(Parameter.UNSPECIFIED)) { options.put(p.names()[0], p); if (d != null) { optionDescs.put(p.names()[0], d.value()); } } else { flags.put(p.names()[0], p); if (d != null) { flagDescs.put(p.names()[0], d.value()); } } } else if (d != null) { params.add(paramTypes[paramIdx].getSimpleName()); params.add(d.value()); } else { params.add(paramTypes[paramIdx].getSimpleName()); params.add(""); } } // Print flags and options. if (flags.size() > 0) { System.out.println(" flags:"); for (Entry<String, Parameter> entry : flags.entrySet()) { // Print all aliases. String[] names = entry.getValue().names(); System.out.print(" " + names[0]); for (int aliasIdx = 1; aliasIdx < names.length; aliasIdx++) { System.out.print(", " + names[aliasIdx]); } System.out.println(" " + flagDescs.get(entry.getKey())); } } if (options.size() > 0) { System.out.println(" options:"); for (Entry<String, Parameter> entry : options.entrySet()) { // Print all aliases. String[] names = entry.getValue().names(); System.out.print(" " + names[0]); for (int aliasIdx = 1; aliasIdx < names.length; aliasIdx++) { System.out.print(", " + names[aliasIdx]); } System.out.println(" " + optionDescs.get(entry.getKey()) + ((entry.getValue().absentValue() == null) ? "" : " [optional]")); } } if (params.size() > 0) { System.out.println(" parameters:"); for (Iterator<String> it = params.iterator(); it.hasNext(); ) { System.out.println(" " + it.next() + " " + it.next()); } } } } } public interface Context { String getProperty(String name); void exit() throws Exception; } }