blob: eaf0901102650953b877a649a6575d9477827bde [file] [log] [blame]
/*
* 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.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
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<String, List<CompletionData>>) 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 {
CharBuffer buf = CharBuffer.allocate(4096);
StringBuilder sb = new StringBuilder();
URLConnection conn = script.toURL().openConnection();
try (InputStreamReader in = new InputStreamReader(conn.getInputStream()))
{
while (in.read(buf) > 0)
{
buf.flip();
sb.append(buf);
buf.clear();
}
}
finally
{
if (conn instanceof HttpURLConnection)
{
((HttpURLConnection) conn).disconnect();
}
}
return sb;
}
@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",
" --nohistory don't save the command history",
" --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());
if (!opt.isSet("nohistory")) {
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) {
writer.write(new String(b, off, len));
}
public void flush() {
writer.flush();
}
public void close() {
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, (Character) 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) {
try {
reader.getHistory().save();
} catch (IOException e1) {
// ignore
}
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;
}
}