Batch find/replace text and text search inside multiple scripts

Probably this is already out there somewhere, but I couldn’t google it up so I wrote it.

This recurses through all scripts in a folder and its subfolders and does find/replace on one or more text strings in all the scripts.

It works on any plain-text .applescript files, compiled .scpt files, and it will find the .scpt files inside applescript .app’s and get them too. It’s using osadecompile and osacompile to get the .scpt files.

It also searches all the scripts for any strings and logs all found instances - it generates a .csv in your downloads folder that shows the file’s modification date, which search text was found, the full path to the script it was found in, the line number in the script the string was found on, and the complete line of script text containing the found string. Note that it’s saving a CSV, but the code lines can contain commas. For this reason, the code lines are put in quotes so they import to spreadsheets in a single column, ignoring any commas in the code.

Comments/improvements welcome.


use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions


set masterScriptFolder to choose folder with prompt "Choose a folder of scripts to process a text change on. All instances will be changed, including all subfolders."

-- globals used in main script and handlers
global originalStrings -- list of text strings you want to replace
global newStrings -- list of text strings you'll replace them with
global logFileData -- to log instances of text you want to search for in the scripts
global logTextInstances -- list of the text strings you want to search for the occurance of
global thisScriptName -- check to see if a script with the name of this script is in the folder you're parsing. If it is, it will be skipped. Don't want to change your "find" value to the "replace" value inside the script you're running...


-- *** SET YOUR SEARCH AND REPLACEMENT STRINGS HERE ***

-- these will be found
set originalStrings to {"/Applications/Adobe Photoshop CS6/Adobe Photoshop CS6.app", "/Applications/Adobe Illustrator CS6/Adobe Illustrator.app"}

-- and replaced with the string in the equivalent positon in this list
set newStrings to {"Adobe Photoshop CC 2019", "Adobe Illustrator"}


-- This script also searches the processed scripts for text. Any strings in this list will be searched for in the script, and a log file will be saved to your downloads folder as a CSV containing these lines, what script they were found in, the line number, and the date modified of the file. If you only want to search your scripts and not perform a find/replace, then leave the above originalStrings and newStrings lists empty. Note, search occurs AFTER find/replace.
set logTextInstances to {"CS6"}
set logFileHeaders to "Date Modified,Search Text,Path,Line Number,Line Text" & return -- column headers for CSV
set logFileData to logFileHeaders
set thisScriptName to name of me

if (count of originalStrings) ≠ (count of newStrings) then display dialog "You don't have a replacement string for each original string. These lists must be the same length."

-- Handles recursion through directory tree and passing off parsable files to main handler
script_text_replace(masterScriptFolder)

if logFileData ≠ logFileHeaders then -- if there's been data added to log
	-- generate/save log file
	tell application "Finder"
		try
			set logSaveFile to ((path to downloads folder) & "Log File.csv" as string) as alias
		on error
			set logSaveFile to (make new file at (path to downloads folder as alias) with properties {name:"Log File.csv", file type:"TEXT", creator type:"ttxt"}) as alias
		end try
	end tell
	try
		set logFileRef to open for access logSaveFile with write permission
	on error
		close access logSaveFile
		set logFileRef to open for access logSaveFile with write permission
	end try
	set eof of logFileRef to 0
	write logFileData to logFileRef
	close access logFileRef
	set dataLogged to true
else
	set dataLogged to false
end if

-- make and display a dialog for when the script is finished
set endDialogText to "Complete."
if dataLogged is true then
	set endDialogText to endDialogText & return & "Your search string(s) resulted in a logged data CSV file called \"Log File\" in your Downloads folder."
	set dialogButtons to {"Done", "Open Log"}
	set defaultButton to "Open Log"
else
	set dialogButtons to {"Done"}
	set defaultButton to "Done"
end if

set dialogData to display dialog endDialogText buttons dialogButtons default button defaultButton
set buttonChoice to the button returned of dialogData
if buttonChoice is "Open Log" then tell application "Finder" to open logSaveFile



