Special Characters in AppleScript & ShellScript

I have a script that finds and replaces characters in filenames. It gives me an unexpected end of file error when trying to find/replace a single quote or asterisk. Is there a way to escape these characters so shellscript can search for them?

Here’s the applescript/shellscript up to the point where it breaks

	--PROMPTING THE USER TO CHOOSE THE FOLDER CONTAINING ITEMS TO EDIT	
set the source_folder to (choose folder with prompt "Folder containing items to edit:") as Unicode text

	--PROMPTING THE USER FOR THE TERM TO SEARCH FOR
	display dialog "Enter text to find in the filenames:" & return & "(Do not enter illegal characters such as :  /  \")" default answer "" buttons {"Cancel", "OK"} default button 2
			set the search_string to the text returned of the result

		set item_reference to paragraphs of (do shell script "find " & quoted form of (POSIX path of source_folder) & " -name '*" & quoted form of (search_string) & "*" & "' -type f") -->searching for images, including pdf files

Browser: Safari 605.1.15
Operating System: macOS 10.14


--PROMPTING THE USER TO CHOOSE THE FOLDER CONTAINING ITEMS TO EDIT	
set the source_folder to (choose folder with prompt "Folder containing items to edit:") as Unicode text

--PROMPTING THE USER FOR THE TERM TO SEARCH FOR
display dialog "Enter text to find in the filenames:" & return & "(Do not enter illegal characters such as :  /  \")" default answer "" buttons {"Cancel", "OK"} default button 2
set the search_string to the text returned of the result

set item_reference to paragraphs of (do shell script "find " & quoted form of (POSIX path of source_folder) & " -name \"*" & search_string & "*" & "\" -type f") -->searching for images, including pdf files

Thanks! That works!
I had tried using backslashes but didn’t have it quite right. You saved me a lot of pulling out my hair.

I tried this with an asterisk search string and the script returned every file in the source folder.

Quoting in this instance seems difficult because the asterisks that bracket the search string have to be quoted so that they are not expanded by the shell before they are seen by the find command and the asterisk that is the search string has to be separately quoted (or escaped) so that it is seen by the find command as the search string.

I got this to work in a terminal window by escaping the asterisk search string but I don’t know how to implement this in a more generalized AppleScript:

find /Users/peavine/Documents -name “*

If you want to search for characters with reserved meaning—e.g. the asterisk or a period—they would need to be bracketed; you would literally search for [asterisk], rather than asterisk. I had to spell it out because the forum thought it was code.

Thanks Marc Anthony–I wasn’t familiar with the use of brackets for that purpose.

So, using Marc Anthony’s suggestion, and KniazidisR’s script, a revised script that works with asterisks and single quotes as requested by the OP is:

set the source_folder to (choose folder with prompt "Folder containing items to edit:") as Unicode text

display dialog "Enter text to find in the filenames:" & return & "(Do not enter illegal characters such as : / \")" default answer "" buttons {"Cancel", "OK"} default button 2
set the search_string to the text returned of the result

if search_string is in {"*", "'"} then
	set search_string to "[" & search_string & "]"
end if

set item_reference to paragraphs of (do shell script "find " & quoted form of (POSIX path of source_folder) & " -name \"*" & search_string & "*" & "\" -type f")

SCRIPT with fixed issue (full generalized form):


--PROMPTING THE USER TO CHOOSE THE FOLDER CONTAINING ITEMS TO EDIT	
set the source_folder to (choose folder with prompt "Folder containing items to edit:") as Unicode text
set source_folder to quoted form of (POSIX path of source_folder)

--PROMPTING THE USER FOR THE TERM TO SEARCH FOR
display dialog "Enter text to find in the filenames:" & return & "(Do not enter illegal characters such as :  /  \")" default answer "" buttons {"Cancel", "OK"} default button 2
set search_string to text returned of result

if search_string contains "*" then set search_string to replace_chars(search_string, "*", "\\*")
set search_string to "\"*" & search_string & "*\""

set myList to (do shell script "find " & source_folder & " -name " & search_string & " -type f")
set myList to paragraphs of myList

on replace_chars(this_text, search_string, replacement_string)
	set AppleScript's text item delimiters to the search_string
	set the item_list to every text item of this_text
	set AppleScript's text item delimiters to the replacement_string
	set this_text to the item_list as string
	set AppleScript's text item delimiters to ""
	return this_text
