de.unkrig.zz.patch.Main Maven / Gradle / Ivy
/*
* de.unkrig.patch - An enhanced version of the UNIX PATCH 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.patch;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.regex.Pattern;
import de.unkrig.commons.file.ExceptionHandler;
import de.unkrig.commons.file.contentstransformation.ContentsTransformer;
import de.unkrig.commons.file.filetransformation.FileTransformations;
import de.unkrig.commons.file.filetransformation.FileTransformer;
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.protocol.Mapping;
import de.unkrig.commons.lang.protocol.Mappings;
import de.unkrig.commons.lang.protocol.PredicateUtil;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.LevelFilteredPrinter;
import de.unkrig.commons.text.Printers;
import de.unkrig.commons.text.StringStream.UnexpectedElementException;
import de.unkrig.commons.text.expression.AbstractExpression;
import de.unkrig.commons.text.expression.EvaluationException;
import de.unkrig.commons.text.expression.Expression;
import de.unkrig.commons.text.expression.ExpressionEvaluator;
import de.unkrig.commons.text.expression.ExpressionUtil;
import de.unkrig.commons.text.parser.ParseException;
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.CommandLineOptionGroup;
import de.unkrig.commons.util.annotation.RegexFlags;
import de.unkrig.commons.util.logging.SimpleLogging;
import de.unkrig.zz.patch.SubstitutionContentsTransformer.Mode;
import de.unkrig.zz.patch.diff.DiffParser.Hunk;
/**
* Implementation of a PATCH command line utility with the following features:
*
* -
* Transforms regular files, directory trees, and optionally compressed files and entries in archive files (also in
* nested ones)
*
* - Reads patch files in NORMAL, CONTEXT and UNIFIED diff format
*
- Can replace the contents of files from "update files"
*
- Can do search-and-replace within files (SED like)
*
- Can transform out-of-place or in-place
*
- Optionally keeps copies of the original files
*
- Can remove files
*
- Can rename files
*
*/
public final
class Main {
private Main() {}
private static final ExceptionHandler PRINT_AND_CONTINUE = new ExceptionHandler() {
@Override public void
handle(String path, IOException ioe) { Printers.error(path, ioe); }
@Override public void
handle(String path, RuntimeException rte) {
if (rte == FileTransformer.NOT_IDENTICAL) {
Printers.verbose(path);
} else {
Printers.error(path, rte);
}
}
};
private static final ExceptionHandler RETHROW = new ExceptionHandler() {
@Override public void
handle(String path, IOException ioe) throws IOException { throw ioe; }
@Override public void
handle(String path, RuntimeException rte) {
if (rte == FileTransformer.NOT_IDENTICAL) {
Printers.verbose(path);
}
throw rte;
}
};
/**
* Usage
*
*
* - {@code zzpatch} [ option ... ]
* -
* Transforms STDIN to STDOUT.
*
* - {@code zzpatch} [ option ... ] file-or-dir
* -
* Transforms file-or-dir in-place.
*
* - {@code zzpatch} [ option ... ] file-or-dir new-file-or-dir
* -
* Transforms file-or-dir into new-file-or-dir.
*
* - {@code zzpatch} [ option ... ] file-or-dir ... existing-dir
* -
* Transforms file-or-dir ..., creating the output in existing-dir.
*
*
*
* Options
*
* File transformation options:
*
* {@main.commandLineOptions File-Transformation}
*
*
* File transformation conditions:
*
* These control the preceding file transformation.
*
*
* - {@code --report} expr
* -
* Evaluate and print the expr each time the preceding file transformation is executed, e.g.
* "{@code path + ": Add '" + name + "' from '" + contentsFile + "'"}". The parameters of the expr
* depend on the file transformation (see above).
*
* - {@code --iff} expr
* -
* Execute the preceding file transformation only iff expr evaluates to true. The parameters of the
* expr depend on the file transformation (see above).
*
* - {@code --mode} {@code REPLACEMENT_STRING|CONSTANT|EXPRESSION} (only with "--substitute")
* -
* Determines how the replacement is processed:
*
* - {@code REPLACEMENT_STRING}
* -
* A JRE replacement string
*
* - {@code CONSTANT}
* -
* A constant string - no character (esp. dollar, backslash) has a special meaning
*
* - {@code EXPRESSION}
* -
* A Java-like expression
*
*
*
*
*
* File processing options:
*
* {@main.commandLineOptions File-Processing}
*
*
* General options:
*
* {@main.commandLineOptions}
*
*
* Globs
*
*
* Check the descriptions of wildcards,
* includes /
* excludes and replacements.
*
* Examples:
*
* - {@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 "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}"
*
*
*
* Regular expressions and replacements
*
*
* For the precise description of the supported regular-expression constructs, see here.
*
*
* For the precise description of the replacement format, see here.
*
*
* Expressions
*
*
* For the precise description of the supported expression constructs, see here.
*
*/
public static void
main(String[] args) {
final Main main = new Main();
final String[] args2;
try {
args2 = CommandLineOptions.parse(args, main);
} catch (CommandLineOptionException cloe) {
Printers.error(cloe.getMessage() + ", try \"--help\".");
System.exit(1);
return;
}
main.levelFilteredPrinter.run(new Runnable() { @Override public void run() { main.main2(args2); } });
}
private void
main2(String[] args) {
try {
this.main3(args);
} catch (Exception e) {
if (e == FileTransformer.NOT_IDENTICAL) {
System.exit(1);
}
Printers.error(null, e);
}
}
// ================ CONFIGURATION FIELDS ================
private final LevelFilteredPrinter levelFilteredPrinter = new LevelFilteredPrinter();
private FileTransformer.Mode mode = FileTransformer.Mode.TRANSFORM;
private final Patch patch = new Patch();
private Charset inputCharset = Charset.defaultCharset();
private Charset outputCharset = Charset.defaultCharset();
private Charset patchFileCharset = Charset.defaultCharset();
{ this.patch.setExceptionHandler(Main.RETHROW); }
// ================ COMMAND LINE OPTION SETTERS AND ADDERS ================
/**
* Look into compressed and archive contents if the format and the path match the 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}}
*
* @param glob format-glob{@code :}path-glob
* @main.commandLineOptionGroup File-Processing
*/
@CommandLineOption public void
lookInto(@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES) Glob glob) {
this.patch.setLookIntoFormat(glob);
}
/**
* By default directory members are processed in lexicographical sequence to achieve deterministic results.
*
* @main.commandLineOptionGroup File-Processing
*/
@CommandLineOption public void
dontSortDirectoryMembers() { this.patch.setDirectoryMemberNameComparator(null); }
/**
* Encoding of patch files (only relevant for "{@code --patch}"); the default is "{@code ${file.encoding}}".
*
* @main.commandLineOptionGroup File-Processing
*/
@CommandLineOption public void
setPatchFileEncoding(Charset charset) { this.patchFileCharset = charset; }
@CommandLineOptionGroup
interface FileTransformerModeCommandLineOptionGroup {}
/**
* Do not create or modify any files; exit with status 1 if this is an in-place transformation, and at least one of
* the files would be changed.
*
* "{@code --check --verbose}" also prints the path of the first file or archive entry that would be
* changed.
*
*
* "{@code --check --verbose --keep-going}" prints the pathes of all files and archive entries that
* would be changed.
*
*
* @main.commandLineOptionGroup File-Processing
*/
@CommandLineOption(group = FileTransformerModeCommandLineOptionGroup.class) public void
check() { this.mode = FileTransformer.Mode.CHECK; }
/**
* Before modifying a file, check whether the change is redundant, i.e. yields an identical result. Could improve
* performance if few or no files are actually modified.
*
* @main.commandLineOptionGroup File-Processing
*/
@CommandLineOption(group = FileTransformerModeCommandLineOptionGroup.class)
public void
checkBeforeTransformation() { this.mode = FileTransformer.Mode.CHECK_AND_TRANSFORM; }
/**
* If existing files would be overwritten, keep copies of the originals.
*
* @main.commandLineOptionGroup File-Processing
*/
@CommandLineOption public void
keep() { this.patch.setKeepOriginals(true); }
/**
* Replace the contents of files/archive entries that match glob (see below) with that of the
* update-file.
*
* @param specification glob{@code =}update-file
* @param updateConditions [ condition ... ]
* @main.commandLineOptionGroup File-Transformation
*/
@CommandLineOption(cardinality = CommandLineOption.Cardinality.ANY) public void
addUpdate(
@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES | Glob.REPLACEMENT) Glob specification,
UpdateConditions updateConditions
) {
ContentsTransformer contentsTransformer = new UpdateContentsTransformer(specification);
this.patch.addContentsTransformation(
PredicateUtil.and(specification, ExpressionUtil.toPredicate(updateConditions.result, "path")),
contentsTransformer
);
}
/**
* Replace all matches of pattern in files/archive entries that match glob (see below) with
* the replacement string. "{@code $0}", "{@code $1}", "{@code $2}", etc. expand to the captured groups
* of the match.
*
* Conditions (see below) apply per match; the parameters of the "{@code --report}" and "{@code --iff}"
* conditions are:
*
*
* - path
* - The "path" of the file or ZIP entry that contains the match
* - match
* - The matching text
* - occurrence
* - The index of the occurrence within the document, starting at zero
*
*
* @param substituteConditions [ condition ... ]
* @main.commandLineOptionGroup File-Transformation
*/
@CommandLineOption(cardinality = CommandLineOption.Cardinality.ANY) public void
addSubstitute(
@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES) Glob glob,
@RegexFlags(Pattern.MULTILINE) Pattern pattern,
String replacement,
SubstituteConditions substituteConditions
) throws ParseException {
final Expression condition = substituteConditions.result;
ContentsTransformer contentsTransformer = new SubstitutionContentsTransformer(
this.inputCharset, // inputCharset
this.outputCharset, // outputCharset
pattern, // pattern
substituteConditions.replacementMode, // replacementMode
replacement, // replacement
( // condition
condition == Expression.TRUE ? SubstitutionContentsTransformer.Condition.ALWAYS :
condition == Expression.FALSE ? SubstitutionContentsTransformer.Condition.NEVER :
new SubstitutionContentsTransformer.Condition() {
@Override public boolean
evaluate(String path, CharSequence match, int occurrence) {
try {
return ExpressionEvaluator.toBoolean(condition.evaluate(
Mappings.mapping(
"path", path, // SUPPRESS CHECKSTYLE Wrap:2
"match", match,
"occurrence", occurrence
)
));
} catch (EvaluationException ee) {
throw new RuntimeException(ee);
}
}
@Override public String
toString() { return condition.toString(); }
}
)
);
this.patch.addContentsTransformation(glob, contentsTransformer);
}
/**
* Apply patch-file to all files/archive entries that match glob (see below).
* patch-file can be in traditional, context or unified diff format.
*
* Conditions (see below) apply per match; the parameters of the "{@code --report}" and "{@code --iff}"
* conditions are:
*
*
* - path
* - The "path" of the file or ZIP entry being patched
* - hunks
* - The hunks being applied
* - hunkIndex
* - The index of the current hunk
* - lineNumber
* - The line number where the hunk starts
*
*
* @param patchConditions [ condition ... ]
* @throws UnexpectedElementException The {@code patchFile} does not contain a valid DIFF document
* @main.commandLineOptionGroup File-Transformation
*/
@CommandLineOption(cardinality = CommandLineOption.Cardinality.ANY) public void
addPatch(
@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES) Glob glob,
File patchFile,
final PatchConditions patchConditions
) throws IOException, UnexpectedElementException {
ContentsTransformer contentsTransformer = new PatchContentsTransformer(
this.inputCharset,
this.outputCharset,
patchFile,
this.patchFileCharset,
new PatchContentsTransformer.Condition() {
@Override public boolean
evaluate(
String path,
List hunks,
int hunkIndex,
Hunk hunk,
int lineNumber
) {
try {
return ExpressionEvaluator.toBoolean(
patchConditions.result.evaluate(Mappings.mapping(
"path", path, // SUPPRESS CHECKSTYLE Wrap:4
"hunks", hunks,
"hunkIndex", hunkIndex,
"lineNumber", lineNumber
))
);
} catch (EvaluationException ee) {
throw new RuntimeException(ee);
}
}
}
);
this.patch.addContentsTransformation(glob, contentsTransformer);
}
/**
* Remove all files/archive entries that match glob (see below).
*
* @param removeConditions [ condition ... ]
* @main.commandLineOptionGroup File-Transformation
*/
@CommandLineOption(cardinality = CommandLineOption.Cardinality.ANY) public void
addRemove(
@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES) Glob glob,
RemoveConditions removeConditions
) {
this.patch.addRemoval(PredicateUtil.and(glob, ExpressionUtil.toPredicate(removeConditions.result, "path")));
}
/**
* Rename files/archive entries according to glob (see below), e.g. "{@code (*).c=$1.c.orig}".
* Multiple "{@code --rename}" options are applied in the given order.
*
* @param renameConditions [ condition ... ]
* @main.commandLineOptionGroup File-Transformation
*/
@CommandLineOption(cardinality = CommandLineOption.Cardinality.ANY) public void
addRename(
@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES | Glob.REPLACEMENT) final Glob glob,
final RenameConditions renameConditions
) {
this.patch.addRenaming(new Glob() {
@Override public boolean
matches(String subject) { return glob.matches(subject); }
@Override @Nullable public String
replace(String subject) {
String result = glob.replace(subject);
return result != null && ExpressionUtil.toPredicate(
renameConditions.result,
"path"
).evaluate(subject) ? result : null;
}
});
}
/**
* To all directories and archives that match glob, add a member/entry name, and fill it
* from contents-file.
*
* @param addConditions [ condition ... ]
* @main.commandLineOptionGroup File-Transformation
*/
@CommandLineOption(cardinality = CommandLineOption.Cardinality.ANY) public void
addAdd(
@RegexFlags(Pattern2.WILDCARD | Glob.INCLUDES_EXCLUDES) Glob glob,
String name,
File contentsFile,
AddConditions addConditions
) {
this.patch.addAddition(PredicateUtil.and(glob, ExpressionUtil.toPredicate(
addConditions.result,
"path"
)), name, contentsFile);
}
/**
* Encoding of input files (only relevant for "{@code --substitute}" and "{@code --patch}"); the default is "{@code
* ${file.encoding}}".
*
* @main.commandLineOptionGroup File-Processing
*/
@CommandLineOption public void
setInputEncoding(Charset charset) { this.inputCharset = charset; }
/**
* Encoding of output files (only relevant for "{@code --substitute}" and "{@code --patch}"); the default is "{@code
* ${file.encoding}}".
*
* @main.commandLineOptionGroup File-Processing
*/
@CommandLineOption public void
setOutputEncoding(Charset charset) { this.outputCharset = charset; }
/**
* All of "--patch-file-encoding", "--input-encoding" and "--output-encoding".
*
* @main.commandLineOptionGroup File-Processing
*/
@CommandLineOption public void
setEncoding(Charset charset) {
this.patchFileCharset = charset;
this.inputCharset = charset;
this.outputCharset = charset;
}
/**
* Print this text and terminate.
*/
@CommandLineOption public static 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);
}
/**
* Print error and continue with next file.
*/
@CommandLineOption public void
keepGoing() { this.patch.setExceptionHandler(Main.PRINT_AND_CONTINUE); }
/**
* Suppress all messages except errors.
*/
@CommandLineOption public void
setNowarn() {
this.levelFilteredPrinter.setNoWarn();
SimpleLogging.setNoWarn();
}
/**
* Suppress normal output.
*/
@CommandLineOption public void
setQuiet() {
this.levelFilteredPrinter.setQuiet();
SimpleLogging.setQuiet();
}
/**
* Print verbose messages.
*/
@CommandLineOption public void
setVerbose() {
this.levelFilteredPrinter.setVerbose();
SimpleLogging.setVerbose();
}
/**
* Print verbose and debug messages.
*/
@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{@code :}logger{@code :}handler{@code
* :}formatter{@code :}format
*/
@CommandLineOption(cardinality = CommandLineOption.Cardinality.ANY) public static void
addLog(String spec) { SimpleLogging.configureLoggers(spec); }
private void
main3(String[] args) throws IOException {
if (args.length == 0) {
if (this.mode != FileTransformer.Mode.TRANSFORM) {
System.err.println(
"\"--check\" or \"--check-before-transformation\" cannot be used if the input is STDIN."
);
System.exit(1);
}
this.patch.contentsTransformer().transform("-", System.in, System.out);
} else {
FileTransformations.transform(
args,
this.patch.fileTransformer(
true, // lookIntoDirectories
false // renameOrRemoveTopLevelFiles
),
this.mode,
this.patch.getExceptionHandler()
);
}
}
/**
* This bean represents the various "conditions" command line options of the "--substitute" action.
*/
public static
class SubstituteConditions extends Conditions {
public Mode replacementMode = Mode.REPLACEMENT_STRING;
public SubstituteConditions() { super("path", "match", "occurrence"); }
/**
* Evaluate and print the expression each time the preceding file transformation is executed, e.g.
* "{@code path + ": Add '" + name + "' from '" + contentsFile + "'"}". The parameters of the
* expression depend on the file transformation (see above).
*/
@CommandLineOption public void
addMode(SubstitutionContentsTransformer.Mode replacementMode) {
this.replacementMode = replacementMode;
}
}
/**
* This bean represents the various "conditions" command line options of the "--patch" action.
*/
public static
class PatchConditions extends Conditions {
public PatchConditions() { super("path", "hunks", "hunkIndex", "hunk", "lineNumber"); }
}
/**
* This bean represents the various "conditions" command line options of the "--remove" action.
*/
public static
class RemoveConditions extends Conditions {
public RemoveConditions() { super("path"); }
}
/**
* This bean represents the various "conditions" command line options of the "--update" action.
*/
public static
class UpdateConditions extends Conditions {
public UpdateConditions() { super("path"); }
}
/**
* This bean represents the various "conditions" command line options of the "--rename" action.
*/
public static
class RenameConditions extends Conditions {
public RenameConditions() { super("path"); }
}
/**
* This bean represents the various "conditions" command line options of the "--add" action.
*/
public static
class AddConditions extends Conditions {
public AddConditions() { super("path"); }
}
/**
* The base class of the various "conditions" command line options beans.
*/
public abstract static
class Conditions {
private final String[] variableNames;
/**
* The expression that implents the "{@code --iff}"s and "{@code --report}"s. Iff none of these is configured,
* then the this field is {@link Expression#TRUE}.
*/
Expression result = Expression.TRUE;
/**
* @param variableNames The names of the variables that expressions can reference
*/
public
Conditions(String... variableNames) { this.variableNames = variableNames; }
/**
* Evaluate and print the expression each time the preceding file transformation is executed, e.g.
* "{@code path + ": Add '" + name + "' from '" + contentsFile + "'"}". The parameters of the
* expression depend on the file transformation (see above).
*/
@CommandLineOption public void
addReport(String expression) throws ParseException {
final Expression reportExpression = new ExpressionEvaluator(this.variableNames).parse(expression);
this.result = ExpressionUtil.logicalAnd(this.result, new AbstractExpression() {
@Override @Nullable public Object
evaluate(Mapping variables) throws EvaluationException {
Printers.info(String.valueOf(reportExpression.evaluate(variables)));
return true;
}
});
}
/**
* Execute the preceding file transformation only iff expression evaluates to true. The parameters
* of the expression depend on the file transformation (see above).
*/
@CommandLineOption public void
addIff(String expression) throws ParseException {
final Expression iffExpression = new ExpressionEvaluator(this.variableNames).parse(expression);
this.result = ExpressionUtil.logicalAnd(this.result, iffExpression);
}
}
}