Previous  |  Main  |  Next
AppleScript Logo

Grep Rename

Script for: MacOS Finder 8.x / 9.x
Copyright: Matthias Steffens, use at your own risk!
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.
Necessary: RegEx Commands 1.0  (command: "REReplace")
Tanaka's osax 2.0  (command: "MT List Files")
Akua Sweets  (command: "order list").
Download: GrepRename_v1.0.hqx
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

 


Contact: Matthias Steffens  |  Previous  |  Main  |  Next  |  Last Updated: 14-Dec-01