refactor Parser - create explicit Tokenizer to enable fix for:
FELIX-1487: commands on multiple lines - NEWLINE can now be used instead of semiclon as a statement separator.
The Parser now tracks the exact location (line and column) when an error occurs.
also fixes:
FELIX-1473: allow method calls on Strings
FELIX-1493: add $argv for access to List rather than expanded $args
FELIX-1474: result of commands is implicitly written to pipe
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@941549 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Closure.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Closure.java
index 6268f8a..6110be1 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Closure.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Closure.java
@@ -18,42 +18,143 @@
*/
package org.apache.felix.gogo.runtime.shell;
+import java.io.EOFException;
+import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
+import org.apache.felix.gogo.runtime.shell.Tokenizer.Type;
import org.osgi.service.command.CommandSession;
import org.osgi.service.command.Function;
-public class Closure extends Reflective implements Function
+public class Closure extends Reflective implements Function, Evaluate
{
- private static final long serialVersionUID = 1L;
- final CharSequence source;
- final Closure parent;
- CommandSessionImpl session;
- List<Object> parms;
+ public static final String LOCATION = ".location";
- Closure(CommandSessionImpl session, Closure parent, CharSequence source)
+ private static final long serialVersionUID = 1L;
+ private static final ThreadLocal<String> location = new ThreadLocal<String>();
+
+ private final CommandSessionImpl session;
+ private final Closure parent;
+ private final CharSequence source;
+ private final List<List<List<Token>>> program;
+ private final Object script;
+
+ private Token errTok;
+ 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;
+ script = session.get("0"); // by convention, $0 is script name
+
+ try
+ {
+ program = new Parser(source).program();
+ }
+ catch (Exception e)
+ {
+ throw setLocation(e);
+ }
+ }
+
+ public CommandSessionImpl session()
+ {
+ return session;
}
+ private Exception setLocation(Exception e)
+ {
+ String loc = location.get();
+ if (null == loc)
+ {
+ loc = (null == script ? "" : script + ":");
+
+ if (e instanceof SyntaxError)
+ {
+ SyntaxError se = (SyntaxError) e;
+ loc += se.line() + "." + se.column();
+
+ if (e instanceof EOFError)
+ { // map to public exception, so interactive clients can provide more input
+ EOFException eofe = new EOFException(e.getMessage());
+ eofe.initCause(e);
+ e = eofe;
+ }
+ }
+ 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());
+
+ return e;
+ }
+
+ // implements Function interface
+ // XXX: session arg x not used?
public Object execute(CommandSession x, List<Object> values) throws Exception
{
- parms = values;
- Parser parser = new Parser(source);
- List<List<List<CharSequence>>> program = parser.program();
- Pipe last = null;
+ try
+ {
+ location.remove();
+ session.variables.remove(LOCATION);
+ return execute(values);
+ }
+ catch (Exception e)
+ {
+ throw setLocation(e);
+ }
+ }
- for (List<List<CharSequence>> pipeline : program)
+ @SuppressWarnings("unchecked")
+ private Object execute(List<Object> values) throws Exception
+ {
+ if (null != values)
+ {
+ parmv = values;
+ parms = new ArgList(parmv);
+ }
+ else if (null != parent)
+ {
+ // inherit parent closure parameters
+ parms = parent.parms;
+ parmv = parent.parmv;
+ }
+ else
+ {
+ // inherit session parameters
+ Object args = session.get("args");
+ if (null != args && args instanceof List<?>)
+ {
+ parmv = (List<Object>) args;
+ parms = new ArgList(parmv);
+ }
+ }
+
+ Pipe last = null;
+ Object[] mark = Pipe.mark();
+
+ for (List<List<Token>> pipeline : program)
{
ArrayList<Pipe> pipes = new ArrayList<Pipe>();
- for (List<CharSequence> statement : pipeline)
+ for (List<Token> statement : pipeline)
{
Pipe current = new Pipe(this, statement);
@@ -97,19 +198,22 @@
if (pipe.exception != null)
{
// can't throw exception, as result is defined by last pipe
- session.err.println("pipe: " + pipe.exception);
+ Object oloc = session.get(LOCATION);
+ String loc = (String.valueOf(oloc).contains(":") ? oloc + ": "
+ : "pipe: ");
+ session.err.println(loc + pipe.exception);
session.put("pipe-exception", pipe.exception);
}
}
if (last.exception != null)
{
- Pipe.reset();
+ Pipe.reset(mark);
throw last.exception;
}
}
- Pipe.reset(); // reset IO in case sshd uses same thread for new client
+ Pipe.reset(mark); // reset IO in case same thread used for new client
if (last == null)
{
@@ -120,110 +224,180 @@
{
return Arrays.asList((Object[]) last.result);
}
+
return last.result;
}
- Object executeStatement(List<CharSequence> statement) throws Exception
+ public Object eval(final Token t) throws Exception
+ {
+ Object v = null;
+
+ switch (t.type)
+ {
+ case WORD:
+ v = Tokenizer.expand(t, this);
+ if (t == v)
+ {
+ String s = t.toString();
+ if ("null".equals(s))
+ v = null;
+ else if ("false".equals(s))
+ v = false;
+ else if ("true".equals(s))
+ v = true;
+ else
+ v = s;
+ }
+ break;
+
+ case CLOSURE:
+ v = new Closure(session, this, t);
+ break;
+
+ case EXECUTION:
+ v = new Closure(session, this, t).execute(session, parms);
+ break;
+
+ case ARRAY:
+ v = array(t);
+ break;
+
+ case ASSIGN:
+ v = t.type;
+ break;
+
+ default:
+ throw new SyntaxError(t.line, t.column, "unexpected token: " + t.type);
+ }
+
+ return v;
+ }
+
+ public Object executeStatement(List<Token> statement) throws Exception
{
// add set -x facility if echo is set
if (Boolean.TRUE.equals(session.get("echo")))
{
StringBuilder buf = new StringBuilder("+");
- for (CharSequence token : statement)
+ for (Token token : statement)
{
buf.append(' ');
- buf.append(token);
+ buf.append(token.source());
}
session.err.println(buf);
}
- if (statement.size() == 1 && statement.get(0).charAt(0) == '(')
- {
- return eval(statement.get(0));
- }
-
- Object result;
List<Object> values = new ArrayList<Object>();
- for (CharSequence token : statement)
+ errTok = statement.get(0);
+
+ for (Token t : statement)
{
- Object v = eval(token);
- if (v != null && v == parms)
+ Object v = eval(t);
+
+ if ((Type.EXECUTION == t.type) && (statement.size() == 1))
{
- for (Object p : parms)
- {
- values.add(p);
- }
+ return v;
+ }
+
+ if (parms == v && parms != null)
+ {
+ values.addAll(parms); // explode $args array
}
else
{
values.add(v);
}
}
- result = execute(values.remove(0), values);
- return result;
- }
- private Object execute(Object cmd, List<Object> values) throws Exception
- {
+ Object cmd = values.remove(0);
if (cmd == null)
{
if (values.isEmpty())
{
return null;
}
- else
- {
- throw new IllegalArgumentException("Command name evaluates to null");
- }
+
+ throw new RuntimeException("Command name evaluates to null: " + errTok);
}
+ return execute(cmd, values);
+ }
+
+ private Object execute(Object cmd, List<Object> values) throws Exception
+ {
// Now there are the following cases
// <string> '=' statement // complex assignment
// <string> statement // cmd call
// <object> // value of <object>
// <object> statement // method call
- if (cmd instanceof CharSequence)
+ boolean dot = values.size() > 1 && ".".equals(String.valueOf(values.get(0)));
+
+ if (cmd instanceof CharSequence && !dot)
{
String scmd = cmd.toString();
- if (values.size() > 0 && "=".equals(values.get(0)))
+ if (values.size() > 0 && Type.ASSIGN.equals(values.get(0)))
{
+ Object value;
+
if (values.size() == 1)
{
return session.variables.remove(scmd);
}
- else if (values.size() == 2)
+
+ if (values.size() == 2)
{
- Object value = values.get(1);
- if (value instanceof CharSequence)
- {
- value = eval((CharSequence) value);
- }
- return assignment(scmd, value);
+ value = values.get(1);
}
else
{
- Object value = execute(values.get(1),
- values.subList(2, values.size()));
- return assignment(scmd, value);
+ value = execute(values.get(1), values.subList(2, values.size()));
}
+
+ return assignment(scmd, value);
}
else
{
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))
{
- throw new IllegalArgumentException("Command not found: "
- + scopedFunction);
+ // 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", "active");
+ values.add(0, scmd);
+ return ((Function) x).execute(session, values);
+ }
+ finally
+ {
+ session.variables.remove(".default.lock");
+ }
+ }
+ }
+
+ throw new IllegalArgumentException("Command not found: " + scmd);
}
}
return ((Function) x).execute(session, values);
@@ -235,6 +409,33 @@
{
return cmd;
}
+ else if (dot)
+ {
+ // FELIX-1473 - allow methods calls on String objects
+ Object target = cmd;
+ ArrayList<Object> args = new ArrayList<Object>();
+ values.remove(0);
+
+ for (Object arg : values)
+ {
+ if (".".equals(arg))
+ {
+ target = method(session, target, args.remove(0).toString(), args);
+ args.clear();
+ }
+ else
+ {
+ args.add(arg);
+ }
+ }
+
+ if (args.size() == 0)
+ {
+ return target;
+ }
+
+ return method(session, target, args.remove(0).toString(), args);
+ }
else
{
return method(session, cmd, values.remove(0).toString(), values);
@@ -248,241 +449,58 @@
return value;
}
- private Object eval(CharSequence seq) throws Exception
+ private Object array(Token array) throws Exception
{
- Object res = null;
- StringBuilder sb = null;
- Parser p = new Parser(seq);
- int start = p.current;
- while (!p.eof())
- {
- char c = p.peek();
- if (!p.escaped)
- {
- if (c == '$' || c == '(' || c == '\'' || c == '"' || c == '[' || c == '{')
- {
- if (start != p.current || res != null)
- {
- if (sb == null)
- {
- sb = new StringBuilder();
- }
- if (res != null)
- {
- if (res == parms)
- {
- for (int i = 0; i < parms.size(); i++)
- {
- if (i > 0)
- {
- sb.append(' ');
- }
- sb.append(parms.get(i));
- }
- }
- else
- {
- sb.append(res);
- }
- res = null;
- }
- if (start != p.current)
- {
- sb.append(new Parser(p.text.subSequence(start, p.current)).unescape());
- start = p.current;
- continue;
- }
- }
- switch (c)
- {
- case '\'':
- p.next();
- p.quote(c);
- res = new Parser(p.text.subSequence(start + 1, p.current - 1)).unescape();
- start = p.current;
- continue;
- case '\"':
- p.next();
- p.quote(c);
- res = eval(p.text.subSequence(start + 1, p.current - 1));
- start = p.current;
- continue;
- case '[':
- p.next();
- res = array(seq.subSequence(start + 1, p.find(']', '[') - 1));
- start = p.current;
- continue;
- case '(':
- p.next();
- Closure cl = new Closure(session, this, p.text.subSequence(
- start + 1, p.find(')', '(') - 1));
- res = cl.execute(session, parms);
- start = p.current;
- continue;
- case '{':
- p.next();
- res = new Closure(session, this, p.text.subSequence(
- start + 1, p.find('}', '{') - 1));
- start = p.current;
- continue;
- case '$':
- p.next();
- res = var(p.findVar());
- start = p.current;
- continue;
- }
- }
- }
- p.next();
- }
- if (start != p.current)
- {
- if (sb == null)
- {
- sb = new StringBuilder();
- }
- if (res != null)
- {
- if (res == parms)
- {
- for (Object v : parms)
- {
- sb.append(v);
- }
- }
- else
- {
- sb.append(res);
- }
- res = null;
- }
- sb.append(new Parser(p.text.subSequence(start, p.current)).unescape());
- }
- if (sb != null)
- {
- if (res != null)
- {
- if (res == parms)
- {
- for (int i = 0; i < parms.size(); i++)
- {
- if (i > 0)
- {
- sb.append(' ');
- }
- sb.append(parms.get(i));
- }
- }
- else
- {
- sb.append(res);
- }
- }
- res = sb;
- }
- if (res instanceof CharSequence)
- {
- String r = res.toString();
- if ("null".equals(r))
- {
- return null;
- }
- else if ("false".equals(r))
- {
- return false;
- }
- else if ("true".equals(r))
- {
- return true;
- }
- return r;
- }
+ List<Token> list = new ArrayList<Token>();
+ Map<Token, Token> map = new LinkedHashMap<Token, Token>();
+ (new Parser(array)).array(list, map);
- return res;
- }
-
- private Object array(CharSequence array) throws Exception
- {
- List<Object> list = new ArrayList<Object>();
- Map<Object, Object> map = new LinkedHashMap<Object, Object>();
- Parser p = new Parser(array);
-
- while (!p.eof())
+ if (map.isEmpty())
{
- CharSequence token = p.value();
-
- p.ws();
- if (p.peek() == '=')
+ List<Object> olist = new ArrayList<Object>();
+ for (Token t : list)
{
- p.next();
- p.ws();
- if (!p.eof())
- {
- CharSequence value = p.messy();
- map.put(eval(token), eval(value));
- }
+ olist.add(eval(t));
}
- else
- {
- list.add(eval(token));
- }
-
- if (p.peek() == ',')
- {
- p.next();
- }
- p.ws();
- }
- p.ws();
- if (!p.eof())
- {
- throw new IllegalArgumentException("Invalid array syntax: " + array);
- }
-
- if (map.size() != 0 && list.size() != 0)
- {
- throw new IllegalArgumentException("You can not mix maps and arrays: "
- + array);
- }
-
- if (map.size() > 0)
- {
- return map;
+ return olist;
}
else
{
- return list;
+ Map<Object, Object> omap = new LinkedHashMap<Object, Object>();
+ 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;
}
}
- private Object var(CharSequence var) throws Exception
- {
- if (var.charAt(0) == '{')
- {
- var = var.subSequence(1, var.length() - 1);
- }
- Object v = eval(var);
- String name = v.toString();
- return get(name);
- }
-
- /**
- * @param name
- * @return
- */
- private Object get(String name)
+ public Object get(String name)
{
if (parms != null)
{
- if ("it".equals(name))
- {
- return parms.get(0);
- }
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';
@@ -492,6 +510,67 @@
}
}
}
+
return session.get(name);
}
+
+ public Object put(String key, Object value)
+ {
+ return session.variables.put(key, value);
+ }
+
+ @Override
+ public String toString()
+ {
+ return source.toString().trim().replaceAll("\n+", "\n").replaceAll(
+ "([^\\\\{(\\[])\n", "\\1;").replaceAll("[ \\\\\t\n]+", " ");
+ }
+
+ /**
+ * List that overrides toString() for implicit $args expansion.
+ * Also checks for index out of bounds, so that $1 evaluates to null
+ * rather than throwing IndexOutOfBoundsException.
+ * e.g. x = { a$args }; x 1 2 => a1 2 and not a[1, 2]
+ */
+ class ArgList extends AbstractList<Object>
+ {
+ private List<Object> list;
+
+ public ArgList(List<Object> args)
+ {
+ this.list = args;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder buf = new StringBuilder();
+ for (Object o : list)
+ {
+ if (buf.length() > 0)
+ buf.append(' ');
+ buf.append(o);
+ }
+ return buf.toString();
+ }
+
+ @Override
+ public Object get(int index)
+ {
+ return index < list.size() ? list.get(index) : null;
+ }
+
+ @Override
+ public Object remove(int index)
+ {
+ return list.remove(index);
+ }
+
+ @Override
+ public int size()
+ {
+ return list.size();
+ }
+ }
+
}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandSessionImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandSessionImpl.java
index d1b76cc..1a26221 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandSessionImpl.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandSessionImpl.java
@@ -35,14 +35,14 @@
public static final String VARIABLES = ".variables";
public static final String COMMANDS = ".commands";
private static final String COLUMN = "%-20s %s\n";
- InputStream in;
- PrintStream out;
+ protected InputStream in;
+ protected PrintStream out;
PrintStream err;
CommandShellImpl service;
- final Map<Object, Object> variables = new HashMap<Object, Object>();
- private boolean closed; // derek
+ protected final Map<String, Object> variables = new HashMap<String, Object>();
+ private boolean closed;
- CommandSessionImpl(CommandShellImpl service, InputStream in, PrintStream out, PrintStream err)
+ protected CommandSessionImpl(CommandShellImpl service, InputStream in, PrintStream out, PrintStream err)
{
this.service = service;
this.in = in;
@@ -52,7 +52,7 @@
public void close()
{
- this.closed = true; // derek
+ this.closed = true;
}
public Object execute(CharSequence commandline) throws Exception
@@ -62,7 +62,7 @@
if (closed)
{
- throw new IllegalStateException("session is closed"); // derek
+ throw new IllegalStateException("session is closed");
}
Closure impl = new Closure(this, null, commandline);
@@ -77,7 +77,6 @@
public Object get(String name)
{
- // XXX: derek.baum@paremus.com
// there is no API to list all variables, so overload name == null
if (name == null || VARIABLES.equals(name))
{
@@ -93,7 +92,7 @@
return variables.get(name);
}
- // XXX: derek: add SCOPE support
+ // add SCOPE support
if (name.startsWith("*:"))
{
String path = variables.containsKey("SCOPE") ? variables.get("SCOPE").toString()
@@ -353,4 +352,4 @@
return "<can not format " + result + ":" + e;
}
}
-}
\ No newline at end of file
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandShellImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandShellImpl.java
index 13a1665..aa6f0c0 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandShellImpl.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandShellImpl.java
@@ -37,7 +37,7 @@
Set<Converter> converters = new HashSet<Converter>();
protected ThreadIO threadIO;
public final static Object NO_SUCH_COMMAND = new Object();
- Map<String, Object> commands = new LinkedHashMap<String, Object>();
+ protected Map<String, Object> commands = new LinkedHashMap<String, Object>();
public CommandShellImpl()
{
@@ -219,4 +219,4 @@
}
return null;
}
-}
\ No newline at end of file
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/EOFError.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/EOFError.java
new file mode 100644
index 0000000..4965e0d
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/EOFError.java
@@ -0,0 +1,30 @@
+/*
+ * 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.shell;
+
+public class EOFError extends SyntaxError
+{
+ private static final long serialVersionUID = 1L;
+
+ public EOFError(int line, int column, String message)
+ {
+ super(line, column, message);
+ }
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Evaluate.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Evaluate.java
new file mode 100644
index 0000000..4111912
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Evaluate.java
@@ -0,0 +1,29 @@
+/*
+ * 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.shell;
+
+public interface Evaluate
+{
+ Object eval(Token t) throws Exception;
+
+ Object get(String key);
+
+ Object put(String key, Object value);
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Parser.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Parser.java
index 6c915ce..de11bfd 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Parser.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Parser.java
@@ -22,421 +22,155 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.gogo.runtime.shell.Tokenizer.Type;
public class Parser
{
- int current = 0;
- CharSequence text;
- boolean escaped;
- static final String SPECIAL = "<;|{[\"'$`(=";
+ private final Tokenizer tz;
public Parser(CharSequence program)
{
- text = program;
+ tz = new Tokenizer(program);
}
- void ws()
+ public List<List<List<Token>>> program()
{
- // derek: BUGFIX: loop if comment at beginning of input
- //while (!eof() && Character.isWhitespace(peek())) {
- while (!eof() && (!escaped && Character.isWhitespace(peek()) || current == 0))
- {
- if (current != 0 || !escaped && Character.isWhitespace(peek()))
- {
- current++;
- }
- if (peek() == '/' && current < text.length() - 2
- && text.charAt(current + 1) == '/')
- {
- comment();
- }
- if (current == 0)
- {
- break;
- }
- }
- }
-
- private void comment()
- {
- while (!eof() && peek() != '\n' && peek() != '\r')
- {
- next();
- }
- }
-
- boolean eof()
- {
- return current >= text.length();
- }
-
- char peek()
- {
- return peek(false);
- }
-
- char peek(boolean increment)
- {
- escaped = false;
- if (eof())
- {
- return 0;
- }
-
- int last = current;
- char c = text.charAt(current++);
-
- if (c == '\\')
- {
- escaped = true;
- if (eof())
- {
- throw new RuntimeException("Eof found after \\"); // derek
- }
-
- c = text.charAt(current++);
-
- switch (c)
- {
- case 't':
- c = '\t';
- break;
- case '\r':
- case '\n':
- c = ' ';
- break;
- case 'b':
- c = '\b';
- break;
- case 'f':
- c = '\f';
- break;
- case 'n':
- c = '\n';
- break;
- case 'r':
- c = '\r';
- break;
- case 'u':
- c = unicode();
- current += 4;
- break;
- default:
- // We just take the next character literally
- // but have the escaped flag set, important for {},[] etc
- }
- }
- if (!increment)
- {
- current = last;
- }
- return c;
- }
-
- public List<List<List<CharSequence>>> program()
- {
- List<List<List<CharSequence>>> program = new ArrayList<List<List<CharSequence>>>();
- ws();
- if (!eof())
+ List<List<List<Token>>> program = new ArrayList<List<List<Token>>>();
+
+ while (tz.next() != Type.EOT)
{
program.add(pipeline());
- while (peek() == ';')
+
+ switch (tz.type())
{
- current++;
- List<List<CharSequence>> pipeline = pipeline();
- if (pipeline.get(0).get(0).length() != 0)
- {
- program.add(pipeline);
- }
+ case SEMICOLON:
+ case NEWLINE:
+ continue;
}
+
+ break;
}
- if (!eof())
- {
- throw new RuntimeException("Program has trailing text: " + context(current));
- }
+
+ if (tz.next() != Type.EOT)
+ throw new RuntimeException("Program has trailing text: " + tz.value());
return program;
}
- CharSequence context(int around)
+ private List<List<Token>> pipeline()
{
- return text.subSequence(Math.max(0, current - 20), Math.min(text.length(),
- current + 4));
+ List<List<Token>> pipeline = new ArrayList<List<Token>>();
+
+ while (true)
+ {
+ pipeline.add(command());
+ switch (tz.type())
+ {
+ case PIPE:
+ if (tz.next() == Type.EOT)
+ {
+ Token t = tz.token();
+ throw new EOFError(t.line, t.column, "unexpected EOT after pipe '|'");
+ }
+ break;
+
+ default:
+ return pipeline;
+ }
+ }
}
- public List<List<CharSequence>> pipeline()
+ private List<Token> command()
{
- List<List<CharSequence>> statements = new ArrayList<List<CharSequence>>();
- statements.add(statement());
- while (peek() == '|')
+ List<Token> command = new ArrayList<Token>();
+
+ while (true)
{
- current++;
- ws();
- if (!eof())
+ Token t = tz.token();
+
+ switch (t.type)
{
- statements.add(statement());
+ case WORD:
+ case CLOSURE:
+ case EXECUTION:
+ case ARRAY:
+ case ASSIGN:
+ break;
+
+ default:
+ throw new SyntaxError(t.line, t.column, "unexpected token: " + t.type);
+ }
+
+ command.add(t);
+
+ switch (tz.next())
+ {
+ case PIPE:
+ case SEMICOLON:
+ case NEWLINE:
+ case EOT:
+ return command;
+ }
+ }
+ }
+
+ public void array(List<Token> list, Map<Token, Token> map) throws Exception
+ {
+ Token lt = null;
+ boolean isMap = false;
+
+ while (tz.next() != Type.EOT)
+ {
+ if (isMap)
+ {
+ Token key = lt;
+ lt = null;
+ if (null == key)
+ {
+ key = tz.token();
+
+ if (tz.next() != Type.ASSIGN)
+ {
+ Token t = tz.token();
+ throw new SyntaxError(t.line, t.column,
+ "map expected '=', found: " + t);
+ }
+
+ tz.next();
+ }
+
+ Token k = (list.isEmpty() ? key : list.remove(0));
+ Token v = tz.token();
+ map.put(k, v);
}
else
{
- throw new RuntimeException("Eof found after pipe |");
- }
- }
- return statements;
- }
-
- public List<CharSequence> statement()
- {
- List<CharSequence> statement = new ArrayList<CharSequence>();
- statement.add(value());
- while (!eof())
- {
- ws();
- if (peek() == '|' || peek() == ';')
- {
- break;
- }
-
- if (!eof())
- {
- statement.add(messy());
- }
- }
- return statement;
- }
-
- public CharSequence messy()
- {
- char c = peek();
- if (c > 0 && SPECIAL.indexOf(c) < 0)
- {
- int start = current++;
- while (!eof())
- {
- c = peek();
- if (!escaped && (c == ';' || c == '|' || Character.isWhitespace(c)))
+ switch (tz.type())
{
- break;
- }
- next();
- }
- return text.subSequence(start, current);
- }
- else
- {
- return value();
- }
- }
+ case WORD:
+ case CLOSURE:
+ case EXECUTION:
+ case ARRAY:
+ lt = tz.token();
+ list.add(lt);
+ break;
- CharSequence value()
- {
- ws();
-
- int start = current;
- char c = next();
- if (!escaped)
- {
- switch (c)
- {
- case '{':
- return text.subSequence(start, find('}', '{'));
- case '(':
- return text.subSequence(start, find(')', '('));
- case '[':
- return text.subSequence(start, find(']', '['));
- case '<':
- return text.subSequence(start, find('>', '<'));
- case '=':
- return text.subSequence(start, current);
- case '"':
- case '\'':
- quote(c);
- break;
- }
- }
-
- // Some identifier or number
- while (!eof())
- {
- c = peek();
- if (!escaped)
- {
- if (Character.isWhitespace(c) || c == ';' || c == '|' || c == '=')
- {
- break;
- }
- else if (c == '{')
- {
- next();
- find('}', '{');
- }
- else if (c == '(')
- {
- next();
- find(')', '(');
- }
- else if (c == '<')
- {
- next();
- find('>', '<');
- }
- else if (c == '[')
- {
- next();
- find(']', '[');
- }
- else if (c == '\'' || c == '"')
- {
- next();
- quote(c);
- next();
- }
- else
- {
- next();
- }
- }
- else
- {
- next();
- }
- }
- return text.subSequence(start, current);
- }
-
- boolean escaped()
- {
- return escaped;
- }
-
- char next()
- {
- return peek(true);
- }
-
- char unicode()
- {
- if (current + 4 > text.length())
- {
- throw new IllegalArgumentException("Unicode \\u escape at eof at pos ..."
- + context(current) + "...");
- }
-
- String s = text.subSequence(current, current + 4).toString();
- int n = Integer.parseInt(s, 16);
- return (char) n;
- }
-
- int find(char target, char deeper)
- {
- int start = current;
- int level = 1;
-
- while (level != 0)
- {
- if (eof())
- {
- throw new RuntimeException("Eof found in the middle of a compound for '"
- + target + deeper + "', begins at " + context(start));
- }
-
- char c = next();
- if (!escaped)
- {
- if (c == target)
- {
- level--;
- }
- else
- {
- if (c == deeper)
- {
- level++;
- }
- else
- {
- if (c == '"')
+ case ASSIGN:
+ if (list.size() == 1)
{
- quote('"');
+ isMap = true;
+ break;
}
- else
- {
- if (c == '\'')
- {
- quote('\'');
- }
- else
- {
- if (c == '`')
- {
- quote('`');
- }
- }
- }
- }
+ // fall through
+ default:
+ lt = tz.token();
+ throw new SyntaxError(lt.line, lt.column,
+ "unexpected token in list: " + lt);
}
}
}
- return current;
}
- int quote(char which)
- {
- while (!eof() && (peek() != which || escaped))
- {
- next();
- }
-
- return current++;
- }
-
- CharSequence findVar()
- {
- int start = current;
- char c = peek();
-
- if (c == '{')
- {
- next();
- int end = find('}', '{');
- return text.subSequence(start, end);
- }
- if (c == '(')
- {
- next();
- int end = find(')', '(');
- return text.subSequence(start, end);
- }
-
- if (Character.isJavaIdentifierPart(c))
- {
- while (c == '$')
- {
- c = next();
- }
- while (!eof() && (Character.isJavaIdentifierPart(c) || c == '.') && c != '$')
- {
- next();
- c = peek();
- }
- return text.subSequence(start, current);
- }
- throw new IllegalArgumentException(
- "Reference to variable does not match syntax of a variable: "
- + context(start));
- }
-
- public String toString()
- {
- return "..." + context(current) + "...";
- }
-
- public String unescape()
- {
- StringBuilder sb = new StringBuilder();
- while (!eof())
- {
- sb.append(next());
- }
- return sb.toString();
- }
-}
\ No newline at end of file
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Pipe.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Pipe.java
index d25c085..237b044 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Pipe.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Pipe.java
@@ -39,16 +39,22 @@
Closure closure;
Exception exception;
Object result;
- List<CharSequence> statement;
+ List<Token> statement;
- public static void reset()
+ public static Object[] mark()
{
- tIn.set(null);
- tOut.set(null);
- tErr.set(null);
+ Object[] mark = {tIn.get(), tOut.get(), tErr.get() };
+ return mark;
}
- public Pipe(Closure closure, List<CharSequence> statement)
+ public static void reset(Object[] mark)
+ {
+ tIn.set((InputStream) mark[0]);
+ tOut.set((PrintStream) mark[1]);
+ tErr.set((PrintStream) mark[2]);
+ }
+
+ public Pipe(Closure closure, List<Token> statement)
{
super("pipe-" + statement);
this.closure = closure;
@@ -94,14 +100,17 @@
tIn.set(in);
tOut.set(out);
tErr.set(err);
- closure.session.service.threadIO.setStreams(in, out, err);
-
+ closure.session().service.threadIO.setStreams(in, out, err);
+
try
{
result = closure.executeStatement(statement);
if (result != null && pout != null)
{
- out.println(closure.session.format(result, Converter.INSPECT));
+ if (!Boolean.FALSE.equals(closure.session().get(".FormatPipe")))
+ {
+ out.println(closure.session().format(result, Converter.INSPECT));
+ }
}
}
catch (Exception e)
@@ -111,7 +120,7 @@
finally
{
out.flush();
- closure.session.service.threadIO.close();
+ closure.session().service.threadIO.close();
try
{
@@ -130,4 +139,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/SyntaxError.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/SyntaxError.java
new file mode 100644
index 0000000..bbaea93
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/SyntaxError.java
@@ -0,0 +1,44 @@
+/*
+ * 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.shell;
+
+public class SyntaxError extends RuntimeException
+{
+ private static final long serialVersionUID = 1L;
+ private final int line;
+ private final int column;
+
+ public SyntaxError(int line, int column, String message)
+ {
+ super(message);
+ this.line = line;
+ this.column = column;
+ }
+
+ public int column()
+ {
+ return column;
+ }
+
+ public int line()
+ {
+ return line;
+ }
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Token.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Token.java
new file mode 100644
index 0000000..097bf0b
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Token.java
@@ -0,0 +1,81 @@
+/*
+ * 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.shell;
+
+import org.apache.felix.gogo.runtime.shell.Tokenizer.Type;
+
+public class Token implements CharSequence
+{
+ Type type;
+ CharSequence value;
+ short line;
+ short column;
+
+ public Token(Type type, CharSequence value, short line, short column)
+ {
+ this.type = type;
+ this.value = value;
+ this.line = line;
+ this.column = column;
+ }
+
+ @Override
+ public String toString()
+ {
+ //return type + "<" + value + ">";
+ return null == value ? type.toString() : value.toString();
+ }
+
+ public char charAt(int index)
+ {
+ return value.charAt(index);
+ }
+
+ public int length()
+ {
+ return (null == value ? 0 : value.length());
+ }
+
+ public CharSequence subSequence(int start, int end)
+ {
+ return value.subSequence(start, end);
+ }
+
+ public String source()
+ {
+ switch (type)
+ {
+ case WORD:
+ return value.toString();
+
+ case CLOSURE:
+ return "{" + value + "}";
+
+ case EXECUTION:
+ return "(" + value + ")";
+
+ case ARRAY:
+ return "[" + value + "]";
+
+ default:
+ return type.toString();
+ }
+ }
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Tokenizer.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Tokenizer.java
new file mode 100644
index 0000000..f70cbe0
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Tokenizer.java
@@ -0,0 +1,755 @@
+/*
+ * 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.shell;
+
+
+/**
+ * Bash-like tokenizer.
+ *
+ * Single and double quotes are just like Bash - single quotes escape everything
+ * (including backslashes), newlines are allowed in quotes.
+ * backslash-newline indicates a line continuation and is removed.
+ *
+ * Variable expansion is just like Bash: $NAME or ${NAME[[:][-+=?WORD]},
+ * except it can yield any Object. Variables expanded within double-quotes,
+ * or adjacent to a String are converted to String.
+ *
+ * Unlike bash, indirect variable expansion is supported using ${$NAME}.
+ *
+ * Only a single variable assignment is recognized, with '=' being the second token.
+ * (Bash allows name1=value1 name2=value2 ... command args)
+ *
+ * Comments can only start where white space is allowed:
+ * # or // starts a line comment, /* starts a block comment.
+ * The following common uses do NOT start comments:
+ * ls http://example.com#anchor
+ * ls $dir/*.java
+ *
+ * @see http://wiki.bash-hackers.org/syntax/basicgrammar
+ */
+public class Tokenizer
+{
+ public enum Type
+ {
+ ASSIGN('='), PIPE('|'), SEMICOLON(';'), NEWLINE, ARRAY, CLOSURE, EXECUTION, WORD, EOT;
+
+ private char c;
+
+ Type()
+ {
+ }
+
+ Type(char c)
+ {
+ this.c = c;
+ }
+
+ @Override
+ public String toString()
+ {
+ return (c == 0 ? super.toString() : "'" + c + "'");
+ }
+ }
+
+ private static final boolean DEBUG = false;
+ private static final char EOT = (char) -1;
+
+ private final CharSequence text;
+ private final Evaluate evaluate;
+ private final boolean inArray;
+ private final boolean inQuote;
+
+ private Type type = Type.NEWLINE;
+ private CharSequence value;
+ private Token token;
+
+ private short line;
+ private short column;
+ private char ch;
+ private int index;
+ private boolean firstWord;
+
+ public Tokenizer(CharSequence text)
+ {
+ this(text, null, false);
+ }
+
+ public Tokenizer(CharSequence text, Evaluate evaluate, boolean inQuote)
+ {
+ this.text = text;
+ this.evaluate = evaluate;
+ this.inQuote = inQuote;
+ index = 0;
+ line = column = 1;
+
+ boolean array = false;
+
+ if (text instanceof Token)
+ {
+ Token t = (Token) text;
+ line = t.line;
+ column = t.column;
+ array = (Type.ARRAY == t.type);
+ }
+
+ inArray = array;
+ getch();
+
+ if (DEBUG)
+ {
+ if (inArray)
+ System.err.println("Tokenizer[" + text + "]");
+ else
+ System.err.println("Tokenizer<" + text + ">");
+ }
+ }
+
+ public Type type()
+ {
+ return type;
+ }
+
+ public CharSequence value()
+ {
+ return value;
+ }
+
+ public Token token()
+ {
+ return token;
+ }
+
+ public Type next()
+ {
+ final Type prevType = type;
+ token = null;
+ value = null;
+
+ short tLine;
+ short tColumn;
+
+ while (true)
+ {
+ skipSpace();
+ tLine = line;
+ tColumn = column;
+
+ switch (ch)
+ {
+ case EOT:
+ type = Type.EOT;
+ break;
+
+ case '\n':
+ getch();
+ if (inArray)
+ continue;
+ // only return NEWLINE once and not if not preceded by ; or |
+ switch (prevType)
+ {
+ case PIPE:
+ case SEMICOLON:
+ case NEWLINE:
+ continue;
+
+ default:
+ type = Type.NEWLINE;
+ break;
+ }
+ break;
+
+ case '{':
+ case '(':
+ case '[':
+ value = group();
+ getch();
+ break;
+
+ case ';':
+ getch();
+ type = Type.SEMICOLON;
+ break;
+
+ case '|':
+ getch();
+ type = Type.PIPE;
+ break;
+
+ case '=':
+ if (firstWord || inArray)
+ {
+ getch();
+ type = Type.ASSIGN;
+ break;
+ }
+ // fall through
+ default:
+ value = word();
+ type = Type.WORD;
+ }
+
+ firstWord = (Type.WORD == type && (Type.WORD != prevType && Type.ASSIGN != prevType));
+ token = new Token(type, value, tLine, tColumn);
+
+ if (DEBUG)
+ {
+ System.err.print("<" + type + ">");
+ if (Type.EOT == type)
+ {
+ System.err.println();
+ }
+ }
+
+ return type;
+ }
+ }
+
+ private CharSequence word()
+ {
+ int start = index - 1;
+ int skipCR = 0;
+
+ do
+ {
+ switch (ch)
+ {
+ case '\n':
+ if (index >= 2 && text.charAt(index - 2) == '\r')
+ skipCR = 1;
+ // fall through
+ case '=':
+ if ((Type.WORD == type || Type.ASSIGN == type) && '=' == ch
+ && !inArray)
+ continue;
+ // fall through
+ case ' ':
+ case '\t':
+ case '|':
+ case ';':
+ return text.subSequence(start, index - 1 - skipCR);
+
+ case '{':
+ group();
+ break;
+
+ case '\\':
+ escape();
+ break;
+
+ case '\'':
+ case '"':
+ skipQuote();
+ break;
+ }
+ }
+ while (getch() != EOT);
+
+ return text.subSequence(start, index - 1);
+ }
+
+ private CharSequence group()
+ {
+ final char push = ch;
+ final char pop;
+
+ switch (ch)
+ {
+ case '{':
+ type = Type.CLOSURE;
+ pop = '}';
+ break;
+ case '(':
+ type = Type.EXECUTION;
+ pop = ')';
+ break;
+ case '[':
+ type = Type.ARRAY;
+ pop = ']';
+ break;
+ default:
+ assert false;
+ pop = 0;
+ }
+
+ short sLine = line;
+ short sCol = column;
+ int start = index;
+ int depth = 1;
+
+ while (true)
+ {
+ boolean comment = false;
+
+ switch (ch)
+ {
+ case '{':
+ case '(':
+ case '[':
+ case '\n':
+ comment = true;
+ break;
+ }
+
+ if (getch() == EOT)
+ {
+ throw new EOFError(sLine, sCol, "unexpected EOT looking for matching '"
+ + pop + "'");
+ }
+
+ // don't recognize comments that start within a word
+ if (comment || isBlank(ch))
+ skipSpace();
+
+ switch (ch)
+ {
+ case '"':
+ case '\'':
+ skipQuote();
+ break;
+
+ case '\\':
+ ch = escape();
+ break;
+
+ default:
+ if (push == ch)
+ depth++;
+ else if (pop == ch && --depth == 0)
+ return text.subSequence(start, index - 1);
+ }
+ }
+
+ }
+
+ private char escape()
+ {
+ assert '\\' == ch;
+
+ switch (getch())
+ {
+ case 'u':
+ getch();
+ getch();
+ getch();
+ getch();
+
+ if (EOT == ch)
+ {
+ throw new EOFError(line, column, "unexpected EOT in \\u escape");
+ }
+
+ String u = text.subSequence(index - 4, index).toString();
+
+ try
+ {
+ return (char) Integer.parseInt(u, 16);
+ }
+ catch (NumberFormatException e)
+ {
+ throw new SyntaxError(line, column, "bad unicode escape: \\u" + u);
+ }
+
+ case EOT:
+ throw new EOFError(line, column, "unexpected EOT in \\ escape");
+
+ case '\n':
+ return '\0'; // line continuation
+
+ case '\\':
+ case '\'':
+ case '"':
+ case '$':
+ return ch;
+
+ default:
+ return ch;
+ }
+ }
+
+ private void skipQuote()
+ {
+ assert '\'' == ch || '"' == ch;
+ final char quote = ch;
+ final short sLine = line;
+ final short sCol = column;
+
+ while (getch() != EOT)
+ {
+ if (quote == ch)
+ return;
+
+ if ((quote == '"') && ('\\' == ch))
+ escape();
+ }
+
+ throw new EOFError(sLine, sCol, "unexpected EOT looking for matching quote: "
+ + quote);
+ }
+
+ private void skipSpace()
+ {
+ while (true)
+ {
+ while (isBlank(ch))
+ {
+ getch();
+ }
+
+ // skip continuation lines, but not other escapes
+ if (('\\' == ch) && (peek() == '\n'))
+ {
+ getch();
+ getch();
+ continue;
+ }
+
+ // skip comments
+ if (('/' == ch) || ('#' == ch))
+ {
+ if (('#' == ch) || (peek() == '/'))
+ {
+ while ((getch() != EOT) && ('\n' != ch))
+ {
+ }
+ continue;
+ }
+ else if ('*' == peek())
+ {
+ short sLine = line;
+ short sCol = column;
+ getch();
+
+ while ((getch() != EOT) && !(('*' == ch) && (peek() == '/')))
+ {
+ }
+
+ if (EOT == ch)
+ {
+ throw new EOFError(sLine, sCol,
+ "unexpected EOT looking for closing comment: */");
+ }
+
+ getch();
+ getch();
+ continue;
+ }
+ }
+
+ break;
+ }
+ }
+
+ private boolean isBlank(char ch)
+ {
+ return ' ' == ch || '\t' == ch;
+ }
+
+ private boolean isName(char ch)
+ {
+ return Character.isJavaIdentifierPart(ch) && (ch != '$') || ('.' == ch);
+ }
+
+ /**
+ * expand variables, quotes and escapes in word.
+ * @param vars
+ * @return
+ */
+ public static Object expand(CharSequence word, Evaluate eval)
+ {
+ return expand(word, eval, false);
+ }
+
+ private static Object expand(CharSequence word, Evaluate eval,
+ boolean inQuote)
+ {
+ final String special = "$\\\"'";
+ int i = word.length();
+
+ while ((--i >= 0) && (special.indexOf(word.charAt(i)) == -1))
+ {
+ }
+
+ // shortcut if word doesn't contain any special characters
+ if (i < 0)
+ return word;
+
+ return new Tokenizer(word, eval, inQuote).expand();
+ }
+
+ public Object expand(CharSequence word, short line, short column)
+ {
+ return expand(new Token(Type.WORD, word, line, column), evaluate, inQuote);
+ }
+
+ private Token word(CharSequence value)
+ {
+ return new Token(Type.WORD, value, line, column);
+ }
+
+ private Object expand()
+ {
+ StringBuilder buf = new StringBuilder();
+
+ while (ch != EOT)
+ {
+ int start = index;
+
+ switch (ch)
+ {
+ case '$':
+ Object val = expandVar();
+
+ if (EOT == ch && buf.length() == 0)
+ {
+ return val;
+ }
+
+ if (null != val)
+ {
+ buf.append(val);
+ }
+
+ continue; // expandVar() has already read next char
+
+ case '\\':
+ ch = (inQuote && ("u$\\\n\"".indexOf(peek()) == -1)) ? '\\'
+ : escape();
+
+ if (ch != '\0') // ignore line continuation
+ {
+ buf.append(ch);
+ }
+
+ break;
+
+ case '"':
+ Token ww = word(null);
+ skipQuote();
+ ww.value = text.subSequence(start, index - 1);
+ value = ww;
+ Object expand = expand(value, evaluate, true);
+
+ if (eot() && buf.length() == 0 && value.equals(expand))
+ {
+ return ww.value;
+ }
+
+ if (null != expand)
+ {
+ buf.append(expand.toString());
+ }
+ break;
+
+ case '\'':
+ if (!inQuote)
+ {
+ skipQuote();
+ value = text.subSequence(start, index - 1);
+
+ if (eot() && buf.length() == 0)
+ {
+ return value;
+ }
+
+ buf.append(value);
+ break;
+ }
+ // else fall through
+ default:
+ buf.append(ch);
+ }
+
+ getch();
+ }
+
+ return buf.toString();
+ }
+
+ private Object expandVar()
+ {
+ assert '$' == ch;
+ Object val;
+
+ if (getch() != '{')
+ {
+ int start = index - 1;
+ while (isName(ch))
+ {
+ getch();
+ }
+
+ if (index - 1 == start)
+ {
+ val = "$";
+ }
+ else
+ {
+ String name = text.subSequence(start, index - 1).toString();
+ val = evaluate.get(name);
+ }
+ }
+ else
+ {
+ // ${NAME[[:]-+=?]WORD}
+ short sLine = line;
+ short sCol = column;
+ CharSequence group = group();
+ char c;
+ int i = 0;
+
+ while (i < group.length())
+ {
+ switch (group.charAt(i))
+ {
+ case ':':
+ case '-':
+ case '+':
+ case '=':
+ case '?':
+ break;
+
+ default:
+ ++i;
+ continue;
+ }
+ break;
+ }
+
+ sCol += i;
+
+ String name = String.valueOf(expand(group.subSequence(0, i), sLine, sCol));
+
+ for (int j = 0; j < name.length(); ++j)
+ {
+ if (!isName(name.charAt(j)))
+ {
+ throw new SyntaxError(sLine, sCol, "bad name: ${" + group + "}");
+ }
+ }
+
+ val = evaluate.get(name);
+
+ if (i < group.length())
+ {
+ c = group.charAt(i++);
+ if (':' == c)
+ {
+ c = (i < group.length() ? group.charAt(i++) : EOT);
+ }
+
+ CharSequence word = group.subSequence(i, group.length());
+
+ switch (c)
+ {
+ case '-':
+ case '=':
+ if (null == val)
+ {
+ val = expand(word, evaluate, false);
+ if ('=' == c)
+ {
+ evaluate.put(name, val);
+ }
+ }
+ break;
+
+ case '+':
+ if (null != val)
+ {
+ val = expand(word, evaluate, false);
+ }
+ break;
+
+ case '?':
+ if (null == val)
+ {
+ val = expand(word, evaluate, false);
+ if (null == val || val.toString().length() == 0)
+ {
+ val = "parameter not set";
+ }
+ throw new IllegalArgumentException(name + ": " + val);
+ }
+ break;
+
+ default:
+ throw new SyntaxError(sLine, sCol, "bad substitution: ${" + group
+ + "}");
+ }
+ }
+ getch();
+ }
+
+ return val;
+ }
+
+ /**
+ * returns true if getch() will return EOT
+ * @return
+ */
+ private boolean eot()
+ {
+ return index >= text.length();
+ }
+
+ private char getch()
+ {
+ return ch = getch(false);
+ }
+
+ private char peek()
+ {
+ return getch(true);
+ }
+
+ private char getch(boolean peek)
+ {
+ if (eot())
+ {
+ if (!peek)
+ {
+ ++index;
+ ch = EOT;
+ }
+ return EOT;
+ }
+
+ int current = index;
+ char c = text.charAt(index++);
+
+ if (('\r' == c) && !eot() && (text.charAt(index) == '\n'))
+ c = text.charAt(index++);
+
+ if (peek)
+ {
+ index = current;
+ }
+ else if ('\n' == c)
+ {
+ ++line;
+ column = 0;
+ }
+ else
+ ++column;
+
+ return c;
+ }
+
+}
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/Context.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/Context.java
index afb8284..505713b 100644
--- a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/Context.java
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/Context.java
@@ -41,7 +41,16 @@
public Object execute(CharSequence source) throws Exception
{
- return session.execute(source);
+ Object result = new Exception();
+ try
+ {
+ return result = session.execute(source);
+ }
+ finally
+ {
+ System.err.println("execute<" + source + "> = ("
+ + (null == result ? "Null" : result.getClass().getSimpleName()) + ")(" + result + ")\n");
+ }
}
public void addCommand(String name, Object target)
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser.java
index b75ec82..c18139d 100644
--- a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser.java
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser.java
@@ -126,14 +126,14 @@
Context c = new Context();
c.addCommand("echo", this);
c.addCommand("grep", this);
- assertEquals("a", c.execute("a = a; echo $$a"));
+ assertEquals("a", c.execute("a = a; echo ${$a}"));
assertEquals("hello", c.execute("echo hello"));
assertEquals("hello", c.execute("a = (echo hello)"));
- assertEquals("a", c.execute("a = a; echo $(echo a)"));
+ //assertEquals("a", c.execute("a = a; echo $(echo a)")); // #p2 - no eval in var expansion
assertEquals("3", c.execute("a=3; echo $a"));
assertEquals("3", c.execute("a = 3; echo $a"));
- assertEquals("a", c.execute("a = a; echo $$a"));
+ assertEquals("a", c.execute("a = a; echo ${$a}"));
}
public void testComment() throws Exception
@@ -149,12 +149,6 @@
c.addCommand("echo", this);
c.addCommand("capture", this);
- assertEquals("http://www.aqute.biz?com=2&biz=1",
- c.execute("['http://www.aqute.biz?com=2&biz=1'] get 0"));
- assertEquals("{a=2, b=3}", c.execute("[a=2 b=3]").toString());
- assertEquals("3", c.execute("[a=2 b=3] get b"));
- assertEquals("[3, 4]", c.execute("[1 2 [3 4] 5 6] get 2").toString());
- assertEquals(5, c.execute("[1 2 [3 4] 5 6] size"));
assertEquals("a", c.execute("e = { echo $1 } ; e a b"));
assertEquals("b", c.execute("e = { echo $2 } ; e a b"));
assertEquals("b", c.execute("e = { $args } ; e echo b"));
@@ -166,6 +160,7 @@
public void testArray() throws Exception
{
Context c = new Context();
+ c.set("echo", true);
assertEquals("http://www.aqute.biz?com=2&biz=1",
c.execute("['http://www.aqute.biz?com=2&biz=1'] get 0"));
assertEquals("{a=2, b=3}", c.execute("[a=2 b=3]").toString());
@@ -174,24 +169,15 @@
assertEquals(5, c.execute("[1 2 [3 4] 5 6] size"));
}
- public void testEscape()
- {
- Parser parser = new Parser("'a|b;c'");
- CharSequence cs = parser.messy();
- assertEquals("'a|b;c'", cs.toString());
- assertEquals("'a|b;c'", new Parser(cs).unescape());
- assertEquals("$a", new Parser("\\$a").unescape());
- }
-
public void testParentheses()
{
Parser parser = new Parser("(a|b)|(d|f)");
- List<List<List<CharSequence>>> p = parser.program();
- assertEquals("(a|b)", p.get(0).get(0).get(0));
+ List<List<List<Token>>> p = parser.program();
+ assertEquals("a|b", p.get(0).get(0).get(0).toString());
parser = new Parser("grep (d.*)|grep (d|f)");
p = parser.program();
- assertEquals("(d.*)", p.get(0).get(0).get(1));
+ assertEquals("d.*", p.get(0).get(0).get(1).toString());
}
public void testEcho() throws Exception
@@ -322,41 +308,42 @@
public void testProgram()
{
- List<List<List<CharSequence>>> x = new Parser("abc def|ghi jkl;mno pqr|stu vwx").program();
- assertEquals("abc", x.get(0).get(0).get(0));
- assertEquals("def", x.get(0).get(0).get(1));
- assertEquals("ghi", x.get(0).get(1).get(0));
- assertEquals("jkl", x.get(0).get(1).get(1));
- assertEquals("mno", x.get(1).get(0).get(0));
- assertEquals("pqr", x.get(1).get(0).get(1));
- assertEquals("stu", x.get(1).get(1).get(0));
- assertEquals("vwx", x.get(1).get(1).get(1));
+ List<List<List<Token>>> x = new Parser("abc def|ghi jkl;mno pqr|stu vwx").program();
+ assertEquals("abc", x.get(0).get(0).get(0).toString());
+ assertEquals("def", x.get(0).get(0).get(1).toString());
+ assertEquals("ghi", x.get(0).get(1).get(0).toString());
+ assertEquals("jkl", x.get(0).get(1).get(1).toString());
+ assertEquals("mno", x.get(1).get(0).get(0).toString());
+ assertEquals("pqr", x.get(1).get(0).get(1).toString());
+ assertEquals("stu", x.get(1).get(1).get(0).toString());
+ assertEquals("vwx", x.get(1).get(1).get(1).toString());
}
public void testStatements()
{
- List<List<CharSequence>> x = new Parser("abc def|ghi jkl|mno pqr").pipeline();
- assertEquals("abc", x.get(0).get(0));
- assertEquals("def", x.get(0).get(1));
- assertEquals("ghi", x.get(1).get(0));
- assertEquals("jkl", x.get(1).get(1));
- assertEquals("mno", x.get(2).get(0));
- assertEquals("pqr", x.get(2).get(1));
+ List<List<Token>> x = new Parser("abc def|ghi jkl|mno pqr").program().get(0);
+ assertEquals("abc", x.get(0).get(0).toString());
+ assertEquals("def", x.get(0).get(1).toString());
+ assertEquals("ghi", x.get(1).get(0).toString());
+ assertEquals("jkl", x.get(1).get(1).toString());
+ assertEquals("mno", x.get(2).get(0).toString());
+ assertEquals("pqr", x.get(2).get(1).toString());
}
public void testSimpleValue()
{
- List<CharSequence> x = new Parser(
- "abc def.ghi http://www.osgi.org?abc=&x=1 [1,2,3] {{{{{{{xyz}}}}}}} (immediate) {'{{{{{'} {\\}} 'abc{}'").statement();
- assertEquals("abc", x.get(0));
- assertEquals("def.ghi", x.get(1));
- assertEquals("http://www.osgi.org?abc=&x=1", x.get(2));
- assertEquals("[1,2,3]", x.get(3));
- assertEquals("{{{{{{{xyz}}}}}}}", x.get(4));
- assertEquals("(immediate)", x.get(5));
- assertEquals("{'{{{{{'}", x.get(6));
- assertEquals("{\\}}", x.get(7));
- assertEquals("'abc{}'", x.get(8));
+ List<Token> x = new Parser(
+ "abc def.ghi http://www.osgi.org?abc=&x=1 [1,2,3] {{{{{{{xyz}}}}}}} (immediate) {'{{{{{'} {\\{} 'abc{}'")
+ .program().get(0).get(0);
+ assertEquals("abc", x.get(0).toString());
+ assertEquals("def.ghi", x.get(1).toString());
+ assertEquals("http://www.osgi.org?abc=&x=1", x.get(2).toString());
+ assertEquals("1,2,3", x.get(3).toString());
+ assertEquals("{{{{{{xyz}}}}}}", x.get(4).toString());
+ assertEquals("immediate", x.get(5).toString());
+ assertEquals("'{{{{{'", x.get(6).toString());
+ assertEquals("\\{", x.get(7).toString());
+ assertEquals("'abc{}'", x.get(8).toString());
}
void each(CommandSession session, Collection<Object> list, Function closure)
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser2.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser2.java
new file mode 100644
index 0000000..175a8fd
--- /dev/null
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser2.java
@@ -0,0 +1,78 @@
+/*
+ * 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.shell;
+
+import java.io.EOFException;
+
+import junit.framework.TestCase;
+
+/*
+ * Test features of the new parser/tokenizer, many of which are not supported
+ * by the original parser.
+ */
+public class TestParser2 extends TestCase
+{
+ public void testComment() throws Exception
+ {
+ Context c = new Context();
+ c.addCommand("echo", this);
+
+ assertEquals("file://wibble#tag", c.execute("echo file://wibble#tag"));
+ assertEquals("file:", c.execute("echo file: //wibble#tag"));
+
+ assertEquals("PWD/*.java", c.execute("echo PWD/*.java"));
+ try
+ {
+ c.execute("echo PWD /*.java");
+ fail("expected EOFException");
+ }
+ catch (EOFException e)
+ {
+ // expected
+ }
+
+ assertEquals("ok", c.execute("// can't quote\necho ok\n"));
+
+ // quote in comment in closure
+ assertEquals("ok", c.execute("x = { // can't quote\necho ok\n}; x"));
+ assertEquals("ok", c.execute("x = {\n// can't quote\necho ok\n}; x"));
+ assertEquals("ok", c.execute("x = {// can't quote\necho ok\n}; x"));
+ }
+
+ public CharSequence echo(Object args[])
+ {
+ if (args == null)
+ {
+ return "";
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (Object arg : args)
+ {
+ if (arg != null)
+ {
+ if (sb.length() > 0)
+ sb.append(' ');
+ sb.append(arg);
+ }
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestTokenizer.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestTokenizer.java
new file mode 100644
index 0000000..714a45c
--- /dev/null
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestTokenizer.java
@@ -0,0 +1,277 @@
+/*
+ * 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.shell;
+
+import java.io.File;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.naming.OperationNotSupportedException;
+
+import org.apache.felix.gogo.runtime.shell.Tokenizer.Type;
+
+import junit.framework.TestCase;
+
+public class TestTokenizer extends TestCase
+{
+ private final Map<String, Object> vars = new HashMap<String, Object>();
+ private final Evaluate evaluate;
+
+ public TestTokenizer()
+ {
+ evaluate = new Evaluate()
+ {
+ public Object eval(Token t) throws Exception
+ {
+ throw new OperationNotSupportedException();
+ }
+
+ public Object get(String key)
+ {
+ return vars.get(key);
+ }
+
+ public Object put(String key, Object value)
+ {
+ return vars.put(key, value);
+ }
+ };
+ }
+
+ public void testHello() throws Exception
+ {
+ testHello("hello world\n");
+ testHello("hello world\n\n"); // multiple \n reduced to single Token.NL
+ testHello("hello world\r\n"); // \r\n -> \n
+
+ // escapes
+
+ testHello("hello \\\nworld\n");
+ try
+ {
+ testHello("hello\\u20world\n");
+ fail("bad unicode accepted");
+ }
+ catch (SyntaxError e)
+ {
+ // expected
+ }
+
+ // whitespace and comments
+
+ testHello(" hello world \n ");
+ testHello("hello world // comment\n\n");
+ testHello("hello world #\\ comment\n\n");
+ testHello("// comment\nhello world\n");
+ testHello("// comment ?\\ \nhello world\n");
+ testHello("hello /*\n * comment\n */ world\n");
+ }
+
+ // hello world
+ private void testHello(CharSequence text) throws Exception
+ {
+ Tokenizer t = new Tokenizer(text);
+ assertEquals(Type.WORD, t.next());
+ assertEquals("hello", t.value().toString());
+ assertEquals(Type.WORD, t.next());
+ assertEquals("world", t.value().toString());
+ assertEquals(Type.NEWLINE, t.next());
+ assertEquals(Type.EOT, t.next());
+ }
+
+ public void testString() throws Exception
+ {
+ testString("'single $quote' \"double $quote\"\n");
+ }
+
+ // 'single quote' "double quote"
+ private void testString(CharSequence text) throws Exception
+ {
+ Tokenizer t = new Tokenizer(text);
+ assertEquals(Type.WORD, t.next());
+ assertEquals("'single $quote'", t.value().toString());
+ assertEquals(Type.WORD, t.next());
+ assertEquals("\"double $quote\"", t.value().toString());
+ assertEquals(Type.NEWLINE, t.next());
+ assertEquals(Type.EOT, t.next());
+ }
+
+ public void testClosure() throws Exception
+ {
+ testClosure2("x = { echo '}' $args //comment's\n}\n");
+ testClosure2("x={ echo '}' $args //comment's\n}\n");
+ assertEquals(Type.CLOSURE, token1("{ echo \\{ $args \n}"));
+ assertEquals(Type.CLOSURE, token1("{ echo \\} $args \n}"));
+ }
+
+ /*
+ * x = {echo $args};
+ */
+ private void testClosure2(CharSequence text) throws Exception
+ {
+ Tokenizer t = new Tokenizer(text);
+ assertEquals(Type.WORD, t.next());
+ assertEquals("x", t.value().toString());
+ assertEquals(Type.ASSIGN, t.next());
+ assertEquals(Type.CLOSURE, t.next());
+ assertEquals(" echo '}' $args //comment's\n", t.value().toString());
+ assertEquals(Type.NEWLINE, t.next());
+ assertEquals(Type.EOT, t.next());
+ }
+
+ private Type token1(CharSequence text) throws Exception
+ {
+ Tokenizer t = new Tokenizer(text);
+ Type type = t.next();
+ assertEquals(Type.EOT, t.next());
+ return type;
+ }
+
+ public void testExpand() throws Exception
+ {
+ final URI home = new URI("/home/derek");
+ final File pwd = new File("/tmp");
+ final String user = "derek";
+
+ vars.clear();
+ vars.put("HOME", home);
+ vars.put("PWD", pwd);
+ vars.put("USER", user);
+ vars.put(user, "Derek Baum");
+
+ // quote removal
+ assertEquals("hello", expand("hello"));
+ assertEquals("hello", expand("'hello'"));
+ assertEquals("\"hello\"", expand("'\"hello\"'"));
+ assertEquals("hello", expand("\"hello\""));
+ assertEquals("'hello'", expand("\"'hello'\""));
+
+ // escapes
+ assertEquals("hello\\w", expand("hello\\\\w"));
+ assertEquals("hellow", expand("hello\\w"));
+ assertEquals("hello\\w", expand("\"hello\\\\w\""));
+ assertEquals("hello\\w", expand("\"hello\\w\""));
+ assertEquals("hello\\\\w", expand("'hello\\\\w'"));
+ assertEquals("hello", expand("he\\\nllo"));
+ assertEquals("he\\llo", expand("'he\\llo'"));
+ assertEquals("he'llo", expand("'he'\\''llo'"));
+ assertEquals("he\"llo", expand("\"he\\\"llo\""));
+ assertEquals("he'llo", expand("he\\'llo"));
+ assertEquals("he$llo", expand("\"he\\$llo\""));
+ assertEquals("he\\'llo", expand("\"he\\'llo\""));
+ assertEquals("hello\\w", expand("\"hello\\w\""));
+
+ // unicode
+
+ // Note: we could use literal Unicode pound '£' instead of \u00a3 in next test.
+ // if above is not UK currency symbol, then your locale is not configured for UTF-8.
+ // Java on Macs cannot handle UTF-8 unless you explicitly set '-Dfile.encoding=UTF-8'.
+ assertEquals("pound\u00a3cent\u00a2", expand("pound\\u00a3cent\\u00a2"));
+ assertEquals("euro\\u20ac", expand("'euro\\u20ac'"));
+ try
+ {
+ expand("eot\\u20a");
+ fail("EOT in unicode");
+ }
+ catch (SyntaxError e)
+ {
+ // expected
+ }
+ try
+ {
+ expand("bad\\u20ag");
+ fail("bad unicode");
+ }
+ catch (SyntaxError e)
+ {
+ // expected
+ }
+
+ // simple variable expansion - quoting or concatenation converts result to String
+ assertEquals(user, expand("$USER"));
+ assertEquals(home, expand("$HOME"));
+ assertEquals(home.toString(), expand("$HOME$W"));
+ assertEquals(pwd, expand("$PWD"));
+ assertEquals("$PWD", expand("'$PWD'"));
+ assertEquals("$PWD", expand("\\$PWD"));
+ assertEquals(pwd.toString(), expand("\"$PWD\""));
+ assertEquals("W" + pwd, expand("W$PWD"));
+ assertEquals(pwd + user, expand("$PWD$USER"));
+
+ // variable substitution ${NAME:-WORD} etc
+ assertNull(expand("$JAVA_HOME"));
+ assertEquals(user, expand("${USER}"));
+ assertEquals(user + "W", expand("${USER}W"));
+ assertEquals("java_home", expand("${JAVA_HOME:-java_home}"));
+ assertEquals(pwd, expand("${NOTSET:-$PWD}"));
+ assertNull(vars.get("JAVA_HOME"));
+ assertEquals("java_home", expand("${JAVA_HOME:=java_home}"));
+ assertEquals("java_home", vars.get("JAVA_HOME"));
+ assertEquals("java_home", expand("$JAVA_HOME"));
+ assertEquals("yes", expand("${JAVA_HOME:+yes}"));
+ assertNull(expand("${NOTSET:+yes}"));
+ assertEquals("", expand("\"${NOTSET:+yes}\""));
+ try
+ {
+ expand("${NOTSET:?}");
+ fail("expected 'not set' exception");
+ }
+ catch (IllegalArgumentException e)
+ {
+ // expected
+ }
+
+ // bad variable names
+ assertEquals("$ W", expand("$ W"));
+ assertEquals("$ {W}", expand("$ {W}"));
+ try
+ {
+ expand("${W }");
+ fail("expected syntax error");
+ }
+ catch (SyntaxError e)
+ {
+ // expected
+ }
+
+ assertEquals(user, expand("${USER\\\n:?}"));
+ assertEquals(user, expand("${US\\u0045R:?}"));
+
+ // bash doesn't supported nested expansions
+ // gogo only supports them in the ${} syntax
+ assertEquals("Derek Baum", expand("${$USER}"));
+ assertEquals("x", expand("${$USR:-x}"));
+ assertEquals("$" + user, expand("$$USER"));
+ }
+
+ private Object expand(CharSequence word) throws Exception
+ {
+ return Tokenizer.expand(word, evaluate);
+ }
+
+ public void testParser() throws Exception
+ {
+ new Parser("// comment\n" + "a=\"who's there?\"; ps -ef;\n" + "ls | \n grep y\n").program();
+ String p1 = "a=1 \\$b=2 c={closure}\n";
+ new Parser(p1).program();
+ new Parser(new Token(Type.ARRAY, p1, (short) 0, (short) 0)).program();
+ }
+
+}