/* * [SortSRS.java] * * Summary: Sorts items in Funduc Search/Replace old-style *.srs scripts. * * Copyright: (c) 2011-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2011-11-11 initial version * 1.1 2011-11-12 more checks for damaged scripts. fix bug that dropped the first [search] * 1.2 2011-11-12 deal with scripts that contain [Search or [Replace as literal data, and two types of dup. * 1.3 2011-11-14 major refactoring. Sort Paths as well and search/replace items. Handle comments. * 1.4 2011-11-16 fix bug when ] in comment. add -strip option * 1.5 2012-11-06 fix dedup bug. Was removing lines that differed only in case. Avoid most common false alarm * possible dup. * 1.6 2012-11-13 rename to make clear search/replace strings are not trimmed. */ package com.mindprod.sortsrs; import com.mindprod.commandline.CommandLine; import com.mindprod.common18.EIO; import com.mindprod.fastcat.FastCat; import com.mindprod.filter.AllButSVNDirectoriesFilter; import com.mindprod.filter.ExtensionListFilter; import com.mindprod.hunkio.HunkIO; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.regex.Pattern; import static java.lang.System.*; /** * Sorts items in Funduc Search/Replace old-style *.srs scripts. *

* Input might use \n or \r\n line ending conventions. Output uses \r\n. *

* To understand this program you need to be familiar with the structure of Funduc S/R files. * Study a number of example, or create a synthetic one that exercises every feature of the script * editor. *

* Unfortunately, there is sort sortsrx yet. * * @author Roedy Green, Canadian Mind Products * @version 1.6 2012-11-13 rename to make clear search/replace strings are not trimmed. * @since 2011-11-11 */ public class SortSRS { /** * true if want extra debugging output */ private static final boolean DEBUGGING = false; /** * a BOM as it appears internally */ private static final char BOM = 0xfeff; private static final int FIRST_COPYRIGHT_YEAR = 2011; /** * undisplayed copyright notice */ @SuppressWarnings( { "UnusedDeclaration" } ) private static final String EMBEDDED_COPYRIGHT = "Copyright: (c) 2011-2017 Roedy Green, Canadian Mind Products, http://mindprod.com"; @SuppressWarnings( { "UnusedDeclaration" } ) private static final String RELEASE_DATE = "2011-11-13"; /** * how to use the command line */ private static final String USAGE = "\nSortSRS [-strip] -s -q E:\\srs (-s including subdirs, -q quiet, " + "list of files/dirs to sort)"; @SuppressWarnings( { "UnusedDeclaration" } ) private static final String VERSION_STRING = "1.6"; /** * comparator to sort items in a SRS script */ private static final Comparator DEFINITE_DEDUP_ORDER = new DefiniteDeDupOrder(); /** * comparator to sort items in a SRS script */ private static final Comparator POSSIBLE_DEDUP_ORDER = new PossibleDeDupOrder(); /** * comparator to sort items in a SRS script in the order the user most likely to prefer. */ private static final Comparator USER_ORDER = new InsensitiveOrder(); /** * regex to split script into individual search items. */ private static final Pattern LINE_SPLITTER = Pattern.compile( "\\r\\n" ); /** * true if should report on files that were already in order */ private static boolean reportAlreadyInOrder; /** * which script we are tidying up now globally accessible */ private static File fileBeingProcessed; /** * true if should strip comments out of scripts */ private static boolean stripComments; /** * ensure we have unencumbered access to the file */ private static void ensureCanAccessFile() { if ( !fileBeingProcessed.exists() ) { throw new IllegalArgumentException( "Program bug[01]: Cannot find the script file." ); } if ( !fileBeingProcessed.canRead() ) { throw new IllegalArgumentException( "Error [02]: Cannot get access to read the script." ); } if ( !fileBeingProcessed.canWrite() ) { // canWrite true implies file exists. throw new IllegalArgumentException( "Error [03]: Cannot get access to rewrite the script." ); } } /** * make sure this file is using Windows \r\n line conventions, not Unix \n or Mac \r * * @param contents content of file to check. */ private static void ensureStandardLineEndings( String contents ) { if ( contents.length() > 0 ) { char prevChar = contents.charAt( 0 ); for ( int i = 1; i < contents.length(); i++ ) { final char thisChar = contents.charAt( i ); // only allow \r\n not \n or \r in isolation if ( ( thisChar == '\n' ) ^ ( prevChar == '\r' ) ) { throw new IllegalArgumentException( "Error [04]: Script file fails to use standard Windows \\r\\n line ending conventions." ); } prevChar = thisChar; } } } /** * split guts into individual sortable SRItem objects. * * @param guts middle part of script with [Search][Replace] pairs. * * @return array of sortable SRItem objects. */ private static Item[] extractItems( final String guts ) { // keep all empty fields, leading, embedded and trailing. final String[] lines = LINE_SPLITTER.split( guts, -1 ); // we don't know precisely how many Items there will be. Could be mix if search/replace/path/black. final ArrayList items = new ArrayList<>( lines.length / 4 + 20 ); for ( int i = 0; i < lines.length; /* no inc */ ) { final String marker = lines[ i ]; if ( marker.startsWith( "[Search" ) ) { // process group of four lines: searchMarker, search, replaceMarker, replace // add new item to the list. if ( i + 4 >= lines.length ) { throw new IllegalArgumentException( "Error [05]: Script corrupt. Incomplete [Search]/[Replace] group." ); } items.add( SRItem.create( marker, lines[ i + 1 ], lines[ i + 2 ], lines[ i + 3 ] ) ); i += 4; } else if ( marker.startsWith( "[Replace]" ) ) { throw new IllegalArgumentException( "Error [06]: Script corrupt. Incomplete [Path] group." ); } else if ( marker.startsWith( "[Path]" ) ) { items.add( PathItem.create( marker, lines[ i + 1 ] ) ); i += 2; } else if ( marker.startsWith( "[End of Search and Replace Script" ) ) { items.add( EndItem.create() ); i++; } else if ( marker.trim().length() == 0 ) { // stray blank line. Get rid of it. It is easy to accidentally contaminate file with them when // manually editing. // If there is more serious trouble, it will show up later i++; } else { throw new IllegalArgumentException( "Error [07]: Script corrupt. Unrecognized [keyword] " + marker ); } } // end for // append possibly missing EndItem int size = items.size(); if ( size == 0 || !( items.get( size - 1 ) instanceof EndItem ) ) { // add missing EndItem; items.add( new EndItem() ); size++; } return items.toArray( new Item[ size ] ); } /** * Regenerate the script from head, tail and sorted contests. * * @param items array of Search/replace items. */ private static void notifyOfPossibleDups( final Item[] items ) { Item prevItem = null; for ( final Item item : items ) { assert item != null : "bug: unexpected null item"; // exact dups have already been removed. // if both were case-sensitive, ok don't exactly match search strings. if ( prevItem != null && POSSIBLE_DEDUP_ORDER.compare( item, prevItem ) == 0 && prevItem instanceof SRItem && item instanceof SRItem && !( ( ( SRItem ) prevItem ).isCaseSensitive() && ( ( SRItem ) item ).isCaseSensitive() ) ) { // identical ignore case for search, but possibly not replace, comments etc. // tell user even with -q out.println( "\nWarning [01]: Possible Duplicate item left in place in " + EIO.getCanOrAbsPath( fileBeingProcessed ) + ":\n" + prevItem + "\n" + item ); // don't actually remove it. } else { prevItem = item; } } // end for } /** * Regenerate the script from head, tail and sorted contests. * * @param items array of Search/replace items. * * @return DeDuped array of items. */ private static Item[] preciseDeDup( final Item[] items ) { Item prevItem = null; int droppedCount = 0; for ( int i = 0; i < items.length; i++ ) { final Item item = items[ i ]; // compare on whole string, not just the search string itself. // We keep unless perfect case-sensitive dup. if ( prevItem != null && DEFINITE_DEDUP_ORDER.compare( item, prevItem ) == 0 ) { // precisely identical in both search and replace. // tell user even with -q out.println( "\nNote: Duplicate item removed from " + EIO.getCanOrAbsPath( fileBeingProcessed ) + ":\n" + item.toString() ); items[ i ] = null; droppedCount++; } else { // leave prev where it was if we dropped the element. prevItem = item; } } // end for if ( droppedCount == 0 ) { return items; } else { // remove dropped items from array. final Item[] deDupedItems = new Item[ items.length - droppedCount ]; int j = 0; for ( Item item : items ) { // copy over just the non-null elements, ones not deleted. if ( item != null ) { deDupedItems[ j++ ] = item; } } return deDupedItems; } } /** * sort the script in one file * * @param file the file where script is * @param quiet true if should suppress status messages. */ private static void processFile( final File file, boolean quiet ) { SortSRS.fileBeingProcessed = file; try { /** * make sure we have unencumbered access to the file. */ ensureCanAccessFile(); // read file final String originalContents = HunkIO.readEntireFile( fileBeingProcessed, HunkIO.UTF8 ); // sort file contents. final String sortedContents = processFileContents( originalContents ); // figure out what we need to do final boolean changed = !sortedContents.equals( originalContents ); final boolean missingBOM = sortedContents.length() == 0 || sortedContents.charAt( 0 ) != BOM; // do it saveFileBack( sortedContents, changed, missingBOM ); // report what we did reportActions( quiet, changed, missingBOM ); } catch ( IOException e ) { if ( DEBUGGING ) { e.printStackTrace( err ); } // add file name to error message in Exception err.println( "\nError in script file: " + EIO.getCanOrAbsPath( fileBeingProcessed ) + ".\n" + e.getClass() + e.getMessage() + "\nScript file left as is.\n" ); } // keep going , even if errors. } /** * Sort the items in the contents of one file. * * @param originalContents String holding the contents of the entire file * * @return the sorted contents */ private static String processFileContents( String originalContents ) { ensureStandardLineEndings( originalContents ); // find start of guts; final int startOfGutsAt = originalContents.indexOf( "\r\n[Search" ); if ( startOfGutsAt < 0 ) { throw new IllegalArgumentException( "Error [09]: Empty script. Nothing in it to sort." ); } // head ahs lead OBM but does not have a trailing \r\n final String head = originalContents.substring( 0, startOfGutsAt ); // guts has no lead or trail \r\n just [Search] [Replace} [Path] and data lines. final String guts = originalContents.substring( startOfGutsAt + "\r\n".length() ); if ( DEBUGGING ) { out.println( EIO.getCanOrAbsPath( fileBeingProcessed ) ); out.println( ">>>> head >>>>>" ); out.println( head ); out.println( "----------------" ); out.println( ">>>> guts >>>>>" ); out.println( guts ); } // split guts into individual sortable SRItem objects. Item[] items = extractItems( guts ); if ( DEBUGGING ) { out.println( ">>>> before sorting >>>>>" ); showAllItems( items ); } // find precise dups Arrays.sort( items, DEFINITE_DEDUP_ORDER ); if ( DEBUGGING ) { out.println( ">>>> sorted by DEFINITE_DEDUP_ORDER >>>>>" ); showAllItems( items ); } items = preciseDeDup( items ); // find possible dups Arrays.sort( items, POSSIBLE_DEDUP_ORDER ); if ( DEBUGGING ) { out.println( ">>>> sorted by POSSIBLE_DEDUP_ORDER >>>>>" ); showAllItems( items ); } notifyOfPossibleDups( items ); // prepare for user Arrays.sort( items, USER_ORDER ); if ( DEBUGGING ) { out.println( ">>>> sorted by USER_ORDER >>>>>" ); showAllItems( items ); } // glue pieces back together. final String regeneratedScript = regenerateScript( head, items ); if ( DEBUGGING ) { out.println( ">>>> regenerated script >>>>>" ); out.println( regeneratedScript ); } return regeneratedScript; } /** * Regenerate the script from head, tail and sorted contests. * * @param head part of original script ahead of [Search] * @param items array of Search/replace items. * * @return completed script as a complete string ready to write to disk. */ private static String regenerateScript( final String head, final Item[] items ) { final FastCat sb = new FastCat( items.length + 2 ); // allow for head and final \r\r sb.append( head ); for ( Item item : items ) { // put back separator that split threw away, plus the item is script format. sb.append( item.combine( stripComments ) ); // combined item has lead \r\n but not trailing \r\n } // final newline for end of file. sb.append( "\r\n" ); return sb.toString(); } /** * tell the user what we just did, if he is interested. * * @param quiet true if we should be quiet and say nothing. * @param changed true if file contents changed as a result of sorting. * @param missingBOM true if file needs a leading BOM inserted. */ private static void reportActions( final boolean quiet, final boolean changed, final boolean missingBOM ) { if ( !quiet ) { if ( changed ) { out.println( "Note: " + EIO.getCanOrAbsPath( fileBeingProcessed ) + " script sorted." ); } else { if ( reportAlreadyInOrder ) { out.println( "Note: " + EIO.getCanOrAbsPath( fileBeingProcessed ) + " script was already in " + "order." ); } } if ( missingBOM ) { out.println( "Warning: " + EIO.getCanOrAbsPath( fileBeingProcessed ) + " repaired the missing" + " BOM for " + "UTF-8 at the beginning of the script." ); } } } /** * save sorted file contents back to disk. * * @param sortedContents sorted file contents to write. * @param changed did the data when we sort it? * @param missingBOM is the contents missing a leading BOM? * * @throws IOException if trouble writing the file. */ private static void saveFileBack( final String sortedContents, final boolean changed, final boolean missingBOM ) throws IOException { final String whatToWrite = missingBOM ? BOM + sortedContents : sortedContents; if ( changed || missingBOM ) { HunkIO.writeEntireFile( fileBeingProcessed, whatToWrite, HunkIO.UTF8 ); } } /** * debugging display off all extracted sortable items * * @param items Array of STItems and PathItems to display. */ private static void showAllItems( final Item[] items ) { for ( Item item : items ) { out.println( item.toString() ); } } public static void main( final String[] args ) { if ( args.length <= 0 ) { err.println( USAGE ); System.exit( 1 ); } stripComments = false; for ( int i = 0; i < args.length; i++ ) { if ( args[ i ].equals( "-strip" ) ) { stripComments = true; args[ i ] = null; break; } } final CommandLine commandLine = new CommandLine( args, new AllButSVNDirectoriesFilter(), new ExtensionListFilter( "srs" ) ); final boolean quiet = commandLine.isQuiet(); reportAlreadyInOrder = commandLine.size() <= 3; for ( File file : commandLine ) { processFile( file, quiet ); } } }