Find and replace in text files using AppleScript - file

I am trying to write an applescript which will run via a launch agent. What the script needs to do is edit a user preference plist file so that default save locations are specific to that user. I am aware that this can be done by just setting "~/documents" as the location in the template plist. But Premier Pro for example also needs to write scratch files to a local drive. For simplicity I would like each user to have these put in a locations based on their username. This script will only need to run if the local profile has just been created from a template at first log on.
I have started by using some sample code found on this site and just making a simple test below. This test should edit a txt file and replace one word with another. This script is currently not working. When tested it opens up test.txt in TextEdit but does nothing more. No errors are displayed either.
Thank you in advance
John.
replaceText("replace this", "replace with this", "/Volumes/USB_Drive/test.txt")
on replaceText(search_string, replacement_text, this_document)
tell application "TextEdit"
open this_document
set AppleScript's text item delimiters to the search_string
set this_text to the text of the front document as list
set AppleScript's text item delimiters to the replacement_text
set the text of the front document to (this_text as string)
close this_document saving yes
end tell
end replaceText

Here an version that doesn't need text edit. It will read the file in utf-8 encoding, update it's contents and store that back into the file as utf-8 encoded text. The reason I use an try block around writing the file is that there will be an error if another application has the file open with read permission at the same time. The considering case block can be wrapped around the set ti to every text item of theContent if you want the search and replace case sensitive. There is no need for this to be active when you replace the string, only for finding it.
set stringToFind to "replace that"
set stringToReplace to "with this"
set theFile to choose file
set theContent to read theFile as «class utf8»
set {oldTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, stringToFind}
set ti to every text item of theContent
set AppleScript's text item delimiters to stringToReplace
set newContent to ti as string
set AppleScript's text item delimiters to oldTID
try
set fd to open for access theFile with write permission
set eof of fd to 0
write newContent to fd as «class utf8»
close access fd
on error
close access theFile
end try

Well, yes, as #dj_bazzie_wazzie points out, you really don't need a text editor for this, you can use the terminal and do something like:
perl -pi -e 's/old text/new text/g' /path/to/theFile.plist
which of course you can use in AppleScript with the powerful do shell script command:
do shell script "perl -pi -e 's/" & search_string & "/" & replacement_text & "/g' " & quoted form of (POSIX path of file_path)
--assuming file_path is a variable with Mac-style (colon-separated) file path.

