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
--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
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:
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")
--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
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.
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
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…”:
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.