blob: 07baea89dd604d20472cf0768c05213ad278abfb [file] [log] [blame]
/**
* 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.hadoop.fs.shell.find;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.shell.CommandFactory;
import org.apache.hadoop.fs.shell.CommandFormat;
import org.apache.hadoop.fs.shell.FsCommand;
import org.apache.hadoop.fs.shell.PathData;
@InterfaceAudience.Private
@InterfaceStability.Unstable
/**
* Implements a Hadoop find command.
*/
public class Find extends FsCommand {
/**
* Register the names for the count command
*
* @param factory the command factory that will instantiate this class
*/
public static void registerCommands(CommandFactory factory) {
factory.addClass(Find.class, "-find");
}
public static final String NAME = "find";
public static final String USAGE = "<path> ... <expression> ...";
public static final String DESCRIPTION;
private static String[] HELP =
{ "Finds all files that match the specified expression and",
"applies selected actions to them. If no <path> is specified",
"then defaults to the current working directory. If no",
"expression is specified then defaults to -print."
};
private static final String OPTION_FOLLOW_LINK = "L";
private static final String OPTION_FOLLOW_ARG_LINK = "H";
/** List of expressions recognized by this command. */
private static final Set<Class<? extends Expression>> EXPRESSIONS =
new HashSet<>();
private static void addExpression(Class<?> clazz) {
EXPRESSIONS.add(clazz.asSubclass(Expression.class));
}
static {
// Initialize the static variables.
// Operator Expressions
addExpression(And.class);
// Action Expressions
addExpression(Print.class);
// Navigation Expressions
// Matcher Expressions
addExpression(Name.class);
DESCRIPTION = buildDescription(ExpressionFactory.getExpressionFactory());
// Register the expressions with the expression factory.
registerExpressions(ExpressionFactory.getExpressionFactory());
}
/** Options for use in this command */
private FindOptions options;
/** Root expression for this instance of the command. */
private Expression rootExpression;
/** Set of path items returning a {@link Result#STOP} result. */
private HashSet<Path> stopPaths = new HashSet<>();
/** Register the expressions with the expression factory. */
private static void registerExpressions(ExpressionFactory factory) {
for (Class<? extends Expression> exprClass : EXPRESSIONS) {
factory.registerExpression(exprClass);
}
}
/** Build the description used by the help command. */
private static String buildDescription(ExpressionFactory factory) {
ArrayList<Expression> operators = new ArrayList<Expression>();
ArrayList<Expression> primaries = new ArrayList<Expression>();
for (Class<? extends Expression> exprClass : EXPRESSIONS) {
Expression expr = factory.createExpression(exprClass, null);
if (expr.isOperator()) {
operators.add(expr);
} else {
primaries.add(expr);
}
}
Collections.sort(operators, new Comparator<Expression>() {
@Override
public int compare(Expression arg0, Expression arg1) {
return arg0.getClass().getName().compareTo(arg1.getClass().getName());
}
});
Collections.sort(primaries, new Comparator<Expression>() {
@Override
public int compare(Expression arg0, Expression arg1) {
return arg0.getClass().getName().compareTo(arg1.getClass().getName());
}
});
StringBuilder sb = new StringBuilder();
for (String line : HELP) {
sb.append(line).append("\n");
}
sb.append("\n")
.append("The following primary expressions are recognised:\n");
for (Expression expr : primaries) {
for (String line : expr.getUsage()) {
sb.append(" ").append(line).append("\n");
}
for (String line : expr.getHelp()) {
sb.append(" ").append(line).append("\n");
}
sb.append("\n");
}
sb.append("The following operators are recognised:\n");
for (Expression expr : operators) {
for (String line : expr.getUsage()) {
sb.append(" ").append(line).append("\n");
}
for (String line : expr.getHelp()) {
sb.append(" ").append(line).append("\n");
}
sb.append("\n");
}
return sb.toString();
}
/** Default constructor for the Find command. */
public Find() {
setRecursive(true);
}
@Override
protected void processOptions(LinkedList<String> args) throws IOException {
CommandFormat cf =
new CommandFormat(1, Integer.MAX_VALUE, OPTION_FOLLOW_LINK,
OPTION_FOLLOW_ARG_LINK, null);
cf.parse(args);
if (cf.getOpt(OPTION_FOLLOW_LINK)) {
getOptions().setFollowLink(true);
} else if (cf.getOpt(OPTION_FOLLOW_ARG_LINK)) {
getOptions().setFollowArgLink(true);
}
// search for first non-path argument (ie starts with a "-") and capture and
// remove the remaining arguments as expressions
LinkedList<String> expressionArgs = new LinkedList<String>();
Iterator<String> it = args.iterator();
boolean isPath = true;
while (it.hasNext()) {
String arg = it.next();
if (isPath) {
if (arg.startsWith("-")) {
isPath = false;
}
}
if (!isPath) {
expressionArgs.add(arg);
it.remove();
}
}
if (args.isEmpty()) {
args.add(Path.CUR_DIR);
}
Expression expression = parseExpression(expressionArgs);
if (!expression.isAction()) {
Expression and = getExpression(And.class);
Deque<Expression> children = new LinkedList<Expression>();
children.add(getExpression(Print.class));
children.add(expression);
and.addChildren(children);
expression = and;
}
setRootExpression(expression);
}
/**
* Set the root expression for this find.
*
* @param expression
*/
@InterfaceAudience.Private
void setRootExpression(Expression expression) {
this.rootExpression = expression;
}
/**
* Return the root expression for this find.
*
* @return the root expression
*/
@InterfaceAudience.Private
Expression getRootExpression() {
return this.rootExpression;
}
/** Returns the current find options, creating them if necessary. */
@InterfaceAudience.Private
FindOptions getOptions() {
if (options == null) {
options = createOptions();
}
return options;
}
/** Create a new set of find options. */
private FindOptions createOptions() {
FindOptions options = new FindOptions();
options.setOut(out);
options.setErr(err);
options.setIn(System.in);
options.setCommandFactory(getCommandFactory());
options.setConfiguration(getConf());
return options;
}
/** Add the {@link PathData} item to the stop set. */
private void addStop(PathData item) {
stopPaths.add(item.path);
}
/** Returns true if the {@link PathData} item is in the stop set. */
private boolean isStop(PathData item) {
return stopPaths.contains(item.path);
}
/**
* Parse a list of arguments to to extract the {@link Expression} elements.
* The input Deque will be modified to remove the used elements.
*
* @param args arguments to be parsed
* @return list of {@link Expression} elements applicable to this command
* @throws IOException if list can not be parsed
*/
private Expression parseExpression(Deque<String> args) throws IOException {
Deque<Expression> primaries = new LinkedList<Expression>();
Deque<Expression> operators = new LinkedList<Expression>();
Expression prevExpr = getExpression(And.class);
while (!args.isEmpty()) {
String arg = args.pop();
if ("(".equals(arg)) {
Expression expr = parseExpression(args);
primaries.add(expr);
prevExpr = new BaseExpression() {
@Override
public Result apply(PathData item, int depth) throws IOException {
return Result.PASS;
}
}; // stub the previous expression to be a non-op
} else if (")".equals(arg)) {
break;
} else if (isExpression(arg)) {
Expression expr = getExpression(arg);
expr.addArguments(args);
if (expr.isOperator()) {
while (!operators.isEmpty()) {
if (operators.peek().getPrecedence() >= expr.getPrecedence()) {
Expression op = operators.pop();
op.addChildren(primaries);
primaries.push(op);
} else {
break;
}
}
operators.push(expr);
} else {
if (!prevExpr.isOperator()) {
Expression and = getExpression(And.class);
while (!operators.isEmpty()) {
if (operators.peek().getPrecedence() >= and.getPrecedence()) {
Expression op = operators.pop();
op.addChildren(primaries);
primaries.push(op);
} else {
break;
}
}
operators.push(and);
}
primaries.push(expr);
}
prevExpr = expr;
} else {
throw new IOException("Unexpected argument: " + arg);
}
}
while (!operators.isEmpty()) {
Expression operator = operators.pop();
operator.addChildren(primaries);
primaries.push(operator);
}
return primaries.isEmpty() ? getExpression(Print.class) : primaries.pop();
}
/** Returns true if the target is an ancestor of the source. */
private boolean isAncestor(PathData source, PathData target) {
for (Path parent = source.path; (parent != null) && !parent.isRoot();
parent = parent.getParent()) {
if (parent.equals(target.path)) {
return true;
}
}
return false;
}
@Override
protected void recursePath(PathData item) throws IOException {
if (isStop(item)) {
// this item returned a stop result so don't recurse any further
return;
}
if (getDepth() >= getOptions().getMaxDepth()) {
// reached the maximum depth so don't got any further.
return;
}
if (item.stat.isSymlink() && getOptions().isFollowLink()) {
PathData linkedItem =
new PathData(item.stat.getSymlink().toString(), getConf());
if (isAncestor(item, linkedItem)) {
getOptions().getErr().println(
"Infinite loop ignored: " + item.toString() + " -> "
+ linkedItem.toString());
return;
}
if (linkedItem.exists) {
item = linkedItem;
}
}
if (item.stat.isDirectory()) {
super.recursePath(item);
}
}
@Override
protected boolean isPathRecursable(PathData item) throws IOException {
if (item.stat.isDirectory()) {
return true;
}
if (item.stat.isSymlink()) {
PathData linkedItem =
new PathData(item.fs.resolvePath(item.stat.getSymlink()).toString(),
getConf());
if (linkedItem.stat.isDirectory()) {
if (getOptions().isFollowLink()) {
return true;
}
if (getOptions().isFollowArgLink() && (getDepth() == 0)) {
return true;
}
}
}
return false;
}
@Override
protected void processPath(PathData item) throws IOException {
if (getOptions().isDepthFirst()) {
// depth first so leave until post processing
return;
}
applyItem(item);
}
@Override
protected void postProcessPath(PathData item) throws IOException {
if (!getOptions().isDepthFirst()) {
// not depth first so already processed
return;
}
applyItem(item);
}
private void applyItem(PathData item) throws IOException {
if (getDepth() >= getOptions().getMinDepth()) {
Result result = getRootExpression().apply(item, getDepth());
if (Result.STOP.equals(result)) {
addStop(item);
}
}
}
@Override
protected void processArguments(LinkedList<PathData> args)
throws IOException {
Expression expr = getRootExpression();
expr.setOptions(getOptions());
expr.prepare();
super.processArguments(args);
expr.finish();
}
/** Gets a named expression from the factory. */
private Expression getExpression(String expressionName) {
return ExpressionFactory.getExpressionFactory().getExpression(
expressionName, getConf());
}
/** Gets an instance of an expression from the factory. */
private Expression getExpression(
Class<? extends Expression> expressionClass) {
return ExpressionFactory.getExpressionFactory().createExpression(
expressionClass, getConf());
}
/** Asks the factory whether an expression is recognized. */
private boolean isExpression(String expressionName) {
return ExpressionFactory.getExpressionFactory()
.isExpression(expressionName);
}
}