All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy