/*
* $Id$
*
* Copyright (C) 2003-2014 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.command.common;
import java.io.File;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Stack;
import org.jnode.shell.AbstractCommand;
import org.jnode.shell.CommandLine;
import org.jnode.shell.syntax.CommandSyntaxException;
/**
* JNode implementation of the UNIX 'test' command
*
* @author crawley@jnode.org
*/
public class UnixTestCommand extends AbstractCommand {
// FIXME convert to use the new commandline syntax mechanism so that
// we get command completion.
private static class Operator {
public final int opNo;
public final int priority;
public final int kind;
public Operator(final int opNo, final int priority, int kind) {
this.opNo = opNo;
this.priority = priority;
this.kind = kind;
}
}
private boolean bracketted;
private int pos;
private String[] args;
private int lastArg;
private Stack<Object> operandStack = new Stack<Object>();
private Stack<Operator> operatorStack = new Stack<Operator>();
private static final int OP_DIRECTORY = 1;
private static final int OP_EXISTS = 2;
private static final int OP_FILE = 3;
private static final int OP_STRING_LENGTH = 4;
private static final int OP_STRING_NONEMPTY = 5;
private static final int OP_STRING_EMPTY = 6;
private static final int OP_READABLE = 7;
private static final int OP_WRITEABLE = 8;
private static final int OP_NONEMPTY = 9;
private static final int OP_EQ = 10;
private static final int OP_NE = 11;
private static final int OP_GT = 12;
private static final int OP_GE = 13;
private static final int OP_LT = 14;
private static final int OP_LE = 15;
private static final int OP_AND = 16;
private static final int OP_OR = 17;
private static final int OP_NOT = 18;
private static final int OP_STRING_EQUALS = 19;
private static final int OP_STRING_NONEQUAL = 20;
private static final int OP_LPAREN = 21;
private static final int OP_RPAREN = 22;
private static final int OP_OLDER = 23;
private static final int OP_NEWER = 24;
private static final int OP_UNARY = 1;
private static final int OP_BINARY = 2;
private static final int OP_SPECIAL = 3;
private static final HashMap<String, Operator> OPERATOR_MAP;
static {
OPERATOR_MAP = new HashMap<String, Operator>();
OPERATOR_MAP.put("-d", new Operator(OP_DIRECTORY, 3, OP_UNARY));
OPERATOR_MAP.put("-e", new Operator(OP_EXISTS, 3, OP_UNARY));
OPERATOR_MAP.put("-f", new Operator(OP_FILE, 3, OP_UNARY));
OPERATOR_MAP.put("-l", new Operator(OP_STRING_LENGTH, 3, OP_UNARY));
OPERATOR_MAP.put("-n", new Operator(OP_STRING_NONEMPTY, 3, OP_UNARY));
OPERATOR_MAP.put("-z", new Operator(OP_STRING_EMPTY, 3, OP_UNARY));
OPERATOR_MAP.put("-r", new Operator(OP_READABLE, 3, OP_UNARY));
OPERATOR_MAP.put("-w", new Operator(OP_WRITEABLE, 3, OP_UNARY));
OPERATOR_MAP.put("-eq", new Operator(OP_EQ, 3, OP_BINARY));
OPERATOR_MAP.put("-ne", new Operator(OP_NE, 3, OP_BINARY));
OPERATOR_MAP.put("-lt", new Operator(OP_LT, 3, OP_BINARY));
OPERATOR_MAP.put("-le", new Operator(OP_LE, 3, OP_BINARY));
OPERATOR_MAP.put("-gt", new Operator(OP_GT, 3, OP_BINARY));
OPERATOR_MAP.put("-ge", new Operator(OP_GE, 3, OP_BINARY));
OPERATOR_MAP.put("-ot", new Operator(OP_OLDER, 3, OP_BINARY));
OPERATOR_MAP.put("-nt", new Operator(OP_NEWER, 3, OP_BINARY));
OPERATOR_MAP.put("-a", new Operator(OP_AND, 1, OP_BINARY));
OPERATOR_MAP.put("-o", new Operator(OP_OR, 0, OP_BINARY));
OPERATOR_MAP.put("!", new Operator(OP_NOT, 2, OP_UNARY));
OPERATOR_MAP.put("=", new Operator(OP_STRING_EQUALS, 5, OP_BINARY));
OPERATOR_MAP.put("!=", new Operator(OP_STRING_NONEQUAL, 5, OP_BINARY));
OPERATOR_MAP.put("(", new Operator(OP_LPAREN, -1, OP_SPECIAL));
OPERATOR_MAP.put(")", new Operator(OP_RPAREN, 6, OP_SPECIAL));
}
public void execute()
throws Exception {
boolean res = false;
CommandLine commandLine = getCommandLine();
String commandName = commandLine.getCommandName();
bracketted = (commandName != null && commandName.equals("["));
args = commandLine.getArguments();
try {
if (bracketted && args.length == 0) {
throw new CommandSyntaxException("missing ']'");
} else if (bracketted && !args[args.length - 1].equals("]")) {
processAsOptions(args);
res = true;
} else {
lastArg = bracketted ? args.length - 2 : args.length - 1;
if (lastArg == -1) {
res = false;
} else {
Object obj = evaluate();
if (pos <= lastArg) {
if (args[pos].equals(")")) {
throw new CommandSyntaxException("unmatched ')'");
} else {
throw new AssertionError("I'm confused! pos = " + pos +
", lastArg = " + lastArg + ", next arg is " + args[pos]);
}
}
if (obj instanceof Boolean) {
res = obj == Boolean.TRUE;
} else if (obj instanceof Long) {
res = (Long) obj != 0;
} else {
res = obj.toString().length() > 0;
}
}
}
if (!res) {
exit(1);
}
} catch (CommandSyntaxException ex) {
getError().getPrintWriter().println(ex.getMessage());
exit(2);
}
}
private Object evaluate() throws CommandSyntaxException {
evaluateExpression(false);
while (!operatorStack.isEmpty()) {
reduce();
}
if (operandStack.size() != 1) {
throw new AssertionError("wrong nos operands left");
}
return operandStack.pop();
}
private void evaluateExpression(boolean nested) throws CommandSyntaxException {
evaluatePrimary(nested);
while (pos <= lastArg) {
String tok = next();
Operator op = OPERATOR_MAP.get(tok);
if (op == null) {
throw new CommandSyntaxException("expected an operator");
}
switch(op.kind) {
case OP_UNARY:
throw new CommandSyntaxException("misplaced unary operator");
case OP_BINARY:
evaluatePrimary(nested);
reduceAndPush(op, popOperand());
break;
case OP_SPECIAL:
if (op.opNo == OP_LPAREN) {
throw new CommandSyntaxException("misplaced '('");
} else if (op.opNo == OP_RPAREN) {
if (!nested) {
throw new CommandSyntaxException("misplaced ')'");
}
back();
return;
}
}
}
}
private void evaluatePrimary(boolean nested) throws CommandSyntaxException {
String tok = next();
Operator op = OPERATOR_MAP.get(tok);
if (op == null) {
pushOperand(tok);
} else {
switch (op.kind) {
case OP_UNARY:
operatorStack.push(op);
operandStack.push(next());
break;
case OP_BINARY:
throw new CommandSyntaxException("misplaced binary operator");
case OP_SPECIAL:
if (op.opNo == OP_LPAREN) {
operatorStack.push(op); // ... as a marker.
evaluateExpression(true);
if (!next().equals(")")) {
throw new CommandSyntaxException("missing ')'");
}
while (operatorStack.peek() != op) {
reduce();
}
if (operatorStack.pop() != op) {
throw new AssertionError("cannot find my marker!");
}
} else {
throw new CommandSyntaxException("unmatched ')'");
}
}
}
}
private void reduceAndPush(Operator operator, Object operand) throws CommandSyntaxException {
while (!operatorStack.isEmpty() && operator.priority <= operatorStack.peek().priority) {
reduce();
}
operatorStack.push(operator);
operandStack.push(operand);
}
private void reduce() throws CommandSyntaxException {
Operator operator = operatorStack.pop();
Object operand = null, operand2 = null;
if (operator.kind == OP_UNARY) {
operand = popOperand();
} else if (operator.kind == OP_BINARY) {
operand2 = popOperand();
operand = popOperand();
}
switch (operator.opNo) {
case OP_EXISTS:
pushOperand(toFile(operand).exists());
break;
case OP_DIRECTORY:
pushOperand(toFile(operand).isDirectory());
break;
case OP_FILE:
pushOperand(toFile(operand).isFile());
break;
case OP_NONEMPTY:
pushOperand(toFile(operand).length() > 0);
break;
case OP_READABLE:
pushOperand(toFile(operand).canRead());
break;
case OP_WRITEABLE:
pushOperand(toFile(popOperand()).canWrite());
break;
case OP_OLDER:
pushOperand(toFile(operand).lastModified() < toFile(operand2).lastModified());
break;
case OP_NEWER:
pushOperand(toFile(operand).lastModified() > toFile(operand2).lastModified());
break;
case OP_STRING_EMPTY:
pushOperand(toString().length() == 0);
break;
case OP_STRING_NONEMPTY:
pushOperand(toString(operand).length() > 0);
break;
case OP_STRING_LENGTH:
pushOperand(toString(operand).length());
break;
case OP_EQ:
pushOperand(toNumber(operand) == toNumber(operand2));
break;
case OP_NE:
pushOperand(toNumber(operand) != toNumber(operand2));
break;
case OP_LT:
pushOperand(toNumber(operand) < toNumber(operand2));
break;
case OP_LE:
pushOperand(toNumber(operand) <= toNumber(operand2));
break;
case OP_GT:
pushOperand(toNumber(operand) > toNumber(operand2));
break;
case OP_GE:
pushOperand(toNumber(operand) >= toNumber(operand2));
break;
case OP_AND:
pushOperand(toBoolean(operand) & toBoolean(operand2));
break;
case OP_OR:
pushOperand(toBoolean(operand) | toBoolean(operand2));
break;
case OP_NOT:
pushOperand(!toBoolean(operand));
break;
case OP_STRING_EQUALS:
pushOperand(toString(operand).equals(toString(operand2)));
break;
case OP_STRING_NONEQUAL:
pushOperand(!toString(operand).equals(toString(operand2)));
break;
default:
throw new AssertionError("bad operator");
}
}
private void processAsOptions(String[] args) throws CommandSyntaxException {
PrintWriter err = getError().getPrintWriter();
for (String option : args) {
if (option.equals("--help")) {
err.println("Don't panic!");
} else if (option.equals("--version")) {
err.println("JNode test 0.0");
} else {
throw new CommandSyntaxException("unknown option '" + option + '\'');
}
}
}
private void pushOperand(Object value) {
operandStack.push(value);
}
private void pushOperand(boolean value) {
operandStack.push(value ? Boolean.TRUE : Boolean.FALSE);
}
private void pushOperand(long value) {
operandStack.push(value);
}
private Object popOperand() throws CommandSyntaxException {
if (operandStack.isEmpty()) {
throw new CommandSyntaxException("missing LHS for operator");
}
return operandStack.pop();
}
private long toNumber(Object obj) throws CommandSyntaxException {
if (obj instanceof Long) {
return (Long) obj;
} else if (obj instanceof String) {
try {
return Long.parseLong((String) obj);
} catch (NumberFormatException ex) {
throw new CommandSyntaxException(
"operand is not a number: '" + obj.toString() + '\'');
}
} else {
throw new CommandSyntaxException("subexpression is not an INTEGER");
}
}
private boolean toBoolean(Object obj) throws CommandSyntaxException {
if (obj instanceof Boolean) {
return obj == Boolean.TRUE;
} else if (obj instanceof String) {
return ((String) obj).length() > 0;
} else {
throw new CommandSyntaxException("operand is an INTEGER");
}
}
private String toString(Object obj) throws CommandSyntaxException {
if (obj instanceof String) {
return (String) obj;
} else {
throw new CommandSyntaxException("operand is not a STRING");
}
}
private File toFile(Object obj) throws CommandSyntaxException {
if (obj instanceof String) {
return new File((String) obj);
} else {
throw new CommandSyntaxException("operand is not a FILENAME");
}
}
private String next() {
if (pos > lastArg) {
return null;
}
return args[pos++];
}
private void back() throws CommandSyntaxException {
pos--;
}
public static void main(String[] args) throws Exception {
// If you invoke it this way you cannot distinguish 'test' from '['
new UnixTestCommand().execute(args);
}
}