Modified from http://discussions.apple.com/message/20542594#20542594, the handler below does what you want. I made a few changes and added your variables. A few notes: (1) the 'my' before the handler call makes sure it is seen as the script's handler and not something TextEdit should interpret 'internally' (because it is in a tell block); (2) 'considering case' makes the handler case sensitive, which I assume you want; (3) You might consider something like TextWrangler, which has robust and feature-rich AppleScript support, and includes text replacement in its AS dictionary, as does Smile, a fantastic Script Editor (which can work with text, and formats plist files nicely).
tell application "TextEdit"
set workingWin to open this_document
set this_text to the text of the workingWin
set the text of the workingWin to (my replaceText(this_text, search_string, replacement_text))
close workingWin saving yes
end tell
to replaceText(someText, oldItem, newItem)
(*
replace all occurances of oldItem with newItem
parameters - someText [text]: the text containing the item(s) to change
oldItem [text, list of text]: the item to be replaced
newItem [text]: the item to replace with
returns [text]: the text with the item(s) replaced
*)
considering case
set {tempTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, oldItem}
try
set {itemList, AppleScript's text item delimiters} to {text items of someText, newItem}
set {someText, AppleScript's text item delimiters} to {itemList as text, tempTID}
on error errorMessage number errorNumber -- oops
set AppleScript's text item delimiters to tempTID
error errorMessage number errorNumber -- pass it on
end try
end considering
return someText
end replaceText

Related

AppleScript to edit an XML file?

I have an Apple Music Library output file that looks like this:
<key>6871</key>
<dict>
<key>Track ID</key><integer>6871</integer>
<key>Name</key><string>12 Wake Up Call</string>
<key>Artist</key><string>Rebelution</string>
<key>Album Artist</key><string>Rebelution</string>
<key>Grouping</key><string>AllMusic</string>
<key>Kind</key><string>Apple Music AAC audio file</string>
<key>Size</key><integer>6178208</integer>
<key>Total Time</key><integer>257332</integer>
<key>Year</key><integer>2009</integer>
<key>Date Modified</key><date>2011-11-22T23:32:45Z</date>
<key>Date Added</key><date>2011-12-14T23:30:26Z</date>
<key>Bit Rate</key><integer>256</integer>
<key>Sample Rate</key><integer>44100</integer>
<key>Play Count</key><integer>101</integer>
<key>Play Date</key><integer>3717804040</integer>
<key>Play Date UTC</key><date>2021-10-23T07:20:40Z</date>
<key>Skip Count</key><integer>10</integer>
<key>Skip Date</key><date>2020-09-16T14:39:31Z</date>
<key>Rating</key><integer>60</integer>
<key>Album Rating</key><integer>60</integer>
<key>Album Rating Computed</key><true/>
<key>Normalization</key><integer>1699</integer>
<key>Artwork Count</key><integer>1</integer>
<key>Persistent ID</key><string>56B43C03AFF476E5</string>
<key>Track Type</key><string>Remote</string>
<key>Apple Music</key><true/>
</dict>
I am trying to make this easier to store in a database (I don't understand SQL, but that's the end goal). For now I am adding and looking up "entries" in an excel sheet. I am able to manipulate the XML file manually by pasting it into a workbook, then I have to use ablebits and vlookups and a bunch of other time consuming operations which I paste into a new text file. End goal of this question is to get my "XML" file to look like this:
<key>5056</key>
<dict>
<TrackID>5056</TrackID>
<Name>Heart Like a Lion</Name>
<Artist>Rebelution</Artist>
<AlbumArtist>Rebelution</AlbumArtist>
<Composer>Eric Ariel Rachmany, Marley D. Williams, Rourke Carey & Wesley Dallas Finley</Composer>
<Album>Courage to Grow</Album>
<Grouping>LIBRARY</Grouping>
<Genre>Reggae</Genre>z
<Kind>Apple Music AAC audio file</Kind>
<Size>11679958</Size>
<TotalTime>338413</TotalTime>
<DiscNumber>1</DiscNumber>
<DiscCount>1</DiscCount>
<TrackNumber>2</TrackNumber>
<TrackCount>12</TrackCount>
<Year>2007</Year>
<DateModified>2021-11-10T08:29:23Z</DateModified>
<DateAdded>2021-11-10T08:29:23Z</DateAdded>
<BitRate>256</BitRate>
<SampleRate>44100</SampleRate>
<PlayCount>8</PlayCount>
<PlayDate>3747937611</PlayDate>
<PlayDateUTC>2022-10-07T01:46:51Z</PlayDateUTC>
<ReleaseDate>2007-06-08T12:00:00Z</ReleaseDate>
<Rating>100</Rating>
<AlbumRating>60</AlbumRating>
<AlbumRatingComputed></AlbumRatingComputed>
<ArtworkCount>1</ArtworkCount>
<SortAlbum>Courage to Grow</SortAlbum>
<SortArtist>Rebelution</SortArtist>
<SortName>Heart Like a Lion</SortName>
<PersistentID>AD1A6E4E78F9C79D</PersistentID>
<TrackType>Remote</TrackType>
<AppleMusic></AppleMusic>
</dict>
Anything will help, this has become more time consuming and difficult than I thought.
Im also open to alternative routes... I just want to backup my metadata because I lost it once (recovered it manually as mentioned above), but I also have some good ideas for making playlists based on timestamps of metadata values.
Oh side note... Im also open to using another language if that's easier. I have minimal background in code and have been teaching myself AppleScript since my scrips are mostly interacting with Apple stuff.
Thanks!
AppleScriptObjC can be used to access the various Cocoa frameworks, for example to read a plist/xml file into an NSDictionary (similar to a record), where the various keys can be accessed programmatically, and for utilities such as date formatting, list sorting, etc.
There is an NSXMLNode class that can be used to create the elements, but in this case manually converting the dictionary keys isn't quite as wordy.
The following script creates a plain XML file from an Apple Music Library export. It extracts the specified key items into a track element and uses the track ID as an element attribute:
use framework "Foundation" -- for the AppleScriptObjC bits
use scripting additions
# the dictionary keys to extract (use an empty list {} for everything):
property keyNames : {"Name", "Kind", "Size", "Total Time", "Date Added", "Track Type", "Location"}
property keepSet : missing value -- this will be an NSSet of the keys
property indent : " " -- formatting
on run -- create an XML file for track data from an exported Music Library plist/XML file
if keyNames is not in {"", {}, missing value} then set keepSet to current application's NSMutableSet's setWithArray:(keyNames as list)
set fileURL to current application's NSURL's fileURLWithPath:(POSIX path of (choose file of type {"com.apple.property-list", "public.xml"} with prompt "Choose the Music Library export file to process:"))
set fileData to current application's NSData's dataWithContentsOfURL:fileURL
try -- read file data (source XML file needs to be in Apple's property list format)
set plist to (current application's NSPropertyListSerialization's propertyListWithData:fileData options:(current application's NSPropertyListMutableContainersAndLeaves) format:(missing value) |error|:(missing value))
if plist is missing value then error "The chosen file is not an Apple plist/XML file."
set trackDict to (plist's valueForKey:"Tracks") -- dictionary of tracks
if trackDict is missing value then error "The chosen file does not have a 'Tracks' key in the root directory."
on error errmess
display alert "Script Error" message errmess
error number -128 -- cancel
end try
set theResult to ""
repeat with trackItem in trackDict's allKeys()
set trackKeyPath to "Tracks." & (trackItem as text) -- dictionary for individual track key
set theResult to theResult & addWrapper(trackItem as text, (XMLtext from (plist's valueForKeyPath:trackKeyPath)))
end repeat
writeToFile((choose file name default name "Converted Library.xml"), addWrapper(missing value, theResult))
end run
# return XML text from simple key/value pairs of a dictionary
on XMLtext from dictionary
set XMLElements to {}
set candidate to current application's NSMutableSet's setWithArray:(dictionary's allKeys())
if keepSet is not missing value then candidate's intersectSet:keepSet -- remove other keys
repeat with keyItem in candidate's allObjects()
try
set theItem to (dictionary's valueForKey:keyItem)
set theValue to theItem as text -- test
on error errmess -- can't coerce object to text
set theClass to current application's NSStringFromClass(theItem's class) as text
if theClass contains "Date" then -- format NSDate
set theValue to (current application's NSISO8601DateFormatter's alloc's init()'s stringFromDate:theItem) as text
else -- something needing additional formatting or processing such as a collection, etc
log theClass & ": " & errmess
set theValue to "*ERROR*" -- or add formatting for the object
end if
end try
set keyName to (keyItem's lowercaseString's stringByReplacingOccurrencesOfString:" " withString:"_") -- no spaces in key names
set end of XMLElements to indent & indent & "<" & keyName & ">" & theValue & "</" & keyName & ">" & linefeed -- can also use NSXMLNode
end repeat
set elementArray to current application's NSArray's arrayWithArray:XMLElements
return (elementArray's sortedArrayUsingSelector:"compare:") as list as text -- sort
end XMLtext
# add wrappers for individual track entries or the document
to addWrapper(theKey, theText)
if theKey is not missing value then -- wrap individual track elements - the key is used as an attribute
return linefeed & indent & "<track id=\"" & theKey & "\">" & linefeed & theText & indent & "</track>" & linefeed
else -- wrapper and root element for a standard XML document
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!-- track data extracted " & ((current date) as «class isot» as string) & " from exported Apple Music Library -->
<music_tracks>" & theText & "</music_tracks>" & linefeed
end if
end addWrapper
on writeToFile(filePath, whatever)
try
set fileRef to (open for access filePath with write permission)
set eof of fileRef to 0 -- overwrite existing
write whatever to fileRef starting at eof
close access fileRef
on error
try
close access fileRef
end try
end try
end writeToFile

Turn list into applescript array

I have an applescript function which processes each item of an array. I'm looking for a way to drop a list like the following into my applescript file:
arrayitem1
arrayitem2
arrayitem3
and to use applescript to automatically format it into the proper applescript array syntax so I end up with this:
set array1 to {"arrayitem1", "arrayitem2", "arrayitem3"}
As you can imagine, for longer arrays it would be a hassle to manually add all the commas and quotations.
Thanks!
set stringofitems to "
item1
item2
item3
"
set text item delimiters to "
"
set listofitems to text items of stringofitems
repeat with theitem in listofitems
# stuff to do with each item
end repeat
If the original file is a line separated text, "paragraphs" can be used to split it. endList has to be defined as list '{}' Now thru repeat, each line of the content in the clipboard will be entered as a new entry into the list endList.
set {var, endList} to {the clipboard, {}}
repeat with x in paragraphs of var
set endList to endList & x
end repeat
If I understand what all you're wanting, I'd suggest a script where you select and copy the string in your google doc, then run a script that pulls the string from the clipboard, does the changes you want, then puts the result onto the clipboard ready to be pasted where you want.
Here's an example using what you said you wanted.
-- get the string into the script through the clipboard
set theString to the clipboard
-- convert the string into a list, using paragraphs
set theParagraphs to paragraphs of theString
-- convert the list to a string, with ", " between items
set AppleScript's text item delimiters to "\", \""
set theResult to ("\"" & theParagraphs as string) & "\""
set AppleScript's text item delimiters to ""
set the clipboard to theResult
-- "arrayitem1", "arrayitem2", "arrayitem3"

how to select a file by default when open a folder in vfp 9.0

I want to open a folder, and select a file by default.
I do it like this:
Declare Long WinExec In kernel32 String #, Integer
WinExec("Explorer /select, C:\tt.txt",5)
But if the folder has been opened, the file can't be selected by default.
How to do it?
What is your purpose of prompting a user with picking a particular file...
The closest you can get from wthin VFP is "GetFile()" where you can give it a default extension of a file you are hoping to find and it brings up a file selection dialog.
lcFileSelected = GetFile( "Txt", "Caption left of combobox selection (but only shows about 16 chars)", "Button Caption", nOptionalButton )
where ex:
nOptionalButton
0 = no extra button at bottom right, just the OK, Cancel (where OK is overridden by the "Button Caption" sample above.
1 = OK, New, Cancel
2 = Ok, None, Cancel
If a value selected, you'll have the file name, otherwise blank.
REVISED ANSWER..
Then what you want is PUTFILE() which allows you to prompt a user a simple message, similar to a "Save to", and allows to put a fully qualified path and file name. Upon return, much like that of doing GETFILE() will return the final path/file name entered by the user. Ex:
lcUserAnswer = PUTFILE( "save where", "C:\program files\myTest.txt" )
now you can do whatever with the "lcUserAnswer" variable...

How to create a new List of files and return them?

I'm using the following AppleScript in my Automator workflow:
on run {input, parameters}
--say input
set result_array to {} as list
try
repeat with this_file in input
tell application "Image Events"
launch
set this_image to open this_file
copy dimensions of this_image to {W, H}
close this_image
end tell
set text item delimiters to ":"
set file_name to last text item of (this_file as string)
set text item delimiters to ""
set filter_string to W & "x" & H as string
if file_name contains filter_string then
set the end of result_array to this_file
end if
end repeat
return result_array
on error error_message
display dialog error_message
end try
end run
The script runs after a Get Folder Contents action.
There is something wrong with the result array but I can't figure out what.
The result has to be a list of files which have their size in their names.
If I understood correctly use one of the following :
if file_name contains filter_string then
-- set the end of result_array to this_file as alias
-- set the end of result_array to this_file as text
end if
Currently this_file is like a pointer to the original list of files. When used directly it includes the index of the current value and the full list.
This is probably due to how the repeat with a in b is implement in Applescript.

How to detect files (even in subfolders) with a specific extension and open them?

I'm trying to make a droplet applscript app. It should do:
when a folder is dropped it should "scan" the folder,its subfolders and files
every file with a specific extension (such as: .txt" should be opened in an program and something should be done
that's all
I get this error when I detected the right file and if it is trying to open it (the app won't be started- before that I get this error and the script cancels):class nmxt of alias "the path to the file" could not be read
Currently my script:
on open {input}
set theFiles to (getFilesRecursively(input, "plhs"))
repeat with oneFile in theFiles
if name extension of oneFile is "plhs" then
tell application "Applic"
open oneFile
activate
tell application "System Events"
tell process "Applic"
click menu item "Save" of menu 1 of menu bar item "File" of menu bar 1
end tell
end tell
end tell
end if
end repeat
end open
on getFilesRecursively(fContainer, fExt)
tell application "Finder"
set recursiveFileList to entire contents of fContainer as alias list
set resultFileList to {}
repeat with aFile in recursiveFileList
if name extension of aFile contains fExt then
set resultFileList to resultFileList & aFile
end if
end repeat
end tell
return resultFileList
end getFilesRecursively
Here is a germinal script that should get you going:
property kTargetFileExtension : "txt"
property pValidFileList : {}
on open of theFiles -- Executed when files or folders are dropped on the script
set fileCount to (get count of items in theFiles)
repeat with thisFile from 1 to fileCount
set theFile to item thisFile of theFiles
tell application "System Events"
set file_info to get info for theFile
end tell
if visible of file_info is true then -- check for the file extension here as well
if folder of file_info is true then
my createList(theFile)
else
set fileName to name of file_info
set targetFileFound to isTargetFile(fileName, kTargetFileExtension) of me
if (targetFileFound) then
set end of pValidFileList to theFile
end if
end if
end if
end repeat
display dialog "pValidFileList = " & pValidFileList
(* do something with your files listed in pValidFileList here *)
end open
on createList(mSource_folder)
set item_list to ""
tell application "System Events"
set item_list to get the name of every disk item of (mSource_folder as alias)
end tell
set item_count to (get count of items in item_list)
repeat with i from 1 to item_count
set the_properties to ""
set the_item to item i of the item_list
set fileName to the_item
set the_item to ((mSource_folder & the_item) as string) as alias
tell application "System Events"
set file_info to get info for the_item
end tell
if visible of file_info is true then -- check for the file extension here as well
if folder of file_info is true then
my createList(the_item)
else
set targetFileFound to isTargetFile(fileName, kTargetFileExtension) of me
if (targetFileFound) then
set end of pValidFileList to the_item
end if
end if
end if
end repeat
end createList
on isTargetFile(theFilename, theTargetExtension) -- (string, string) as boolean
set AppleScript's text item delimiters to "."
set fileNameList to every text item of theFilename
set AppleScript's text item delimiters to ""
try
set theFileExtension to item 2 of fileNameList as string
on error
return false
end try
if theFileExtension is theTargetExtension then
return true
end if
return false
end isTargetFile
A couple things to note:
System Events is the current best practice for getting lists and information about files. Just asking for the entire contents is faster but known to be unreliable. This method of crawling manually is slower but there is no doubt you will be getting the files you need.
isTargetFile actually just works with the filename as a string, as opposed to relying on the system to give the information. Six of one, half dozen of the other if you ask me, but this does reduce the number of calls to the system, so I imagine it makes this a bit faster.
I also tend to add an on run {} block to these things to allow for manual selection of a folder. Doing so also facilitates testing.
How to use:
Save the script as an application, and you should get a droplet (the Applescript application icon with the arrow pointing down).
on open of theFiles is the equivalent of main(). You can drop any combination of files and folders onto the droplet, and it will handle the rest. It ends up with a list of target files that you can then loop through for processing. I will leave it as an exercise for you to add in that bit.
To customize the target, change the string in the first line, property kTargetFileExtension : "txt", to whatever extension you are looking for. This can also be changed to an array—property kTargetFileExtension : {"txt", "rtf", "doc} for example—but you will also need to update isTargetFile(theFilename, theTargetExtension) to loop through those as well.
Beyond that, this just works. Right now, it will gather a list of txt files for processing.
Add salt to taste.

Resources