blob: 177c3386f8cc99ee971b5e304dd4c89ac783e40d [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.shell;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Map.Entry;
import org.apache.felix.gogo.options.Option;
import org.apache.felix.gogo.options.Options;
import org.apache.felix.service.command.CommandSession;
import org.apache.felix.service.command.Converter;
/**
* gosh built-in commands.
*/
public class Builtin
{
static final String[] functions = { "format", "getopt", "new", "set", "tac", "type" };
private static final String[] packages = { "java.lang", "java.io", "java.net",
"java.util" };
public CharSequence format(CommandSession session)
{
return format(session, session.get("_")); // last result
}
public CharSequence format(CommandSession session, Object arg)
{
CharSequence result = session.format(arg, Converter.INSPECT);
System.out.println(result);
return result;
}
/**
* script access to Options.
* @param spec the spec
* @param args the args
* @return Option
*/
public Option getopt(List<Object> spec, Object[] args)
{
String[] optSpec = new String[spec.size()];
for (int i = 0; i < optSpec.length; ++i)
{
optSpec[i] = spec.get(i).toString();
}
return Options.compile(optSpec).parse(args);
}
// FIXME: the "new" command should be provided by runtime,
// so it can leverage same argument coercion mechanism, used to invoke methods.
public Object _new(CommandSession session, Object name, Object[] argv) throws Exception
{
Class<?> clazz = null;
if (name instanceof Class<?>)
{
clazz = (Class<?>) name;
}
else
{
clazz = loadClass(session, name.toString());
}
for (Constructor<?> c : clazz.getConstructors())
{
Class<?>[] types = c.getParameterTypes();
if (types.length != argv.length)
{
continue;
}
boolean match = true;
for (int i = 0; i < argv.length; ++i)
{
if (!types[i].isAssignableFrom(argv[i].getClass()))
{
if (!types[i].isAssignableFrom(String.class))
{
match = false;
break;
}
argv[i] = argv[i].toString();
}
}
if (!match)
{
continue;
}
try
{
return c.newInstance(argv);
}
catch (InvocationTargetException ite)
{
Throwable cause = ite.getCause();
if (cause instanceof Exception)
{
throw (Exception) cause;
}
throw ite;
}
}
throw new IllegalArgumentException("can't coerce " + Arrays.asList(argv)
+ " to any of " + Arrays.asList(clazz.getConstructors()));
}
private Class<?> loadClass(CommandSession session, String name) throws ClassNotFoundException
{
if (!name.contains("."))
{
for (String p : packages)
{
String pkg = p + "." + name;
try
{
return Class.forName(pkg, true, session.classLoader());
}
catch (ClassNotFoundException e)
{
}
}
}
return Class.forName(name, true, session.classLoader());
}
public void set(CommandSession session, String[] argv) {
final String[] usage = {
"set - show session variables",
"Usage: set [OPTIONS] [PREFIX]",
" -? --help show help",
" -a --all show all variables, including those starting with .",
" -x set xtrace option",
" +x unset xtrace option",
"If PREFIX given, then only show variable(s) starting with PREFIX" };
Option opt = Options.compile(usage).parse(argv);
if (opt.isSet("help"))
{
opt.usage();
return;
}
List<String> args = opt.args();
String prefix = (args.isEmpty() ? "" : args.get(0));
if (opt.isSet("x"))
{
session.put("echo", true);
}
else if ("+x".equals(prefix))
{
session.put("echo", null);
}
else
{
boolean all = opt.isSet("all");
for (String key : new TreeSet<>(Shell.getVariables(session)))
{
if (!key.startsWith(prefix))
continue;
if (key.startsWith(".") && !(all || prefix.length() > 0))
continue;
Object target = session.get(key);
String type = null;
String value = null;
if (target != null)
{
Class<?> clazz = target.getClass();
type = clazz.getSimpleName();
value = target.toString();
}
String trunc = value == null || value.length() < 55 ? "" : "...";
System.out.println(String.format("%-15.15s %-15s %.45s%s", type, key,
value, trunc));
}
}
}
public Object tac(CommandSession session, String[] argv) throws IOException
{
final String[] usage = {
"tac - capture stdin as String or List and optionally write to file.",
"Usage: tac [-al] [FILE]", " -a --append append to FILE",
" -l --list return List<String>",
" -? --help show help" };
Option opt = Options.compile(usage).parse(argv);
if (opt.isSet("help"))
{
opt.usage();
return null;
}
List<String> args = opt.args();
BufferedWriter fw = null;
if (args.size() == 1)
{
String path = args.get(0);
File file = new File(Shell.cwd(session).resolve(path));
fw = new BufferedWriter(new FileWriter(file, opt.isSet("append")));
}
StringWriter sw = new StringWriter();
BufferedReader rdr = new BufferedReader(new InputStreamReader(System.in));
ArrayList<String> list = null;
if (opt.isSet("list"))
{
list = new ArrayList<>();
}
boolean first = true;
String s;
while ((s = rdr.readLine()) != null)
{
if (list != null)
{
list.add(s);
}
else
{
if (!first)
{
sw.write(' ');
}
first = false;
sw.write(s);
}
if (fw != null)
{
fw.write(s);
fw.newLine();
}
}
if (fw != null)
{
fw.close();
}
return list != null ? list : sw.toString();
}
// FIXME: expose API in runtime so type command doesn't have to duplicate the runtime
// command search strategy.
public boolean type(CommandSession session, String[] argv) throws Exception
{
final String[] usage = { "type - show command type",
"Usage: type [OPTIONS] [name[:]]",
" -a --all show all matches",
" -? --help show help",
" -q --quiet don't print anything, just return status",
" -s --scope=NAME list all commands in named scope",
" -t --types show full java type names" };
Option opt = Options.compile(usage).parse(argv);
List<String> args = opt.args();
if (opt.isSet("help"))
{
opt.usage();
return true;
}
boolean all = opt.isSet("all");
String optScope = null;
if (opt.isSet("scope"))
{
optScope = opt.get("scope");
}
if (args.size() == 1)
{
String arg = args.get(0);
if (arg.endsWith(":"))
{
optScope = args.remove(0);
}
}
if (optScope != null || (args.isEmpty() && all))
{
Set<String> snames = new TreeSet<>();
for (String sname : (getCommands(session)))
{
if ((optScope == null) || sname.startsWith(optScope))
{
snames.add(sname);
}
}
for (String sname : snames)
{
System.out.println(sname);
}
return true;
}
if (args.size() == 0)
{
Map<String, Integer> scopes = new TreeMap<>();
for (String sname : getCommands(session))
{
int colon = sname.indexOf(':');
String scope = sname.substring(0, colon);
Integer count = scopes.get(scope);
if (count == null)
{
count = 0;
}
scopes.put(scope, ++count);
}
for (Entry<String, Integer> entry : scopes.entrySet())
{
System.out.println(entry.getKey() + ":" + entry.getValue());
}
return true;
}
final String name = args.get(0).toLowerCase();
final int colon = name.indexOf(':');
final String MAIN = "_main"; // FIXME: must match Reflective.java
StringBuilder buf = new StringBuilder();
Set<String> cmds = new LinkedHashSet<>();
// get all commands
if ((colon != -1) || (session.get(name) != null))
{
cmds.add(name);
}
else if (session.get(MAIN) != null)
{
cmds.add(MAIN);
}
else
{
String path = session.get("SCOPE") != null ? session.get("SCOPE").toString()
: "*";
for (String s : path.split(":"))
{
if (s.equals("*"))
{
for (String sname : getCommands(session))
{
if (sname.endsWith(":" + name))
{
cmds.add(sname);
if (!all)
{
break;
}
}
}
}
else
{
String sname = s + ":" + name;
if (session.get(sname) != null)
{
cmds.add(sname);
if (!all)
{
break;
}
}
}
}
}
for (String key : cmds)
{
Object target = session.get(key);
if (target == null)
{
continue;
}
CharSequence source = getClosureSource(session, key);
if (source != null)
{
buf.append(name);
buf.append(" is function {");
buf.append(source);
buf.append("}");
continue;
}
for (Method m : getMethods(session, key))
{
StringBuilder params = new StringBuilder();
for (Class<?> type : m.getParameterTypes())
{
if (params.length() > 0)
{
params.append(", ");
}
params.append(type.getSimpleName());
}
String rtype = m.getReturnType().getSimpleName();
if (buf.length() > 0)
{
buf.append("\n");
}
if (opt.isSet("types"))
{
String cname = m.getDeclaringClass().getName();
buf.append(String.format("%s %s.%s(%s)", rtype, cname, m.getName(),
params));
}
else
{
buf.append(String.format("%s is %s %s(%s)", name, rtype, key, params));
}
}
}
if (buf.length() > 0)
{
if (!opt.isSet("quiet"))
{
System.out.println(buf);
}
return true;
}
if (!opt.isSet("quiet"))
{
System.err.println("type: " + name + " not found.");
}
return false;
}
/*
* the following methods depend on the internals of the runtime implementation.
* ideally, they should be available via some API.
*/
@SuppressWarnings("unchecked")
static Set<String> getCommands(CommandSession session)
{
return (Set<String>) session.get(".commands");
}
private boolean isClosure(Object target)
{
return target.getClass().getSimpleName().equals("Closure");
}
private boolean isCommand(Object target)
{
return target.getClass().getSimpleName().equals("CommandProxy");
}
private CharSequence getClosureSource(CommandSession session, String name)
throws Exception
{
Object target = session.get(name);
if (target == null)
{
return null;
}
if (!isClosure(target))
{
return null;
}
Field sourceField = target.getClass().getDeclaredField("source");
sourceField.setAccessible(true);
return (CharSequence) sourceField.get(target);
}
private List<Method> getMethods(CommandSession session, String scmd) throws Exception
{
final int colon = scmd.indexOf(':');
final String function = colon == -1 ? scmd : scmd.substring(colon + 1);
final String name = KEYWORDS.contains(function) ? ("_" + function) : function;
final String get = "get" + function;
final String is = "is" + function;
final String set = "set" + function;
final String MAIN = "_main"; // FIXME: must match Reflective.java
Object target = session.get(scmd);
if (target == null)
{
return null;
}
if (isClosure(target))
{
return null;
}
if (isCommand(target))
{
Method method = target.getClass().getMethod("getTarget", (Class[])null);
method.setAccessible(true);
target = method.invoke(target, (Object[])null);
}
ArrayList<Method> list = new ArrayList<>();
Class<?> tc = (target instanceof Class<?>) ? (Class<?>) target
: target.getClass();
Method[] methods = tc.getMethods();
for (Method m : methods)
{
String mname = m.getName().toLowerCase();
if (mname.equals(name) || mname.equals(get) || mname.equals(set)
|| mname.equals(is) || mname.equals(MAIN))
{
list.add(m);
}
}
return list;
}
private final static Set<String> KEYWORDS = new HashSet<>(
Arrays.asList("abstract", "continue", "for", "new", "switch",
"assert", "default", "goto", "package", "synchronized", "boolean", "do",
"if", "private", "this", "break", "double", "implements", "protected",
"throw", "byte", "else", "import", "public", "throws", "case", "enum",
"instanceof", "return", "transient", "catch", "extends", "int", "short",
"try", "char", "final", "interface", "static", "void", "class",
"finally", "long", "strictfp", "volatile", "const", "float", "native",
"super", "while"));
}