Description:
|
This script let's you rename any bunch of files with grep by use of the excellent
RegEx Commands osax. It will allow you to use grep metacharacters to easily rename
hundreds of files in one step. Automatic batch numbering of files is supported as well
(see below).
Simply drag all files / folders to process onto this script applet (or double click the
applet to specify a folder containing the files to process). When processing folders it
can be specified if nested folders shall be processed as well.
You will be asked for a grep pattern that (fully or partially) matches the file names to
rename; then you'll need to specify a replace pattern from which the new file names will
be build.
Grep Syntax:
|
|
Grep capabilities are provided by the RegEx Commands osax. For example, within search patterns it supports the following metacharacters:
Character
|
Matches
|
.
|
any single character
|
?
*
+
|
the preceding character if it occurrs
zero or one
zero or more
one or more times
|
|
|
either the pattern to the left or the pattern to the right of the pipe
|
[ ...]
[^ ...]
|
any character that
belongs
does not belong
to the group of chars specified within the angle brackets
|
^
$
|
the beginning of the file name
the end of the file name
|
\<
\>
|
the beginning of a word
the end of a word
|
\b
\B
|
the beginning or end of a word
anywhere except at the beginning or end of a word
|
\l
\u
|
any lowercase letter
any uppercase letter
|
\a
\d
|
any letter (i.e., an alphabetical character)
any number (matches the same as [0-9] )
|
\w
\W
|
any alphanumeric character (i.e., a letter or number)
any character other than a letter or number
|
\p
\y
|
any punctuation (i.e., a comma, quote, semicolon, etc.)
any symbol (such as an @, #, $, etc.)
|
\s
\S
|
any "white space" character (i.e., a space, return, or tab)
any character other than "white space"
|
( ...)
|
defines subpatterns and remembers (up to nine of) them
|
\1 ...\9
|
backreferences are supported: use a previously matched part of your search pattern as a later part of that same search pattern (by recalling up to nine defined subpatterns with \1 ...\9 )
|
Within replace patterns the following metacharacters are supported:
Character
|
Result
|
& or \0
\1 ...\9
|
recalls the entire matched pattern
recalls the 1st ... 9th defined subpattern
|
\u
\l
\-
|
converts everything that follows to uppercase (until \l or \- is met)
converts everything that follows to lowercase (until \u or \- is met)
removes the effect of any prior \u or \l characters
(i.e., partial case transformations are supported!)
|
#
%
@
|
inserts the line number (since file names will always have only one line, this metacharacter is of no real use here)
inserts the match number (i.e., the number of times that the pattern has been found so far)
inserts the character offset of the matched text within the original text
|
For detailed information about the grep flavor used by this osax please refer to the excellent PDF documentation that comes with the RegEx Commands package (it will also serve as a good introduction to the concept of 'grep' itself!).
|
|
Examples:
-
Replace any consecutive series of characters other than a letter or number with a single substring:
Search for: \W+
Replace with: _
Example: "Prices in % US-$!?" -> "Prices_in_US_"
-
Apply case transformations, e.g. form words (i.e. make its first char uppercase and the following chars lowercase):
Search for: (\a)(\a+)
Replace with: \u\1\l\2
Example: "my grep patterns" -> "My Grep Patterns"
-
Re-arrange date information (dd.mm.yyyy -> yyyy.mm.dd) so that they sort correctly in the finder:
Search for: (\d\d)\.(\d\d)\.(\d\d\d\d)
Replace with: \3.\2.\1
Example: "01.04.1999.jpg" -> "1999.04.01.jpg"
|
|
Warning:
|
|
If you're not familiar with grep or the grep flavor used by the RegEx Commands osax I strongly recommend to read the above mentioned PDF documentation first! It's pretty easy to screw up hundreds of file names if you don't know what you're doing! ;-) That said, you might want to try your pattern with some sample files first, or, alternatively, use BBEdit or a similar grep-capable text editor with some sample file names to simulate the replace results.
|
Auto Numbering:
|
|
Within the replace pattern, you can use notations of the form
|
|
{[-]digit, [-]digit[, [-]digit]}
|
|
which will invoke auto numbering for all matched files. I.e., the syntax for the auto numbering feature is composed out of two [optionally three] positive or negative integers that are separated by commas and enclosed by curly brackets (delimiting spaces are optional). Hereby, the first digit represents the starting number, the second one the increment steps and the [optional] third digit defines the minimum number of digits.
|
|
Examples:
|
|
Consider you dropped three files named "FileOne", "FileTwo" and "FileThree":
-
Search for:
^
Replace with: {1, 1}
Result: "1FileOne", "2FileTwo" and "3FileThree"
-
Search for:
^
Replace with: {0, 2}-
Result: "0-FileOne", "2-FileTwo" and "4-FileThree"
-
Search for:
$
Replace with: {1,1,3}
Result: "FileOne 001", "FileTwo 002" and "FileThree 003"
-
Search for:
^
Replace with: {30, -10, 2}_
Result: "30_FileOne", "20_FileTwo" and "10_FileThree"
|
Log File:
|
|
By default, every file renaming action (as well as any occurring errors) will be written to a log file (located within your system prefs folder). You can open this log file in any text editor to easily control what files got renamed or what went wrong (hint: in order to view the log file columns more easily set its tab width to 43 or greater). Additionally, the log file serves as a good means to revert any erroneous renaming action.
|
Limitations:
|
|
Aliases:
|
|
Directly dropped aliases will get resolved before processing (i.e. the original files will get renamed!); contrastingly, sub-level aliases (i.e. aliases contained in folders) will be renamed themselves! This is since directly dropped aliases get resolved by the finder while sub-level aliases get handled by Tanaka's osax 2.0 (which hands off references to the aliases instead). I don't see any solution to remove this inconsistent behaviour.
|
|
Auto Numbering:
|
|
While very exotic, some might consider the use of \1 within {...} constructs. You can't do that -- or more precisely: though you can use \1 within {...} constructs it won't result in what you might expect! The current implementation of how replacement patterns are resolved prevents this from working as expected.
|
|
The Script:
|
-- You might want to edit the following properties in order to customize particular
-- features or toggle some features ON or OFF:
-- WriteLog: if set to 'true' every file renaming action (as well as any occurring errors)
-- will be written to a log file (located within your system prefs folder).
property WriteLog : true
-- LogFileName: this is the default name of the log file (which will be created
-- inside your prefs folder if the WriteLog property is set to 'true');
-- edit the string in order to change the log file name.
property LogFileName : "GrepRename LOG.txt"
-- PreferredTextEditor: this is the 4-letter creator code which is unique to every
-- Macintosh application, e.g., "R*ch" is the creator code for BBEdit; edit the string
-- in order to control which text editor app will be called to open the log file.
property PreferredTextEditor : "R*ch"
-- LAbelFiles: if set to 'true' any file that couldn't be renamed (due to some error)
-- will get labeled by use of either label #6 (->file name already exists) or
-- label #7 (->length of file name exceeds 31 chars or any other error).
property LAbelFiles : true
-- ShowDialogAlerts: if set to 'true' the script will popup a modal dialog if an error
-- occurs (so that you've got any chance to cancel the whole thing). Setting this property
-- to 'false' will suppress any error dialogs, but then its a good idea to keep the log feature on!
property ShowDialogAlerts : true
-- SortFiles: if set to 'true' the file list will be sorted with Akua Sweets
-- (or ACME Script Widgets 2.5.2) before processing. Sorting will make sure
-- that files get processed in the same order as they are displayed in the finder.
property SortFiles : true
-- You don't need to edit the following three properties, you'll get asked about it anyhow...
property ProcessSubLevel : true
property SearchPattern : ""
property ReplacePattern : ""
global Proceed, FileCounter, LogFile, LogFilePath, oldDelims, ct_FilesProcessed, ct_FilesRenamed, ct_FileErrors
on run -- pop up a dialog where you can choose a folder which contains the files to process:
set theFolder to choose folder with prompt "Process Files of Folder:"
my Initialize()
my AskProcessSubLevel(theFolder) -- ask the user if sub-level files shall be processed
if Proceed then my AskSearchPattern("Search pattern:", 1) -- ask the user to specify search (and replace) patterns
if Proceed then
set TheFiles to my ProcessFolder(theFolder)
if WriteLog then my WriteHeaderToLog()
my RenameItems(TheFiles) -- process all files
my Finalize()
end if
end run
on open (DroppedItems) -- extract the dropped files and/or folders:
my Initialize()
if Proceed then
set ContainsFolder to false
set ct to (count DroppedItems)
set TheFiles to {}
repeat with i from 1 to ct
set theItem to file (item i of DroppedItems) as alias -- this will resolve aliases (so that we can check if the alias *file* actually points to a *folder*)
-- check if there are folders amongst the dropped items (so that we can process them separately):
if (theItem as string) does not end with ":" then -- a file
copy theItem to end of TheFiles -- append it to our list of files to process
else -- a folder
if not ContainsFolder then
my AskProcessSubLevel(theItem) -- ask the user if sub-level files shall be processed
set ContainsFolder to true -- remember that we've found a folder already (so that we only ask *once* for processing of sub-level files)
end if
if Proceed then
set TheFolderFiles to my ProcessFolder(theItem) -- extract all files of this particular folder
set TheFiles to TheFiles & TheFolderFiles -- append them to our list of files to process
end if
end if
end repeat
end if
if Proceed then my AskSearchPattern("Search pattern:", 1) -- ask the user to specify search (and replace) patterns
if Proceed then
if WriteLog then my WriteHeaderToLog()
my RenameItems(TheFiles) -- process all files
my Finalize()
end if
end open
on Initialize()
set oldDelims to AppleScript's text item delimiters
set Proceed to true
set FileCounter to 0
set ct_FilesProcessed to 0
set ct_FilesRenamed to 0
set ct_FileErrors to 0
end Initialize
on Finalize()
if WriteLog then my WriteFooterToLog()
set AppleScript's text item delimiters to oldDelims
if Proceed then
set FilesProcessed to ("∑ Files processed: " & ct_FilesProcessed)
set FilesRenamed to (return & "∑ Files renamed: " & ct_FilesRenamed)
set FileErrors to (return & "∑ Errors: " & ct_FileErrors)
if ct_FileErrors > 0 then -- some errors occurred
set DialogIcon to 2
set DefaultButton to 1
else -- no errors
set DialogIcon to 1
if WriteLog then
set DefaultButton to 2
else
set DefaultButton to 1
end if
end if
if WriteLog then
set LogInfo to (return & return & "Please see the log file for details.")
set ButtonsList to {"Open Log", "OK"}
else
set LogInfo to ""
set ButtonsList to {"OK"}
end if
-- present a final summary:
set DialogResult to display dialog "GrepRename Summary:" & return & return & FilesProcessed & FilesRenamed & FileErrors & LogInfo with icon DialogIcon buttons ButtonsList default button DefaultButton
if button returned of DialogResult is "Open Log" then my OpenLog()
end if
end Finalize
on RenameItems(TheFiles) -- rename files:
set ct to (count TheFiles)
if SortFiles then set TheFiles to order list TheFiles -- Akua Sweets 1.4.3
-- or, if you prefer to use ACME Script Widgets:
-- if SortFiles then set TheFiles to ACME sort TheFiles -- ACME Script Widgets 2.5.2
tell application "Finder"
repeat with i from 1 to ct
set TheFile to (item i of TheFiles)
set FileName to name of TheFile
set ct_FilesProcessed to ct_FilesProcessed + 1
set NewFileName to REReplace FileName with ReplacePattern pattern SearchPattern -- "RegEx Commands" osax
considering case
if NewFileName is not FileName then
if NewFileName is not "" then
set NewFileName to my CalculateReplacePattern(NewFileName)
set FileCounter to FileCounter + 1
try
set name of TheFile to NewFileName
set ct_FilesRenamed to ct_FilesRenamed + 1
if WriteLog then my WriteToLog(FileName, NewFileName, TheFile, "")
on error ErrMsg number ErrNo
set ct_FileErrors to ct_FileErrors + 1
if ErrNo = -48 then -- file name already exists!
if LAbelFiles then
set label index of TheFile to 6
set FileLAbelInfo to " but labeled with label index #6"
else
set FileLAbelInfo to ""
end if
if WriteLog then my WriteToLog(FileName, NewFileName, TheFile, "File wasn't renamed" & FileLAbelInfo & " since new file name already exists!")
if ShowDialogAlerts then set DialogResult to display dialog "New file name \"" & NewFileName & "\" already exists! Please refine your patterns!" & return & return & ¬
"[the conflicting file \"" & FileName & "\" wasn't renamed" & FileLAbelInfo & "]" with icon caution buttons {"Cancel", "OK"} default button 2
else if (ErrNo = -37) or (ErrNo = -50) then -- bad file name, e.g. file name exceeds 31 characters!
if LAbelFiles then
set label index of TheFile to 7
set FileLAbelInfo to " but labeled with label index #7"
else
set FileLAbelInfo to ""
end if
if WriteLog then my WriteToLog(FileName, NewFileName, TheFile, "File wasn't renamed" & FileLAbelInfo & " since new file name would be invalid, e.g. exceeding 31 characters!")
if ShowDialogAlerts then set DialogResult to display dialog "New file name \"" & NewFileName & "\" is invalid (e.g. exceeds 31 characters). Please refine your patterns!" & return & return & ¬
"[the conflicting file \"" & FileName & "\" wasn't renamed" & FileLAbelInfo & "]" with icon caution buttons {"Cancel", "OK"} default button 2
else
if LAbelFiles then
set label index of TheFile to 7
set FileLAbelInfo to " but labeled with label index #7"
else
set FileLAbelInfo to ""
end if
if WriteLog then my WriteToLog(FileName, NewFileName, TheFile, ("File wasn't renamed" & FileLAbelInfo & " since an error occurred: " & ErrMsg & " (Error number: " & ErrNo & ")"))
if ShowDialogAlerts then set DialogResult to display dialog "The following error has occurred:" & return & return & ErrMsg & return & ¬
"(Error number: " & ErrNo & ")" with icon caution buttons {"Cancel", "OK"} default button 2
end if
if ShowDialogAlerts then
if button returned of DialogResult is "Cancel" then
set Proceed to false
my Finalize()
error number -128
end if
end if
end try
else
set ct_FileErrors to ct_FileErrors + 1
if LAbelFiles then
set label index of TheFile to 7
set FileLAbelInfo to " but labeled with label index #7"
else
set FileLAbelInfo to ""
end if
if WriteLog then my WriteToLog(FileName, NewFileName, TheFile, ("File wasn't renamed" & FileLAbelInfo & " since new file name would be empty!"))
if ShowDialogAlerts then
set DialogResult to display dialog "New file name would be empty! Please refine your patterns!" & return & return & ¬
"[the conflicting file \"" & FileName & "\" wasn't renamed" & FileLAbelInfo & "]" with icon caution buttons {"Cancel", "OK"} default button 2
if button returned of DialogResult is "Cancel" then
set Proceed to false
my Finalize()
error number -128
end if
end if
end if
end if
end considering
end repeat
end tell
end RenameItems
on AskProcessSubLevel(theFolder) -- ask the user if sub-level files shall be processed
if ProcessSubLevel then
set DefaultButton to 3
else
set DefaultButton to 2
end if
set DialogResult to display dialog "When processing folders, include nested folders?" with icon note buttons {"Cancel", "No - top level only", "Yes - include nested"} default button DefaultButton
if button returned of DialogResult is not "Cancel" then
if button returned of DialogResult is "Yes - include nested" then
set ProcessSubLevel to true
else
set ProcessSubLevel to false
end if
else
set Proceed to false
end if
end AskProcessSubLevel
on AskSearchPattern(DialogText, DialogIcon)
set DialogResult to display dialog DialogText with icon DialogIcon default answer SearchPattern buttons {"Cancel", "OK"} default button 2
if button returned of DialogResult is not "Cancel" then
set NewSearchPattern to text returned of DialogResult
if NewSearchPattern is not "" then
set SearchPattern to NewSearchPattern
my AskReplacePattern("Replace pattern:", 1)
else
set DialogText to "Empty search patterns are not allowed!" & return & return & "Please specify a search pattern:"
my AskSearchPattern(DialogText, 2)
end if
else -- cancel
set Proceed to false
end if
end AskSearchPattern
on AskReplacePattern(DialogText, DialogIcon)
set DialogResult to display dialog DialogText with icon DialogIcon default answer ReplacePattern buttons {"Back", "Cancel", "OK"} default button 3
if button returned of DialogResult is not "Cancel" then
if button returned of DialogResult is "OK" then
set NewReplacePattern to text returned of DialogResult
if NewReplacePattern does not contain ":" then
set ReplacePattern to NewReplacePattern
else
set DialogText to "The folder delimiter \":\" is not allowed within replace patterns!" & return & return & "Please specify a replace pattern:"
my AskReplacePattern(DialogText, 2)
end if
else if button returned of DialogResult is "Back" then
set ReplacePattern to text returned of DialogResult
my AskSearchPattern("Search pattern:", 1)
end if
else -- cancel
set Proceed to false
end if
end AskReplacePattern
on CalculateReplacePattern(NewFileName)
local CalcReplacePatternList, NewCalcReplacePatternList, StartNumber, IncrementNumber, DigitNumber, ResultNumber
-- the following will catch notations of the form "{1,1}", "{0, 10}", "{-1,-1}", etc.;
-- plus, optionally a third number can be specified, as in "{1,1,2}", {0, 2, 3}", etc.
-- these notations are to provide automatic numbering thru several file names:
-- the first number represents the starting number, the second number defines the increment steps
-- [the optional third number defines the minimum number of digits]
set CalcReplacePattern to REReplace NewFileName pattern "{(-?\\d+), *(-?\\d+)(, *(-?\\d+))?}" with "•◊◊•\\1•◊◊•\\2•◊◊•\\4•◊◊•" -- "RegEx Commands" osax
set AppleScript's text item delimiters to {"•◊◊•"}
set CalcReplacePatternList to every text item of CalcReplacePattern
set ct_n to count CalcReplacePatternList
set NewCalcReplacePatternList to {}
repeat with n from 2 to (ct_n - 1) by 4
set StartNumber to item n of CalcReplacePatternList
set IncrementNumber to item (n + 1) of CalcReplacePatternList
set DigitNumber to item (n + 2) of CalcReplacePatternList
set ResultNumber to (StartNumber + (IncrementNumber * FileCounter)) as string
-- fill with trailing zeros up to specified DigitNumber:
set done to false
repeat until done
set ResultInteger to ResultNumber
if ResultInteger begins with "-" then
set ResultInteger to text 2 thru -1 of ResultNumber
set IsNegative to true
else
set IsNegative to false
end if
if (count ResultInteger) < DigitNumber then
if IsNegative then
set ResultNumber to "-0" & ResultInteger
else
set ResultNumber to "0" & ResultNumber
end if
else
set done to true
end if
end repeat
-- more elegant solution defined in Tanaka's osax: Convert Integer into Format string (e.g. 2 -> "002")
-- MT Digit String ResultNumber digit DigitNumber -- Tanaka's osax
copy (item (n - 1) of CalcReplacePatternList) to end of NewCalcReplacePatternList
copy ResultNumber to end of NewCalcReplacePatternList
end repeat
copy (item ct_n of CalcReplacePatternList) to end of NewCalcReplacePatternList
set AppleScript's text item delimiters to ""
return (NewCalcReplacePatternList as string)
end CalculateReplacePattern
on ProcessFolder(theFolder)
tell application "Finder" -- extract all contained files as alias list:
try
set TheFiles to MT List Files theFolder of attribute {visible:true} return as alias sub folders ProcessSubLevel -- Tanaka's osax
(*
-- Finder based solution (buggy!):
if ProcessSubLevel then -- process files within subfolders:
set TheFiles to ((every file of entire contents of theFolder) as alias) as list
else -- process only first level files:
set TheFiles to ((every file in theFolder) as alias) as list
end if
*)
on error number -1700 from f -- this smart error block is by Walter Ian Kaye!
-- errors if count of TheFiles > 1: "Can't make {alias "...", alias "...", ...} into an alias"
set TheFiles to f as list
end try
end tell
return TheFiles
end ProcessFolder
on WriteHeaderToLog()
set oldDelims to AppleScript's text item delimiters
set LogFilePath to (((path to preferences) as string) & LogFileName)
-- open the log file:
set LogFile to (open for access file LogFilePath with write permission)
-- build date header:
set CurrentDate to (current date) as string
set ct_datestring to count CurrentDate
set UnderLineDate to ""
repeat ct_datestring times
set UnderLineDate to UnderLineDate & "-"
end repeat
-- write header to log file:
set eofPos to get eof LogFile
write (return & return & CurrentDate & return & UnderLineDate & return & "Search pattern: " & SearchPattern & return & "Replace pattern: " & ReplacePattern & return & return) to LogFile starting at (eofPos + 1)
end WriteHeaderToLog
on WriteFooterToLog()
-- build footer:
set FilesProcessed to ("∑ Files processed: " & ct_FilesProcessed & return)
if (ct_FilesRenamed is not 0) or (ct_FileErrors is not 0) then set FilesProcessed to (return & FilesProcessed)
set FilesRenamed to ("∑ Files renamed: " & ct_FilesRenamed & return)
set FileErrors to "∑ Errors: " & ct_FileErrors & return
-- write footer to log file:
set eofPos to get eof LogFile
write (FilesProcessed & FilesRenamed & FileErrors) to LogFile starting at (eofPos + 1)
close access LogFile -- close log file
set AppleScript's text item delimiters to oldDelims
end WriteFooterToLog
on WriteToLog(OldFileName, NewFileName, FilePath, ErrMsg)
set AppleScript's text item delimiters to {":"}
if ErrMsg is "" then
-- FilePath contains the path to the renamed file!
set NewFilePath to (FilePath as string)
set OldFilePath to (text items 1 thru -2 of NewFilePath) as string
set OldFilePath to {OldFilePath, OldFileName} as string
set ErrIdentifier to "◊"
else
-- FilePath contains the path to the old file (file wasn't renamed)!
set OldFilePath to (FilePath as string)
set NewFilePath to (text items 1 thru -2 of OldFilePath) as string
set NewFilePath to {NewFilePath, NewFileName} as string
set ErrIdentifier to "•"
end if
set OldFileInfo to (" Old: " & OldFileName & tab & OldFilePath & return)
set NewFileInfo to (" New: " & NewFileName & tab & NewFilePath & return)
set ErrInfo to (ErrIdentifier & " Err: " & ErrMsg & return)
-- write info to log file:
set eofPos to get eof LogFile
write (ErrInfo & OldFileInfo & NewFileInfo) to LogFile starting at (eofPos + 1)
end WriteToLog
on OpenLog() -- attempt to open the log file with the text editor app specified in the PreferredTextEditor property
tell application "Finder"
-- get the file's creator code:
set LogFileCreator to creator type of file LogFilePath
if LogFileCreator is not PreferredTextEditor then
try -- check if the preferred text editor is installed:
set TextEditorPath to (application file id PreferredTextEditor) as text
set AppInstalled to true
on error
set AppInstalled to false
end try
if AppInstalled then
-- change the file's creator code to the desired one:
set creator type of file LogFilePath to PreferredTextEditor
end if
end if
open file LogFilePath -- ask the finder to open the log file
end tell
end OpenLog
|