From: <cr...@us...> - 2009-08-20 11:24:49
|
Revision: 5656 http://jnode.svn.sourceforge.net/jnode/?rev=5656&view=rev Author: crawley Date: 2009-08-20 11:24:38 +0000 (Thu, 20 Aug 2009) Log Message: ----------- Working on support for $((expression)) ... Modified Paths: -------------- trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneBuiltin.java trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneContext.java Added Paths: ----------- trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneArithmeticEvaluator.java trunk/shell/src/test/org/jnode/test/shell/bjorne/BjorneArithmeticEvaluatorTest.java Added: trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneArithmeticEvaluator.java =================================================================== --- trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneArithmeticEvaluator.java (rev 0) +++ trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneArithmeticEvaluator.java 2009-08-20 11:24:38 UTC (rev 5656) @@ -0,0 +1,324 @@ +/* + * $Id$ + * + * Copyright (C) 2003-2009 JNode.org + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +package org.jnode.shell.bjorne; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; + +import org.jnode.shell.ShellException; +import org.jnode.shell.ShellFailureException; +import org.jnode.shell.ShellSyntaxException; + +/** + * This class parses and evaluates the bjorne shell's arithmetic expression sublanguage. + * + * @author cr...@jn... + */ +public class BjorneArithmeticEvaluator { + + private static final int NONE = 1; + private static final int PERCENT = 2; + private static final int MINUS = 3; + private static final int PLUS = 4; + private static final int STAR = 5; + private static final int SLASH = 6; + private static final int PLUSPLUS = 7; + private static final int MINUSMINUS = 8; + private static final int PLING = 9; + private static final int TWIDDLE = 10; + private static final int STARSTAR = 11; + + private static final int PREFIX = 16; + + private static final HashMap<Integer, Integer> precedence = new HashMap<Integer, Integer>(); + private static final HashSet<Integer> unaryOps; + static { + precedence.put(PLUSPLUS, 1); + precedence.put(MINUSMINUS, 1); + precedence.put(PLUSPLUS + PREFIX, 2); + precedence.put(MINUSMINUS + PREFIX, 2); + precedence.put(PLUS + PREFIX, 3); + precedence.put(MINUS + PREFIX, 3); + precedence.put(PLING + PREFIX, 4); + precedence.put(TWIDDLE + PREFIX, 4); + precedence.put(STARSTAR, 5); + precedence.put(STAR, 6); + precedence.put(SLASH, 6); + precedence.put(PERCENT, 6); + precedence.put(PLUS, 7); + precedence.put(MINUS, 7); + unaryOps = new HashSet<Integer>(Arrays.asList(new Integer[]{ + PLUS + PREFIX, PLUSPLUS, PLUSPLUS + PREFIX, + MINUS + PREFIX, MINUSMINUS, MINUSMINUS + PREFIX})); + }; + + + private class Primary { + private final String name; + private final long value; + + public Primary(String name, long value) { + super(); + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public long getValue() throws ShellSyntaxException { + return name != null ? evalName(name) : value; + } + } + + private final BjorneContext context; + private final Deque<Integer> opStack = new ArrayDeque<Integer>(); + private final Deque<Primary> valStack = new ArrayDeque<Primary>(); + + + public BjorneArithmeticEvaluator(BjorneContext context) { + super(); + this.context = context; + } + + protected synchronized String evaluateExpression(CharSequence source) + throws ShellException { + opStack.clear(); + valStack.clear(); + CharIterator ci = new CharIterator(source); + Primary res = evalExpression(ci); + return Long.toString(res.getValue()); + } + + private Primary evalExpression(CharIterator ci) throws ShellException { + int ch = skipWhiteSpace(ci); + while ((ch = skipWhiteSpace(ci)) != -1 && ch != ')') { + int prefixOp = parseExpressionOperator(ci); + switch (prefixOp) { + case NONE: + break; + case PLUS: + case MINUS: + case PLUSPLUS: + case MINUSMINUS: + prefixOp += PREFIX; + break; + default: + throw new ShellSyntaxException("Unexpected infix operator"); + } + skipWhiteSpace(ci); + pushOperand(evalPrimary(ci)); + skipWhiteSpace(ci); + int op = parseExpressionOperator(ci); + if (prefixOp != NONE) { + if (op == PLUSPLUS || op == MINUSMINUS) { + pushOperator(op); + skipWhiteSpace(ci); + op = parseExpressionOperator(ci); + } + pushOperator(prefixOp); + } + ch = skipWhiteSpace(ci); + if (op == NONE) { + if (ch != -1 && ch != ')') { + throw new ShellSyntaxException("Expected an infix operator in expression"); + } + break; + } else if (op == PLUSPLUS || op == MINUSMINUS) { + throw new ShellSyntaxException("Expected an infix operator in expression"); + } else if (ch == ')') { + throw new ShellSyntaxException("Expected a number or variable name in expression"); + } + pushOperator(op); + } + if (valStack.size() == 0) { + throw new ShellSyntaxException("No expression within \"$((...))\""); + } + while (!opStack.isEmpty()) { + evalOperation(); + } + return valStack.getFirst(); + } + + private void pushOperator(int op) throws ShellException { + while (!opStack.isEmpty() && opStack.getFirst() <= op) { + evalOperation(); + } + opStack.addFirst(op); + } + + private void pushOperand(Primary operand) { + valStack.addFirst(operand); + } + + private void evalOperation() throws ShellException { + Integer op = opStack.removeFirst(); + Primary operand1; + Primary operand2; + if (unaryOps.contains(op)) { + operand1 = valStack.removeFirst(); + operand2 = null; + } else { + System.err.println(op); + operand2 = valStack.removeFirst(); + operand1 = valStack.removeFirst(); + } + long value; + Primary res; + switch (op) { + case PLUS + PREFIX: + res = new Primary(null, operand1.getValue()); + break; + case MINUS + PREFIX: + res = new Primary(null, -operand1.getValue()); + break; + case PLUSPLUS + PREFIX: + case MINUSMINUS + PREFIX: + if (operand1.name == null) { + throw new ShellSyntaxException("Cannot apply ++ or -- to a number or a subexpression"); + } + value = evalName(operand1.name) + (op == PLUSPLUS ? 1 : -1); + context.setVariable(operand1.name, Long.toString(value)); + res = new Primary(null, value); + break; + case PLUSPLUS: + case MINUSMINUS: + if (operand1.name == null) { + throw new ShellSyntaxException("Cannot apply ++ or -- to a number or a subexpression"); + } + value = evalName(operand1.name) + (op == PLUSPLUS ? 1 : -1); + context.setVariable(operand1.name, Long.toString(value)); + res = new Primary(null, value); + break; + case PLUS: + res = new Primary(null, operand1.getValue() + operand2.getValue()); + break; + case MINUS: + res = new Primary(null, operand1.getValue() - operand2.getValue()); + break; + case STAR: + res = new Primary(null, operand1.getValue() * operand2.getValue()); + break; + case STARSTAR: + res = new Primary(null, Math.round(Math.pow(operand1.getValue(), operand2.getValue()))); + break; + case SLASH: + value = operand2.getValue(); + if (value == 0) { + throw new ShellException("Divide by zero in expression"); + } + res = new Primary(null, operand1.getValue() / value); + break; + case PERCENT: + value = operand2.getValue(); + if (value == 0) { + throw new ShellException("Divide by zero in expression"); + } + res = new Primary(null, operand1.getValue() % value); + break; + default: + throw new ShellFailureException("operator not supported"); + } + valStack.addFirst(res); + } + + private Primary evalPrimary(CharIterator ci) throws ShellException { + int ch = ci.peekCh(); + if (Character.isLetter(ch) || ch == '_') { + return new Primary(context.parseParameter(ci), 0L); + } else if (Character.isDigit(ch)) { + return new Primary(null, parseNumber(ci)); + } else if (ch == '(') { + ci.nextCh(); + Primary res = evalExpression(ci); + if (ci.nextCh() != ')') { + throw new ShellSyntaxException("Unmatched \"(\" (left parenthesis) in arithmetic expression"); + } + return res; + } else { + throw new ShellSyntaxException("Expected a number or variable name"); + } + } + + private long evalName(String name) throws ShellSyntaxException { + try { + String value = context.variable(name); + return value == null ? 0L : Long.parseLong(value); + } catch (NumberFormatException ex) { + throw new ShellSyntaxException( + "expression syntax error: '" + context.variable(name) + "' is not an integer"); + } + } + + private int skipWhiteSpace(CharIterator ci) { + int ch = ci.peekCh(); + while (ch == ' ' || ch == '\t' || ch == '\n') { + ci.nextCh(); + ch = ci.peekCh(); + } + return ch; + } + + private int parseExpressionOperator(CharIterator ci) throws ShellSyntaxException { + int ch = ci.peekCh(); + switch (ch) { + case '+': + ci.nextCh(); + if (ci.peekCh() == '+') { + ci.nextCh(); + return PLUSPLUS; + } else { + return PLUS; + } + case '-': + ci.nextCh(); + if (ci.peekCh() == '-') { + ci.nextCh(); + return MINUSMINUS; + } else { + return MINUS; + } + case '/': + ci.nextCh(); + return SLASH; + case '*': + ci.nextCh(); + return STAR; + case '%': + ci.nextCh(); + return PERCENT; + default: + return NONE; + } + } + + private long parseNumber(CharIterator ci) { + StringBuilder sb = new StringBuilder(); + int ch; + while (Character.isDigit((char) (ch = ci.nextCh()))) { + sb.append((char) ch); + } + return Long.parseLong(sb.toString()); + } +} Modified: trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneBuiltin.java =================================================================== --- trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneBuiltin.java 2009-08-16 15:07:51 UTC (rev 5655) +++ trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneBuiltin.java 2009-08-20 11:24:38 UTC (rev 5656) @@ -17,7 +17,6 @@ * along with this library; If not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ - package org.jnode.shell.bjorne; import org.jnode.shell.AbstractCommand; Modified: trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneContext.java =================================================================== --- trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneContext.java 2009-08-16 15:07:51 UTC (rev 5655) +++ trunk/shell/src/shell/org/jnode/shell/bjorne/BjorneContext.java 2009-08-20 11:24:38 UTC (rev 5656) @@ -74,29 +74,17 @@ public class BjorneContext { private static final int NONE = 1; - private static final int HASH = 2; - private static final int DHASH = 3; - private static final int PERCENT = 4; - private static final int DPERCENT = 5; - - private static final int HYPHEN = 6; - - private static final int COLONHYPHEN = 7; - + private static final int MINUS = 6; + private static final int COLONMINUS = 7; private static final int EQUALS = 8; - private static final int COLONEQUALS = 9; - private static final int PLUS = 10; - private static final int COLONPLUS = 11; - private static final int QUERY = 12; - private static final int COLONQUERY = 13; private final BjorneInterpreter interpreter; @@ -835,7 +823,7 @@ operator = COLONQUERY; break; case '-': - operator = COLONHYPHEN; + operator = COLONMINUS; break; default: throw new ShellSyntaxException("bad substitution operator"); @@ -852,7 +840,7 @@ operator = PLUS; break; case '-': - operator = HYPHEN; + operator = MINUS; break; default: throw new ShellSyntaxException("unrecognized substitution operator (\"" + (char) ch + "\")"); @@ -866,9 +854,9 @@ throw new ShellSyntaxException("Unmatched \"{\""); } switch (operator) { - case HYPHEN: + case MINUS: return (value == null) ? word : value; - case COLONHYPHEN: + case COLONMINUS: return (value == null || value.length() == 0) ? word : value; case PLUS: return (value == null) ? "" : word; @@ -917,7 +905,7 @@ } } - private String parseParameter(CharIterator ci) throws ShellSyntaxException { + String parseParameter(CharIterator ci) throws ShellSyntaxException { StringBuilder sb = new StringBuilder(); int ch = ci.peekCh(); while (Character.isLetterOrDigit((char) ch) || ch == '_') { @@ -1040,82 +1028,45 @@ return runBacktickCommand(commandLine); } } - - private CharSequence dollarParenParenExpand(CharIterator ci) { - // TODO Auto-generated method stub - return null; + + private CharSequence dollarParenParenExpand(CharIterator ci) throws ShellException { + // Different shells handle $(( ... )) differently, but dash seems to do what + // the POSIX spec seems to say. In the first phase, we look for the matching '))' + // keeping track of nested parentheses and performing any $ expansions. Double + // quotes should be treated as literal. + StringBuilder sb = new StringBuilder(); + int nesting = 0; + boolean done = false; + do { + int ch = ci.peekCh(); + switch (ch) { + case '(': + nesting++; + sb.append('('); + break; + case ')': + if (nesting > 0) { + nesting--; + sb.append(')'); + } else if (ci.peekCh() == ')') { + ci.nextCh(); + done = true; + } else { + sb.append(')'); + } + break; + case '$': + sb.append(dollarExpand(ci, '\000')); + break; + case -1: + throw new ShellSyntaxException("Unmatched \"((\" (double left parenthesis)"); + default: + sb.append((char) ch); + } + } while (!done); + return new BjorneArithmeticEvaluator(this).evaluateExpression(sb); } -// private String dollarParenExpand(CharIterator ci) throws ShellException { -// StringBuilder sb = extractToMatchingParen(ci); -// if (sb.length() > 0 && sb.charAt(sb.length()) == ')') { -// throw new ShellSyntaxException( -// "There should be a space between the two ')'s in '$(...))'"); -// } -// return runBacktickCommand(sb.toString()).toString(); -// } -// -// private StringBuilder extractToMatchingParen(CharIterator ci) throws ShellSyntaxException { -// StringBuilder sb = new StringBuilder(40); -// Deque<Character> stack = new ArrayDeque<Character>(); -// int ch; -// boolean more = true; -// do { -// ch = ci.nextCh(); -// switch (ch) { -// case -1: -// if (!stack.isEmpty()) { -// throw new ShellSyntaxException("unmatched '('"); -// } -// more = false; -// break; -// case ')': -// if (stack.isEmpty()) { -// more = false; -// } else { -// sb.append(')'); -// if (stack.peekFirst() == '(') { -// stack.removeFirst(); -// } -// } -// break; -// case '(': -// if (stack.isEmpty() || stack.peekFirst() == '(') { -// stack.addFirst('('); -// } -// sb.append('('); -// break; -// case '"': -// case '\'': -// case '`': -// sb.append((char) ch); -// if (stack.isEmpty()) { -// stack.addFirst((char) ch); -// } else { -// char top = stack.peekFirst(); -// if (top != '"' && top != '\'' && top != '`') { -// stack.addFirst('"'); -// } else if (top == ch) { -// stack.removeFirst(); -// } -// } -// break; -// case '\\': -// sb.append('\\'); -// ch = ci.nextCh(); -// if (ch == -1) { -// more = false; -// } else { -// sb.append((char) ch); -// } -// break; -// default: -// sb.append((char) ch); -// } -// } while (more); -// return sb; -// } - int execute(CommandLine command, CommandIO[] streams, boolean isBuiltin) throws ShellException { if (isEchoExpansions()) { StringBuilder sb = new StringBuilder(); Added: trunk/shell/src/test/org/jnode/test/shell/bjorne/BjorneArithmeticEvaluatorTest.java =================================================================== --- trunk/shell/src/test/org/jnode/test/shell/bjorne/BjorneArithmeticEvaluatorTest.java (rev 0) +++ trunk/shell/src/test/org/jnode/test/shell/bjorne/BjorneArithmeticEvaluatorTest.java 2009-08-20 11:24:38 UTC (rev 5656) @@ -0,0 +1,100 @@ +/* + * $Id$ + * + * Copyright (C) 2003-2009 JNode.org + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; If not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +package org.jnode.test.shell.bjorne; + +import junit.framework.TestCase; + +import org.jnode.shell.ShellException; +import org.jnode.shell.bjorne.BjorneArithmeticEvaluator; +import org.jnode.shell.bjorne.BjorneContext; +import org.jnode.shell.io.CommandIOHolder; + +/** + * Some unit tests for the BjorneArithmeticEvaluator class. + * + * @author cr...@jn... + */ +public class BjorneArithmeticEvaluatorTest extends TestCase { + + // This class simply allows us to call the setVariable method directly + private static class TestBjorneContext extends BjorneContext { + TestBjorneContext(CommandIOHolder[] holders) { + super(null, holders); + } + + TestBjorneContext() { + super(null, null); + } + + /** + * Expose method for testing + */ + @Override + protected void setVariable(String name, String value) { + super.setVariable(name, value); + } + } + + private static class TestBjorneArithmeticEvaluator extends BjorneArithmeticEvaluator { + public TestBjorneArithmeticEvaluator(BjorneContext context) { + super(context); + } + + @Override + public synchronized String evaluateExpression(CharSequence source) throws ShellException { + return super.evaluateExpression(source); + } + } + + + public void testConstructor() { + new BjorneArithmeticEvaluator(new TestBjorneContext()); + } + + public void testLiterals() throws ShellException { + TestBjorneArithmeticEvaluator ev = new TestBjorneArithmeticEvaluator(new TestBjorneContext()); + assertEquals("1", ev.evaluateExpression("1")); + assertEquals("1", ev.evaluateExpression(" 1 ")); + assertEquals("42", ev.evaluateExpression("42")); + } + + public void testVariable() throws ShellException { + TestBjorneContext context = new TestBjorneContext(); + context.setVariable("A", "1"); + TestBjorneArithmeticEvaluator ev = new TestBjorneArithmeticEvaluator(context); + assertEquals("1", ev.evaluateExpression("A")); + assertEquals("1", ev.evaluateExpression(" A ")); + assertEquals("0", ev.evaluateExpression(" B")); + } + + public void testUnaryPlusMinus() throws ShellException { + TestBjorneContext context = new TestBjorneContext(); + context.setVariable("A", "1"); + TestBjorneArithmeticEvaluator ev = new TestBjorneArithmeticEvaluator(context); + assertEquals("1", ev.evaluateExpression("+A")); + assertEquals("1", ev.evaluateExpression(" + A ")); + assertEquals("0", ev.evaluateExpression(" + B")); + assertEquals("-1", ev.evaluateExpression("-A")); + assertEquals("-1", ev.evaluateExpression(" - A ")); + assertEquals("0", ev.evaluateExpression(" - B")); + } + +} This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |