de.unkrig.zz.find.Main 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.IOException;
import java.nio.charset.Charset;
import java.security.Provider;
import java.security.Provider.Service;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import de.unkrig.commons.file.org.apache.commons.compress.archivers.ArchiveFormatFactory;
import de.unkrig.commons.file.org.apache.commons.compress.compressors.CompressionFormatFactory;
import de.unkrig.commons.lang.AssertionUtil;
import de.unkrig.commons.lang.protocol.ConsumerWhichThrows;
import de.unkrig.commons.lang.protocol.ProducerUtil;
import de.unkrig.commons.text.LevelFilteredPrinter;
import de.unkrig.commons.text.Printers;
import de.unkrig.commons.text.pattern.Glob;
import de.unkrig.commons.text.pattern.Pattern2;
import de.unkrig.commons.util.CommandLineOptionException;
import de.unkrig.commons.util.CommandLineOptions;
import de.unkrig.commons.util.annotation.CommandLineOption;
import de.unkrig.commons.util.annotation.RegexFlags;
import de.unkrig.commons.util.logging.SimpleLogging;
/**
* A FIND utility that can recurse into directories, archive files and compressed files.
*/
public final
class Main {
static { AssertionUtil.enableAssertionsForThisClass(); }
private Main() {}
private final Find find = new Find();
private final LevelFilteredPrinter levelFilteredPrinter = new LevelFilteredPrinter(Printers.get());
/**
* A command line utility to find files, directories and archive entries by various criteria, and optionally
* execute some actions.
* Usage
*
* - {@code zzfind} [ option ] ... file-or-dir ... [ expression ]
* -
* Apply "expression" to "file-or-dir ..." and all nested directory members,
* archive entries and compressed files.
*
*
*
* File name "-" stands for STDIN.
*
*
* Options
*
*
* {@main.commandLineOptions}
*
*
* Expressions
*
* Expressions are either tests or actions. Both evaluate to a boolean value.
*
* Tests
*
* - {@code -name} glob
* -
* Name matches "glob" (see below).
*
* - {@code -path} glob
* -
* The full path (e.g. "{@code dir/file.zip!dir/file.zip!dir/file}" or "{@code dir/file}") matches
* "glob" (see below).
*
* - {@code -type} glob
* -
* Whether the type matches the glob. See the "type" property, described below.
* If you are unsure about types, run {@code zzfind} with
-echo "@{type} @{path}"
first.
*
* - {@code -readable}
* -
* Whether this file is readable.
*
* - {@code -writable}
* -
* Whether this file is writable.
*
* - {@code -executable}
* -
* Whether this file is executable.
*
* - {@code -size} N
* - {@code -size -}N
* - {@code -size +}N
* -
* Whether the size is exactly/less than/more than N (e.g. "{@code 100}", "{@code -1K}",
* "{@code +10M}").
*
* - {@code -mtime} N
* - {@code -mtime +}N
* - {@code -mtime -}N
* -
* Whether this file/archive entry was last modified (exactly/more than/less than) N days ago
* (N=0: 0...24h, N=1: 24...48h, ...).
*
* - {@code -mmin} N
* - {@code -mmin +}N
* - {@code -mmin -}N
* -
* Whether this file/archive entry was last modified (exactly/more than/less than) N minutes ago
* (N=0: 0...59sec, N=1: 60...119sec, ...).
*
* - exp1 {@code -a} exp2
* - exp1 exp2
* -
* Whether both exp1 and exp2 are true. exp2 is not evaluated if
* exp1 is false.
*
* - exp1 {@code -o} exp2
* -
* Whether exp1 or exp2 is true. exp2 is not evaluated if exp1
* is true.
*
* - exp1 {@code ,} exp2
* -
* Evaluates both expressions and returns the result of exp2.
*
* - {@code -not} exp
* - {@code !} exp
* -
* Whether exp is false.
*
* - {@code (} exp {@code )}
* -
* Whether exp is true.
*
*
*
* Actions
*
* - {@code -print}
* -
* Print file path and return true.
*
* - {@code -echo} message
* -
* Print the message and return true.
* All occurrences of "
@{property-name}
" in the message are replaced with
* the value of the property. For the list of supported properties, see section "Properties of files and
* archive entries", below.
*
* - {@code -ls}
* -
* Print file type, readability, writability, executability, size, modification time and path, and return true.
*
* - {@code -exec} word{@code ... ;}
* -
* Execute "word{@code ...}" as an external command; "{@code {}}" is replaced with the current
* file's path (which may contain "{@code !}" and would then NOT denote a physical file in the file system).
*
* - {@code -pipe} word{@code ... ;}
* -
* Copy the file contents to the standard input of an external command "word{@code ...}";
* "{@code {}}" is replaced with the current file's path (which may contain "{@code !}" and would then
* NOT denote a physical file in the file system).
*
* - {@code -cat}
* -
* Print file contents and return true.
*
* - {@code -copy} tofile
* -
* Copy file contents to the named file.
* All occurrences of "
@{property-name}
" in the tofile are replaced with
* the value of the property. For the list of supported properties, see section "Properties of files and
* archive entries", below.
*
* - {@code -disassemble} [ {@code -hideLines} ] [ {@code -hideVars} ]
* -
* Disassembles a Java class file.
*
* - {@code -digest} algorithm
* -
* Calculate a "message digest" of the contents, print it and return true.
*
* Algorithms available in this environment are: ${digest.providers}
*
* - {@code -checksum} CRC32|ADLER32
* -
* Calculate a checksum of the contents, print it and return true.
*
*
* - {@code -true}
* -
* Return {@code true}.
*
* - {@code -false}
* -
* Return {@code false}.
*
*
*
* If no action is given, then "{@code -print}" is implicitly added.
*
*
* Properties of files and archive entries
*
* Various tests and actions (e.g. "{@code -echo}") have access to a set of "properties" of the current file
* or archive entry.
*
*
* - {@code "name"}:
* -
* The last component of the path.
*
* - {@code "path"}:
* -
* The path to the resources as it was found. "!" indicates an archive, "%" a compressed file.
*
* - {@code "depth"}:
* -
* Zero for the files/directories specified on the command line, +1 for files/directories in a subdirectory,
* +1 for compressed content, and +1 for the entries of archive files.
*
* - {@code "type"}:
* -
* The "type" of the current subject; the actual types are:
*
* - {@code directory}
* - A directory
* - {@code file}
* - A (non-archive, not-compressed) file
* - {@code archive-file}
* - An archive file
* - {@code compressed-file}
* - A compressed file
* - {@code archive}
* - A nested archive
* - {@code normal-contents}
* - Normal (non-archive, not-compressed) content
* - {@code directory-entry}
* - A "directory entry" in an archive.
*
*
* - {@code "absolutePath"}:
* -
* The absolute path of the file or directory, typically by resolving relative paths against the current user
* directory. For all other types: That of the enclosing file.
*
* - {@code "canonicalPath"}:
* -
* The absolute path of the file or directory, typically by resolving relative paths against the current user
* directory and resolving all "." and ".." infixes. For all other types: That of the enclosing file.
*
* - {@code "lastModifiedDate"}:
* -
* The date and time of the last modification of the file, directory or archive entry, in the format "{@code
* dow mon dd hh:mm:ss zzz yyyy}".
*
* - {@code "size"}:
* -
* The size, in bytes, for types "archive", "archive-file", "compressed-contents", "compressed-file", "file" or
* "normal-contents", -1 for type "directory-entry", and 0 for type "directory".
*
* - {@code "isDirectory"}:
* -
* Whether the type is "directory" or "directory-entry".
*
* - {@code "isFile"}:
* -
* Whether the type is "archive", "archive-file", "compressed-contents", "compressed-file", "directory-entry",
* "file" or "normal-contents".
*
* - {@code "isHidden"}:
* -
* Whether the file or directory is "hidden" in the file system; {@code false} for all other types.
*
* - {@code "isReadable"}:
* -
* Whether the file or directory is "readable" in the file system; {@code false} for all other types.
*
* - {@code "isWritable"}:
* -
* Whether the file or directory is "writable" in the file system; {@code false} for all other types.
*
* - {@code "isExecutable"}:
* -
* Whether the file or directory is "executable" in the file system; {@code false} for all other types.
*
* - {@code "archiveFormat"}:
* -
* For types "archive" and "archive-file": The format.
*
* For types "normal-contents", "compressed-contents" and "directory-entry": The format of the immediately
* enclosing archive.
*
* For all other types: Empty.
*
* - {@code "compressionFormat"}:
* -
* For types "compressed-contents" and "compressed-file": The format.
*
* For type "normal-contents": The format of the immediately enclosing compressed contents.
*
* For all other types: Empty.
*
*
*
* Some archive entries have additional properties:
*
*
* - Archive format "ar":
* -
* "groupId", "mode", "userId"
*
* - Archive format "arj":
* -
* "hostOs", "method", "mode", "unixMode", "isHostOsUnix"
*
* - Archive format "cpio":
* -
* "alignmentBoundary", "chksum", "dataPadCount", "device", "format", "gID", "headerPadCount", "headerSize",
* "inode", "mode", "numberOfLinks", "remoteDevice", "uID", "isBlockDevice", "isNetwork", "isPipe",
* "isRegularFile", "isSocket", "isSymbolicLink"
*
* - Archive format "dump":
* -
* "accessTime", "creationTime", "entrySize", "generation", "groupId", "headerCount", "headerHoles",
* "headerType", "ino", "mode", "nlink", "offset", "originalName", "permissions", "simpleName", userId",
* "volume", "isBlkDev", "isChrDev", isDeleted", "isFifo", isSocket"
*
* - Archive format "7z":
* -
* "accessDate", "crcValue", "creationDate", hasAccessDate", "hasCrc", hasCreationDate", "hasLastModifiedDate",
* "hasWindowsAttributes", "windowsAttributes", "isAntiItem"
*
* - Archive format "tar":
* -
* "devMajor", "devMinor", "groupId", "groupName", "linkName", "mode", "modTime", "realSize", "userId",
* "userName", "isBlockDevice", "isCharacterDevice", "isExtended", "isFIFO", isGlobalPaxHeader",
* "isGNULongLinkEntry", "isGNULongNameEntry", "isGNUSparse", "isLink", "isPaxHeader", "isSymbolicLink"
*
* - Archive format "zip":
* -
* "externalAttributes", "internalAttributes", "method", "platform", "unixMode", "isUnixSymlink"
*
*
*
* Example globs
*
*
* - {@code dir/file}
* -
* File "{@code file}" in directory "{@code dir}"
*
* - {@code file.gz%}
* -
* Compressed file "{@code file.gz}"
*
* - {@code file.zip!dir/file}
* -
* Entry "{@code dir/file}" in archive file "{@code dir/file.zip}"
*
* - {@code file.tar.gz%!dir/file}
* -
* Entry "{@code dir/file}" in the compressed archive file "{@code file.tar.gz}"
*
* */x
* -
* File "{@code x}" in an immediate subdirectory
*
* **/x
* -
* File "{@code x}" in any subdirectory
*
* ***/x
* -
* File "{@code x}" in any subdirectory, or any entry "
**/x
" in any archive file in any
* subdirectory
*
* - {@code a,dir/file.7z!dir/b}
* -
* File "{@code a}" and entry "{@code dir/b}" in archive file "{@code dir/file.7z}"
*
* - {@code ~*.c}
* -
* Files that don't end with "{@code .c}"
*
* - {@code ~*.c~*.h}
* -
* Files that don't end with "{@code .c}" or "{@code .h}"
*
* - {@code ~*.c~*.h,foo.c}
* -
* "{@code foo.c}" plus all files that don't end with "{@code .c}" or "{@code .h}"
*
*
*/
public static void
main(final String[] args) {
final Main main = new Main();
Printers.withPrinter(main.levelFilteredPrinter, new Runnable() {
@Override public void run() { main.main2(args); }
});
}
private void
main2(String[] args) {
try {
args = CommandLineOptions.parse(args, this);
} catch (CommandLineOptionException cloe) {
Printers.error(cloe.getMessage() + ", try \"--help\".");
System.exit(1);
}
// Parse the file names on the command line.
int i = 0;
List files = new ArrayList();
for (; i < args.length; i++) {
String arg = args[i];
if ((arg.startsWith("-") && !"-".equals(arg)) || "(".equals(arg) || "!".equals(arg)) break;
files.add(arg);
}
// Parse the FIND expression.
// Notice: Even ZERO tokens are a valid FIND expression.
try {
Parser parser = new Parser(ProducerUtil.fromArray(args, i, args.length), System.out);
this.find.setExpression(parser.parse());
} catch (Exception e) {
Printers.error("Parsing predicates", e);
System.exit(1);
}
final boolean[] hadExceptions = new boolean[1];
ConsumerWhichThrows
exceptionHandler = new ConsumerWhichThrows() {
@Override public void
consume(IOException ioe) {
Printers.error(ioe.toString());
hadExceptions[0] = true;
}
};
this.find.setExceptionHandler(exceptionHandler);
// Execute the search.
try {
for (String file : files) {
try {
if ("-".equals(file)) {
this.find.findInStream(System.in);
} else {
this.find.findInFile(new File(file));
}
} catch (IOException ioe) {
exceptionHandler.consume(ioe);
}
}
if (hadExceptions[0]) System.exit(2);
} catch (Exception e) {
Printers.error(null, e);
System.exit(2);
}
}
/**
* Print this text and terminate.
*/
@CommandLineOption public static void
help() throws IOException {
List algorithms = new ArrayList();
for (Provider p : Security.getProviders()) {
for (Service s : p.getServices()) {
if ("MessageDigest".equals(s.getType())) {
algorithms.add(s.getAlgorithm());
}
}
}
System.setProperty("archive.formats", ArchiveFormatFactory.allFormats().toString());
System.setProperty("compression.formats", CompressionFormatFactory.allFormats().toString());
System.setProperty("digest.providers", algorithms.toString());
CommandLineOptions.printResource(Main.class, "main(String[]).txt", Charset.forName("UTF-8"), System.out);
System.exit(0);
}
/**
* Look into compressed and archive contents if the format and the path match the globs.
* The default is to look into any recognised archive or compressed contents.
*
* Supported archive formats in this runtime configuration are:
*
* {@code ${archive.formats}}
*
* Supported compression formats in this runtime configuration are:
*
* {@code ${compression.formats}}
*
* @param fomatAndPath format-glob{@code :}path-glob
*/
@CommandLineOption public void
setLookInto(@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES) Glob fomatAndPath) {
this.find.setLookIntoFormat(fomatAndPath);
}
/**
* Process each directory's contents before the directory itself, and each archive's entries before the archive
* itself, and each compressed contents before the enclosing file or archive entry.
*/
@CommandLineOption public void
setDepth() { this.find.setDepth(true); }
/**
* Do not apply any tests or actions at levels less than levels. E.g. "1" means "process all files
* except the top level files and directories".
*/
@CommandLineOption public void
setMinDepth(int levels) { this.find.setMinDepth(levels); }
/**
* Descend at most levels of directories below the top level files and directories. "0" means "only
* apply the tests and actions to the top level files and directories".
*
* If in doubt, try:
*
* zzfind ... -print -echo @{depth}
*/
@CommandLineOption public void
setMaxDepth(int levels) { this.find.setMaxDepth(levels); }
/**
* Suppress all messages except errors.
*/
@CommandLineOption public void
setNowarn() { this.levelFilteredPrinter.setNoWarn(); }
/**
* Suppress normal output.
*/
@CommandLineOption public void
setQuiet() { this.levelFilteredPrinter.setQuiet(); }
/**
* Print verbose messages.
*/
@CommandLineOption public void
setVerbose() { this.levelFilteredPrinter.setVerbose(); }
/**
* Print verbose and debug messages.
*/
@CommandLineOption public void
setDebug() { this.levelFilteredPrinter.setDebug(); }
/**
* Add logging at level {@code FINE} on logger "{@code de.unkrig}" to STDERR using the "{@code FormatFormatter}"
* and "{@code SIMPLE}" format, or the given arguments, which are all optional.
*
* @param spec level{@code :}logger{@code :}handler{@code
* :}formatter{@code :}format
*/
@CommandLineOption(cardinality = CommandLineOption.Cardinality.ANY) public static void
addLog(String spec) { SimpleLogging.configureLoggers(spec); }
/**
* (These are no longer supported; use "--look-into
" instead.)
*/
@CommandLineOption(name = { "-zip", "-zz", "-nested-zip", "-z" }) public static void
noLongerSupported() {
System.err.println("Command line option is no longer supported - try \"--help\".");
System.exit(1);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy