de.unkrig.zz.diff.Main Maven / Gradle / Ivy
The newest version!
/*
* de.unkrig.diff - An advanced version of the UNIX DIFF 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. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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.diff;
import java.io.File;
import java.io.IOException;
import java.net.Authenticator;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.regex.Pattern;
import org.apache.commons.compress.utils.Charsets;
import de.unkrig.commons.file.ExceptionHandler;
import de.unkrig.commons.file.org.apache.commons.compress.archivers.ArchiveFormatFactory;
import de.unkrig.commons.file.org.apache.commons.compress.archivers.sevenz.SevenZArchiveFormat;
import de.unkrig.commons.file.org.apache.commons.compress.compressors.CompressionFormatFactory;
import de.unkrig.commons.file.resourceprocessing.ResourceProcessings;
import de.unkrig.commons.lang.AssertionUtil;
import de.unkrig.commons.lang.protocol.RunnableWhichThrows;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.AbstractPrinter.Level;
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.CommandLineOptions;
import de.unkrig.commons.util.annotation.CommandLineOption;
import de.unkrig.commons.util.annotation.CommandLineOptionGroup;
import de.unkrig.commons.util.annotation.RegexFlags;
import de.unkrig.commons.util.logging.SimpleLogging;
import de.unkrig.zip4jadapter.archivers.zip.ZipArchiveFormat;
import de.unkrig.zz.diff.Diff.AbsentFileMode;
import de.unkrig.zz.diff.Diff.DiffMode;
import de.unkrig.zz.diff.DocumentDiff.LineEquivalence;
import de.unkrig.zz.diff.DocumentDiff.Tokenization;
/**
* A DIFF utility that processes not only files and directories, but also web resources, compressed contents and
* archives.
*/
public
class Main {
static { AssertionUtil.enableAssertionsForThisClass(); }
/**
* Usage:
*
*
* - {@code zzdiff} [ option ] ... file-or-url1 file-or-url2
* -
* Show contents differences between file-or-url1 and file-or-url2 in DIFF format.
* The "path" (relevant, e.g., for the "--path" command line option, see below) is "" (the empty string).
* One of the two inputs may be "-" (standard input).
*
* - {@code zzdiff} [ option ] ... dir1 dir2
* -
* Show which files were added (missing in dir1) or deleted (missing in dir2) and their
* subdirectories, and any contents differences for the remaining files in DIFF format. The "path" (relevant,
* e.g., for the "--path" command line option, see below) is relative to dir1, e.g. "file.txt"
* or "subdir/file.zip!dir/file.txt" or "archive.tgz%!dir/file.txt" or "dir/file.Z%".
*
*
*
* Description:
*
*
* Compares files and web resources line by line; detects directories, compressed and archive files, and even
* nested archives, and compares their entries instead of their raw contents.
*
*
* The default output format is the "normal
* format", also known as the "traditional format". Other output formats can be chosen through command
* line options, see "Output generation", below.
*
*
* Options:
*
* General
*
*
* {@main.commandLineOptions}
*
*
* File selection
*
*
* {@main.commandLineOptions File-Selection}
*
*
* Content processing
*
*
* {@main.commandLineOptions Contents-Processing}
*
*
* Output generation
*
*
* {@main.commandLineOptions Output-Generation}
*
*
* Globs
*
*
* A glob can have the form
*
*
* {@code *~*.c~*.h,foo.c}
*
*
* , which means "foo.c plus all that don't end with .c or .h".
*
*
* Example globs:
*
*
* - {@code dir/file}
* -
* File "file" in directory "dir".
*
* - {@code file.gz%}
* -
* Compressed file "file.gz".
*
* - {@code file.zip!dir/file}
* -
* Entry "dir/file" in archive file "file.zip".
*
* - {@code file.tar.gz%!dir/file}
* -
* Entry "dir/file" in the compressed archive file "file.tar.gz".
*
* */x
* -
* File "x" in an immediate subdirectory.
*
* **/x
* -
* File "x" in any subdirectory.
*
* ***/x
* -
* File "x" in any subdirectory, or any entry "**/x" in any archive file in any subdirectory.
*
* - {@code a,dir/file.7z!dir/b}
* -
* File "a" and entry "dir/b" in archive file "dir/file.7z".
*
* - {@code ~*.c}
* -
* Files that don't end with ".c".
*
* - {@code ~*.c~*.h}
* -
* Files that don't end with ".c" or ".h".
*
* - {@code ~*.c~*.h,foo.c}
* -
* "foo.c" plus all files that don't end with ".c" or ".h".
*
*
*/
public static void
main(final String[] args) {
// Install the "de.unkrig.commons.net.authenticator.CustomAuthenticator", if it can be found on the
// classpath.
try {
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class> cacheModeClass = cl.loadClass("de.unkrig.commons.net.authenticator.CustomAuthenticator$CacheMode");
Class> storeModeClass = cl.loadClass("de.unkrig.commons.net.authenticator.CustomAuthenticator$StoreMode");
Authenticator customAuthenticator = (Authenticator) (
cl
.loadClass("de.unkrig.commons.net.authenticator.CustomAuthenticator")
.getConstructor(cacheModeClass, storeModeClass)
.newInstance(cacheModeClass.getEnumConstants()[2], storeModeClass.getEnumConstants()[2])
);
Authenticator.setDefault(customAuthenticator);
} catch (Exception e) {
;
}
final Main main = new Main();
main.levelFilteredPrinter.run(new Runnable() {
@Override public void run() { main.main2(args); }
});
}
private final Diff diff = new Diff();
private final LevelFilteredPrinter levelFilteredPrinter = new LevelFilteredPrinter();
@Nullable private File outputFile;
public Main() {}
/**
* Processes any thrown exceptions.
*/
private void
main2(String[] args) {
try {
this.main3(args);
} catch (Exception e) {
Printers.error(null, e);
System.exit(1);
}
}
/**
* Processes the command line options and arguments.
*/
private void
main3(String[] args) throws Exception {
args = CommandLineOptions.parse(args, this);
if (args.length != 2) {
Printers.error("Wrong number of file or directory names - try \"--help\".");
System.exit(2);
}
this.main4(ResourceProcessings.toUrl(args[0]), ResourceProcessings.toUrl(args[1]));
}
/**
* Handles the {@link #outputFile}.
*/
private void
main4(final URL resource1, final URL resource2) throws Exception {
Printers.redirectToFile(
Level.INFO, // level
this.outputFile, // outputFile
null, // charset
new RunnableWhichThrows() { // runnable
@Override public void run() throws Exception { Main.this.main5(resource1, resource2); }
}
);
}
/**
* Exits with status "1" iff there are one or more differences.
*/
private void
main5(final URL resource1, final URL resource2) throws Exception {
long differenceCount = Main.this.diff.execute(resource1, resource2);
if (differenceCount > 0) System.exit(1);
}
/**
* Print this text and terminate.
*/
@CommandLineOption public void
help() throws IOException {
System.setProperty("archive.formats", ArchiveFormatFactory.allFormats().toString());
System.setProperty("compression.formats", CompressionFormatFactory.allFormats().toString());
CommandLineOptions.printResource(Main.class, "main(String[]).txt", Charset.forName("UTF-8"), System.out);
System.exit(0);
}
/**
* Process only matching files/entries, e.g."{@code dir/file.zip!dir/file}" or "**/file
".
* See also "Globs", below.
*
* @main.commandLineOptionGroup File-Selection
*/
@CommandLineOption public void
setPath(@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES) Glob glob) {
this.diff.setPathPredicate(glob);
}
/**
* Look into compressed and archive contents if its format matches format-glob and its path matches the
* path-glob.
* 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}}
*
* @main.commandLineOptionGroup File-Selection
* @param discriminator format-glob:path-glob
*/
@CommandLineOption public void
setLookInto(@RegexFlags(Glob.INCLUDES_EXCLUDES | Pattern2.WILDCARD) Glob discriminator) {
this.diff.setLookInto(discriminator);
}
/**
* Files with different names map iff their names match the path-regex and all capturing groups are
* equal. May be given more than once.
*
* @main.commandLineOptionGroup File-Selection
*/
@CommandLineOption(name = { "path-equivalence", "pe" }, cardinality = CommandLineOption.Cardinality.ANY) public void
addPathEquivalence(Pattern pathRegex) { this.diff.addEquivalentPath(pathRegex); }
/**
* @main.commandLineOptionGroup File-Selection
* @deprecated Equivalent with "{@code --path-equivalence} path-regex".
*/
@Deprecated @CommandLineOption(name = "ne", cardinality = CommandLineOption.Cardinality.ANY) public void
addNameEquivalence(Pattern pathRegex) { this.addPathEquivalence(pathRegex); }
/**
* Don't recurse through subdirectories; just compare the existence of subdirectories.
*
* @main.commandLineOptionGroup File-Selection
*/
@CommandLineOption public void
setNoRecurseSubdirctories() { this.diff.setRecurseSubdirectories(false); }
// CONTENTS PROCESSING OPTIONS
/**
* Disassemble .class files.
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption(name = { "disassemble", "da" }) public void
setDisassemble() { this.diff.setDisassembleClassFiles(true); }
/**
* When disassembling .class files, include a constant pool dump, constant pool indexes, and hex dumps of all
* attributes in the disassembly output.
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption public void
setDaVerbose() { this.diff.setDisassembleClassFilesVerbose(true); }
/**
* When disassembling .class files, look for source files in this directory. Source file loading is disabled by
* default.
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption public void
setDaSourceDirectory(File directory) { this.diff.setDisassembleClassFilesSourceDirectory(directory); }
/**
* When disassembling .class files, don't display lines debug info.
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption public void
setDaNoLines() { this.diff.setDisassembleClassFilesButHideLines(true); }
/**
* When disassembling .class files, don't display local variables debug info.
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption public void
setDaNoVars() { this.diff.setDisassembleClassFilesButHideVars(true); }
/**
* When disassembling .class files, use symbolic labels /'L12') instead of numeric labels ('#123').
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption public void
setDaSymbolicLabels() { this.diff.setDisassembleClassFilesSymbolicLabels(true); }
/**
* Lines in files path-pattern that contain line-regex and all capturing groups are equal are
* regarded as equal.
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption(cardinality = CommandLineOption.Cardinality.ANY) public void
addLineEquivalence(
@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES) Glob pathPattern,
Pattern lineRegex
) { this.diff.addEquivalentLine(new LineEquivalence(pathPattern, lineRegex)); }
/**
* Ignore differences in files path-pattern where all lines (deleted, changed or added) match the given
* line-regex.
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption(name = { "ignore", "I" }, cardinality = CommandLineOption.Cardinality.ANY) public void
addIgnore(
@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES) Glob pathPattern,
Pattern lineRegex
) { this.diff.addIgnore(new LineEquivalence(pathPattern, lineRegex)); }
/**
* Ignore whitespace differences.
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption(name = { "ignore-whitespace", "w" }) public void
setIgnoreWhitespace() { this.diff.setIgnoreWhitespace(true); }
@CommandLineOptionGroup interface AddedFileModeOptionGroup {}
@CommandLineOptionGroup interface DeletedFileModeOptionGroup {}
/**
* How to deal with added resp. deleted files:
*
* - REPORT (the default):
* -
* Print "File added path", resp. "File deleted path" (or, with "--exist" or "--brief",
* print "+ path" resp. "- path").
*
* - COMPARE_WITH_EMPTY:
* -
* Generate a diff document by comparing with the empty document.
*
* - IGNORE:
* -
* Do nothing.
*
*
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption(group = AddedFileModeOptionGroup.class) public void
setAddedFile(AbsentFileMode mode) { this.diff.setAddedFileMode(mode); }
/**
* @see #setAddedFile(Diff.AbsentFileMode)
*/
@CommandLineOption(group = DeletedFileModeOptionGroup.class) public void
setDeletedFile(AbsentFileMode mode) { this.diff.setDeletedFileMode(mode); }
/**
* Treat absent files as empty. This is shorthand for "{@code --added-file COMPARE_WITH_EMPTY --deleted-file
* COMPARE_WITH_EMPTY}".
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption(
name = { "new-file", "N" },
group = { AddedFileModeOptionGroup.class, DeletedFileModeOptionGroup.class }
) public void
setNewFile() {
this.diff.setAddedFileMode(AbsentFileMode.COMPARE_WITH_EMPTY);
this.diff.setDeletedFileMode(AbsentFileMode.COMPARE_WITH_EMPTY);
}
/**
* Treat added files as (previously) empty; deleted files are only reported. This is shorthand for "{@code
* --added-file COMPARE_WITH_EMPTY --deleted-file REPORT}".
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption(group = { AddedFileModeOptionGroup.class, DeletedFileModeOptionGroup.class }) public void
setUnidirectionalNewFile() {
this.diff.setAddedFileMode(AbsentFileMode.COMPARE_WITH_EMPTY);
this.diff.setDeletedFileMode(AbsentFileMode.REPORT);
}
/**
* Also report unchanged files.
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption public void
setUnchangedFiles() { this.diff.setReportUnchangedFiles(true); }
/**
* Encoding of the files being compared (defaults to the JVM default charset, "${file.encoding}").
*
* @main.commandLineOptionGroup Contents-Processing
*/
@CommandLineOption public void
setEncoding(Charset charsetName) { this.diff.setCharset(charsetName); }
// OUTPUT GENERATION OPTIONS
/**
* Write DIFF to output-file instead of STDOUT.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption public void
setOut(File outputFile) { this.outputFile = outputFile; }
@CommandLineOptionGroup
interface DiffModeOptionGroup {}
/**
* Output "normal diff format"; this is the default.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption(group = DiffModeOptionGroup.class) public void
setNormal() { this.diff.setDiffMode(DiffMode.NORMAL); }
/**
* Report only which files were added or deleted (do not report changed content).
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption(group = DiffModeOptionGroup.class) public void
setExist() { this.diff.setDiffMode(DiffMode.EXIST); }
/**
* Report only which files were added, deleted or changed.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption(name = { "brief", "q" }, group = DiffModeOptionGroup.class) public void
setBrief() { this.diff.setDiffMode(DiffMode.BRIEF); }
/**
* Output "context diff format" with three
* lines of context.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption(name = { "context", "c" }, group = DiffModeOptionGroup.class) public void
setContext() { this.diff.setDiffMode(DiffMode.CONTEXT); }
/**
* Output "context diff format" with
* amount lines of context.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption(name = { "Context", "C" }, group = DiffModeOptionGroup.class) public void
setContext2(int amount) {
this.diff.setDiffMode(DiffMode.CONTEXT);
this.diff.setContextSize(amount);
}
/**
* Output "unified diff format" with three
* lines of context.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption(name = { "unified", "u" }, group = DiffModeOptionGroup.class) public void
setUnified() { this.diff.setDiffMode(DiffMode.UNIFIED); }
/**
* Output "unified diff format" with
* amount lines of context.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption(name = "Unified", group = DiffModeOptionGroup.class) public void
setUnified2(int amount) {
this.diff.setDiffMode(DiffMode.UNIFIED);
this.diff.setContextSize(amount);
}
/**
* Report errors and continue with next file.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption public void
setKeepGoing() {
this.diff.setExceptionHandler(new ExceptionHandler() {
@Override public void handle(String path, IOException ioe) { Printers.error(path, ioe.getMessage()); }
@Override public void handle(String path, RuntimeException re) { Printers.error(path, re.getMessage()); }
});
}
/**
* Scan directories strictly sequentially. The default is to parallelize the directory scan in multiple threads.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption public void
setSequential() { this.diff.setSequential(true); }
/**
* Regard documents as streams of Java tokens; the whitespace between tokens (including line breaks) is then
* insignificant.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption public void
setJavaTokenization() { this.diff.setTokenization(Tokenization.JAVA); }
/**
* Don't regard C-style comments ("/* ... */
") as relevant for comparison.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption public void
setIgnoreCStyleComments() { this.diff.setIgnoreCStyleComments(true); }
/**
* Don't regard C++-style comments ("// ...
") as relevant for comparison.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption(name = "ignore-c++-style-comments") public void
setIgnoreCppStyleComments() { this.diff.setIgnoreCPlusPlusStyleComments(true); }
/**
* Don't regard doc comments ("/** ... */
") as relevant for comparison.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption public void
setIgnoreDocComments() { this.diff.setIgnoreDocComments(true); }
/**
* Password to decrypt password-protected 7ZIP input files.
*/
@SuppressWarnings("static-method")
@CommandLineOption public void
set7zInputFilePassword(String value) { SevenZArchiveFormat.setPassword(value.getBytes(Charsets.UTF_16LE)); }
/**
* Password to decrypt password-protected zip archive entries.
*/
@SuppressWarnings("static-method")
@CommandLineOption public void
setZipInputFilePassword(String value) { ZipArchiveFormat.setInputFilePasswordChars(value.toCharArray()); }
/**
* All of the above.
*/
@CommandLineOption public void
setPassword(String value) {
this.set7zInputFilePassword(value);
this.setZipInputFilePassword(value);
}
/**
* Suppress all output except errors.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption public void
setNowarn() {
this.levelFilteredPrinter.setNoWarn();
SimpleLogging.setNoWarn();
}
/**
* Suppress "normal" output; print only errors and warnings.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption public void
setQuiet() {
this.levelFilteredPrinter.setQuiet();
SimpleLogging.setQuiet();
}
/**
* Print verbose messages.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption public void
setVerbose() {
this.levelFilteredPrinter.setVerbose();
SimpleLogging.setVerbose();
}
/**
* Print verbose and debug messages.
*
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption public void
setDebug() {
this.levelFilteredPrinter.setDebug();
SimpleLogging.setDebug();
SimpleLogging.setDebug();
SimpleLogging.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:logger:handler:formatter:format
* @main.commandLineOptionGroup Output-Generation
*/
@CommandLineOption(cardinality = CommandLineOption.Cardinality.ANY) public static void
addLog(String spec) { SimpleLogging.configureLoggers(spec); }
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy