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}