de.unkrig.zz.find.Parser Maven / Gradle / Ivy
/*
* de.unkrig.find - An advanced version of the UNIX FIND utility
*
* Copyright (c) 2011, Arno Unkrig
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package de.unkrig.zz.find;
import java.io.File;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import de.unkrig.commons.lang.ExceptionUtil;
import de.unkrig.commons.lang.protocol.Predicate;
import de.unkrig.commons.lang.protocol.ProducerWhichThrows;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.parser.AbstractParser;
import de.unkrig.commons.text.parser.ParseException;
import de.unkrig.commons.text.scanner.AbstractScanner;
import de.unkrig.commons.text.scanner.AbstractScanner.Token;
import de.unkrig.commons.text.scanner.ScanException;
import de.unkrig.zz.find.Find.AndTest;
import de.unkrig.zz.find.Find.CatAction;
import de.unkrig.zz.find.Find.ChecksumAction;
import de.unkrig.zz.find.Find.ChecksumAction.ChecksumType;
import de.unkrig.zz.find.Find.CommaTest;
import de.unkrig.zz.find.Find.CopyAction;
import de.unkrig.zz.find.Find.DigestAction;
import de.unkrig.zz.find.Find.DisassembleAction;
import de.unkrig.zz.find.Find.EchoAction;
import de.unkrig.zz.find.Find.ExecAction;
import de.unkrig.zz.find.Find.ExecutabilityTest;
import de.unkrig.zz.find.Find.Expression;
import de.unkrig.zz.find.Find.LsAction;
import de.unkrig.zz.find.Find.ModificationTimeTest;
import de.unkrig.zz.find.Find.NameTest;
import de.unkrig.zz.find.Find.NotExpression;
import de.unkrig.zz.find.Find.OrTest;
import de.unkrig.zz.find.Find.PathTest;
import de.unkrig.zz.find.Find.PipeAction;
import de.unkrig.zz.find.Find.PrintAction;
import de.unkrig.zz.find.Find.ReadabilityTest;
import de.unkrig.zz.find.Find.SizeTest;
import de.unkrig.zz.find.Find.Test;
import de.unkrig.zz.find.Find.TypeTest;
import de.unkrig.zz.find.Find.WritabilityTest;
/**
* Parses the predicate syntax of the UNIX FIND command line utility.
*/
public
class Parser {
/**
* The token types known to this scanner.
*
* @see AbstractScanner#AbstractScanner()
*/
enum TokenType { LITERAL }
private final AbstractParser parser;
private final OutputStream outOS;
private boolean hadAction;
/**
* @param producer The source of tokens to parse
* @param out Where the {@link CatAction} writes its output
*/
public
Parser(final ProducerWhichThrows producer, OutputStream out) {
this.parser = new AbstractParser(new ProducerWhichThrows, ScanException>() {
@Override @Nullable public Token
produce() throws ScanException {
String text;
try {
text = producer.produce();
} catch (Exception e) {
throw ExceptionUtil.wrap(null, e, ScanException.class);
}
if (text == null) return null;
return new Token(TokenType.LITERAL, text);
}
});
this.outOS = out;
}
/**
*
* expression := [ or-expression ]
*
* "No token" means "-print".
*
* An expression without any actions is silently converted to "expr && -print".
*
* Example:
*
* -name "*.java" -o ( -name "*foo*" ! -name "*foobar" )
*
*/
public Expression
parse() throws ParseException {
if (this.parser.peek() == null) return new PrintAction();
final Expression result = this.parseComma();
this.parser.eoi();
return this.hadAction ? result : new AndTest(result, new PrintAction());
}
/**
*
* comma-expression := or-expression [ ',' comma-expression ]
*
*/
private Expression
parseComma() throws ParseException {
Expression lhs = this.parseOr();
return this.parser.peekRead(",") ? new CommaTest(lhs, this.parseComma()) : lhs;
}
/**
*
* or-expression := and-expression [ ( '-o' | '-or' ) or-expression ]
*
*/
private Expression
parseOr() throws ParseException {
Expression lhs = this.parseAnd();
if (this.parser.peekRead("-o", "-or") == -1) return lhs;
Expression rhs = this.parseOr();
return new OrTest(lhs, rhs);
}
/**
*
* and-expression := primary-expression [ [ '-a' | '-and' ] and-expression ]
*
*/
private Expression
parseAnd() throws ParseException {
Expression lhs = this.parsePrimary();
return (
this.parser.peekRead("-a", "-and") != -1
|| this.parser.peek("-o", "-or", ")", ",", null) == -1
) ? new AndTest(lhs, this.parseAnd()) : lhs;
}
/**
*
* primary-expression :=
* '(' or-expression ')'
* | ( '!' | '-not' ) primary-expression
* | '-name' literal
* | '-path' literal
* | '-type' glob
* | '-readable'
* | '-writable'
* | '-executable'
* | '-size' numeric
* | '-mtime' numeric
* | '-mmin' numeric
* | '-print'
* | '-echo'
* | '-ls'
* | '-exec' { literal } ';'
* | '-pipe' { literal } ';'
* | '-cat'
* | '-copy' string
* | '-disassemble' boolean boolean file
* | '-digest' string
* | '-checksum' string
* | '-true'
* | '-false'
*
*/
private Expression
parsePrimary() throws ParseException {
switch (this.parser.read(
"(", "!", "-not", "-name", "-path",
"-type", "-readable", "-writable", "-executable", "-size",
"-mtime", "-mmin", "-print", "-echo", "-ls",
"-exec", "-pipe", "-cat", "-copy", "-disassemble",
"-digest", "-checksum", "-true", "-false"
)) {
case 0: // '('
final Expression result = this.parseComma();
this.parser.read(")");
return result;
case 1: // '!'
case 2: // '-not'
return new NotExpression(this.parsePrimary());
case 3: // '-name'
return new NameTest(this.parser.read().text);
case 4: // '-path'
return new PathTest(this.parser.read().text);
case 5: // '-type' glob
return new TypeTest(this.parser.read().text);
case 6: // '-readable'
return new ReadabilityTest();
case 7: // '-writable'
return new WritabilityTest();
case 8: // '-executable'
return new ExecutabilityTest();
case 9: // '-size'
return new SizeTest(Parser.parseNumericArgument(this.parser.read().text));
case 10: // '-mtime'
return new ModificationTimeTest(
Parser.parseNumericArgument(this.parser.read().text),
ModificationTimeTest.DAYS
);
case 11: // '-mmin'
return new ModificationTimeTest(
Parser.parseNumericArgument(this.parser.read().text),
ModificationTimeTest.MINUTES
);
case 12: // '-print'
this.hadAction = true;
return new PrintAction();
case 13: // '-echo'
this.hadAction = true;
return new EchoAction(this.parser.read().text);
case 14: // '-ls'
this.hadAction = true;
return new LsAction();
case 15: // '-exec' word ... ';'
{
List command = new ArrayList();
while (!this.parser.peekRead(";")) command.add(this.parser.read().text);
this.hadAction = true;
return new ExecAction(command);
}
case 16: // '-pipe' word ... ';'
{
List command = new ArrayList();
while (!this.parser.peekRead(";")) command.add(this.parser.read().text);
this.hadAction = true;
return new PipeAction(command, null);
}
case 17: // '-cat'
this.hadAction = true;
return new CatAction(this.outOS);
case 18: // '-copy'
this.hadAction = true;
return new CopyAction(new File(this.parser.read().text), false);
case 19: // "-disassemble"
this.hadAction = true;
return new DisassembleAction(
this.parser.peekRead("-hideLines"), // hideLines
this.parser.peekRead("-hideVars"), // hideVars
null // file
);
case 20: // "-digest"
this.hadAction = true;
return new DigestAction(this.parser.read().text);
case 21: // "-checksum"
this.hadAction = true;
ChecksumType cst;
try {
cst = ChecksumAction.ChecksumType.valueOf(this.parser.read().text);
} catch (IllegalArgumentException iae) {
throw new IllegalArgumentException(
"Invalid checksum type \""
+ this.parser.read().text
+ "\"; allowed values are "
+ Arrays.toString(ChecksumAction.ChecksumType.values())
);
}
return new ChecksumAction(cst);
case 22: // "-true"
return Test.TRUE;
case 23: // "-false"
return Test.FALSE;
default:
throw new IllegalStateException();
}
}
/**
*
* numeric := [ '+' | '-' ] digit { digit } [ 'k' | 'M' | 'G' ]
*
*/
public static Predicate
parseNumericArgument(String text) {
final Mode mode;
final long n;
if (text.startsWith("+")) {
mode = Mode.MORE_THAN;
text = text.substring(1);
} else
if (text.startsWith("-")) {
mode = Mode.LESS_THAN;
text = text.substring(1);
} else
{
mode = Mode.EXACTLY;
}
long multiplier;
if (text.endsWith("k")) {
multiplier = 1024L;
text = text.substring(0, text.length() - 1);
} else
if (text.endsWith("M")) {
multiplier = 1024L * 1024L;
text = text.substring(0, text.length() - 1);
} else
if (text.endsWith("G")) {
multiplier = 1024L * 1024L * 1024L;
text = text.substring(0, text.length() - 1);
} else
{
multiplier = 1L;
}
n = multiplier * Long.parseLong(text);
return new Predicate() {
@Override public boolean
evaluate(Long subject) {
switch (mode) {
case EXACTLY:
return subject == n;
case LESS_THAN:
return subject < n;
case MORE_THAN:
return subject > n;
default:
throw new IllegalStateException();
}
}
@Override public String
toString() {
return (
mode == Mode.MORE_THAN ? "> " :
mode == Mode.LESS_THAN ? "< " :
mode == Mode.EXACTLY ? "== " :
"??? "
) + n;
}
};
}
private enum Mode { MORE_THAN, LESS_THAN, EXACTLY }
}