on script_text_replace(anItem)
	tell application "Finder"
		set anItem to anItem as alias
		if the kind of anItem is in {"Folder", "Application"} then --applications are actually bundles, and the Applescript code lives inside
			set subItems to every item of anItem
			repeat with anItem in subItems
				my script_text_replace(anItem) -- recurse through all files in all subfolders
			end repeat
		else
			set fileExtension to the name extension of anItem
			--ignore files we can't parse. Currently works on .app (the actual code is stored an .rtf) and .applescript, not on .scpt's (yet).
			if fileExtension is in {"applescript", "scpt"} then
				my change_text_in_file(anItem, fileExtension)
			end if
		end if
	end tell
end script_text_replace

-- Main function, handles the actual text replacement, search, log generation, and saving changes.
on change_text_in_file(theFile, fileExtension)
	tell application "Finder" to set fileName to the name of theFile
	if fileName ≠ thisScriptName then
		tell application "Finder" to set fileSize to the size of theFile
		if fileSize ≠ 0 then --skip empty files
			if fileExtension is "scpt" then
				set scriptPOSIXpath to quoted form of POSIX path of theFile
				set fileText to do shell script ("osadecompile " & scriptPOSIXpath)
			else if fileExtension is "applescript" then
				try
					set fileRef to open for access theFile with write permission
				on error
					close access theFile
					set fileRef to open for access theFile with write permission
				end try
				set fileText to read fileRef
			end if
			set replacementItemsCount to count of originalStrings
			set outputText to fileText
			set textChanged to false --only bother saving if we change something
			-- go through each string we want to search for
			repeat with i from 1 to replacementItemsCount
				set anOriginalString to item i of originalStrings
				set aNewString to item i of newStrings
				if fileText contains anOriginalString then
					--do the replace
					set outputText to my replace_chars(fileText, anOriginalString, aNewString)
					set textChanged to true
				end if
			end repeat
			--search for those strings we want to find and log
			set textParagraphs to paragraphs of outputText
			tell application "Finder"
				set filePath to theFile as text
				set modDate to the modification date of theFile
				set {year:y, day:d} to modDate
				tell modDate to set m to its month as integer
				set modDateText to m & "/" & d & "/" & y
			end tell
			set lineNumber to 0
			repeat with aParagraph in textParagraphs
				set lineNumber to lineNumber + 1
				repeat with someLogText in logTextInstances
					if aParagraph contains someLogText then
						-- add line to CSV for found match
						set logFileData to logFileData & modDateText & "," & someLogText & "," & filePath & "," & lineNumber & "," & "\"" & aParagraph & "\"" & return
					end if
				end repeat
			end repeat
			if textChanged is true then
				if fileExtension is "scpt" then
					set shellCompileCommand to "osacompile -o " & scriptPOSIXpath & " << \"HEREDOC_END_TEXT\"" & linefeed & outputText & linefeed & "HEREDOC_END_TEXT"
					do shell script shellCompileCommand
				else if fileExtension is "applescript" then
					set eof of fileRef to 0 -- writes over existing log files
					write outputText to fileRef
					close access fileRef
				end if
			end if
		end if
		-- don't understand why the previous line occasionally fails, but without this next line here, Script Debugger regularly reports assets left open.
		if fileExtension is "applescript" then
			try
				close access fileRef
			end try
		end if
	end if
end change_text_in_file

-- standard find and replace
on replace_chars(theText, searchString, replacementString)
	set AppleScript's text item delimiters to the searchString
	set the item_list to every text item of theText
	set AppleScript's text item delimiters to the replacementString
	set theText to the item_list as string
	set AppleScript's text item delimiters to ""
	return theText
end replace_chars

:slight_smile:

Hi t.spoon.

