Batch convert text in .script files

Hi there,

how can I batch convert hundreds of Script-Editor .script documents in a folder and different subfolders to replace text “application1” with “application2” without opening the documents (Sierra)?

e.g. tell application “System Events” to tell process “application1”
set frontmost to true

to

tell application “System Events” to tell process “application2”
set frontmost to true

Thanks for your help.

Do you really have hundreds of script files requiring that change throughout?! :slight_smile:

Well. It’s an interesting problem, although it requires the use of one of the system’s private frameworks, for which there’s no published documentation.

The script here displays a couple of dialogs which allow you to specify the search and replace strings to be universally applied, to choose whether to save the edited scripts to new files (recommended) or to overwrite the originals (at your own risk), and to choose the root folder of the hierarchy containing the files. It assumes that all the files have the correct name extensions for their type (not “.script”) and saves the edited versions as the same type: compiled script file (“.scpt”), compiled script bundle (“.scptd”), script application (“.app”), or uncompiled source code (“.applescript”). It’s not able to tell if script applications have been set to be stay-open or to display a start-up screen. Scripts not containing the search string aren’t resaved. It’s quite fast, but I haven’t tried it with hundreds of files.

The two strings you mentioned are the default text in the first dialog, the quotes being literal characters in them.

main()

on main()
	script ASObjCStuff
		use AppleScript version "2.4" -- Yosemite (10.10) or later
		use framework "Foundation"
		use framework "OSAKit" -- No Xcode documentation.
		use scripting additions
		
		on doTheBusiness()
			global |⌘|, searchString, replaceString, savingToNewFiles, fileManager, directoryAndBundleKeys, directoryResult, skipHiddenFiles, relevantExtensions, storageTypes, extensionRegex, insertionTemplate
			
			-- Preset a few ASObjC values.
			set |⌘| to current application
			set fileManager to |⌘|'s class "NSFileManager"'s defaultManager()
			set directoryAndBundleKeys to |⌘|'s class "NSArray"'s arrayWithArray:({|⌘|'s NSURLIsDirectoryKey, |⌘|'s NSURLIsPackageKey})
			set directoryResult to |⌘|'s NSDictionary's dictionaryWithObjects:({true, false}) forKeys:(directoryAndBundleKeys)
			set skipHiddenFiles to |⌘|'s NSDirectoryEnumerationSkipsHiddenFiles
			set relevantExtensions to |⌘|'s class "NSArray"'s arrayWithArray:({"scpt", "scptd", "app", "applescript"})
			set storageTypes to |⌘|'s class "NSDictionary"'s dictionaryWithObjects:({|⌘|'s OSAStorageScriptType, |⌘|'s OSAStorageScriptBundleType, |⌘|'s OSAStorageApplicationBundleType, |⌘|'s OSAStorageTextType}) forKeys:(relevantExtensions)
			set extensionRegex to |⌘|'s class "NSString"'s stringWithString:("(?i)\\.(?:scptd?|app(?:lescript)?)$")
			set insertionTemplate to |⌘|'s class "NSString"'s stringWithString:(" (edited)$0")
			-- Get input from the user.
			set confirmation to missing value
			repeat until (confirmation is "OK")
				set {text returned:input, button returned:mode} to (display dialog "Please enter the search and replace strings on separate lines and click the appropriate button:" default answer ("\"application1\"" & linefeed & "\"application2\"") buttons {"Cancel", "Overwriting existing files", "Saving to new files"} cancel button 1 with title "Replace text in multiple script files")
				set {searchString, replaceString} to input's paragraphs
				set confirmation to button returned of (display dialog ("Replace every instance of:" & linefeed & "“" & searchString & "”" & linefeed & "with:" & linefeed & "“" & replaceString & "”?" & linefeed & mode & "?") buttons {"Cancel", "No. Go back", "OK"} cancel button 1 with title "Replace text in multiple script files")
			end repeat
			set mainFolderPath to POSIX path of (choose folder with prompt ("Please choose a folder hierarchy containing hundreds of script files in which" & linefeed & "every instance of “" & searchString & "” has to be changed to “" & replaceString & "”:"))
			-- If the script's still running, set ASObjC versions of the two strings and process the chosen folder.
			set searchString to |⌘|'s class "NSString"'s stringWithString:(searchString)
			set replaceString to |⌘|'s class "NSString"'s stringWithString:(replaceString)
			set savingToNewFiles to (mode is "Saving to new files")
			set mainFolderURL to |⌘|'s class "NSURL"'s fileURLWithPath:(mainFolderPath) isDirectory:(true)
			dealWithFolder(mainFolderURL)
		end doTheBusiness
		
		(* Recursively sift through the contents of a folder. *)
		on dealWithFolder(folderURL)
			global fileManager, directoryAndBundleKeys, directoryResult, skipHiddenFiles
			
			set thisFoldersContents to fileManager's contentsOfDirectoryAtURL:(folderURL) includingPropertiesForKeys:(directoryAndBundleKeys) options:(skipHiddenFiles) |error|:(missing value)
			repeat with thisURL in thisFoldersContents
				-- If this URL points to a folder, recurse to deal with its contents. Otherwise deal with the file or bundle.
				if (((thisURL's resourceValuesForKeys:(directoryAndBundleKeys) |error|:(missing value))'s isEqualToDictionary:(directoryResult)) as boolean) then
					dealWithFolder(thisURL)
				else
					dealWithFile(thisURL)
				end if
			end repeat
		end dealWithFolder
		
		(* Deal with a file or bundle, acting if it's a script. *)
		on dealWithFile(fileURL)
			global |⌘|, relevantExtensions, storageTypes, searchString, replaceString, savingToNewFiles, extensionRegex, insertionTemplate
			
			-- Act if the file has one of the recognised name extensions …
			set nameExtension to fileURL's pathExtension()'s lowercaseString()
			if ((relevantExtensions's containsObject:(nameExtension)) as boolean) then
				-- … and the script's successfully retrieved …
				set thisScript to (|⌘|'s class "OSAScript"'s alloc()'s initWithContentsOfURL:(fileURL) |error|:(missing value))
				if (thisScript is not missing value) then
					-- … and its source code exists and contains the search string.
					set sourceCode to thisScript's source()
					if ((sourceCode is not missing value) and ((sourceCode's containsString:(searchString)) as boolean)) then
						-- Edit the source code and make a new script document from it.
						set editedCode to (sourceCode's stringByReplacingOccurrencesOfString:(searchString) withString:(replaceString))
						set editedScript to (|⌘|'s class "OSAScript"'s alloc()'s initWithSource:(editedCode))
						-- Sort out a file URL for saving the edited script.
						if (savingToNewFiles) then
							-- If saving to a new file, the new URL's path is the the old one with " (edited)" inserted before the extension dot.
							set oldPath to fileURL's |path|()
							set newPath to (oldPath's stringByReplacingOccurrencesOfString:(extensionRegex) withString:(insertionTemplate) options:(|⌘|'s NSRegularExpressionSearch) range:({0, oldPath's |length|()}))
							set newURL to (|⌘|'s class "NSURL"'s fileURLWithPath:(newPath))
						else
							-- Otherwise the new URL _is_ the old.
							set newURL to fileURL
						end if
						-- Determine the required script file type from the name extension.
						set scriptFileType to storageTypes's objectForKey:(nameExtension)
						-- Save the edited script.
						tell editedScript to writeToURL:(newURL) ofType:(scriptFileType) usingStorageOptions:(|⌘|'s OSANull) |error|:(missing value)
					end if
				end if
			end if
		end dealWithFile
	end script
	
	tell ASObjCStuff to doTheBusiness()
end main

Edit: Changed the ways of identifying relevant name extensions (from an array) and file-storage types (from a dictionary).

Hello Nigel,

sorry, of course I meant .scpt. Your solution is awesome and works perfect. Thanks so much.
Yes, I have hundreds of .scpt files to control the usage of features of an symbol opentype font in applications that do not support Applescript such as Affinity Designer and Affinity Photo.

Thanks again!!