/* * 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.runtime; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.channels.Channel; import java.nio.channels.Channels; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.felix.service.command.Job.Status; import org.apache.felix.gogo.runtime.Parser.Array; import org.apache.felix.gogo.runtime.Parser.Executable; import org.apache.felix.gogo.runtime.Parser.Operator; import org.apache.felix.gogo.runtime.Parser.Pipeline; import org.apache.felix.gogo.runtime.Parser.Program; import org.apache.felix.gogo.runtime.Parser.Sequence; import org.apache.felix.gogo.runtime.Parser.Statement; import org.apache.felix.gogo.runtime.Pipe.Result; import org.apache.felix.service.command.CommandSession; import org.apache.felix.service.command.Function; public class Closure implements Function, Evaluate { public static final String LOCATION = ".location"; public static final String PIPE_EXCEPTION = "pipe-exception"; private static final String DEFAULT_LOCK = ".defaultLock"; private static final ThreadLocal<String> location = new ThreadLocal<>(); private final CommandSessionImpl session; private final Closure parent; private final CharSequence source; private final Program program; private final Object script; private Token errTok; private Token errTok2; private List<Object> parms = null; private List<Object> parmv = null; public Closure(CommandSessionImpl session, Closure parent, CharSequence source) throws Exception { this.session = session; this.parent = parent; this.source = source; this.script = session.get("0"); // by convention, $0 is script name if (source instanceof Program) { program = (Program) source; } else { try { this.program = new Parser(source).program(); } catch (Exception e) { throw setLocation(e); } } } public Closure(CommandSessionImpl session, Closure parent, Program program) { this.session = session; this.parent = parent; this.source = program; this.script = session.get("0"); // by convention, $0 is script name this.program = program; } public CommandSessionImpl session() { return session; } private Exception setLocation(Exception e) { if (session.get(DEFAULT_LOCK) == null) { String loc = location.get(); if (null == loc) { loc = (null == script ? "" : script + ":"); if (e instanceof SyntaxError) { SyntaxError se = (SyntaxError) e; loc += se.line() + "." + se.column(); } else if (null != errTok) { loc += errTok.line + "." + errTok.column; } location.set(loc); } else if (null != script && !loc.contains(":")) { location.set(script + ":" + loc); } session.put(LOCATION, location.get()); } if (e instanceof EOFError) { // map to public exception, so interactive clients can provide more input EOFException eofe = new EOFException(e.getMessage()); eofe.initCause(e); return eofe; } return e; } // implements Function interface public Object execute(CommandSession x, List<Object> values) throws Exception { return execute(x, values, null); } public Object execute(CommandSession x, List<Object> values, Channel capturingOutput) throws Exception { if (x != null && x != session) { if (x instanceof CommandSessionImpl) { return new Closure((CommandSessionImpl) x, null, program).execute(x, values, capturingOutput); } else { throw new IllegalStateException("The current session is not a Gogo session"); } } try { location.remove(); session.put(LOCATION, null); return execute(values, capturingOutput); } catch (Exception e) { throw setLocation(e); } } @SuppressWarnings("unchecked") private Object execute(List<Object> values, Channel capturingOutput) throws Exception { if (null != values) { parmv = new ArrayList<>(values); parms = new ArgList(parmv); } else if (null != parent) { // inherit parent closure parameters parmv = parent.parmv != null ? new ArrayList<>(parent.parmv) : null; parms = parmv != null ? new ArgList(parmv) : null; } else { // inherit session parameters Object args = session.get("args"); if (null != args && args instanceof List<?>) { parmv = new ArrayList<>((List<Object>) args); parms = new ArgList(parmv); } } Result last = null; Operator operator = null; for (Iterator<Executable> iterator = program.tokens().iterator(); iterator.hasNext();) { Operator prevOperator = operator; Executable executable = iterator.next(); if (iterator.hasNext()) { operator = (Operator) iterator.next(); } else { operator = null; } if (prevOperator != null) { if (Token.eq("&&", prevOperator)) { if (!last.isSuccess()) { continue; } } else if (Token.eq("||", prevOperator)) { if (last.isSuccess()) { continue; } } } Channel[] streams; boolean[] toclose = new boolean[10]; if (Pipe.getCurrentPipe() != null) { streams = Pipe.getCurrentPipe().streams.clone(); } else { streams = new Channel[10]; System.arraycopy(session.channels, 0, streams, 0, 3); } if (capturingOutput != null) { streams[1] = capturingOutput; toclose[1] = true; } CommandSessionImpl.JobImpl job; if (executable instanceof Pipeline) { Pipeline pipeline = (Pipeline) executable; List<Executable> exec = pipeline.tokens(); Token s = exec.get(0); Token e = exec.get(exec.size() - 1); Token t = program.subSequence(s.start - program.start, e.start + e.length - program.start); job = session().createJob(t); for (int i = 0; i < exec.size(); i++) { Statement ex = (Statement) exec.get(i); Operator op = i < exec.size() - 1 ? (Operator) exec.get(++i) : null; Channel[] nstreams; boolean[] ntoclose; boolean endOfPipe; if (i == exec.size() - 1) { nstreams = streams; ntoclose = toclose; endOfPipe = true; } else if (Token.eq("|", op)) { PipedInputStream pis = new PipedInputStream(); PipedOutputStream pos = new PipedOutputStream(pis); nstreams = streams.clone(); nstreams[1] = Channels.newChannel(pos); ntoclose = toclose.clone(); ntoclose[1] = true; streams[0] = Channels.newChannel(pis); toclose[0] = true; endOfPipe = false; } else if (Token.eq("|&", op)) { PipedInputStream pis = new PipedInputStream(); PipedOutputStream pos = new PipedOutputStream(pis); nstreams = streams.clone(); nstreams[1] = nstreams[2] = Channels.newChannel(pos); ntoclose = toclose.clone(); ntoclose[1] = ntoclose[2] = true; streams[0] = Channels.newChannel(pis); toclose[0] = true; endOfPipe = false; } else { throw new IllegalStateException("Unrecognized pipe operator: '" + op + "'"); } Pipe pipe = new Pipe(this, job, ex, nstreams, ntoclose, endOfPipe); job.addPipe(pipe); } } else { job = session().createJob(executable); Pipe pipe = new Pipe(this, job, (Statement) executable, streams, toclose, true); job.addPipe(pipe); } // Start pipe in background if (operator != null && Token.eq("&", operator)) { job.start(Status.Background); last = new Result((Object) null); } // Start in foreground and wait for results else { last = job.start(Status.Foreground); if (last == null) { last = new Result((Object) null); } else if (last.exception != null) { throw last.exception; } } } return last == null ? null : last.result; } static Object eval(Object v) { String s = v.toString(); if ("null".equals(s)) { v = null; } else if ("false".equals(s)) { v = false; } else if ("true".equals(s)) { v = true; } else { try { v = s; v = Double.parseDouble(s); // if it parses as double v = Long.parseLong(s); // see whether it is integral } catch (NumberFormatException e) { // Ignore } } return v; } public Object eval(final Token t) throws Exception { return eval(t, true); } public Object eval(final Token t, boolean convertNumeric) throws Exception { if (t instanceof Parser.Closure) { return new Closure(session, this, ((Parser.Closure) t).program()); } else if (t instanceof Sequence) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); Channel out = Channels.newChannel(baos); Object result = new Closure(session, this, ((Sequence) t).program()) .execute(session, parms, out); if (result != null) { return result; } else { String s = baos.toString(); while (!s.isEmpty() && s.charAt(s.length() - 1) == '\n') { s = s.substring(0, s.length() - 1); } return s; } } else if (t instanceof Array) { return array((Array) t); } else { Object v = Expander.expand(t, this); if (t == v) { if (convertNumeric) { v = eval(v); } } return v; } } public Object execute(Executable executable) throws Exception { if (executable instanceof Statement) { return executeStatement((Statement) executable); } else if (executable instanceof Sequence) { return new Closure(session, this, ((Sequence) executable).program()).execute(new ArrayList<>(), null); } else { throw new IllegalStateException(); } } /* * executeStatement handles the following cases: * <string> '=' word // simple assignment * <string> '=' word word.. // complex assignment * <bareword> word.. // command invocation * <object> // value of <object> * <object> word.. // method call */ public Object executeStatement(Statement statement) throws Exception { Object echo = session.get("echo"); String xtrace = null; if (echo != null && !"false".equals(echo.toString())) { // set -x execution trace xtrace = "+" + statement; session.perr.println(xtrace); } List<Token> tokens = statement.tokens(); if (tokens.isEmpty()) { return null; } List<Object> values = new ArrayList<>(); errTok = tokens.get(0); if ((tokens.size() > 3) && Token.eq("=", tokens.get(1))) { errTok2 = tokens.get(2); } for (Token t : tokens) { Object v = eval(t, values.isEmpty()); // if ((Token.Type.EXECUTION == t.type) && (tokens.size() == 1)) { // return v; // } if (v instanceof ArgList) { values.addAll((ArgList) v); // explode $args array } else { values.add(v); } } Object cmd = values.remove(0); if (cmd == null) { if (values.isEmpty()) { return null; } throw new RuntimeException("Command name evaluates to null: " + errTok); } if (cmd instanceof CharSequence && values.size() > 0 && Token.eq("=", tokens.get(1))) { values.remove(0); String scmd = cmd.toString(); Object value; if (values.size() == 0) { return session.put(scmd, null); } if (values.size() == 1) { value = values.get(0); } else { cmd = values.remove(0); if (null == cmd) { throw new RuntimeException("Command name evaluates to null: " + errTok2); } trace2(xtrace, cmd, values); value = bareword(tokens.get(2), cmd) ? executeCmd(cmd.toString(), values) : executeMethod(cmd, values); } return assignment(scmd, value); } trace2(xtrace, cmd, values); return bareword(tokens.get(0), cmd) ? executeCmd(cmd.toString(), values) : executeMethod(cmd, values); } // second level expanded execution trace private void trace2(String trace1, Object cmd, List<Object> values) { if ("verbose".equals(session.get("echo"))) { StringBuilder buf = new StringBuilder("+ " + cmd); for (Object value : values) { buf.append(' '); buf.append(value); } String trace2 = buf.toString(); if (!trace2.equals(trace1)) { session.perr.println("+" + trace2); } } } private boolean bareword(Token t, Object v) throws Exception { return v instanceof CharSequence && Token.eq(t, (CharSequence) v); } private Object executeCmd(String scmd, List<Object> values) throws Exception { String scopedFunction = scmd; Object x = get(scmd); if (!(x instanceof Function)) { if (scmd.indexOf(':') < 0) { scopedFunction = "*:" + scmd; } x = get(scopedFunction); if (x == null || !(x instanceof Function)) { // try default command handler if (session.get(DEFAULT_LOCK) == null) { x = get("default"); if (x == null) { x = get("*:default"); } if (x instanceof Function) { try { session.put(DEFAULT_LOCK, true); values.add(0, scmd); return ((Function) x).execute(session, values); } finally { session.put(DEFAULT_LOCK, null); } } } throw new CommandNotFoundException(scmd); } } return ((Function) x).execute(session, values); } private Object executeMethod(Object cmd, List<Object> values) throws Exception { if (values.isEmpty()) { return cmd; } boolean dot = values.size() > 1 && ".".equals(String.valueOf(values.get(0))); // FELIX-1473 - allow method chaining using dot pseudo-operator, e.g. // (bundle 0) . loadClass java.net.InetAddress . localhost . hostname // (((bundle 0) loadClass java.net.InetAddress ) localhost ) hostname if (dot) { Object target = cmd; ArrayList<Object> args = new ArrayList<>(); values.remove(0); for (Object arg : values) { if (".".equals(arg)) { target = Reflective.invoke(session, target, args.remove(0).toString(), args); args.clear(); } else { args.add(arg); } } if (args.size() == 0) { return target; } return Reflective.invoke(session, target, args.remove(0).toString(), args); } else if (cmd.getClass().isArray() && values.size() == 1) { Object[] cmdv = (Object[]) cmd; String index = values.get(0).toString(); return "length".equals(index) ? cmdv.length : cmdv[Integer.parseInt(index)]; } else { return Reflective.invoke(session, cmd, values.remove(0).toString(), values); } } private Object assignment(String name, Object value) { session.put(name, value); return value; } public Object expr(Token expr) { return session.expr(expr); } private Object array(Array array) throws Exception { List<Token> list = array.list(); Map<Token, Token> map = array.map(); if (list != null) { List<Object> olist = new ArrayList<>(); for (Token t : list) { Object oval = eval(t); if (oval.getClass().isArray()) { Collections.addAll(olist, (Object[]) oval); } else if (oval instanceof ArgList) { olist.addAll((ArgList) oval); } else { olist.add(oval); } } return olist; } else { Map<Object, Object> omap = new LinkedHashMap<>(); for (Entry<Token, Token> e : map.entrySet()) { Token key = e.getKey(); Object k = eval(key); if (!(k instanceof String)) { throw new SyntaxError(key.line(), key.column(), "map key null or not String: " + key); } omap.put(k, eval(e.getValue())); } return omap; } } public Object get(String name) { if (parms != null) { if ("args".equals(name)) { return parms; } if ("argv".equals(name)) { return parmv; } if ("it".equals(name)) { return parms.get(0); } if (name.length() == 1 && Character.isDigit(name.charAt(0))) { int i = name.charAt(0) - '0'; if (i > 0) { return parms.get(i - 1); } } } return session.get(name); } public Object put(String key, Object value) { return session.put(key, value); } @Override public Path currentDir() { return isSet(CommandSession.OPTION_NO_GLOB, false) ? null : session().currentDir(); } protected boolean isSet(String name, boolean def) { Object v = session.get(name); if (v instanceof Boolean) { return (Boolean) v; } else if (v != null) { String s = v.toString(); return s.isEmpty() || s.equalsIgnoreCase("on") || s.equalsIgnoreCase("1") || s.equalsIgnoreCase("true"); } return def; } @Override public String toString() { return source.toString().trim().replaceAll("\n+", "\n").replaceAll( "([^\\\\{}(\\[])[\\s\n]*\n", "$1;").replaceAll("[ \\\\\t\n]+", " "); } }