There’s a script of mine from a couple of years ago on this very site (https://macscripter.net/viewtopic.php?id=46031), but it only replaces one string per sweep and doesn’t keep a log. It does however offer a choice between editing files in place or creating new, edited copies.

In a quick test (replacing “–” comments with “#”), yours missed .app and .scptd files and didn’t create a .csv — although in the latter case, it was because I’d misunderstood what was being logged. A second test with logTextInstances properly set to something in the scripts did produce a .csv. The result looked very smart when opened in Numbers, except that the modification dates were all in US m/d/y format, which is unreadable in other parts of the world. You can get short dates in the user’s preferred format by replacing these three lines …

set {year:y, day:d} to modDate
tell modDate to set m to its month as integer
set modDateText to m & "/" & d & "/" & y

… with this:

set modDateText to modDate's short date string

Otherwise your script seems to work very well. :slight_smile:

I finally got back to looking at this.

In my testing, it is working on .app files without any changes… I don’t know what could cause it to not work for you but work for me.

It was missing .scptd files because I forgot to include “scptd” in the extensions it was looking for. (I never use them). I fixed that so it works with .scptd files now too.

I changed the date to be internationally-friendly as suggested.


use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions


set masterScriptFolder to choose folder with prompt "Choose a folder of scripts to process a text change on. All instances will be changed, including all subfolders."

-- globals used in main script and handlers
global originalStrings -- list of text strings you want to replace
global newStrings -- list of text strings you'll replace them with
global logFileData -- to log instances of text you want to search for in the scripts
global logTextInstances -- list of the text strings you want to search for the occurance of
global thisScriptName -- check to see if a script with the name of this script is in the folder you're parsing. If it is, it will be skipped. Don't want to change your "find" value to the "replace" value inside the script you're running...


-- *** SET YOUR SEARCH AND REPLACEMENT STRINGS HERE ***

-- these will be found
set originalStrings to {"/Applications/Adobe Photoshop CS6/Adobe Photoshop CS6.app", "/Applications/Adobe Illustrator CS6/Adobe Illustrator.app"}

-- and replaced with the string in the equivalent positon in this list
set newStrings to {"Adobe Photoshop CC 2019", "Adobe Illustrator"}


-- This script also searches the processed scripts for text. Any strings in this list will be searched for in the script, and a log file will be saved to your downloads folder as a CSV containing these lines, what script they were found in, the line number, and the date modified of the file. If you only want to search your scripts and not perform a find/replace, then leave the above originalStrings and newStrings lists empty. Note, search occurs AFTER find/replace.
set logTextInstances to {"CS6"}
set logFileHeaders to "Date Modified,Search Text,Path,Line Number,Line Text" & return -- column headers for CSV
set logFileData to logFileHeaders
set thisScriptName to name of me

if (count of originalStrings) ≠ (count of newStrings) then display dialog "You don't have a replacement string for each original string. These lists must be the same length."

-- Handles recursion through directory tree and passing off parsable files to main handler
script_text_replace(masterScriptFolder)

if logFileData ≠ logFileHeaders then -- if there's been data added to log
	-- generate/save log file
	tell application "Finder"
		try
			set logSaveFile to ((path to downloads folder) & "Log File.csv" as string) as alias
		on error
			set logSaveFile to (make new file at (path to downloads folder as alias) with properties {name:"Log File.csv", file type:"TEXT", creator type:"ttxt"}) as alias
		end try
	end tell
	try
		set logFileRef to open for access logSaveFile with write permission
	on error
		close access logSaveFile
		set logFileRef to open for access logSaveFile with write permission
	end try
	set eof of logFileRef to 0
	write logFileData to logFileRef
	close access logFileRef
	set dataLogged to true
else
	set dataLogged to false
end if

-- make and display a dialog for when the script is finished
set endDialogText to "Complete."
if dataLogged is true then
	set endDialogText to endDialogText & return & "Your search string(s) resulted in a logged data CSV file called \"Log File\" in your Downloads folder."
	set dialogButtons to {"Done", "Open Log"}
	set defaultButton to "Open Log"
else
	set dialogButtons to {"Done"}
	set defaultButton to "Done"
end if

set dialogData to display dialog endDialogText buttons dialogButtons default button defaultButton
set buttonChoice to the button returned of dialogData
if buttonChoice is "Open Log" then tell application "Finder" to open logSaveFile



on script_text_replace(anItem)
	tell application "Finder"
		set anItem to anItem as alias
		if the kind of anItem is in {"Folder", "Application"} then --applications are actually bundles, and the Applescript code lives inside
			set subItems to every item of anItem
			repeat with anItem in subItems
				my script_text_replace(anItem) -- recurse through all files in all subfolders
			end repeat
		else
			set fileExtension to the name extension of anItem
			if fileExtension is in {"applescript", "scpt", "scptd"} then
				my change_text_in_file(anItem, fileExtension)
			end if
		end if
	end tell
end script_text_replace

-- Main function, handles the actual text replacement, search, log generation, and saving changes.
on change_text_in_file(theFile, fileExtension)
	tell application "Finder" to set fileName to the name of theFile
	if fileName ≠ thisScriptName then
		tell application "Finder" to set fileSize to the size of theFile
		if fileSize ≠ 0 then --skip empty files
			if (fileExtension is "scpt") or (fileExtension is "scptd") then
				set scriptPOSIXpath to quoted form of POSIX path of theFile
				set fileText to do shell script ("osadecompile " & scriptPOSIXpath)
			else if fileExtension is "applescript" then
				try
					set fileRef to open for access theFile with write permission
				on error
					close access theFile
					set fileRef to open for access theFile with write permission
				end try
				set fileText to read fileRef
			end if
			set replacementItemsCount to count of originalStrings
			set outputText to fileText
			set textChanged to false --only bother saving if we change something
			-- go through each string we want to search for
			repeat with i from 1 to replacementItemsCount
				set anOriginalString to item i of originalStrings
				set aNewString to item i of newStrings
				if fileText contains anOriginalString then
					--do the replace
					set outputText to my replace_chars(fileText, anOriginalString, aNewString)
					set textChanged to true
				end if
			end repeat
			--search for those strings we want to find and log
			set textParagraphs to paragraphs of outputText
			tell application "Finder"
				set filePath to theFile as text
				set modDate to the modification date of theFile
				set modDateText to modDate's short date string
			end tell
			set lineNumber to 0
			repeat with aParagraph in textParagraphs
				set lineNumber to lineNumber + 1
				repeat with someLogText in logTextInstances
					if aParagraph contains someLogText then
						-- add line to CSV for found match
						set logFileData to logFileData & modDateText & "," & someLogText & "," & filePath & "," & lineNumber & "," & "\"" & aParagraph & "\"" & return
					end if
				end repeat
			end repeat
			if textChanged is true then
				if fileExtension is "scpt" or fileExtension is "scptd" then
					set shellCompileCommand to "osacompile -o " & scriptPOSIXpath & " << \"HEREDOC_END_TEXT\"" & linefeed & outputText & linefeed & "HEREDOC_END_TEXT"
					do shell script shellCompileCommand
				else if fileExtension is "applescript" then
					set eof of fileRef to 0 -- writes over existing log files
					write outputText to fileRef
					close access fileRef
				end if
			end if
		end if
		-- don't understand why the previous line occasionally fails, but without this next line here, Script Debugger regularly reports assets left open.
		if fileExtension is "applescript" then
			try
				close access fileRef
			end try
		end if
	end if
end change_text_in_file

-- standard find and replace
on replace_chars(theText, searchString, replacementString)
	set AppleScript's text item delimiters to the searchString
	set the item_list to every text item of theText
	set AppleScript's text item delimiters to the replacementString
	set theText to the item_list as string
	set AppleScript's text item delimiters to ""
	return theText
end replace_chars