end replace_chars

Your script works fine with “*”, “sample”, but gives wrong result with “*sample”

If you’re happy to use some AppleScriptObjC, you could do this:

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

set searchString to "one.^$*+?[(){}|/two"
set searchString to (current application's NSRegularExpression's escapedPatternForString:searchString) as text
--> "one\\.\\^\\$\\*\\+\\?\\[\\(\\)\\{\\}\\|\\/two"

But then if you’re willing to use ASObjC, you might as well skip find altogether:

use scripting additions
use framework "Foundation"

on searchDirectory:posixPath forFilesContaining:searchString
	set fileManager to current application's NSFileManager's defaultManager()
	set theURL to current application's NSURL's fileURLWithPath:posixPath
	set theURLs to (fileManager's enumeratorAtURL:theURL includingPropertiesForKeys:(missing value) options:((current application's NSDirectoryEnumerationSkipsPackageDescendants) + (current application's NSDirectoryEnumerationSkipsHiddenFiles as integer)) errorHandler:(missing value))'s allObjects()
	set thePred to current application's NSPredicate's predicateWithFormat:"lastPathComponent CONTAINS[cd] %@ AND pathExtension != %@" argumentArray:{searchString, "app"}
	set finishedURLs to theURLs's filteredArrayUsingPredicate:thePred
	return finishedURLs as list
end searchDirectory:forFilesContaining:

set theFiles to my searchDirectory:(POSIX path of (path to documents folder)) forFilesContaining:"ideas*"

After seeing Marc Anthony’s excellent suggestion, I decided to rewrite my script. It’s pretty much the same as Marc Anthony’s but in a different style and with a bit of error correction.

The OP has a lot of good suggestions to choose from and that’s great.

set legalCharacters to "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789"

set sourceFolder to (choose folder with prompt "Select source folder:")
set sourceFolder to quoted form of POSIX path of sourceFolder

display dialog "Enter search string:" default answer "" buttons {"Cancel", "OK"} default button 2
set searchString to text returned of result
if searchString = "" then error number -128

set searchStringTemp to {}
repeat with aCharacter in searchString
	set aCharacter to contents of aCharacter
	if aCharacter is in legalCharacters then
		set the end of searchStringTemp to aCharacter
	else
		set the end of searchStringTemp to "[" & aCharacter & "]"
	end if
end repeat

set searchString to quoted form of ("*" & (searchStringTemp as text) & "*")

try
	do shell script "find " & sourceFolder & " -name " & searchString & " -type f"
	set itemReference to paragraphs of result
on error
	display dialog "The find utility reported an error." buttons {"OK"} cancel button 1 default button 1
end try

get itemReference

Shane’s escaped quote method is useful, but I’d get rid of the double quotes, altogether. The pattern is arguably easier to read by bracketizing specials and using a single quote group.

set legal to "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789"'s text items
set interim to (display dialog "Enter text to find in the filenames:" default answer "")'s text returned
set corrected to {}

repeat with this in interim
	if this is in legal then
		set corrected's end to this's text
	else
		set corrected's end to "[" & this's text & "]"
	end if
end repeat

(do shell script "find " & (choose folder with prompt "Folder containing items to edit:")'s POSIX path's quoted form & " -type f  -name  " & ("*" & corrected & "*")'s quoted form & space)'s paragraphs

It is. The real problem, though, is that your specials include some people’s everydays – and bracketing them doesn’t get around the fact that find can’t deal with them, bracketed or not. Perhaps you should replace them with simple ? or * characters, so at least they have a chance of being found.

Your script fails with nonEnglish chars (wrong result).

Did you change the path to something appropriate? It works fine with that here.

My apologies. When testing, I made a mistake somewhere. Your script, Shane, once again proved to be the best, since it does not require manual intervention.

Here’s the full code I ended up with. It works great :slight_smile:

with timeout of 1500 seconds
	
	
	------------------------------------------------------------------------------------------------------------
	--CREATING A BLANK PROBLEM LIST JUST IN CASE 
	------------------------------------------------------------------------------------------------------------
	
	set problemList to {}
	set renamedList to {}
	
	------------------------------------------------------------------------------------------------------------
	------------------------------------------------------------------------------------------------------------
	
	
	
	------------------------------------------------------------------------------------------------------------
	--PROMPTING THE USER TO CHOOSE THE FOLDER CONTAINING ITEMS TO EDIT
	------------------------------------------------------------------------------------------------------------
	
	set the source_folder to (choose folder with prompt "Folder containing items to edit:") as Unicode text
	
	------------------------------------------------------------------------------------------------------------
	------------------------------------------------------------------------------------------------------------
	
	
	
	------------------------------------------------------------------------------------------------------------
	--PROMPTING THE USER FOR THE TERM TO SEARCH FOR
	------------------------------------------------------------------------------------------------------------
	
	considering case
		
		repeat
			display dialog "Enter text to find in the filenames:" & return & "(Do not enter illegal characters such as :  /  \")" default answer "" buttons {"Cancel", "OK"} default button 2
			set the search_string to the text returned of the result
			if the search_string is not "" and the search_string does not contain ":" and the search_string does not contain "/" and the search_string does not contain "\"" then
				exit repeat
			else if the search_string contains ":" then
				beep
				display dialog "A filename cannot contain a colon (:)." & return & "Please click ok to re-enter a filename without a colon (:)" buttons {"Cancel", "OK"} default button 2
			else if the search_string contains "/" then
				beep
				display dialog "A filename cannot contain a forward slash (/)." & return & "Please click ok to re-enter a filename without a forward slash (/)" buttons {"Cancel", "OK"} default button 2
			else if the search_string contains "\"" then
				beep
				display dialog "A filename cannot contain a quote (\")." & return & "Please click ok to re-enter a filename without a quote (\")" buttons {"Cancel", "OK"} default button 2
			end if
		end repeat
		
		------------------------------------------------------------------------------------------------------------
		------------------------------------------------------------------------------------------------------------
		
		
		
		------------------------------------------------------------------------------------------------------------
		--PROMPTING THE USER FOR THE REPLACEMENT TEXT
		------------------------------------------------------------------------------------------------------------
		
		repeat
			display dialog "Enter replacement text:" & return & "(Do not enter illegal characters such as :  /  \")" default answer "" buttons {"Cancel", "OK"} default button 2
			set the replacement_string to the text returned of the result
			if the replacement_string contains ":" then
				beep
				display dialog "A filename cannot contain a colon (:)." & return & "Please click ok to re-enter replacement text without a colon (:)" buttons {"Cancel", "OK"} default button 2
			else if the replacement_string contains "/" then
				beep
				display dialog "A filename cannot contain a forward slash (/)." & return & "Please click ok to re-enter replacement text without a forward slash (/)" buttons {"Cancel", "OK"} default button 2
			else if the replacement_string contains "\"" then
				beep
				display dialog "A filename cannot contain a quote (\")." & return & "Please click ok to re-enter replacement text without a quote (\")" buttons {"Cancel", "OK"} default button 2
				
			else
				exit repeat
			end if
		end repeat
		
		display dialog "Replace “" & the search_string & "” with “" & the replacement_string & "” in every filename?" buttons {"Cancel", "OK"} default button 2
		
		
		------------------------------------------------------------------------------------------------------------
		------------------------------------------------------------------------------------------------------------
		
		
		
		
		------------------------------------------------------------------------------------------------------------
		--REPLACING THE SEARCH TERM WITH THE REPLACEMENT TEXT
		------------------------------------------------------------------------------------------------------------
		
		--Fixing bug with asterisks
		if search_string is "*" then
			set final_search_string to "[" & search_string & "]"
		else
			set final_search_string to search_string
		end if
		
		
		-- Get a Finder reference to the relvant items. Switched to shell script because the shell script find is much faster
		set item_reference to paragraphs of (do shell script "find " & quoted form of (POSIX path of source_folder) & " -name \"*" & final_search_string & "*" & "\" -type f")
		
		
		tell application "Finder"
			
			-- Get a list of aliases to the items.
			-- (Individual Finder references might fail when renaming items within renamed folders.)
			try
				set item_list to item_reference as alias list
			on error
				set item_list to item_reference as alias as list
			end try
			
			
			if item_list is not {} then
				
				--Coercing each POSIX style file path from the shell script list to an alias
				--It needs to be an alias for the Mac OS Finder to rename it
				repeat with i from 1 to (count item_list)
					
					set filePath to item i of item_list
					set myFile to filePath as POSIX file
					set myFileAlias to myFile as alias
					tell application "Finder" to set fileName to name of myFileAlias
					tell application "Finder" to set fileExt to name extension of myFileAlias
					
					
					--Doctor each name...
					set astid to AppleScript's text item delimiters
					set AppleScript's text item delimiters to search_string
					set text_items to text items of fileName
					set AppleScript's text item delimiters to replacement_string
					set new_item_name to text_items as Unicode text
					set AppleScript's text item delimiters to astid
					--display dialog new_item_name as string
					
					-- ... and rename the associated item.
					--if fileExt does not contain "png" and fileExt does not contain "PNG" and fileExt does not contain "psd" and fileExt does not contain "PSD" and fileExt does not contain "jpg" and fileExt does not contain "JPG" and fileExt does not contain "tif" and fileExt does not contain "TIF" and fileExt does not contain "ai" and fileExt does not contain "eps" and fileExt does not contain "jpeg" and fileExt does not contain "JPEG" and fileExt does not contain "eml" and fileExt does not contain "ttf" and fileExt does not contain "TTF" and myFileAlias does not contain "Link" and myFileAlias does not contain "link" and myFileAlias does not contain "font" and myFileAlias does not contain "FONT" and myFileAlias does not contain "Font" then
					try
						set name of myFileAlias to new_item_name
						set renamedList to renamedList & "• " & new_item_name & return as string
					on error
						set problemList to problemList & "• " & fileName & return
					end try
					
					--end if
					
				end repeat
				
				--Adding the filename to a problem list if there is a problem
				
			end if
		end tell
	end considering
	
	------------------------------------------------------------------------------------------------------------
	------------------------------------------------------------------------------------------------------------
	
	
	
	------------------------------------------------------------------------------------------------------------
	--DISPLAYING DIALOGS TO THE USER
	------------------------------------------------------------------------------------------------------------
	
	--Displaying error dialogs
	if problemList is not {} then
		tell application "SystemUIServer" to display dialog "The script could not rename the following files:" & return & return & problemList & return & return & "The script renamed " & (count of renamedList) & " files." with icon caution giving up after 4500
		
	else
		--Getting count of renamed files
		if renamedList is not {} then
			set allParagraphs to paragraphs in renamedList
			set allParagraphCount to count allParagraphs
			repeat with aParagraph in allParagraphs
				if aParagraph as string is "" then
					log aParagraph
					set allParagraphCount to (allParagraphCount - 1)
				end if
			end repeat
			
			--Displaying a success message to the user
			tell application "SystemUIServer" to display dialog "SUCCESS!" & return & "The script renamed " & allParagraphCount & " files:" & return & return & renamedList with icon note giving up after 1500
			
			--Displaying a message to the user that no files were renamed
		else
			tell application "SystemUIServer" to display dialog "NO FILES TO RENAME" & return & return & "This script is setup to only rename files that aren't images." & return & "There were no non-image files found to rename." with icon note giving up after 1500
		end if
	end if
	
end timeout
beep 2

------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------


------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------

--NOT USING THESE FUNCTIONS RIGHT NOW, LEAVE IN JUST IN CASE
on set_item_name(this_item, new_item_name)
	tell application "Finder"
		--activate
		set the parent_container to this_item
		if not (exists item new_item_name of the parent_container) then
			try
				set the name of this_item to new_item_name
			on error the error_message number the error_number
				if the error_number is -59 then
					set the error_message to "This name contains improper characters, such as a colon (:)."
				else --the suggested name is too long
					--set the error_message to error_message -- "The name is more than 31 characters long."
				end if
				--beep
				set new_item_name to my get_new_name(error_message, new_item_name)
				if (new_item_name is 0) then return 0
				my set_item_name(this_item, new_item_name)
			end try
		else --the name already exists
			--beep
			set new_item_name to my get_new_name("This name is already taken, please rename.", new_item_name)
			if (new_item_name is 0) then return 0
			my set_item_name(this_item, new_item_name)
		end if
	end tell
end set_item_name

on get_new_name(msg, default_answer)
	tell application (path to frontmost application as Unicode text)
		set {text returned:new_item_name, button returned:button_pressed} to (display dialog msg default answer default_answer buttons {"Cancel", "Skip", "OK"} default button 3)
		if (button_pressed is "OK") then
			return new_item_name
		else if (button_pressed is "Skip") then
			return 0
		else
			error number -128 -- only necessary on non-English systems.
		end if
	end tell
end get_new_name



--FUNCTION TO REPLACE CHARACTERS
on replace_characters(the_phrase, search_string, replacement_string)
	--Changing illegal xml characters to ones xml can understand
	
	set tid to AppleScript's text item delimiters
	set AppleScript's text item delimiters to search_string
	--set item_count to count of text items of the_phrase
	set text_items to text items of the_phrase
	set AppleScript's text item delimiters to replacement_string
	set the_phrase to text_items as Unicode text
	set AppleScript's text item delimiters to tid
	log the_phrase
	
	return the_phrase
end replace_characters

FWIW, here it is in ASObjC (I’ve left out the verification code with the dialogs for simplicity). It should be considerably faster.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"

set problemList to {}
set renamedList to {}
display dialog "Enter text to find in the filenames:" & return & "(Do not enter illegal characters such as : / \")" default answer "" buttons {"Cancel", "OK"} default button 2
set the search_string to the text returned of the result
display dialog "Enter replacement text:" & return & "(Do not enter illegal characters such as : / \")" default answer "" buttons {"Cancel", "OK"} default button 2
set the replacement_string to the text returned of the result
set source_folder to (choose folder with prompt "Folder containing items to edit:")

-- get list of files that match
set fileManager to current application's NSFileManager's defaultManager()
set theURLs to (fileManager's enumeratorAtURL:source_folder includingPropertiesForKeys:(missing value) options:((current application's NSDirectoryEnumerationSkipsPackageDescendants) + (current application's NSDirectoryEnumerationSkipsHiddenFiles as integer)) errorHandler:(missing value))'s allObjects()
set thePred to current application's NSPredicate's predicateWithFormat:"lastPathComponent CONTAINS[cd] %@ AND pathExtension != %@ AND pathExtension != %@" argumentArray:{search_string, "app", ""}
set matchingURLs to theURLs's filteredArrayUsingPredicate:thePred
-- try renaming the files
repeat with oneURL in matchingURLs
	set oldName to oneURL's lastPathComponent()
	set newName to (oldName's stringByReplacingOccurrencesOfString:search_string withString:replacement_string)
	set newURL to (oneURL's URLByDeletingLastPathComponent()'s URLByAppendingPathComponent:newName)
	set theResult to (fileManager's moveItemAtURL:oneURL toURL:newURL |error|:(missing value))
	if theResult as boolean then
		set end of renamedList to "• " & newName as text
	else
		set end of problemList to "• " & oldName as text
	end if
end repeat

I think I was actually over-bracketing non-special items, but I’m not certain what you mean with this advice. I think this edit addresses the problem, and the resulting pattern in the Editor is still a bit easier to read than escaped quotes.

I wasn’t considering this criterion, as it wasn’t requested, but I found that my initial attempt did handle some non-Latin characters; it chokes on ones with diacriticals—most likely because those are in decomposed form. This tests better, in that regard.

set interim to (display dialog "Enter text to find in the filenames:" default answer "")'s text returned
set corrected to {}

repeat with this in interim
	if this is in {"*", "?"} then --special
		set corrected's end to "[" & this's text & "]"
	else
		set corrected's end to this as text
	end if
end repeat

do shell script "find " & (choose folder with prompt "Folder containing items to edit:")'s POSIX path's quoted form & " -type f  -name  " & ("*" & corrected & "*")'s quoted form & space

It’s not a composed/decomposed issue – it’s a simple Unicode issue. The find tool can’t deal with non-ASCII characters. See Apple’s tech note on do shell script, where it says “What does do shell script do with non-ASCII text…”:

https://developer.apple.com/library/archive/technotes/tn2065/_index.html

When find was the only tool in town that didn’t take an age, it was probably worth the compromise, at least for English-speakers. But those days are surely long gone.