001/**
002 * RenameWand 2.2
003 * Copyright 2007 Zach Scrivena
004 * 2007-12-09
005 * zachscrivena@gmail.com
006 * http://renamewand.sourceforge.net/
007 *
008 * RenameWand is a simple command-line utility for renaming files or
009 * directories using an intuitive but powerful syntax.
010 *
011 * TERMS AND CONDITIONS:
012 * This program is free software: you can redistribute it and/or modify
013 * it under the terms of the GNU General Public License as published by
014 * the Free Software Foundation, either version 3 of the License, or
015 * (at your option) any later version.
016 *
017 * This program is distributed in the hope that it will be useful,
018 * but WITHOUT ANY WARRANTY; without even the implied warranty of
019 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
020 * GNU General Public License for more details.
021 *
022 * You should have received a copy of the GNU General Public License
023 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
024 */
025
026package ca.bc.webarts.tools.renamewand;
027
028import java.io.Console;
029import java.io.File;
030import java.io.PrintWriter;
031import java.util.ArrayDeque;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Deque;
035import java.util.Iterator;
036import java.util.LinkedList;
037import java.util.List;
038import java.util.Locale;
039import java.util.Map;
040import java.util.NavigableMap;
041import java.util.Scanner;
042import java.util.regex.Matcher;
043import java.util.regex.Pattern;
044import java.util.TreeMap;
045import java.util.regex.PatternSyntaxException;
046
047
048/**
049 * RenameWand is a simple command-line utility for renaming files or
050 * directories using an intuitive but powerful syntax.
051 */
052public class RenameWand
053{
054  /**************************************
055   * CONSTANTS AND MISCELLANEOUS FIELDS *
056   **************************************/
057
058  /** constant: program title */
059  private static final String PROGRAM_TITLE =
060      "RenameWand 2.2   Copyright 2007 Zach Scrivena   2007-12-09";
061
062  /** constant: special construct separator character */
063  private static final char SPECIAL_CONSTRUCT_SEPARATOR_CHAR = '|';
064
065  /** constant: substring range character */
066  private static final char SUBSTRING_RANGE_CHAR = ':';
067
068  /** constant: substring delimiter character */
069  private static final char SUBSTRING_DELIMITER_CHAR = ',';
070
071  /** constant: integer filter indicator character */
072  private static final char INTEGER_FILTER_INDICATOR_CHAR = '@';
073
074  /** constant: regex pattern for register names */
075  private static final Pattern REGISTER_NAME_PATTERN = Pattern.compile(
076      "[a-zA-Z_][a-zA-Z_0-9]*");
077
078  /**
079   * constant: regex pattern for special construct "<length|@expr>" in source pattern string.
080   * Match groups: (1,"length"), (2,"@"), (3,"expr")
081   */
082  private static final Pattern SOURCE_SPECIAL_CONSTRUCT_PATTERN = Pattern.compile(
083      "\\<(?:([\\sa-zA-Z_0-9\\." +
084      Pattern.quote("+-*/^()[]!" + RenameWand.SUBSTRING_RANGE_CHAR  + RenameWand.SUBSTRING_DELIMITER_CHAR) +
085      "]+)" + Pattern.quote(RenameWand.SPECIAL_CONSTRUCT_SEPARATOR_CHAR + "") + ")?(" +
086      Pattern.quote(RenameWand.INTEGER_FILTER_INDICATOR_CHAR + "") +
087      ")?([\\sa-zA-Z_0-9\\." +
088      Pattern.quote("+-*/^()[]!" + RenameWand.SUBSTRING_RANGE_CHAR  + RenameWand.SUBSTRING_DELIMITER_CHAR) +
089      "]+)\\>");
090
091  /**
092   * constant: regex pattern for special construct "<length|expr>" in target pattern string.
093   * Match groups: (1,"length"), (2,"expr")
094   */
095  private static final Pattern TARGET_SPECIAL_CONSTRUCT_PATTERN = Pattern.compile(
096      "\\<(?:([\\sa-zA-Z_0-9\\." +
097      Pattern.quote("+-*/^()[]#!@" + RenameWand.SUBSTRING_RANGE_CHAR  + RenameWand.SUBSTRING_DELIMITER_CHAR) +
098      "]+)" + Pattern.quote(RenameWand.SPECIAL_CONSTRUCT_SEPARATOR_CHAR + "") +
099      ")?([\\sa-zA-Z_0-9\\." +
100      Pattern.quote("+-*/^()[]#!@" + RenameWand.SUBSTRING_RANGE_CHAR  + RenameWand.SUBSTRING_DELIMITER_CHAR) +
101      "]+)\\>");
102
103  /**
104   * constant: regex pattern for a numeric pattern, e.g. -123.45
105   * Match groups: (1,"+" or "-"), (2,"123.45")
106   */
107  private static final Pattern NUMERIC_PATTERN = Pattern.compile(
108      "([\\+\\-]?)([0-9]*(?:\\.[0-9]*)?)");
109
110  /** constant: regex pattern for a positive integer pattern, e.g. 42 */
111  private static final Pattern POSITIVE_INTEGER_PATTERN = Pattern.compile(
112      "\\+?[0-9]+");
113
114  /** operator precedence table for stack evaluation */
115  private static final Map<String,Integer> OPERATOR_PRECEDENCE = new TreeMap<String,Integer>();
116
117  /** singular noun for file/directory */
118  private static String SINGULAR_NOUN;
119
120  /** plural noun for files/directories */
121  private static String PLURAL_NOUN;
122
123  /** standard output */
124  static PrintWriter stdout = null;
125
126  /** standard error */
127  static PrintWriter stderr = null;
128
129  /** true if this is a Windows OS, false otherwise */
130  private static boolean isWindowsOperatingSystem;
131
132  /** current directory (absolute and canonical pathname) */
133  static File currentDirectory;
134
135  /** full pathname of the current directory (includes trailing separator) */
136  static String currentDirectoryFullPathname;
137
138  /** length of the full pathname of the current directory */
139  static int currentDirectoryFullPathnameLength;
140
141  /** register names mapping (register name ---> capture group index) */
142  static final Map<String,Integer> registerNames = new TreeMap<String,Integer>();
143
144  /** number of capture groups in source regex pattern */
145  private static int numCaptureGroups;
146
147  /** true if the source pattern string is reusable for different files/directories; false otherwise */
148  private static boolean sourcePatternIsReusable = false;
149
150  /** subdirectory counter */
151  private static int numDirs = 0;
152
153  /*********************
154   * RENAME PARAMETERS *
155   *********************/
156
157  /** parameter: simulate only; do not actually rename files/directories (default = false) */
158  private static boolean simulateOnly = false;
159
160  /** parameter: ignore warnings; do not pause (default = false) */
161  private static boolean ignoreWarnings = false;
162
163  /** parameter: recurse into subdirectories (default = false) */
164  private static boolean recurseIntoSubdirectories = false;
165
166  /** parameter: automatically rename files/directories without prompting (default = false) */
167  private static boolean automaticRename = false;
168
169  /** parameter: match relative pathname, not just the name, of the files/directories (default = false) */
170  private static boolean matchRelativePathname = false;
171
172  /** parameter: match lower case name of the files/directories (default = false) */
173  private static boolean matchLowerCase = false;
174
175  /** parameter: true if renaming directories; false if renaming files (default = false) */
176  private static boolean renameDirectories = false;
177
178  /** parameter: default action on rename operation error (default = '\0') */
179  private static char defaultActionOnRenameOperationError = '\0';
180
181  /** parameter: source pattern string */
182  private static String sourcePatternString;
183
184  /** parameter: target pattern string */
185  private static String targetPatternString;
186
187  /*********************
188   * REPORT STATISTICS *
189   *********************/
190
191  /** statistic: number of warnings encountered */
192  private static int reportNumWarnings = 0;
193
194
195  /**
196   * Main entry point for the RenameWand program.
197   *
198   * @param args
199   *     Command-line argument strings
200   */
201  public static void main(
202      final String[] args)
203  {
204    /* initialize standard output and error streams */
205    final Console console = System.console();
206
207    if (console == null)
208    {
209      RenameWand.stdout = new PrintWriter(System.out);
210      RenameWand.stderr = new PrintWriter(System.err);
211    }
212    else
213    {
214      RenameWand.stdout = console.writer();
215      RenameWand.stderr = console.writer();
216    }
217
218    RenameWand.stdout.print("\n" + RenameWand.PROGRAM_TITLE);
219    RenameWand.stdout.flush();
220
221    /* exit status code to be reported to the OS when exiting (default = 0) */
222    int exitCode = 0;
223
224    try
225    {
226      /* determine if this is a Windows OS */
227      RenameWand.isWindowsOperatingSystem = System.getProperty("os.name").toUpperCase(Locale.ENGLISH).contains("WINDOWS") &&
228          (File.separatorChar == '\\');
229
230      /* initialize operator precedence table for stack evaluation */
231      RenameWand.OPERATOR_PRECEDENCE.put("#",   7);
232      RenameWand.OPERATOR_PRECEDENCE.put("#!",  7);
233      RenameWand.OPERATOR_PRECEDENCE.put("##",  7);
234      RenameWand.OPERATOR_PRECEDENCE.put("##!", 7);
235      RenameWand.OPERATOR_PRECEDENCE.put("@",   7);
236      RenameWand.OPERATOR_PRECEDENCE.put("@!",  7);
237      RenameWand.OPERATOR_PRECEDENCE.put("@@",  7);
238      RenameWand.OPERATOR_PRECEDENCE.put("@@!", 7);
239      RenameWand.OPERATOR_PRECEDENCE.put("^",   6);
240      RenameWand.OPERATOR_PRECEDENCE.put("~",   5); // unary minus (negative) sign
241      RenameWand.OPERATOR_PRECEDENCE.put("*",   4);
242      RenameWand.OPERATOR_PRECEDENCE.put("/",   4);
243      RenameWand.OPERATOR_PRECEDENCE.put("+",   3);
244      RenameWand.OPERATOR_PRECEDENCE.put("-",   3);
245      RenameWand.OPERATOR_PRECEDENCE.put(RenameWand.SUBSTRING_RANGE_CHAR  + "", 2);
246      RenameWand.OPERATOR_PRECEDENCE.put(RenameWand.SUBSTRING_DELIMITER_CHAR + "", 1);
247
248      /* process command-line arguments and configure rename parameters */
249      processArguments(args);
250
251      /* nouns for file/directory */
252      RenameWand.SINGULAR_NOUN = RenameWand.renameDirectories ? "directory" : "file";
253      RenameWand.PLURAL_NOUN = RenameWand.renameDirectories ? "directories" : "files";
254
255      RenameWand.stdout.print("\n\nSource pattern: \"" + RenameWand.sourcePatternString +
256          "\"\nTarget pattern: \"" + RenameWand.targetPatternString +
257          "\"\n\nGetting all candidate " + RenameWand.SINGULAR_NOUN +
258          " names in the current directory" +
259          (RenameWand.recurseIntoSubdirectories ? " recursively..." : "..."));
260      RenameWand.stdout.flush();
261
262      /* files/directories to be renamed */
263      List<FileUnit> files = null;
264
265      /* get match candidates */
266      files = getMatchCandidates();
267      final int numMatchCandidates = files.size();
268
269      if (numMatchCandidates == 0)
270      {
271        RenameWand.stdout.print("\nNo candidate " + RenameWand.SINGULAR_NOUN + " names to match.");
272      }
273      else
274      {
275        /* perform source pattern matching on candidate file/directory names */
276        RenameWand.stdout.print("\nPerforming source pattern matching on " + numMatchCandidates +
277            " candidate " + RenameWand.SINGULAR_NOUN + " " +
278            ((numMatchCandidates == 1) ? "name" : "names") + "...");
279        RenameWand.stdout.flush();
280
281        files = performSourcePatternMatching(files);
282        final int numMatched = files.size();
283
284        RenameWand.stdout.print("\n" + numMatched + " out of " + numMatchCandidates +
285            " " + RenameWand.SINGULAR_NOUN + " " +
286            ((numMatched == 1) ? "name" : "names") + " matched.");
287        RenameWand.stdout.flush();
288
289        if (numMatched == 0)
290        {
291          RenameWand.stdout.print("\nNo " + RenameWand.PLURAL_NOUN + " to rename.");
292        }
293        else
294        {
295          /* evaluate target file/directory names */
296          RenameWand.stdout.print("\nDetermining target " + RenameWand.SINGULAR_NOUN + " " +
297              ((numMatched == 1) ? "name" : "names") + " and renaming sequence...");
298          RenameWand.stdout.flush();
299
300          evaluateTargetPattern(files.toArray(new FileUnit[numMatched]));
301
302          /* determine renaming sequence and find clashes, bad names, etc. */
303          final List<RenameFilePair> renameOperations = getRenameOperations(files);
304          final int numRenameOperations = renameOperations.size();
305
306          /* prompt user before renaming */
307          final boolean proceedToRename = promptUserOnRename(files, numRenameOperations);
308
309          /* perform rename operations */
310          final int numRenameOperationsPerformed = proceedToRename ? performRenameOperations(renameOperations) : 0;
311
312          if (!RenameWand.simulateOnly && proceedToRename)
313          {
314            RenameWand.stdout.print("\n\n" + numRenameOperationsPerformed + " out of " +
315                numRenameOperations + " " + RenameWand.SINGULAR_NOUN + " rename " +
316                ((numRenameOperations == 1) ? "operation" : "operations") + " performed.");
317            RenameWand.stdout.flush();
318          }
319
320          /* report statistics */
321          final StringBuilder report = new StringBuilder();
322          report.append("\n\n" + (RenameWand.renameDirectories ? "DIRECTORY" : "FILE") + " RENAME REPORT");
323
324          if (RenameWand.reportNumWarnings > 0)
325          {
326            report.append("\n " + RenameWand.reportNumWarnings + " " +
327                ((RenameWand.reportNumWarnings == 1) ? "warning" : "warnings") + " encountered.");
328          }
329
330          report.append("\n No. of candidate " + RenameWand.SINGULAR_NOUN + " names to match  : " + numMatchCandidates +
331              "\n No. of " + RenameWand.SINGULAR_NOUN + " names matched             : " + numMatched +
332              "\n No. of " + RenameWand.SINGULAR_NOUN + " rename operations required: " + numRenameOperations);
333
334          if (!RenameWand.simulateOnly && (numRenameOperations > 0))
335          {
336            report.append("\n No. of successful " + RenameWand.SINGULAR_NOUN +
337                " rename operations performed: " + numRenameOperationsPerformed);
338          }
339
340          RenameWand.stdout.print(report.toString());
341        }
342      }
343
344      RenameWand.stdout.print("\n\nRenameWand is done!\n\n");
345    }
346    catch (TerminatingException e)
347    {
348      /* terminating exception thrown; proceed to abort program */
349      /* (this should be the only place where a TerminatingException is caught) */
350
351      exitCode = e.getExitCode();
352
353      if (exitCode != 0)
354      {
355        /* abnormal termination; print error message */
356        RenameWand.stderr.print("\n\nERROR: " + e.getMessage() + "\n");
357        RenameWand.stdout.print("\nRenameWand aborted.\n\n");
358      }
359    }
360    catch (Exception e)
361    {
362      /* catch all other exceptions; proceed to abort program */
363      RenameWand.stderr.print("\n\nERROR: An unexpected error has occurred:\n" +
364          getExceptionInformation(e) + "\n");
365
366      exitCode = 1;
367      RenameWand.stdout.print("\nRenameWand aborted.\n\n");
368    }
369    finally
370    {
371      /* perform clean-up before exiting */
372      RenameWand.stderr.flush();
373      RenameWand.stdout.flush();
374    }
375
376    System.exit(exitCode);
377  }
378
379
380  /**
381   * Process command-line arguments.
382   *
383   * @param args
384   *     Command-line argument strings
385   */
386  private static void processArguments(
387      final String[] args)
388  {
389    final String howHelp = "\nTo display help, run RenameWand without any command-line arguments.";
390
391    /* print usage documentation, if no arguments */
392    if (args.length == 0)
393    {
394      printUsage();
395      throw new TerminatingException(null, 0);
396    }
397
398    if (args.length < 2)
399      throw new TerminatingException("Insufficient arguments:\nThe source and target pattern strings must be specified." + howHelp);
400
401    /* source and target pattern strings */
402    RenameWand.sourcePatternString = args[args.length - 2];
403    RenameWand.targetPatternString = args[args.length - 1];
404
405    /* check for illegal characters in pattern strings */
406    if (RenameWand.sourcePatternString.contains("\0"))
407      throw new TerminatingException("Illegal null-character found in source pattern string.");
408
409    if (RenameWand.targetPatternString.contains("\0"))
410      throw new TerminatingException("Illegal null-character found in target pattern string.");
411
412    /* default action on rename operation error */
413    int skipOnRenameOperationError = 0;
414    int undoAllOnRenameOperationError = 0;
415    int abortOnRenameOperationError = 0;
416
417    /* process command-line switches */
418    for (int i = 0; i < args.length - 2; i++)
419    {
420      final String sw = args[i];
421
422      if ("--recurse".equals(sw) || "-r".equals(sw))
423      {
424        /* recurse into subdirectories */
425        RenameWand.recurseIntoSubdirectories = true;
426      }
427      else if ("--dirs".equals(sw) || "-d".equals(sw))
428      {
429        /* rename directories instead of files */
430        RenameWand.renameDirectories = true;
431      }
432      else if ("--path".equals(sw) || "-p".equals(sw))
433      {
434        /* match relative pathname, not just the name, of the files/directories */
435        RenameWand.matchRelativePathname = true;
436      }
437      else if ("--lower".equals(sw) || "-l".equals(sw))
438      {
439        /* match lower case name of the files/directories */
440        RenameWand.matchLowerCase = true;
441      }
442      else if ("--yes".equals(sw) || "-y".equals(sw))
443      {
444        /* automatically rename files/directories without prompting */
445        RenameWand.automaticRename = true;
446      }
447      else if ("--simulate".equals(sw) || "-s".equals(sw))
448      {
449        /* simulate only; do not actually rename files/directories */
450        RenameWand.simulateOnly = true;
451        RenameWand.ignoreWarnings = true;
452      }
453      else if ("--ignorewarnings".equals(sw) || "-i".equals(sw))
454      {
455        /* ignore warnings; do not pause */
456        RenameWand.ignoreWarnings = true;
457      }
458      else if ("--skip".equals(sw))
459      {
460        /* skip on rename operation error */
461        skipOnRenameOperationError = 1;
462      }
463      else if ("--undoall".equals(sw))
464      {
465        /* undo all on rename operation error */
466        undoAllOnRenameOperationError = 1;
467      }
468      else if ("--abort".equals(sw))
469      {
470        /* abort on rename operation error */
471        abortOnRenameOperationError = 1;
472      }
473      else
474      {
475        /* invalid switch */
476        throw new TerminatingException("\"" + sw + "\" is not a valid switch." + howHelp);
477      }
478    }
479
480    if (RenameWand.simulateOnly && RenameWand.automaticRename)
481      throw new TerminatingException("Switches --simulate and --yes cannot be used together." + howHelp);
482
483    if (skipOnRenameOperationError + undoAllOnRenameOperationError + abortOnRenameOperationError > 1)
484      throw new TerminatingException("Only one of the three switches --skip, --undoall, and --abort may be specified." + howHelp);
485
486    if (RenameWand.simulateOnly && (skipOnRenameOperationError + undoAllOnRenameOperationError + abortOnRenameOperationError > 0))
487      throw new TerminatingException("Switches --skip, --undoall, and --abort cannot be used together with --simulate." + howHelp);
488
489    /* default action on rename operation error */
490    if (skipOnRenameOperationError > 0)
491      RenameWand.defaultActionOnRenameOperationError = 'S';
492
493    if (undoAllOnRenameOperationError > 0)
494      RenameWand.defaultActionOnRenameOperationError = 'U';
495
496    if (abortOnRenameOperationError > 0)
497      RenameWand.defaultActionOnRenameOperationError = 'A';
498  }
499
500
501  /**
502   * Scan current directory to get candidate files/directories for matching.
503   *
504   * @return
505   *     Candidate files/directories for matching
506   */
507  private static List<FileUnit> getMatchCandidates()
508  {
509    /* get absolute canonical path of the current directory */
510    RenameWand.currentDirectory = new File("");
511
512    try
513    {
514      RenameWand.currentDirectory = RenameWand.currentDirectory.getCanonicalFile();
515    }
516    catch (Exception e)
517    {
518      throw new TerminatingException("Failed to get full pathname of the current directory \"" +
519          RenameWand.currentDirectory.getPath() + "\":\n" + getExceptionInformation(e));
520    }
521
522    RenameWand.currentDirectoryFullPathname = RenameWand.currentDirectory.getPath();
523
524    /* include trailing separator */
525    if (!RenameWand.currentDirectoryFullPathname.endsWith(File.separator))
526      RenameWand.currentDirectoryFullPathname += File.separator;
527
528    RenameWand.currentDirectoryFullPathnameLength = RenameWand.currentDirectoryFullPathname.length();
529
530    /* return value: match candidate files/directories */
531    final List<FileUnit> matchCandidates = new ArrayList<FileUnit>();
532
533    /* stack containing the subdirectories to be scanned */
534    final Deque<File> subdirectories = new ArrayDeque<File>();
535    subdirectories.push(RenameWand.currentDirectory);
536
537    /* reset number of subdirectories scanned */
538    RenameWand.numDirs = 0;
539
540    /* perform a DFS scanning of the subdirectories */
541    while (!subdirectories.isEmpty())
542    {
543      RenameWand.numDirs++;
544
545      /* get a directory to be scanned */
546      final File dir = subdirectories.pop();
547      final File[] listFiles = dir.listFiles();
548
549      if (listFiles == null)
550      {
551        final String path = dir.getPath();
552
553        reportWarning("Failed to get contents of directory \"" + path +
554            (path.endsWith(File.separator) ? "" : File.separator) +
555            "\".\nThis directory will be ignored.");
556      }
557      else
558      {
559        /* subdirectories under this directory */
560        final List<File> subdirs = new ArrayList<File>();
561
562        for (File f : listFiles)
563        {
564          final boolean isDirectory = f.isDirectory();
565
566          if (RenameWand.renameDirectories == isDirectory)
567          {
568            final FileUnit u = new FileUnit();
569            u.source = f;
570            u.parentDirId = RenameWand.numDirs;
571            matchCandidates.add(u);
572          }
573
574          if (isDirectory)
575            subdirs.add(f);
576        }
577
578        if (RenameWand.recurseIntoSubdirectories)
579        {
580          for (int i = subdirs.size() - 1; i >= 0; i--)
581            subdirectories.push(subdirs.get(i));
582        }
583      }
584    }
585
586    return matchCandidates;
587  }
588
589
590  /**
591   * Perform source pattern matching against the names of the candidate
592   * files/directories, and return files/directories that match.
593   *
594   * @param matchCandidates
595   *     Candidate files/directories
596   * @return
597   *     Files/directories with names that match the source pattern
598   */
599  private static List<FileUnit> performSourcePatternMatching(
600      final List<FileUnit> matchCandidates)
601  {
602    /* return value: files/directories with names that match the source pattern */
603    final List<FileUnit> matched = new ArrayList<FileUnit>();
604
605    /* regex pattern used for matching file/directory names */
606    Pattern sourcePattern = null;
607
608    /* is the regex pattern matcher reusable for different files/directories? */
609    RenameWand.sourcePatternIsReusable = false;
610
611    /* match each candidate file or directory */
612    for (FileUnit u : matchCandidates)
613    {
614      if (!RenameWand.sourcePatternIsReusable)
615        sourcePattern = getFileSourcePattern(u);
616
617      /* check if source pattern is successfully generated */
618      if (sourcePattern != null)
619      {
620        /* name string to be matched */
621        String name = null;
622
623        if (RenameWand.matchRelativePathname)
624        {
625          name = u.source.getPath();
626
627          if (name.startsWith(RenameWand.currentDirectoryFullPathname))
628            name = name.substring(RenameWand.currentDirectoryFullPathnameLength);
629        }
630        else
631        {
632          name = u.source.getName();
633        }
634
635        /* trim off trailing separator */
636        while (name.endsWith(File.separator))
637          name = name.substring(0, name.length() - File.separator.length());
638
639        if (RenameWand.matchLowerCase)
640          name = name.toLowerCase(Locale.ENGLISH);
641
642        /* regex pattern matcher */
643        final Matcher sourceMatcher = sourcePattern.matcher(name);
644
645        if (sourceMatcher.matches())
646        {
647          /* add capture group values to FileUnit's registerValues, and */
648          /* add this file/directory to our list of successful matches  */
649
650          u.registerValues = new String[RenameWand.numCaptureGroups + 1]; // add index offset 1
651
652          for (int i = 1; i <= RenameWand.numCaptureGroups; i++)
653            u.registerValues[i] = sourceMatcher.group(i);
654
655          matched.add(u);
656        }
657      }
658    }
659
660    return matched;
661  }
662
663
664  /**
665   * Generate source regex pattern corresponding to the given file/directory.
666   *
667   * @param u
668   *     File/directory for which to generate source regex pattern
669   * @return
670   *     Source regex pattern corresponding to the given file/directory;
671   *     null if regex pattern cannot be generated.
672   */
673  private static Pattern getFileSourcePattern(
674      final FileUnit u)
675  {
676    /* reset register names and capture group counter */
677    RenameWand.registerNames.clear();
678    int captureGroupIndex = 0;
679
680    /* assume that source pattern is reusable */
681    RenameWand.sourcePatternIsReusable = true;
682
683    /* Stack to keep track of the parser mode: */
684    /* "--" : Base mode (first on the stack)   */
685    /* "[]" : Square brackets mode "[...]"     */
686    /* "{}" : Curly braces mode "{...}"        */
687    final Deque<String> parserMode = new ArrayDeque<String>();
688    parserMode.push("--"); // base mode
689
690    final int sourcePatternStringLength = RenameWand.sourcePatternString.length();
691    int index = 0; // index in sourcePatternString
692
693    /* regex pattern equivalent to sourcePatternString */
694    final StringBuilder sourceRegex = new StringBuilder();
695
696    /* parse each character of the source pattern string */
697    while (index < sourcePatternStringLength)
698    {
699      char c = RenameWand.sourcePatternString.charAt(index++);
700
701      if (c == '\\')
702      {
703        /***********************
704         * (1) ESCAPE SEQUENCE *
705         ***********************/
706
707        if (index == sourcePatternStringLength)
708        {
709          /* no characters left, so treat '\' as literal char */
710          sourceRegex.append(Pattern.quote("\\"));
711        }
712        else
713        {
714          /* read next character */
715          c = RenameWand.sourcePatternString.charAt(index);
716          final String s = c + "";
717
718          if (("--".equals(parserMode.peek()) && "\\<>[]{}?*".contains(s)) ||
719              ("[]".equals(parserMode.peek()) && "\\<>[]{}?*!-".contains(s)) ||
720              ("{}".equals(parserMode.peek()) && "\\<>[]{}?*,".contains(s)))
721          {
722            /* escape the construct char */
723            index++;
724            sourceRegex.append(Pattern.quote(s));
725          }
726          else
727          {
728            /* treat '\' as literal char */
729            sourceRegex.append(Pattern.quote("\\"));
730          }
731        }
732      }
733      else if (c == '*')
734      {
735        /************************
736         * (2) GLOB PATTERN '*' *
737         ************************/
738
739        /* create non-capturing group to match zero or more characters */
740        sourceRegex.append(".*");
741      }
742      else if (c == '?')
743      {
744        /************************
745         * (3) GLOB PATTERN '?' *
746         ************************/
747
748        /* create non-capturing group to match exactly one character */
749        sourceRegex.append('.');
750      }
751      else if (c == '[')
752      {
753        /****************************
754         * (4) GLOB PATTERN "[...]" *
755         ****************************/
756
757        /* opening square bracket '[' */
758        /* create non-capturing group to match exactly one character */
759        /* inside the sequence */
760        sourceRegex.append('[');
761        parserMode.push("[]");
762
763        /* check for negation character '!' immediately after */
764        /* the opening bracket '[' */
765        if ((index < sourcePatternStringLength) &&
766            (RenameWand.sourcePatternString.charAt(index) == '!'))
767        {
768          index++;
769          sourceRegex.append('^');
770        }
771      }
772      else if ((c == ']') && "[]".equals(parserMode.peek()))
773      {
774        /* closing square bracket ']' */
775        sourceRegex.append(']');
776        parserMode.pop();
777      }
778      else if ((c == '-') && "[]".equals(parserMode.peek()))
779      {
780        /* character range '-' in "[...]" */
781        sourceRegex.append('-');
782      }
783      else if (c == '{')
784      {
785        /****************************
786         * (5) GLOB PATTERN "{...}" *
787         ****************************/
788
789        /* opening curly brace '{' */
790        /* create non-capturing group to match one of the */
791        /* strings inside the sequence */
792        sourceRegex.append("(?:(?:");
793        parserMode.push("{}");
794      }
795      else if ((c == '}') && "{}".equals(parserMode.peek()))
796      {
797        /* closing curly brace '}' */
798        sourceRegex.append("))");
799        parserMode.pop();
800      }
801      else if ((c == ',') && "{}".equals(parserMode.peek()))
802      {
803        /* comma between strings in "{...}" */
804        sourceRegex.append(")|(?:");
805      }
806      else if (c == '<')
807      {
808        /*********************************
809         * (6) SPECIAL CONSTRUCT "<...>" *
810         *********************************/
811
812        final StringBuilder specialConstruct = new StringBuilder("<");
813        boolean closingAngleBracketFound = false;
814
815        /* read until the first (unescaped) closing '>' */
816        while (!closingAngleBracketFound && (index < sourcePatternStringLength))
817        {
818          c = RenameWand.sourcePatternString.charAt(index++);
819          specialConstruct.append(c);
820
821          if ((c == '>') && (specialConstruct.charAt(specialConstruct.length() - 2) != '\\'))
822            closingAngleBracketFound = true;
823        }
824
825        if (!closingAngleBracketFound)
826          throw new TerminatingException("Failed to find matching closing angle bracket > for special construct \"" +
827              specialConstruct + "\" in source pattern string. Please ensure that each literal < is escaped as \\<.");
828
829        /* check if special construct is in the form "<length|@expr>" */
830        final Matcher specialConstructMatcher =
831            RenameWand.SOURCE_SPECIAL_CONSTRUCT_PATTERN.matcher(specialConstruct);
832
833        if (!specialConstructMatcher.matches())
834          throw new TerminatingException("Invalid special construct \"" + specialConstruct + "\" in source pattern string: " +
835              "Not in the form <length" + RenameWand.SPECIAL_CONSTRUCT_SEPARATOR_CHAR + RenameWand.INTEGER_FILTER_INDICATOR_CHAR + "expr>.");
836
837        /* match groups */
838        String length = specialConstructMatcher.group(1);
839        if (length != null) length = length.trim();
840        final boolean integerFilter = (specialConstructMatcher.group(2) != null);
841        String expr = specialConstructMatcher.group(3).trim();
842
843        /* evaluate the length string if it is not already a positive integer */
844        if ((length != null) &&
845            !RenameWand.POSITIVE_INTEGER_PATTERN.matcher(length).matches())
846        {
847          RenameWand.sourcePatternIsReusable = false; // this construct is file-specific
848          final EvaluationResult<String> result = evaluateSpecialConstruct(new FileUnit[] {u}, length);
849
850          if (result.success)
851          {
852            length = result.output[0];
853
854            /* check that length string is a positive integer now */
855            if (!RenameWand.POSITIVE_INTEGER_PATTERN.matcher(length).matches())
856              throw new TerminatingException("Invalid length string for special construct \"" +
857                  specialConstruct + "\" in source pattern string for " +
858                  RenameWand.SINGULAR_NOUN + " \"" + u.source.getPath() + "\": " +
859                  length + " is not a positive integer.");
860          }
861          else
862          {
863            reportWarning("Failed to evaluate length string of special construct \"" +
864                specialConstruct + "\" in source pattern string for " +
865                RenameWand.SINGULAR_NOUN + " \"" + u.source.getPath() + "\": " +
866                result.error + "\nThis " + RenameWand.SINGULAR_NOUN + " will be ignored.");
867
868            return null; // failed to generate regex pattern
869          }
870        }
871
872        /* check if this construct is a register assignment or back reference */
873        if (((length == null) || RenameWand.POSITIVE_INTEGER_PATTERN.matcher(length).matches()) &&
874            (u.evaluateMacro(expr) == null) &&
875            RenameWand.REGISTER_NAME_PATTERN.matcher(expr).matches())
876        {
877          if (RenameWand.registerNames.containsKey(expr))
878          {
879            /* register is already captured, so we create a back reference to it */
880            if ((length != null) || integerFilter)
881              throw new TerminatingException("Invalid back reference \"" + specialConstruct +
882                  "\" to register \"" + expr + "\" near position " + index +
883                  " of source pattern string: Length string and integer filter indicator @ not allowed.");
884
885            sourceRegex.append("\\" + RenameWand.registerNames.get(expr));
886          }
887          else
888          {
889            /* register has not been captured, so we create a regex capturing group for it */
890            RenameWand.registerNames.put(expr, ++captureGroupIndex);
891
892            sourceRegex.append("(" + (integerFilter ? "[0-9]" : ".") +
893                ((length == null) ? "*" : ("{" + Integer.parseInt(length.trim()) + "}")) + ")");
894          }
895        }
896        else
897        {
898          /* proceed to parse the expression string */
899
900          RenameWand.sourcePatternIsReusable = false; // this construct is file-specific
901
902          if (integerFilter)
903            throw new TerminatingException("Invalid special construct expression \"" + specialConstruct +
904                "\" in source pattern string: Integer filter indicator @ not allowed here because \"" +
905                expr + "\" is not a register name.");
906
907          /* evaluate expr string */
908          final EvaluationResult<String> result = evaluateSpecialConstruct(new FileUnit[] {u}, expr);
909
910          if (result.success)
911          {
912            expr = result.output[0];
913
914            /* perform length formatting if specified */
915            if (length != null)
916              expr = padString(expr, Integer.parseInt(length), isNumeric(expr));
917
918            /* convert literal string to a regex string */
919            sourceRegex.append(Pattern.quote(expr));
920          }
921          else
922          {
923            reportWarning("Failed to evaluate expression string of special construct \"" +
924                specialConstruct + "\" in source pattern string for " +
925                RenameWand.SINGULAR_NOUN + " \"" + u.source.getPath() + "\": " +
926                result.error + "\nThis " + RenameWand.SINGULAR_NOUN + " will be ignored.");
927
928            return null; // failed to generate regex pattern
929          }
930        }
931      }
932      else if ((c == '/') && RenameWand.isWindowsOperatingSystem)
933      {
934        /****************************************
935         * (7) ALTERNATE WINDOWS FILE SEPARATOR *
936         ****************************************/
937
938        sourceRegex.append(Pattern.quote("\\"));
939      }
940      else
941      {
942        /*************************
943         * (8) LITERAL CHARACTER *
944         *************************/
945
946        /* convert literal character to a regex string */
947        sourceRegex.append(Pattern.quote(c + ""));
948      }
949    }
950
951    /* check for mismatched [...] or {...} */
952    if ("[]".equals(parserMode.peek()))
953      throw new TerminatingException("Failed to find matching closing square bracket ] in source pattern string.");
954
955    if ("{}".equals(parserMode.peek()))
956      throw new TerminatingException("Failed to find matching closing curly brace } in source pattern string.");
957
958    /* set total number of capture groups in the source pattern */
959    RenameWand.numCaptureGroups = captureGroupIndex;
960
961    /* compile regex string */
962    Pattern sourceRegexCompiledPattern = null;
963
964    try
965    {
966      sourceRegexCompiledPattern = Pattern.compile(sourceRegex.toString());
967    }
968    catch (PatternSyntaxException e)
969    {
970      throw new TerminatingException("Failed to compile source pattern string for " +
971          RenameWand.SINGULAR_NOUN + " \"" + u.source.getPath() + "\":\n" +
972          getExceptionInformation(e));
973    }
974
975    return sourceRegexCompiledPattern;
976  }
977
978
979  /**
980   * Evaluate the target pattern for the given matched files/directories,
981   * and store the results in the respective FileUnit's.
982   *
983   * @param matchedFiles
984   *     Matched files/directories for which to evaluate the target pattern
985   */
986  private static void evaluateTargetPattern(
987      final FileUnit[] matchedFiles)
988  {
989    /* number of matched files */
990    final int n = matchedFiles.length;
991
992    /* count number of files in each local directory */
993    int[] localCounts = new int[RenameWand.numDirs + 1];
994
995    Arrays.fill(localCounts, 0);
996
997    for (FileUnit u : matchedFiles)
998      localCounts[u.parentDirId]++;
999
1000    /* reset file/directory attributes */
1001    for (FileUnit u : matchedFiles)
1002    {
1003      u.globalCount = n;
1004      u.localCount = localCounts[u.parentDirId];
1005      u.targetFilename = new StringBuilder();
1006    }
1007
1008    final int targetPatternStringLength = RenameWand.targetPatternString.length();
1009    int index = 0; // index in targetPatternString
1010
1011    /* parse each character of the target pattern string */
1012    while (index < targetPatternStringLength)
1013    {
1014      char c = RenameWand.targetPatternString.charAt(index++);
1015
1016      if (c == '\\')
1017      {
1018        /***********************
1019         * (1) ESCAPE SEQUENCE *
1020         ***********************/
1021
1022        if (index == targetPatternStringLength)
1023        {
1024          /* no characters left, so treat '\' as literal char */
1025          for (FileUnit u : matchedFiles)
1026            u.targetFilename.append('\\');
1027        }
1028        else
1029        {
1030          /* read next char */
1031          c = RenameWand.targetPatternString.charAt(index);
1032
1033          if ((c == '<') || (c == '\\'))
1034          {
1035            /* escape the construct char */
1036            index++;
1037
1038            for (FileUnit u : matchedFiles)
1039              u.targetFilename.append(c);
1040          }
1041          else
1042          {
1043            /* treat '\' as literal char */
1044            for (FileUnit u : matchedFiles)
1045              u.targetFilename.append('\\');
1046          }
1047        }
1048      }
1049      else if (c == '<')
1050      {
1051        /*********************************
1052         * (2) SPECIAL CONSTRUCT "<...>" *
1053         *********************************/
1054
1055        final StringBuilder specialConstruct = new StringBuilder("<");
1056        boolean closingAngleBracketFound = false;
1057
1058        /* read until the first (unescaped) closing '>' */
1059        while ((!closingAngleBracketFound) && (index < targetPatternStringLength))
1060        {
1061          c = RenameWand.targetPatternString.charAt(index++);
1062          specialConstruct.append(c);
1063
1064          if ((c == '>') && (specialConstruct.charAt(specialConstruct.length() - 2) != '\\'))
1065            closingAngleBracketFound = true;
1066        }
1067
1068        if (!closingAngleBracketFound)
1069          throw new TerminatingException("Failed to find matching closing angle bracket > for special construct \"" +
1070              specialConstruct + "\" in target pattern string. Please ensure that each literal < is escaped as \\<.");
1071
1072        /* check if special construct is in the form "<length|expr>" */
1073        final Matcher specialConstructMatcher =
1074            RenameWand.TARGET_SPECIAL_CONSTRUCT_PATTERN.matcher(specialConstruct);
1075
1076        if (!specialConstructMatcher.matches())
1077          throw new TerminatingException("Invalid special construct \"" + specialConstruct +
1078              "\" in target pattern string: Not in the form <length" + RenameWand.SPECIAL_CONSTRUCT_SEPARATOR_CHAR + "expr>.");
1079
1080        /* match groups */
1081        final String length = specialConstructMatcher.group(1);
1082        final String expr = specialConstructMatcher.group(2).trim();
1083
1084        String[] lengths = null;
1085
1086        /* evaluate the length string if it is not already a positive integer */
1087        if ((length != null) &&
1088            (!RenameWand.POSITIVE_INTEGER_PATTERN.matcher(length).matches()))
1089        {
1090          final EvaluationResult<String> result = evaluateSpecialConstruct(matchedFiles, length);
1091
1092          if (result.success)
1093          {
1094            lengths = result.output;
1095
1096            /* check that length string is a positive integer now */
1097            for (int i = 0; i < n; i++)
1098              if (!RenameWand.POSITIVE_INTEGER_PATTERN.matcher(lengths[i]).matches())
1099                throw new TerminatingException("Invalid length string for special construct \"" +
1100                    specialConstruct + "\" in target pattern string for " + SINGULAR_NOUN + " \"" +
1101                    matchedFiles[i].source.getPath() + "\": " + lengths[i] + " is not a positive integer.");
1102          }
1103          else
1104          {
1105            throw new TerminatingException("Failed to evaluate length string of special construct \"" +
1106                specialConstruct + "\" in target pattern string: " + result.error);
1107          }
1108        }
1109
1110        /* proceed to parse the expression string */
1111        final EvaluationResult<String> result = evaluateSpecialConstruct(matchedFiles, expr);
1112
1113        if (!result.success)
1114          throw new TerminatingException("Failed to evaluate expression string of special construct \"" +
1115              specialConstruct + "\" in target pattern string: " + result.error);
1116
1117        /* perform length formatting */
1118        if (lengths == null)
1119        {
1120          if (length == null)
1121          {
1122            /* no length string specified */
1123            for (int i = 0; i < n; i++)
1124              matchedFiles[i].targetFilename.append(result.output[i]);
1125          }
1126          else
1127          {
1128            /* constant length string specified */
1129            final int len = Integer.parseInt(length);
1130
1131            /* check if all expr are numeric */
1132            final boolean isNumeric = (getNonNumericIndex(result.output) < 0);
1133
1134            for (int i = 0; i < n; i++)
1135              matchedFiles[i].targetFilename.append(
1136                  padString(result.output[i], len, isNumeric));
1137          }
1138        }
1139        else
1140        {
1141          /* file-specific length strings used */
1142
1143          /* check if all expr are numeric */
1144          final boolean isNumeric = (getNonNumericIndex(result.output) < 0);
1145
1146          for (int i = 0; i < n; i++)
1147            matchedFiles[i].targetFilename.append(
1148                padString(result.output[i], Integer.parseInt(lengths[i]), isNumeric));
1149        }
1150      }
1151      else if ((c == '/') && RenameWand.isWindowsOperatingSystem)
1152      {
1153        /****************************************
1154         * (3) ALTERNATE WINDOWS FILE SEPARATOR *
1155         ****************************************/
1156
1157        for (FileUnit u : matchedFiles)
1158          u.targetFilename.append('\\');
1159      }
1160      else
1161      {
1162        /*************************
1163         * (4) LITERAL CHARACTER *
1164         *************************/
1165
1166        /* handle all other characters as literals */
1167        for (FileUnit u : matchedFiles)
1168          u.targetFilename.append(c);
1169      }
1170    }
1171  }
1172
1173
1174  /**
1175   * Evaluate the given special construct for the given files/directories, and
1176   * return the results.
1177   *
1178   * @param file
1179   *     Files/directories for which to evaluate the given special construct
1180   * @param specialConstruct
1181   *     Special construct string
1182   * @return
1183   *     Results of evaluation
1184   */
1185  private static EvaluationResult<String> evaluateSpecialConstruct(
1186      final FileUnit[] files,
1187      final String specialConstruct)
1188  {
1189    /* use a stack to evaluate the special construct */
1190
1191    /* return value */
1192    final EvaluationResult<String> result = new EvaluationResult<String>();
1193
1194    /* tokenize expr */
1195    final StringManipulator.Token[] tempTokens = StringManipulator.tokenize(
1196        "(" + specialConstruct + ")",  /* surround special construct with (...) */
1197        "(((\\#++)\\!?)|((\\@++)\\!?)|[" +
1198        Pattern.quote("[]()*/^+-" + RenameWand.SUBSTRING_RANGE_CHAR  + RenameWand.SUBSTRING_DELIMITER_CHAR) + "])",
1199        true);
1200
1201    /* preprocess tokens */
1202    final List<StringManipulator.Token> tokens = new ArrayList<StringManipulator.Token>();
1203
1204    for (StringManipulator.Token token : tempTokens)
1205    {
1206      token.val = token.val.trim();
1207
1208      if (token.val.isEmpty())
1209        continue;
1210
1211      if ("-".equals(token.val) &&
1212          tokens.get(tokens.size() - 1).isDelimiter &&
1213          !")]".contains(tokens.get(tokens.size() - 1).val))
1214      {
1215        /* unary minus (negative) sign */
1216        token.val = "~";
1217      }
1218      else if  ("(".equals(token.val) &&
1219          (tokens.size() > 0) &&
1220          (!tokens.get(tokens.size() - 1).isDelimiter ||
1221          ")".equals(tokens.get(tokens.size() - 1).val)))
1222      {
1223        /* implicit multiplication sign */
1224        final StringManipulator.Token multiplicationSign =
1225            new StringManipulator.Token("*", true);
1226
1227        tokens.add(multiplicationSign);
1228      }
1229
1230      tokens.add(token);
1231    }
1232
1233    /* number of file units */
1234    final int n = files.length;
1235
1236    /* operator and operand stacks */
1237    final Deque<String> operators = new ArrayDeque<String>();
1238    final Deque<String[]> operands = new ArrayDeque<String[]>();
1239
1240    /* process each token */
1241    ProcessNextToken:
1242    for (StringManipulator.Token token : tokens)
1243    {
1244      final String tokenVal = token.val;
1245
1246      if (token.isDelimiter)
1247      {
1248        /* token is a delimiter */
1249
1250        final String op = token.val;
1251
1252        if ("(".equals(op))
1253        {
1254          operators.push(op);
1255        }
1256        else if ("[".equals(op))
1257        {
1258          operators.push(op);
1259        }
1260        else if (")".equals(op))
1261        {
1262          /* eval (...) */
1263          while (!operators.isEmpty() &&
1264              !"(".equals(operators.peek()))
1265          {
1266            final EvaluationResult<Void> stepResult = evaluateStep(files, operators, operands);
1267
1268            if (!stepResult.success)
1269            {
1270              result.error = stepResult.error;
1271              return result;
1272            }
1273          }
1274
1275          if (operators.isEmpty() || !"(".equals(operators.pop()))
1276          {
1277            result.error = "Mismatched brackets ( ) in special construct expression.";
1278            return result;
1279          }
1280        }
1281        else if ("]".equals(op))
1282        {
1283          /* eval substring a[...] */
1284          while ((!operators.isEmpty()) &&
1285              (!"[".equals(operators.peek())))
1286          {
1287            final EvaluationResult<Void> stepResult = evaluateStep(files, operators, operands);
1288
1289            if (!stepResult.success)
1290            {
1291              result.error = stepResult.error;
1292              return result;
1293            }
1294          }
1295
1296          if (operators.isEmpty() || !"[".equals(operators.pop()))
1297          {
1298            result.error = "Mismatched brackets [ ] in special construct expression.";
1299            return result;
1300          }
1301
1302          /* proceed to evaluate substring */
1303          if (operands.size() < 2)
1304          {
1305            result.error = "Insufficient operands for substring [ ] operation.";
1306            return result;
1307          }
1308
1309          final String[] formats = operands.pop();
1310          final String[] exprs = operands.pop();
1311          String[] ans = new String[n];
1312
1313          for (int i = 0; i < n; i++)
1314          {
1315            final String format = formats[i];
1316            final String expr = exprs[i];
1317
1318            ans[i] = StringManipulator.substring(
1319                expr,
1320                format,
1321                RenameWand.SUBSTRING_RANGE_CHAR ,
1322                RenameWand.SUBSTRING_DELIMITER_CHAR);
1323
1324            if (ans[i] == null)
1325            {
1326              result.error = "Invalid substring operation \"" +
1327                  expr + "[" + format + "]\".";
1328
1329              return result;
1330            }
1331          }
1332
1333          /* push answer on operand stack */
1334          operands.push(ans);
1335        }
1336        else
1337        {
1338          /* all other operators */
1339
1340          /* eval stack if possible */
1341          while (!operators.isEmpty() &&
1342              !"[]()".contains(operators.peek()) &&
1343              (RenameWand.OPERATOR_PRECEDENCE.get(op).intValue() <=
1344              RenameWand.OPERATOR_PRECEDENCE.get(operators.peek()).intValue()))
1345          {
1346            final EvaluationResult<Void> stepResult = evaluateStep(files, operators, operands);
1347
1348            if (!stepResult.success)
1349            {
1350              result.error = stepResult.error;
1351              return result;
1352            }
1353          }
1354
1355          operators.push(op);
1356        }
1357      }
1358      else
1359      {
1360        /* token is an operand */
1361
1362        String[] tokenVals = new String[n];
1363
1364        if (RenameWand.NUMERIC_PATTERN.matcher(tokenVal).matches())
1365        {
1366          /* token is a numeric value */
1367          for (int i = 0; i < n; i++)
1368            tokenVals[i] = tokenVal;
1369        }
1370        else if (files[0].evaluateMacro(tokenVal) != null)
1371        {
1372          /* token is a register or macro */
1373          for (int i = 0; i < n; i++)
1374            tokenVals[i] = files[i].evaluateMacro(tokenVal);
1375        }
1376        else
1377        {
1378          /* treat as literal text */
1379          for (int i = 0; i < n; i++)
1380            tokenVals[i] = tokenVal;
1381        }
1382
1383        /* push evaluated token onto operands stack */
1384        operands.push(tokenVals);
1385      }
1386    }
1387
1388    /* extract final result */
1389    if (operators.isEmpty() && (operands.size() == 1))
1390    {
1391      /* valid return value */
1392      result.success = true;
1393      result.output = operands.pop();
1394    }
1395    else
1396    {
1397      /* error in evaluation */
1398      result.error = "Mismatched operators/operands in special construct expression.";
1399    }
1400
1401    return result;
1402  }
1403
1404
1405  /**
1406   * Evaluate a single step, given the operators and operands stacks, for the
1407   * given files/directories.
1408   *
1409   * @param files
1410   *     Files/directories for which to evaluate the single step
1411   * @param operators
1412   *     Operators stack
1413   * @param operands
1414   *     Operands stack
1415   * @return
1416   *     Results of the evaluation
1417   */
1418  private static EvaluationResult<Void> evaluateStep(
1419      final FileUnit[] files,
1420      final Deque<String> operators,
1421      final Deque<String[]> operands)
1422  {
1423    final int n = files.length;
1424
1425    /* return value */
1426    final EvaluationResult<Void> result = new EvaluationResult<Void>();
1427
1428    if (operators.isEmpty())
1429    {
1430      result.error = "Operators stack is empty.";
1431      return result;
1432    }
1433
1434    final String op = operators.pop();
1435
1436    if ("^".equals(op))
1437    {
1438      /**************************
1439       * (1) EXPONENTIATION '^' *
1440       **************************/
1441
1442      if (operands.size() < 2)
1443      {
1444        result.error = "Insufficient operands for exponentiation '^' operation.";
1445        return result;
1446      }
1447
1448      final String[] args2 = operands.pop();
1449      final String[] args1 = operands.pop();
1450
1451      /* check that arguments are all numeric */
1452      final int args1NonNumericIndex = getNonNumericIndex(args1);
1453      final int args2NonNumericIndex = getNonNumericIndex(args2);
1454
1455      if ((args1NonNumericIndex >= 0) || (args2NonNumericIndex >= 0))
1456      {
1457        int nonNumericIndex;
1458        String nonNumericArg;
1459
1460        if (args1NonNumericIndex >= 0)
1461        {
1462          nonNumericIndex = args1NonNumericIndex;
1463          nonNumericArg = args1[args1NonNumericIndex];
1464        }
1465        else
1466        {
1467          nonNumericIndex = args2NonNumericIndex;
1468          nonNumericArg = args2[args2NonNumericIndex];
1469        }
1470
1471        result.error = "Invalid operand encountered for exponentiation '^' operation: " +
1472            "The operand \"" + nonNumericArg + "\" corresponding to " +
1473            RenameWand.SINGULAR_NOUN + " \"" + files[nonNumericIndex].source.getPath() +
1474            "\" is non-numeric.";
1475
1476        operators.push(op);
1477        operands.push(args1);
1478        operands.push(args2);
1479        return result;
1480      }
1481
1482      String[] ans = new String[n];
1483
1484      for (int i = 0; i < n; i++)
1485        ans[i] = (int) Math.pow((int) Double.parseDouble(args1[i]), (int) Double.parseDouble(args2[i])) + "";
1486
1487      /* push answer on operand stack */
1488      operands.push(ans);
1489      result.success = true;
1490    }
1491    else if ("*".equals(op))
1492    {
1493      /**************************
1494       * (2) MULTIPLICATION '*' *
1495       **************************/
1496
1497      if (operands.size() < 2)
1498      {
1499        result.error = "Insufficient operands for multiplication '*' operation.";
1500        return result;
1501      }
1502
1503      final String[] args2 = operands.pop();
1504      final String[] args1 = operands.pop();
1505
1506      /* check that arguments are all numeric */
1507      final int args1NonNumericIndex = getNonNumericIndex(args1);
1508      final int args2NonNumericIndex = getNonNumericIndex(args2);
1509
1510      if ((args1NonNumericIndex >= 0) || (args2NonNumericIndex >= 0))
1511      {
1512        int nonNumericIndex;
1513        String nonNumericArg;
1514
1515        if (args1NonNumericIndex >= 0)
1516        {
1517          nonNumericIndex = args1NonNumericIndex;
1518          nonNumericArg = args1[args1NonNumericIndex];
1519        }
1520        else
1521        {
1522          nonNumericIndex = args2NonNumericIndex;
1523          nonNumericArg = args2[args2NonNumericIndex];
1524        }
1525
1526        result.error = "Invalid operand encountered for multiplication '*' operation: " +
1527            "The operand \"" + nonNumericArg + "\" corresponding to " +
1528            RenameWand.SINGULAR_NOUN + " \"" + files[nonNumericIndex].source.getPath() +
1529            "\" is non-numeric.";
1530
1531        operators.push(op);
1532        operands.push(args1);
1533        operands.push(args2);
1534        return result;
1535      }
1536
1537      String[] ans = new String[n];
1538
1539      for (int i = 0; i < n; i++)
1540        ans[i] = (((int) Double.parseDouble(args1[i])) * ((int) Double.parseDouble(args2[i]))) + "";
1541
1542      /* push answer on operand stack */
1543      operands.push(ans);
1544      result.success = true;
1545    }
1546    else if ("/".equals(op))
1547    {
1548      /********************
1549       * (3) DIVISION '/' *
1550       ********************/
1551
1552      if (operands.size() < 2)
1553      {
1554        result.error = "Insufficient operands for division '/' operation.";
1555        return result;
1556      }
1557
1558      final String[] args2 = operands.pop();
1559      final String[] args1 = operands.pop();
1560
1561      /* check that arguments are numeric */
1562      final int args1NonNumericIndex = getNonNumericIndex(args1);
1563      final int args2NonNumericIndex = getNonNumericIndex(args2);
1564
1565      if ((args1NonNumericIndex >= 0) || (args2NonNumericIndex >= 0))
1566      {
1567        int nonNumericIndex;
1568        String nonNumericArg;
1569
1570        if (args1NonNumericIndex >= 0)
1571        {
1572          nonNumericIndex = args1NonNumericIndex;
1573          nonNumericArg = args1[args1NonNumericIndex];
1574        }
1575        else
1576        {
1577          nonNumericIndex = args2NonNumericIndex;
1578          nonNumericArg = args2[args2NonNumericIndex];
1579        }
1580
1581        result.error = "Invalid operand encountered for division '/' operation: " +
1582            "The operand \"" + nonNumericArg + "\" corresponding to " +
1583            RenameWand.SINGULAR_NOUN + " \"" + files[nonNumericIndex].source.getPath() +
1584            "\" is non-numeric.";
1585
1586        operators.push(op);
1587        operands.push(args1);
1588        operands.push(args2);
1589        return result;
1590      }
1591
1592      String[] ans = new String[n];
1593
1594      for (int i = 0; i < n; i++)
1595      {
1596        try
1597        {
1598          ans[i] = (((int) Double.parseDouble(args1[i])) / ((int) Double.parseDouble(args2[i]))) + "";
1599        }
1600        catch (ArithmeticException e)
1601        {
1602          result.error = "Division by zero.";
1603          operators.push(op);
1604          operands.push(args1);
1605          operands.push(args2);
1606          return result;
1607        }
1608      }
1609
1610      /* push answer on operand stack */
1611      operands.push(ans);
1612      result.success = true;
1613    }
1614    else if ("-".equals(op))
1615    {
1616      /***********************
1617       * (4) SUBTRACTION '-' *
1618       ***********************/
1619
1620      if (operands.size() < 2)
1621      {
1622        result.error = "Insufficient operands for subtraction '-' operation.";
1623        return result;
1624      }
1625
1626      final String[] args2 = operands.pop();
1627      final String[] args1 = operands.pop();
1628
1629      /* check that arguments are numeric */
1630      final int args1NonNumericIndex = getNonNumericIndex(args1);
1631      final int args2NonNumericIndex = getNonNumericIndex(args2);
1632
1633      if ((args1NonNumericIndex >= 0) || (args2NonNumericIndex >= 0))
1634      {
1635        int nonNumericIndex;
1636        String nonNumericArg;
1637
1638        if (args1NonNumericIndex >= 0)
1639        {
1640          nonNumericIndex = args1NonNumericIndex;
1641          nonNumericArg = args1[args1NonNumericIndex];
1642        }
1643        else
1644        {
1645          nonNumericIndex = args2NonNumericIndex;
1646          nonNumericArg = args2[args2NonNumericIndex];
1647        }
1648
1649        result.error = "Invalid operand encountered for subtraction '-' operation: " +
1650            "The operand \"" + nonNumericArg + "\" corresponding to " +
1651            RenameWand.SINGULAR_NOUN + " \"" + files[nonNumericIndex].source.getPath() +
1652            "\" is non-numeric.";
1653
1654        operators.push(op);
1655        operands.push(args1);
1656        operands.push(args2);
1657        return result;
1658      }
1659
1660      String[] ans = new String[n];
1661
1662      for (int i = 0; i < n; i++)
1663        ans[i] = (((int) Double.parseDouble(args1[i])) - ((int) Double.parseDouble(args2[i]))) + "";
1664
1665      /* push answer on operand stack */
1666      operands.push(ans);
1667      result.success = true;
1668    }
1669    else if ("+".equals(op))
1670    {
1671      /********************
1672       * (5) ADDITION '+' *
1673       ********************/
1674
1675      if (operands.size() < 2)
1676      {
1677        result.error = "Insufficient operands for addition '+' operation.";
1678        return result;
1679      }
1680
1681      final String[] args2 = operands.pop();
1682      final String[] args1 = operands.pop();
1683      String[] ans = new String[n];
1684
1685      /* check if arguments are numeric */
1686      final int args1NonNumericIndex = getNonNumericIndex(args1);
1687      final int args2NonNumericIndex = getNonNumericIndex(args2);
1688
1689      if ((args1NonNumericIndex < 0) && (args2NonNumericIndex < 0))
1690      {
1691        /* add the two arguments */
1692        for (int i = 0; i < n; i++)
1693          ans[i] = (((int) Double.parseDouble(args1[i])) + ((int) Double.parseDouble(args2[i]))) + "";
1694      }
1695      else
1696      {
1697        /* concatenate the two arguments */
1698        for (int i = 0; i < n; i++)
1699          ans[i] = args1[i] + args2[i];
1700      }
1701
1702      /* push answer on operand stack */
1703      operands.push(ans);
1704      result.success = true;
1705    }
1706    else if (":".equals(op))
1707    {
1708      /********************************************
1709       * (6) RANGE OPERATOR FOR SUBSTRING "[ : ]" *
1710       ********************************************/
1711
1712      if (operands.size() < 2)
1713      {
1714        result.error = "Insufficient operands for substring range operator ':'.";
1715        return result;
1716      }
1717
1718      final String[] args2 = operands.pop();
1719      final String[] args1 = operands.pop();
1720      String[] ans = new String[n];
1721
1722      for (int i = 0; i < n; i++)
1723        ans[i] = args1[i] + ":" + args2[i];
1724
1725      /* push answer on operand stack */
1726      operands.push(ans);
1727      result.success = true;
1728    }
1729    else if (",".equals(op))
1730    {
1731      /************************************************
1732       * (7) DELIMITER OPERATOR FOR SUBSTRING "[ , ]" *
1733       ************************************************/
1734
1735      if (operands.size() < 2)
1736      {
1737        result.error = "Insufficient operands for substring delimiter operator ','.";
1738        return result;
1739      }
1740
1741      final String[] args2 = operands.pop();
1742      final String[] args1 = operands.pop();
1743      String[] ans = new String[n];
1744
1745      for (int i = 0; i < n; i++)
1746        ans[i] = args1[i] + "," + args2[i];
1747
1748      /* push answer on operand stack */
1749      operands.push(ans);
1750      result.success = true;
1751    }
1752    else if ("~".equals(op))
1753    {
1754      /***************************************
1755       * (8) UNARY MINUS (NEGATIVE) SIGN '~' *
1756       ***************************************/
1757
1758      if (operands.size() < 1)
1759      {
1760        result.error = "Insufficient operands for negative sign '-' operator.";
1761        return result;
1762      }
1763
1764      final String[] args1 = operands.pop();
1765
1766      /* check that argument is numeric */
1767      final int nonNumericIndex = getNonNumericIndex(args1);
1768
1769      if (nonNumericIndex >= 0)
1770      {
1771        final String nonNumericArg = args1[nonNumericIndex];
1772
1773        result.error = "Invalid operand encountered for negative sign '-' operator: " +
1774            "The operand \"" + nonNumericArg + "\" corresponding to " +
1775            RenameWand.SINGULAR_NOUN + " \"" + files[nonNumericIndex].source.getPath() +
1776            "\" is non-numeric.";
1777
1778        operators.push(op);
1779        operands.push(args1);
1780        return result;
1781      }
1782
1783      String[] ans = new String[n];
1784
1785      for (int i = 0; i < n; i++)
1786        ans[i] = (-((int) Double.parseDouble(args1[i]))) + "";
1787
1788      /* push answer on operand stack */
1789      operands.push(ans);
1790      result.success = true;
1791
1792    }
1793    else if ("#".equals(op) ||
1794        "#!".equals(op) ||
1795        "##".equals(op) ||
1796        "##!".equals(op) ||
1797        "@".equals(op) ||
1798        "@!".equals(op) ||
1799        "@@".equals(op) ||
1800        "@@!".equals(op))
1801    {
1802      /*****************************
1803       * (9) ENUMERATION OPERATORS *
1804       *****************************/
1805
1806      if (operands.size() < 1)
1807      {
1808        result.error = "Insufficient operands for enumeration operator " + op + ".";
1809        return result;
1810      }
1811
1812      /* sort files, and enumerate them accordingly */
1813
1814      final String[] args1 = operands.pop();
1815
1816      /* global and local order */
1817      int[] globalOrder = new int[n];
1818      int[] localOrder = new int[n];
1819
1820      /* directory counts */
1821      int[] dirCount = new int[RenameWand.numDirs + 1];
1822      Arrays.fill(dirCount, 0);
1823
1824      if (getNonNumericIndex(args1) < 0)
1825      {
1826        /* treat arguments as doubles */
1827        DoubleEnumerationUnit[] eu = new DoubleEnumerationUnit[n];
1828
1829        for (int i = 0; i < n; i++)
1830          eu[i] = new DoubleEnumerationUnit(i, Double.parseDouble(args1[i]));
1831
1832        Arrays.sort(eu);
1833
1834        for (int i = 0; i < n; i++)
1835        {
1836          final int index = eu[i].index;
1837          final int parentDirId = files[index].parentDirId;
1838          dirCount[parentDirId]++;
1839          globalOrder[index] = i + 1;
1840          localOrder[index] = dirCount[parentDirId];
1841        }
1842      }
1843      else
1844      {
1845        /* treat arguments as strings */
1846        StringEnumerationUnit[] eu = new StringEnumerationUnit[n];
1847
1848        for (int i = 0; i < n; i++)
1849          eu[i] = new StringEnumerationUnit(i, args1[i]);
1850
1851        Arrays.sort(eu);
1852
1853        for (int i = 0; i < n; i++)
1854        {
1855          final int index = eu[i].index;
1856          final int parentDirId = files[index].parentDirId;
1857          dirCount[parentDirId]++;
1858          globalOrder[index] = i + 1;
1859          localOrder[index] = dirCount[parentDirId];
1860        }
1861      }
1862
1863      String[] ans = new String[n];
1864
1865      if ("#".equals(op))
1866      {
1867        /* directory ordering */
1868        for (int i = 0; i < n; i++)
1869          ans[i] = localOrder[i] + "";
1870      }
1871      else if ("#!".equals(op))
1872      {
1873        /* reverse directory ordering */
1874        for (int i = 0; i < n; i++)
1875          ans[i] = (files[i].localCount + 1 - localOrder[i]) + "";
1876      }
1877      else if ("##".equals(op))
1878      {
1879        /* global ordering */
1880        for (int i = 0; i < n; i++)
1881          ans[i] = globalOrder[i] + "";
1882      }
1883      else if ("##!".equals(op))
1884      {
1885        /* reverse global ordering */
1886        for (int i = 0; i < n; i++)
1887          ans[i] = (n + 1 - globalOrder[i]) + "";
1888      }
1889      else if ("@".equals(op))
1890      {
1891        /* first elements in directory ordering */
1892        int[] firsts = new int[RenameWand.numDirs + 1];
1893
1894        for (int i = 0; i < n; i++)
1895          if (localOrder[i] == 1) firsts[files[i].parentDirId] = i;
1896
1897        for (int i = 0; i < n; i++)
1898          ans[i] = args1[firsts[files[i].parentDirId]];
1899      }
1900      else if ("@!".equals(op))
1901      {
1902        /* last elements in directory ordering */
1903        int[] lasts = new int[RenameWand.numDirs + 1];
1904
1905        for (int i = 0; i < n; i++)
1906          if (localOrder[i] == files[i].localCount) lasts[files[i].parentDirId] = i;
1907
1908        for (int i = 0; i < n; i++)
1909          ans[i] = args1[lasts[files[i].parentDirId]];
1910      }
1911      else if ("@@".equals(op))
1912      {
1913        /* look for first element */
1914        int first = 0;
1915
1916        for (int i = 0; i < n; i++)
1917          if (globalOrder[i] == 1) first = i;
1918
1919        Arrays.fill(ans, args1[first]);
1920      }
1921      else if ("@@!".equals(op))
1922      {
1923        /* look for last element */
1924        int last = 0;
1925
1926        for (int i = 0; i < n; i++)
1927          if (globalOrder[i] == n) last = i;
1928
1929        Arrays.fill(ans, args1[last]);
1930      }
1931
1932      /* push answer on operands stack */
1933      operands.push(ans);
1934      result.success = true;
1935    }
1936    else
1937    {
1938      /* error */
1939      result.error = "Unexpected operator '" + op + "' encountered.";
1940    }
1941
1942    return result;
1943  }
1944
1945
1946  /**
1947   * Determine sequence of rename operations to be performed, in order to rename
1948   * the given matched files/directories.
1949   *
1950   * @param matchedFiles
1951   *     Matched files/directories to be renamed
1952   * @return
1953   *     Rename file/directory pairs indicating sequence of rename operations
1954   */
1955  private static List<RenameFilePair> getRenameOperations(
1956      final List<FileUnit> matchedFiles)
1957  {
1958    /* determine target files, check validity, and detect clashes */
1959    final Map<File,FileUnit> targetFilesMap = new TreeMap<File,FileUnit>();
1960
1961    for (FileUnit u : matchedFiles)
1962    {
1963      final String targetFilename = u.targetFilename.toString();
1964
1965      /* check for empty file/directory name */
1966      if (targetFilename.isEmpty())
1967        throw new TerminatingException("Invalid target " + RenameWand.SINGULAR_NOUN + " name encountered:\n" +
1968            "\"" + u.source.getPath() + "\" ---> \"" + targetFilename + "\"");
1969
1970      if (targetFilename.contains(File.separator))
1971      {
1972        /* contains a filename separator, so we rename          */
1973        /* file/directory relative to the present work directory */
1974        u.target = new File(targetFilename);
1975      }
1976      else
1977      {
1978        /* does not contain filename separator, so we        */
1979        /* rename file/directory in its original subdirectory */
1980        u.target = new File(u.source.getParentFile(), targetFilename);
1981      }
1982
1983      try
1984      {
1985        /* get canonical pathname */
1986        u.target = new File(u.target.getCanonicalFile().getParentFile(),
1987            u.target.getName());
1988      }
1989      catch (Exception e)
1990      {
1991        throw new TerminatingException("Invalid target " + RenameWand.SINGULAR_NOUN + " name encountered:\n" +
1992            "\"" + u.source.getPath() + "\" ---> \"" + targetFilename + "\"\n" +
1993            getExceptionInformation(e));
1994      }
1995
1996      /* check for clash (i.e. nonunique target filenames) */
1997      final FileUnit w = targetFilesMap.get(u.target);
1998
1999      if (w == null)
2000      {
2001        targetFilesMap.put(u.target, u);
2002      }
2003      else
2004      {
2005        throw new TerminatingException("Target " + RenameWand.SINGULAR_NOUN + " name clash:\n" +
2006          "[A] \"" + w.source.getPath() + "\"\n  ---> \"" + w.target.getPath() + "\"\n" +
2007          "[B] \"" + u.source.getPath() + "\"\n  ---> \"" + u.target.getPath() + "\"");
2008      }
2009    }
2010
2011    /* create (source,target) rename pairs, and determine renaming sequence */
2012    final NavigableMap<File,LinkedList<RenameFilePair>> sequenceHeads = new TreeMap<File,LinkedList<RenameFilePair>>();
2013    final NavigableMap<File,LinkedList<RenameFilePair>> sequenceTails = new TreeMap<File,LinkedList<RenameFilePair>>();
2014
2015    for (FileUnit u : matchedFiles)
2016    {
2017      /* check for unnecessary rename operations */
2018      if (u.source.getPath().equals(u.target.getPath()))
2019        continue;
2020
2021      /* look for a sequence head with source = this target */
2022      final LinkedList<RenameFilePair> headSequence = sequenceHeads.get(u.target);
2023
2024      /* look for a sequence tail with target = this source */
2025      final LinkedList<RenameFilePair> tailSequence = sequenceTails.get(u.source);
2026
2027      if ((headSequence == null) && (tailSequence == null))
2028      {
2029        /* add this pair as a new sequence */
2030        final LinkedList<RenameFilePair> ns = new LinkedList<RenameFilePair>();
2031        ns.add(new RenameFilePair(u.source, u.target));
2032        sequenceHeads.put(u.source, ns);
2033        sequenceTails.put(u.target, ns);
2034
2035      }
2036      else if ((headSequence != null) && (tailSequence == null))
2037      {
2038        /* add this pair to the head of an existing sequence */
2039        headSequence.addFirst(new RenameFilePair(u.source, u.target));
2040        sequenceHeads.remove(u.target);
2041        sequenceHeads.put(u.source, headSequence);
2042      }
2043      else if ((headSequence == null) && (tailSequence != null))
2044      {
2045        /* add this pair to the tail of an existing sequence */
2046        tailSequence.addLast(new RenameFilePair(u.source, u.target));
2047        sequenceTails.remove(u.source);
2048        sequenceTails.put(u.target, tailSequence);
2049      }
2050      else if ((headSequence != null) && (tailSequence != null))
2051      {
2052        if (headSequence == tailSequence)
2053        {
2054          /* loop detected, so we use a temporary target file/directory name */
2055
2056          /* create a temporary file/directory name */
2057          final File parentDir = u.target.getParentFile();
2058          final String filename = u.target.getName();
2059
2060          File temp =  new File(parentDir, filename + ".rw");
2061
2062          if (temp.exists() || targetFilesMap.containsKey(temp))
2063          {
2064            /* temp filename is already used; find another temp filename */
2065            for (long i = 0; i < Long.MAX_VALUE; i++)
2066            {
2067              temp = new File(parentDir, filename + ".rw." + i);
2068
2069              if (temp.exists() || targetFilesMap.containsKey(temp))
2070              {
2071                /* temp filename is already used; find another temp filename */
2072                temp = null;
2073              }
2074              else
2075              {
2076                /* found an unused name */
2077                targetFilesMap.put(temp, null);
2078                break;
2079              }
2080            }
2081          }
2082
2083          if (temp == null)
2084            throw new TerminatingException("Ran out of suffixes for temporary name of " +
2085                RenameWand.SINGULAR_NOUN + " \"" + u.target.getPath() + "\".");
2086
2087          /* add a leading and trailing rename file pair to the existing sequence */
2088          headSequence.addFirst(new RenameFilePair(temp, u.target));
2089          tailSequence.addLast(new RenameFilePair(u.source, temp));
2090          sequenceHeads.remove(u.target);
2091          sequenceHeads.put(temp, headSequence);
2092          sequenceTails.remove(u.source);
2093          sequenceTails.put(temp, tailSequence);
2094        }
2095        else
2096        {
2097          /* link two distinct sequences together */
2098          tailSequence.addLast(new RenameFilePair(u.source, u.target));
2099          tailSequence.addAll(headSequence);
2100          sequenceHeads.remove(u.target);
2101          sequenceTails.remove(u.source);
2102          sequenceTails.put(tailSequence.peekLast().target, tailSequence);
2103        }
2104      }
2105    }
2106
2107    /* return value */
2108    final List<RenameFilePair> renameOperations = new ArrayList<RenameFilePair>();
2109
2110    /* sequence deeper subdirectories for renaming first (approx), if renaming directories */
2111    final NavigableMap<File,LinkedList<RenameFilePair>> sequences =
2112        RenameWand.renameDirectories ? sequenceHeads.descendingMap() : sequenceHeads;
2113
2114    for (LinkedList<RenameFilePair> s : sequences.values())
2115    {
2116      /* get reversed order of rename file pairs within the sequence */
2117      final Iterator<RenameFilePair> iter = s.descendingIterator();
2118
2119      while (iter.hasNext())
2120      {
2121        final RenameFilePair r = iter.next();
2122
2123        if (!r.source.getPath().equals(r.target.getPath()))
2124          renameOperations.add(r);
2125      }
2126    }
2127
2128    return renameOperations;
2129  }
2130
2131
2132  /**
2133   * Print the matched files/directories and their target names, and
2134   * prompt the user on whether to proceed with the renaming operations.
2135   *
2136   * @param matchedFiles
2137   *     Matched files to be renamed
2138   * @param numRenameOperations
2139   *     Number of rename operations to be performed
2140   * @return
2141   *     True if proceeding with rename; false otherwise
2142   */
2143  private static boolean promptUserOnRename(
2144      final List<FileUnit> matchedFiles,
2145      final int numRenameOperations)
2146  {
2147    /* categorize files/directories by their subdirectory */
2148    final List<List<FileUnit>> subdirs = new ArrayList<List<FileUnit>>();
2149
2150    subdirs.add(0, null);
2151
2152    for (int parentDirId = 1; parentDirId <= RenameWand.numDirs; parentDirId++)
2153      subdirs.add(parentDirId, new ArrayList<FileUnit>());
2154
2155    for (FileUnit u : matchedFiles)
2156      subdirs.get(u.parentDirId).add(u);
2157
2158    /* subdirectory counter */
2159    int subdirCount = 0;
2160
2161    /* file/directory counter */
2162    int fileCount = 0;
2163
2164    for (List<FileUnit> subdir : subdirs)
2165    {
2166      if ((subdir == null) || subdir.isEmpty())
2167        continue;
2168
2169      subdirCount++;
2170
2171      final String subdirPath = subdir.get(0).source.getParent();
2172
2173      RenameWand.stdout.print("\n\nSUBDIRECTORY: \"" +
2174          (subdirPath.endsWith(File.separator) ? subdirPath : (subdirPath + File.separator)) + "\"");
2175
2176      for (FileUnit u : subdir)
2177      {
2178        RenameWand.stdout.print("\n["+ (++fileCount) +"] " +
2179            "\"" + u.source.getName() + "\" ---> \"" + u.targetFilename + "\"");
2180      }
2181    }
2182
2183    /* number of files/directories to rename */
2184    final int n = matchedFiles.size();
2185
2186    RenameWand.stdout.print("\n\n" + numRenameOperations + " " +
2187        ((numRenameOperations == 1) ? "operation" : "operations") +
2188        " required to rename the above " +
2189        ((n == 1) ? RenameWand.SINGULAR_NOUN : (n + " " + RenameWand.PLURAL_NOUN)) +
2190        " in " + subdirCount + " " +
2191        ((subdirCount == 1) ? "subdirectory" : "subdirectories") + ".\n");
2192    RenameWand.stdout.flush();
2193
2194    /* no rename operations to perform */
2195    if (numRenameOperations == 0)
2196      return false;
2197
2198    /* prompt user on whether to continue with renaming operations */
2199    char choice = '\0';
2200
2201    if (RenameWand.simulateOnly)
2202    {
2203      choice =  'Y';
2204    }
2205    else if (RenameWand.automaticRename)
2206    {
2207      RenameWand.stdout.print("Proceed to rename " +
2208          ((n == 1) ? RenameWand.SINGULAR_NOUN : RenameWand.PLURAL_NOUN) +
2209          "? (Y)es/(N)o: Y");
2210      RenameWand.stdout.flush();
2211
2212      choice =  'Y';
2213    }
2214    else
2215    {
2216      choice = UserIO.userCharPrompt("Proceed to rename " +
2217          ((n == 1) ? RenameWand.SINGULAR_NOUN : RenameWand.PLURAL_NOUN) +
2218          "? (Y)es/(N)o: ",
2219          "YN");
2220    }
2221
2222    return (choice == 'Y');
2223  }
2224
2225
2226  /**
2227   * Perform rename operations on files/directories.
2228   *
2229   * @param renameOperations
2230   *     Sequence of rename operations to be performed
2231   * @return
2232   *     Number of successful rename operations performed
2233   */
2234  private static int performRenameOperations(
2235      final List<RenameFilePair> renameOperations)
2236  {
2237    if (RenameWand.simulateOnly)
2238    {
2239      RenameWand.stdout.print("\n\nSimulating renaming of " +
2240          ((renameOperations.size() == 1) ? RenameWand.SINGULAR_NOUN : RenameWand.PLURAL_NOUN) +
2241          "...");
2242      RenameWand.stdout.flush();
2243    }
2244    else
2245    {
2246      RenameWand.stdout.print("\n\nRenaming " +
2247          ((renameOperations.size() == 1) ? RenameWand.SINGULAR_NOUN : RenameWand.PLURAL_NOUN) +
2248          "...");
2249      RenameWand.stdout.flush();
2250    }
2251
2252    for (int i = 0; i < renameOperations.size(); i++)
2253    {
2254      final RenameFilePair r = renameOperations.get(i);
2255
2256      RenameWand.stdout.print("\n[R" + (i + 1) + "] "+
2257          "\"" + r.source.getPath() + "\"\n  ---> \"" + r.target.getPath() + "\"");
2258      RenameWand.stdout.flush();
2259
2260      /* if simulating, just continue to the next rename operation */
2261      if (RenameWand.simulateOnly)
2262        continue;
2263
2264      /* check for existing distinct target file/directory */
2265      if (r.target.exists() && !r.target.equals(r.source))
2266      {
2267        r.success = false;
2268
2269        RenameWand.stdout.print("\nRename operation failed: A " +
2270            (r.target.isDirectory() ? "directory" : "file") +
2271            " of the same target name already exists.\n");
2272        RenameWand.stdout.flush();
2273      }
2274      else
2275      {
2276        r.target.getParentFile().mkdirs();
2277        r.success = r.source.renameTo(r.target);
2278
2279        if (!r.success)
2280        {
2281          RenameWand.stdout.print("\nRename operation failed. ");
2282          RenameWand.stdout.flush();
2283        }
2284      }
2285
2286      /* check if renaming operation was successful */
2287      if (!r.success)
2288      {
2289        /* prompt user on action */
2290        char choice = '\0';
2291
2292        if (RenameWand.defaultActionOnRenameOperationError == '\0')
2293        {
2294          if (RenameWand.automaticRename)
2295          {
2296            RenameWand.stdout.print("(R)etry/(S)kip/(U)ndo all/(A)bort: U\n");
2297            RenameWand.stdout.flush();
2298
2299            choice = 'U';
2300          }
2301          else
2302          {
2303            choice = UserIO.userCharPrompt("(R)etry/(S)kip/(U)ndo all/(A)bort: ", "RSUA");
2304          }
2305        }
2306        else
2307        {
2308          /* use default action */
2309          choice = RenameWand.defaultActionOnRenameOperationError;
2310        }
2311
2312        /* take action */
2313        if (choice == 'R')
2314        {
2315          /* retry rename operation */
2316          i--;
2317        }
2318        else if (choice == 'S')
2319        {
2320          /* skip to next file/directory */
2321          continue;
2322        }
2323        else if (choice == 'U')
2324        {
2325          /* undo all previous rename operations */
2326          RenameWand.stdout.print("\nUndoing previous " + RenameWand.SINGULAR_NOUN + " rename operations...");
2327          RenameWand.stdout.flush();
2328
2329          for (int j = i - 1; j >= 0; j--)
2330          {
2331            final RenameFilePair t = renameOperations.get(j);
2332
2333            if (t.success)
2334            {
2335              RenameWand.stdout.print("\n[R" + (j + 1) + "] "+
2336                  "\"" + t.source.getPath() + "\"\n  <--- \"" + t.target.getPath() + "\"");
2337              RenameWand.stdout.flush();
2338
2339              t.source.getParentFile().mkdirs();
2340              t.success = !t.target.renameTo(t.source);
2341
2342              if (t.success)
2343                reportWarning("Rename operation failed.");
2344            }
2345          }
2346
2347          break;
2348        }
2349        else if (choice == 'A')
2350        {
2351          /* abort */
2352          break;
2353        }
2354      }
2355    }
2356
2357    /* return value */
2358    int numRenameOperationsPerformed = 0;
2359
2360    for (RenameFilePair r : renameOperations)
2361      if (r.success) numRenameOperationsPerformed++;
2362
2363    return numRenameOperationsPerformed;
2364  }
2365
2366
2367  /**
2368   * Pad the given string so that it is at least the specified number of
2369   * character long. If the string is numeric, pad it with leading zeros;
2370   * otherwise, pad it with trailing spaces.
2371   *
2372   * @param in
2373   *     String to be padded
2374   * @param len
2375   *     Desired length of padded string
2376   * @param isNumeric
2377   *     Indicates if given string is to be treated as numeric
2378   * @return
2379   *     The padded string
2380   */
2381  private static String padString(
2382      final String in,
2383      final int len,
2384      final boolean isNumeric)
2385  {
2386    /* return value */
2387    final StringBuilder out = new StringBuilder();
2388
2389    /* number of additional characters to insert */
2390    final int padLen = len - in.length();
2391
2392    if (isNumeric)
2393    {
2394      /* pad with leading zeros */
2395      final Matcher numericMatcher = RenameWand.NUMERIC_PATTERN.matcher(in);
2396
2397      if (numericMatcher.matches())
2398      {
2399        /* match groups */
2400        final String sign = numericMatcher.group(1);
2401        final String val = numericMatcher.group(2);
2402
2403        if (sign != null)
2404          out.append(sign);
2405
2406        for (int i = 0; i < padLen; i++)
2407          out.append('0');
2408
2409        out.append(val);
2410        return out.toString();
2411      }
2412    }
2413
2414    /* pad with trailing spaces */
2415    out.append(in);
2416
2417    for (int i = 0; i < padLen; i++)
2418      out.append(' ');
2419
2420    return out.toString();
2421  }
2422
2423
2424  /**
2425   * Return true if the given string is numeric; false otherwise.
2426   *
2427   * @param arg
2428   *     String to be tested
2429   * @return
2430   *     True if the given string is numeric; false otherwise
2431   */
2432  private static boolean isNumeric(
2433      final String arg)
2434  {
2435    /* check if argument is numeric */
2436
2437    /* check if empty string */
2438    if (arg.isEmpty())
2439      return false;
2440
2441    /* check if string matches numeric pattern */
2442    if (!RenameWand.NUMERIC_PATTERN.matcher(arg).matches())
2443      return false;
2444
2445    /* argument is numeric */
2446    return true;
2447  }
2448
2449
2450  /**
2451   * Return -1 if all the strings in the given array are numeric;
2452   * otherwise, return the index of a non-numeric string.
2453   *
2454   * @param arg
2455   *     Strings to be tested
2456   * @return
2457   *     -1 if all the strings in the given array are numeric;
2458   *     otherwise, return the index of a non-numeric string
2459   */
2460  private static int getNonNumericIndex(
2461      final String[] args)
2462  {
2463    /* check if all arguments are numeric */
2464    for (int i = 0; i < args.length; i++)
2465    {
2466      /* check if empty string */
2467      if (args[i].isEmpty())
2468        return i;
2469
2470      /* check if string matches numeric pattern */
2471      if (!RenameWand.NUMERIC_PATTERN.matcher(args[i]).matches())
2472        return i;
2473    }
2474
2475    /* all strings are numeric */
2476    return -1;
2477  }
2478
2479
2480  /**
2481   * Print a warning message and pause.
2482   *
2483   * @param message
2484   *     Warning message to be printed on issuing the warning
2485   */
2486  private static void reportWarning(
2487      final Object message)
2488  {
2489    RenameWand.reportNumWarnings++;
2490
2491    if (RenameWand.ignoreWarnings)
2492    {
2493      RenameWand.stderr.print("\n\nWARNING: " + message + "\n");
2494      RenameWand.stderr.flush();
2495    }
2496    else
2497    {
2498      RenameWand.stderr.print("\n\nWARNING: " + message + "\nPress ENTER to continue...");
2499      RenameWand.stderr.flush();
2500
2501      (new Scanner(System.in)).nextLine(); // blocks until user responds
2502    }
2503  }
2504
2505
2506  /**
2507   * Get custom exception information string for the given exception.
2508   * String contains the exception class name, error description string,
2509   * and stack trace.
2510   *
2511   * @param e
2512   *     Exception for which to generate the custom exception information string
2513   * @return
2514   *     Custom exception information string
2515   */
2516  private static String getExceptionInformation(
2517      final Exception e)
2518  {
2519    final StringBuilder s = new StringBuilder();
2520
2521    s.append("\nJava exception information (" + e.getClass() +
2522        "):\n\"" + e.getMessage() + "\"");
2523
2524    for (StackTraceElement t : e.getStackTrace())
2525    {
2526      s.append("\n  at ");
2527      s.append(t.toString());
2528    }
2529
2530    s.append('\n');
2531    return s.toString();
2532  }
2533
2534
2535  /**
2536   * Print out usage syntax, notes, and comments.
2537   */
2538  private static void printUsage()
2539  {
2540    /* RULER   00000000011111111112222222222333333333344444444445555555555666666666677777777778 */
2541    /* RULER   12345678901234567890123456789012345678901234567890123456789012345678901234567890 */
2542    RenameWand.stdout.print("\n" +
2543        "\nRenameWand is a simple command-line utility for renaming files or directories" +
2544        "\nusing an intuitive but powerful syntax." +
2545        "\n" +
2546        "\nUSAGE:" +
2547        "\n" +
2548        "\njava -jar RenameWand.jar  <switches>  [\"SourcePattern\"]  [\"TargetPattern\"]" +
2549        "\n" +
2550        "\nFor each file in the current directory with a name that matches" +
2551        "\n[\"SourcePattern\"], rename it to [\"TargetPattern\"]. Patterns should be in" +
2552        "\nquotes so that the shell passes them to RenameWand correctly." +
2553        "\nThe user is prompted before files are renamed, and whenever errors occur." +
2554        "\nFile rename operations are sequenced so that they are conflict-free," +
2555        "\nand temporary filenames are automatically used when necessary." +
2556        "\n" +
2557        "\n<Switches>:" +
2558        "\n" +
2559        "\n -r, --recurse         Recurse into subdirectories" +
2560        "\n -d, --dirs            Rename directories instead of files" +
2561        "\n -p, --path            Match [\"SourcePattern\"] against relative pathnames" +
2562        "\n                        (e.g. \"2007\\Jan\\Report.txt\" instead of \"Report.txt\")" +
2563        "\n -l, --lower           Match [\"SourcePattern\"] against lower case names" +
2564        "\n                        (e.g. \"HelloWorld2007.JPG\" ---> \"helloworld2007.jpg\")" +
2565        "\n -y, --yes             Automatically rename files without prompting" +
2566        "\n -s, --simulate        Simulate only; do not actually rename files" +
2567        "\n -i, --ignorewarnings  Ignore warnings; do not pause" +
2568        "\n     --skip            Skip files that cannot be renamed successfully" +
2569        "\n     --undoall         Undo all previous renames when a file cannot be renamed" +
2570        "\n                        successfully" +
2571        "\n     --abort           Abort subsequent renames when a file cannot be renamed" +
2572        "\n                        successfully" +
2573        "\n" +
2574        "\n[\"SourcePattern\"]:" +
2575        "\n" +
2576        "\n Files with names matching [\"SourcePattern\"] are renamed. This pattern string" +
2577        "\n may contain literal (i.e. ordinary) characters and the following constructs" +
2578        "\n (more details below):" +
2579        "\n" +
2580        "\n  1. Glob patterns and wildcards, e.g. *, ?, [ ], { }" +
2581        "\n  2. Register capture groups, e.g. <a>, <5|song>, <2|@track>" +
2582        "\n  3. Special construct <...> that may involve macros," +
2583        "\n      e.g. <FN.parent>, <CT.date>, <FS+FT.yyyy>" +
2584        "\n" +
2585        "\n To use a construct symbol (e.g. [, {, ?) as a literal character, insert a" +
2586        "\n backslash before it, e.g. use \\[ for the literal character [." +
2587        "\n Use \\\\ for the literal backslash character \\." +
2588        "\n" +
2589        "\n The file separator in Windows can be specified by \\\\ or /." +
2590        "\n" +
2591        "\n[\"TargetPattern\"]:" +
2592        "\n" +
2593        "\n The target names of the matched files are specified by [\"TargetPattern\"]." +
2594        "\n This pattern string may contain literal (i.e. ordinary) characters and the" +
2595        "\n special construct <...> that may involve registers and macros (more details" +
2596        "\n below)." +
2597        "\n" +
2598        "\n To use a construct symbol (e.g. <, >) as a literal character, insert a" +
2599        "\n backslash before it, e.g. use \\< for the literal character <." +
2600        "\n Use \\\\ for the literal backslash character \\." +
2601        "\n" +
2602        "\n The file separator in Windows can be specified by \\\\ or /." +
2603        "\n" +
2604        "\n If the evaluated pattern string contains a file separator (e.g. / or \\)," +
2605        "\n then the target name is resolved with respect to the current directory;" +
2606        "\n otherwise, the target name shares the same parent directory as the source" +
2607        "\n file (i.e. the source is renamed \"in place\")." +
2608        "\n" +
2609        "\nGLOB PATTERNS AND WILDCARDS" +
2610        "\n" +
2611        "\n The four common glob patterns are supported in [\"SourcePattern\"]:" +
2612        "\n" +
2613        "\n  *    Match a string of 0 or more characters" +
2614        "\n  ?    Match exactly 1 character" +
2615        "\n [ ]   Match exactly 1 character inside the brackets:" +
2616        "\n         [abc]       Match a, b, or c" +
2617        "\n         [!abc]      Match any character except a, b, or c (negation)" +
2618        "\n         [a-z0-9]    Match any character a through z, or 0 through 9," +
2619        "\n                      inclusive (range)" +
2620        "\n { }   Match exactly 1 comma-delimited string inside the braces:" +
2621        "\n         {a,bc,def}  Match either a, bc, or def" +
2622        "\n" +
2623        "\nREGISTER CAPTURE GROUPS" +
2624        "\n" +
2625        "\n Register capture groups are used in [\"SourcePattern\"] to capture a string of" +
2626        "\n zero or more characters. A register name can contain only letters, digits, or" +
2627        "\n underscores, but cannot begin with a digit." +
2628        "\n" +
2629        "\n  <myreg>     Capture a string of 0 or more characters" +
2630        "\n  <@myreg>    Capture a string of 0 or more digits" +
2631        "\n  <n|myreg>   Capture a string of exactly n characters" +
2632        "\n  <n|@myreg>  Capture a string of exactly n digits" +
2633        "\n" +
2634        "\n Backreferences can be applied by reusing the register name, e.g." +
2635        "\n \"<3|myreg><myreg>.txt\" matches a filename that is a repetition of a" +
2636        "\n three-character string, followed by the txt extension." +
2637        "\n" +
2638        "\nSPECIAL CONSTRUCTS" +
2639        "\n" +
2640        "\n Special constructs are supported in both [\"SourcePattern\"] and" +
2641        "\n [\"TargetPattern\"]. In general, they take the form <length|expr>, where the" +
2642        "\n expressions \"length\" and \"expr\" can involve registers, macros, and use" +
2643        "\n operations involving arithmetic, substrings, and enumerations (more details" +
2644        "\n below). Parentheses ( ) can be used to group values." +
2645        "\n" +
2646        "\n  <expr>         Insert the evaluated expression \"expr\" using as many" +
2647        "\n                  characters as necessary" +
2648        "\n  <length|expr>  Insert the evaluated expression \"expr\" with padding:" +
2649        "\n                  If the evaluated expression is numeric, pad the number with" +
2650        "\n                  leading zeros so that it occupies at least \"length\"" +
2651        "\n                  characters;" +
2652        "\n                  if the evaluated expression is non-numeric, pad the string" +
2653        "\n                  with trailing spaces so that it occupies at least \"length\"" +
2654        "\n                  characters." +
2655        "\n" +
2656        "\nARITHMETIC OPERATIONS" +
2657        "\n" +
2658        "\n The standard arithmetic operations + - * / ^ are supported for numeric values;" +
2659        "\n for non-numeric strings, + is interpreted as concatenation. The standard rules" +
2660        "\n of operator precedence are observed. Values are automatically cast as integers" +
2661        "\n before and after every operation." +
2662        "\n" +
2663        "\n For example, <a+b/(c-d)^(e*f)> evaluates an arithmetic expression involving" +
2664        "\n registers a, b, c, d, e, and f." +
2665        "\n" +
2666        "\nSUBSTRING OPERATIONS" +
2667        "\n" +
2668        "\n Substrings can be extracted from any value by inserting a comma-delimited list" +
2669        "\n of indices and index ranges in brackets [ ] after the value. Index 1 denotes" +
2670        "\n the 1st character, index 2 the 2nd character, and so on." +
2671        "\n" +
2672        "\n Negative indices denote character positions counting from the end of the" +
2673        "\n string, i.e. index -1 denotes the last character, index -2 the second-last" +
2674        "\n character, and so on." +
2675        "\n" +
2676        "\n Index ranges are denoted using three parameters, e.g. 1:2:11 is equivalent to" +
2677        "\n indices 1, 3, 5, ..., 11. The middle parameter is optional; it is assumed to" +
2678        "\n be 1 if missing, e.g. 5:8 is equivalent to indices 5, 6, 7, 8." +
2679        "\n" +
2680        "\n  myreg[1,5,3]   Extract the 1st, 5th, and 3rd characters, in that order" +
2681        "\n  myreg[2:6,1]   Extract the 2nd through 6th characters, followed by the 1st" +
2682        "\n                  character, in that order" +
2683        "\n  myreg[1:2:-1]  Extract the 1st, 3rd, 5th, ... characters" +
2684        "\n  myreg[-1:1]    Extract the last through first characters" +
2685        "\n                  (effectively reverses the string)" +
2686        "\n" +
2687        "\n Indices are automatically clipped if they are too big or small, e.g. if" +
2688        "\n myreg is a 3-character string, then myreg[10] is equivalent to myreg[3]." +
2689        "\n" +
2690        "\nENUMERATION OPERATIONS" +
2691        "\n" +
2692        "\n Matched files can be sorted by any value, and then numbered in sequence." +
2693        "\n Numerical sorting is applied if all the values are numeric; otherwise," +
2694        "\n lexicographic sorting is applied." +
2695        "\n" +
2696        "\n The 1st file in the sorted sequence is assigned the number 1, the 2nd is" +
2697        "\n assigned number 2, and so on; ties are broken arbitrarily." +
2698        "\n" +
2699        "\n  #myreg   Return the sequence number of the respective file when sorted in" +
2700        "\n            ascending order of the value in register myreg" +
2701        "\n  #!myreg  Return the sequence number of the respective file when sorted in" +
2702        "\n            descending order of the value in register myreg" +
2703        "\n  @myreg   Return the value in register myreg of the first file in the" +
2704        "\n            sequence sorted in ascending order" +
2705        "\n  @!myreg  Return the value in register myreg of the first file in the" +
2706        "\n            sequence sorted in descending order" +
2707        "\n" +
2708        "\n The above operations apply to matched files enumerated locally within their" +
2709        "\n respective subdirectories; to enumerate all matched files globally (i.e. when" +
2710        "\n using the --recurse switch), use ## instead of #, and @@ instead of @." +
2711        "\n" +
2712        "\n For example, to enumerate matched files locally and globally by their" +
2713        "\n last-modified time, we can use the target pattern string" +
2714        "\n \"Local <2|#FT> of <2|RW.N> -vs- Global <2|##FT> of <2|RW.NN>.txt\"," +
2715        "\n where macros FT, RW.N, and RW.NN represent the last-modified time of the file," +
2716        "\n the number of local matched files, and the number of global matched files," +
2717        "\n respectively (more details below)." +
2718        "\n" +
2719        "\n Arbitrary values such as arithmetic expressions and substrings can also be" +
2720        "\n used for enumeration, e.g. <#(a*(b+c))>, <#(FN.name[1:3])>." +
2721        "\n" +
2722        "\nMACROS" +
2723        "\n" +
2724        "\n Macros are defined for a variety of file and system attributes, such as" +
2725        "\n file name, file size, file last-modified time, current time," +
2726        "\n system environment variables, and system properties." +
2727        "\n" +
2728        "\n FILE NAME" +
2729        "\n" +
2730        "\n  Example: \"C:\\Work\\2007\\Jan\\Report.txt\", with \"C:\\Work\" as current directory" +
2731        "\n" +
2732        "\n  FN              Filename (\"Report.txt\")" +
2733        "\n  FN.ext          File extension (\"txt\")" +
2734        "\n  FN.name         Base filename without extension (\"Report\")" +
2735        "\n  FN.path         Relative pathname (\"2007\\Jan\\Report.txt\")" +
2736        "\n  FN.parent       Parent directory (\"Jan\")" +
2737        "\n  FN.parentpath   Relative pathname of parent directory (\"2007\\Jan\")" +
2738        "\n" +
2739        "\n FILE SIZE (all values are cast as integers)" +
2740        "\n" +
2741        "\n  FS              File size in bytes" +
2742        "\n  FS.kB           File size in kilobytes (2^10 bytes)" +
2743        "\n  FS.MB           File size in megabytes (2^20 bytes)" +
2744        "\n  FS.GB           File size in gigabytes (2^30 bytes)" +
2745        "\n  FS.TB           File size in terabytes (2^40 bytes)" +
2746        "\n" +
2747        "\n FILE LAST-MODIFIED TIME" +
2748        "\n" +
2749        "\n  FT              Number of milliseconds since the epoch" +
2750        "\n                   (January 1, 1970 00:00:00.000 GMT, Gregorian)" +
2751        "\n  FT.date         Date in the form yyyyMMdd" +
2752        "\n  FT.time         Time in the form HHmmss" +
2753        "\n  FT.ap           am/pm in lower case" +
2754        "\n  FT.AP           AM/PM in upper case" +
2755        "\n" +
2756        "\n  Date and time pattern letters from Java are also supported." +
2757        "\n  Repeat letters to change the representation, e.g. FT.MMMM, FT.MMM," +
2758        "\n  FT.MM could represent \"April\", \"Apr\", \"4\", respectively." +
2759        "\n" +
2760        "\n    G  Era designator            H  Hour in day (0-23)" +
2761        "\n    y  Year                      k  Hour in day (1-24)" +
2762        "\n    M  Month in year             K  Hour in AM/PM (0-11)" +
2763        "\n    w  Week in year              h  Hour in AM/PM (1-12)" +
2764        "\n    W  Week in month             m  Minute in hour" +
2765        "\n    D  Day in year               s  Second in minute" +
2766        "\n    d  Day in month              S  Millisecond" +
2767        "\n    F  Day of week in month      z  Time zone (general time zone)" +
2768        "\n    E  Day in week               Z  Time zone (RFC 822 time zone)" +
2769        "\n    a  AM/PM marker" +
2770        "\n" +
2771        "\n CURRENT TIME" +
2772        "\n" +
2773        "\n  Macros for the current time are obtained by using CT instead of FT in the" +
2774        "\n  above macros for file last-modified time." +
2775        "\n" +
2776        "\n SYSTEM ENVIRONMENT VARIABLES" +
2777        "\n" +
2778        "\n  ENV.var   System environment variable named \"var\"" +
2779        "\n" +
2780        "\n SYSTEM PROPERTIES (see Java API for the full list)" +
2781        "\n" +
2782        "\n  SYS.os.name         Operating system name" +
2783        "\n  SYS.os.arch         Operating system architecture" +
2784        "\n  SYS.os.version      Operating system version" +
2785        "\n  SYS.file.separator  File separator (e.g. \"/\" or \"\\\")" +
2786        "\n  SYS.path.separator  Path separator (e.g. \":\" or \";\")" +
2787        "\n  SYS.line.separator  Line separator" +
2788        "\n  SYS.user.name       User's account name" +
2789        "\n  SYS.user.home       User's home directory" +
2790        "\n  SYS.user.dir        User's current working directory" +
2791        "\n" +
2792        "\n MISCELLANEOUS" +
2793        "\n" +
2794        "\n  RW.cd      Full pathname of the current directory (e.g. \"C:\\Work\")" +
2795        "\n  RW.N       Number of local matched files (i.e. in the file's subdirectory)" +
2796        "\n  RW.NN      Number of global matched files (i.e. in all subdirectories)" +
2797        "\n  RW.random  Generate a string of 10 random digits" +
2798        "\n" +
2799        "\nMACRO & REGISTER MODIFIERS" +
2800        "\n" +
2801        "\n To use the following modifiers, append a period followed by the modifier name" +
2802        "\n to a macro or register name. Modifiers can also be chained, e.g." +
2803        "\n myreg.title.trim, FN.name.pascal.trim." +
2804        "\n" +
2805        "\n  Example: \"hello WORLD\"" +
2806        "\n" +
2807        "\n  len             Length of the string (11)" +
2808        "\n  upper           Convert to upper case (\"HELLO WORLD\")" +
2809        "\n  lower           Convert to lower case (\"hello world\")" +
2810        "\n  capitalize      Capitalize only the first character (\"Hello world\")" +
2811        "\n  title           Convert to title case (\"Hello World\")" +
2812        "\n  camel           Convert to camelCase (\"helloWorld\")" +
2813        "\n  pascal          Convert to PascalCase (\"HelloWorld\")" +
2814        "\n  swapcase        Swap the case (\"HELLO world\")" +
2815        "\n  abbrev          Abbreviate to initials (\"h W\")" +
2816        "\n  reverse         Reverse the string (\"DLROW olleh\")" +
2817        "\n  trim            Trim away whitespace on the left and right" +
2818        "\n  ltrim           Trim away whitespace on the left" +
2819        "\n  rtrim           Trim away whitespace on the right" +
2820        "\n  delspace        Delete whitespace in the string (\"helloWORLD\")" +
2821        "\n  delextraspace   Delete extra whitespace by replacing contiguous whitespace" +
2822        "\n                   with a single space (\"How   are  YOU\" ---> \"How are YOU\")" +
2823        "\n  delpunctuation  Delete punctuation marks in the string" +
2824        "\n  spaceout        Space out words by inserting a space between connected words" +
2825        "\n                   (\"HowAreYou\" ---> \"How Are You\")" +
2826        "\n" +
2827        "\n Shortcut macros are defined for single-letter register names for convenience." +
2828        "\n Suppose we have the register \"a\"; then the following macros are automatically" +
2829        "\n defined if there are no name clashes:" +
2830        "\n" +
2831        "\n  AA  Convert to upper case (.upper)" +
2832        "\n  aa  Convert to lower case (.lower)" +
2833        "\n  Aa  Convert to title case (.title)" +
2834        "\n  aA  Swap the case (.swapcase)" +
2835        "\n" +
2836        "\nEXAMPLES:" +
2837        "\n" +
2838        "\n1. Convert the filename, less extension, to title case," +
2839        "\n    e.g. \"foo bar.txt\" ---> \"Foo Bar.txt\":" +
2840        "\n   java -jar RenameWand.jar \"<a>.<b>\" \"<Aa>.<b>\"" +
2841        "\n" +
2842        "\n2. Convert disc and track numbers to a single number, and swap artist name" +
2843        "\n    with song name," +
2844        "\n    e.g. \"Disc 2 Track 5_SONG_ARTIST.mp3\" --->" +
2845        "\n    \"015-Artist-Song.mp3\":" +
2846        "\n   java -jar RenameWand.jar \"Disc <@disc> Track <@track>_<song>_<artist>.mp3\"" +
2847        "\n    \"<3|10*(disc-1)+track>-<artist.title>-<song.title>.mp3\"" +
2848        "\n" +
2849        "\n3. Insert the file date, and enumerate files by their last-modified time," +
2850        "\n    e.g. \"SCAN004001.jpg\" ---> \"doc20050512 (Page 01 of 42).jpg\":" +
2851        "\n   java -jar RenameWand.jar \"SCAN*.jpg\"" +
2852        "\n    \"doc<FT.date> (Page <2|#FT> of <2|RW.N>).jpg\"" +
2853        "\n" +
2854        "\n4. Rename files into directories based on their names," +
2855        "\n    e.g. \"Daily Report May-28-2007.doc\" ---> \"2007/May/Daily Report 28.doc\":" +
2856        "\n   java -jar RenameWand.jar \"Daily Report <month>-<day>-<year>.doc\"" +
2857        "\n    \"<year>/<month>/Daily Report <day>.doc\"" +
2858        "\n\n");
2859  }
2860}