/*
* [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 );
}
}
}