I have the below script, probably written by @ShaneStanley, that will return the recent files for the frontmost app. How can I change this to provide these options:
All apps
Only files that were modified
Only files that were opened, but not modified
Only files that were created (have not been modified)
Only files opened by a list of apps (think “Fav apps”, “Recent Apps”, etc).
which leads me to these questions: How do I get a list of recent apps and open apps?
I know how to get open apps using System Events, but I thought there might be a better way.
Finally, is there a way to distinguish between script files that have had the source changed vs just being recompiled?
In case you’re curious, Keyboard Maestro uses osascript file, and this causes the file to be recompiled and thus the modification date updated. I’d like to identify only those script files whose source has been changed.
Can osascript be used without recompiling the script file?
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
# AUTHOR: Probably @ShaneStanley and/or @ccstone
-------------------------------------------------------------------------------------------
#### USER SETTINGS ####
-------------------------------------------------------------------------------------------
set dataFilePath to "~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.apple.scripteditor2.sfl"
set outputType to "file_path" -- the alternatives are two: "file_path" and "folder_path"
-------------------------------------------------------------------------------------------
set dataFilePath to (current application's NSString's stringWithString:dataFilePath)'s stringByExpandingTildeInPath
set theData to the current application's NSData's dataWithContentsOfFile:dataFilePath
set {theDict, theError} to current application's NSPropertyListSerialization's propertyListWithData:theData ¬
options:(current application's NSPropertyListMutableContainersAndLeaves) |format|:(missing value) |error|:(reference)
if theDict = missing value then error (theError's localizedDescription() as text)
set theValues to (theDict's valueForKey:"$objects")
set thePred to current application's NSPredicate's ¬
predicateWithFormat:"(self isKindOfClass: %@) AND (self BEGINSWITH %@)" argumentArray:{current application's NSString's |class|(), "file:///"}
set theFiles to (theValues's filteredArrayUsingPredicate:thePred)
set theSet to current application's NSMutableSet's |set|()
repeat with aFile in theFiles
if outputType is "folder_path" then -- return container folder path of each recent file:
(theSet's addObject:((current application's |NSURL|'s URLWithString:aFile)'s |path|()'s stringByDeletingLastPathComponent()))
else if outputType is "file_path" then -- return full path of each recent file:
(theSet's addObject:((current application's |NSURL|'s URLWithString:aFile)'s |path|()))
end if
end repeat
# Remove any duplicate entries:
set outputPathList to theSet's allObjects() as list -- returns POSIX Paths
set AppleScript's text item delimiters to linefeed
return outputPathList as text
The OS has stopped using this method to store recent files and apps since that script was written, and I haven’t really looked at what it uses now.
No.
Just to be clear, this is not really an issue with osascript. It’s normal for an app, after it has run a script, to check if anything has changed, and if so update the file on disk. If it didn’t, properties would not be persistent. I’m not sure if KM or osascript is doing it, but it’s standard behavior.
Two ideas to consider. First, using version numbers. Second, stop the scripts from being saved after running. You can do this by changing permissions, but you can also do it by avoiding changes to any top-level variables, or going the other way and setting one to a Cocoa object when the script runs.
I’m already using version numbers, but that does not really help my issue. When I do a search for scripts, I often want to view the scripts in order of reverse mod date.
So, let’s talk about permissions. Is there a way I can make all files in a folder read only to a specific app, like KM, but still be writeable by SD?
I don’t know what that means. Could you please give me an example?
After testing both of your shell scripts, I find this AppleScript to be simpler and faster:
tell application "System Events"
set appNameList to name of every application process whose background only is false
end tell
For reference, here is the full script using lsappinfo required to produce the same results:
set appInfoStr to do shell script "lsappinfo find applicationtype=\"Foreground\""
--- Requires Satimage.osax ---
set regexFind to "ASN:.+?\"(.+?)\": ?"
set regexReplace to "\\1" & linefeed
set appNameTextList to change regexFind into regexReplace in appInfoStr syntax "PERL" with regexp
set AppleScript's text item delimiters to linefeed
set appNameList to text items of appNameTextList
The script in my OP still works, it is just limited to the current frontmost app.
It is easy enough to get a list of currently running apps, and with more difficulty, a list of recent apps. So, given a list of apps, is there a way to loop the script through each app without activating each app?
Actually, I just looked at the files in this folder: ~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments
Sorted on rev Mod Date, it looks like a list of my recent apps where I have opened a document:
Could we not just go through this list until our criteria are met (last 7 days, for ex.)?
I won’t argue about the simplicity, but the speed drag is down to using an external osax to do regex instead of using native macOS APIs. You should find this faster and more reliable than calling on System Events:
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
property NSString : a reference to current application's NSString
property NSBackwardsSearch : a reference to 4
set foregroundApps to {}
set appText to (do shell script "lsappinfo find applicationtype=\"Foreground\"")
set appString to NSString's stringWithString:appText
set appList to appString's componentsSeparatedByString:"ASN:"
repeat with anApp in appList
set aRange to (anApp's rangeOfString:"\"")
set bRange to (anApp's rangeOfString:"\"" options:NSBackwardsSearch)
try
set appName to (anApp's substringWithRange:{(aRange's location) + (aRange's |length|), (((bRange's location) - 1) - (aRange's location))})
set end of foregroundApps to appName as text
end try
end repeat
foregroundApps
It looks like I was getting methods confused, and that that file is still used.
It’s readable, but not particularly intelligible. If you look at it, you can see it’s been produced by NSKeyedArchiver, not NSPropertyListSerialization. And if you try to unarchive it, you get an error:
-[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (SFLListItem) for key (NS.objects); the class may be defined in source code or a library that is not linked
In other words, an instance of a custom class (SFLListItem) has been serialized (presumably securely) and written to disk. Even if you knew how that class was implemented, picking out the pieces would be quite the challenge. The original script just plucked out all the URLs; I suspect anything more is going to be very tricky.
Sorry, on further investigation I think you may be on a hiding to nothing here.
First, those .sfl files don’t store dates, so I’m afraid you’re out of luck on that one. And as Shane says, parsing the data out of a custom class in any reasonable way with a script is going to be tricky (and fragile).
Second, looking at what’s in my folder, it doesn’t look like all applications are actually recording recent items.
About the best I can offer is a list of recent filenames (no paths, no parent apps), using strings and grep with something like this:
(that’s a shell command, so wrap it in a do shell script and escape appropriately if you’re going to run it as an AppleScript)
but you may be able to do better yourself with your regex skills.
Addendum: you already indicated this wasn’t something you wanted to invest much time with, but I’ll just add for the sake of completion that you may be able to parse this info out of fsevents. I have an interest in macos forensics and have played with this a couple of times before, but you’ll need to roll your sleeves up
Based largely on the information in this thread so far, here’s a handler to get the URLs of the currently open apps’ recent files. It returns an array of dictionaries, each dictionary representing an app and containing the app’s bundle ID and an array of file info dictionaries. Each of these info dictionaries contains a file URL and a boolean indicating whether or not the bookmark from which it was derived is “stale” — whatever that means. The hope is that the information you need can be gleened from the URLs’ resource values.
On my High Sierra machine, every SFL file dated since 15th November 2017 has an “.sfl2” extension, which seems to indicate a change of format around then. The script here works on both my 10.13.4 and 10.11.6 machines.
use AppleScript version "2.4" -- Mac OS 10.10 (Yosemite) or later.
use framework "Foundation"
use scripting additions
(*
Return an array of dictionaries containing URLs for the recent files of all open foreground applications which have them.
Each dictionary represents an application and is in the form:
{docURLs:{{|url|:<file URL>, staleBookmark:<boolean>}, …}, appBundleID:<string>}
*)
on getRecentFileURLsForOpenForegroundApps()
-- Get the bundle IDs of the currently open foreground applications.
set foregroundAppIDs to paragraphs of (do shell script "lsappinfo list | sed -En '
/^[[:space:]]+ bundleID=\"/ h
/type=\"Foreground\"/ {
g
s/^[^\"]+\"|\"[^\"]*$//g
p
}'")
set |⌘| to current application
-- Get the POSIX paths of all visible items in ~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/.
set fileManager to |⌘|'s class "NSFileManager"'s defaultManager()
set applicationRecentDocsFolderURL to |⌘|'s class "NSURL"'s fileURLWithPath:(POSIX path of (path to application support from user domain) & "com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments")
set applicationRecentDocsSFLPaths to (fileManager's contentsOfDirectoryAtURL:(applicationRecentDocsFolderURL) includingPropertiesForKeys:({}) options:(|⌘|'s NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value))'s valueForKey:("path")
-- Filter for paths with ".sfl" extensions and names corresponding to the bundle IDs of the currently open foreground applications.
-- The IDs in the paths are lower cased, whereas the bundle IDs proper may be mixed cased.
set filteredSFLPaths to applicationRecentDocsSFLPaths's mutableCopy() -- This is filtered below.
set foregroundAppIDs to |⌘|'s class "NSArray"'s arrayWithArray:(foregroundAppIDs)
set foregroundAppSLFsOnlyFilter to |⌘|'s class "NSPredicate"'s predicateWithFormat:("(pathExtension BEGINSWITH[c] 'sfl') AND (lastPathComponent.stringByDeletingPathExtension IN %@)") argumentArray:({foregroundAppIDs's valueForKey:("lowercaseString")})
tell filteredSFLPaths to filterUsingPredicate:(foregroundAppSLFsOnlyFilter)
-- In the folder on my 10.13.4 system, all the SFL files dated on or after 15th November 2017 have ".sfl2" extensions. Possibly a new format, since the "items.Bookmark" key path below doesn't work with the ".sfl" files on my 10.11.6 machine. Here, filter out any obsolete path matches by working backwards through a reversed lexical sort of the matches and, where a later path is followed by an earlier version, removing the latter from the array.
set reverseSortDescriptor to |⌘|'s class "NSSortDescriptor"'s sortDescriptorWithKey:("self") ascending:(false) selector:("caseInsensitiveCompare:")
tell filteredSFLPaths to sortUsingDescriptors:({reverseSortDescriptor})
repeat with i from (count filteredSFLPaths) - 1 to 1 by -1
set thisSFLPath to (item i of filteredSFLPaths)
if not ((thisSFLPath's hasSuffix:(".sfl")) as boolean) then
set originalPath to (thisSFLPath's stringByDeletingPathExtension()'s stringByAppendingPathExtension:("sfl"))
if (((item (i + 1) of filteredSFLPaths)'s hasPrefix:(originalPath)) as boolean) then tell filteredSFLPaths to removeObjectAtIndex:(i)
end if
end repeat
-- Build an array of dictionaries, each dictionary containing an app bundle ID and an array of associated recent file URL info dictionaries.
set outputArray to |⌘|'s class "NSMutableArray"'s new()
repeat with thisSFLPath in filteredSFLPaths
-- Unarchive the data from this SFL file and extract any bookmark data values.
set unarchivedSFLData to (|⌘|'s class "NSKeyedUnarchiver"'s unarchiveObjectWithFile:(thisSFLPath))
if ((thisSFLPath's hasSuffix:(".sfl")) as boolean) then -- Assuming this to be the criterion for the different case in the keypath.
set bookmarkDataValues to (unarchivedSFLData's valueForKeyPath:("items.bookmark"))
else
set bookmarkDataValues to (unarchivedSFLData's valueForKeyPath:("items.Bookmark"))
end if
if ((count bookmarkDataValues) > 0) then
-- If bookmark data values are found, create an array containing URL/staleBookmark dictionaries for each.
set URLInfoArray to |⌘|'s class "NSMutableArray"'s new()
repeat with theseBookmarkData in bookmarkDataValues
set URLInfoDictionary to (|⌘|'s class "NSDictionary"'s dictionaryWithObjects:(|⌘|'s class "NSURL"'s URLByResolvingBookmarkData:(theseBookmarkData) options:(0) relativeToURL:(missing value) bookmarkDataIsStale:(reference) |error|:(missing value)) forKeys:{"URL", "staleBookmark"})
tell URLInfoArray to addObject:(URLInfoDictionary)
end repeat
-- Get the original, possibly mixed-case bundle ID for the app associated with this SFL file.
set IDMatchFilter to (|⌘|'s class "NSPredicate"'s predicateWithFormat:("self ==[c] %@") argumentArray:({thisSFLPath's lastPathComponent()'s stringByDeletingPathExtension()}))
set appBundleID to (foregroundAppIDs's filteredArrayUsingPredicate:(IDMatchFilter))'s firstObject()
-- Create a dictionary containing the bundle ID and URL info dictionary array for this app and add it to the output array.
set appIDAndDocURLInfo to (|⌘|'s class "NSDictionary"'s dictionaryWithObjects:({appBundleID, URLInfoArray}) forKeys:({"appBundleID", "RecentDocURLInfo"}))
tell outputArray to addObject:(appIDAndDocURLInfo)
end if
end repeat
return outputArray
end getRecentFileURLsForOpenForegroundApps
getRecentFileURLsForOpenForegroundApps()
Pity — although in fact most of the other twelve in your list either don’t have or probably don’t have “Open Recent” submenus in their “File” menus. I can confirm though that the URLs returned for TextWrangler are all NSNulls. The data values are there in the SFL2 file, but are obviously not straight bookmark data. TextWrangler’s/BBEdit’s “Open Recent” menu has a non-standard format.
This is actually safer than the lsappinfo/sed shell script I used, in which the sed code depends on the bundleID line coming before the type line in the lsappinfo result!
use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
property NSString : a reference to current application's NSString
property NSCharacterSet : a reference to current application's NSCharacterSet
set foregroundBundleIDs to {}
set appText to NSString's stringWithString:(do shell script "lsappinfo list")
set appEntries to appText's componentsSeparatedByString:(return & return)
repeat with entry in appEntries
set entryTxt to entry as text
if entryTxt contains "Foreground" then
set theLoc to (entry's rangeOfString:"bundleID=\"")
set theLength to (entry's rangeOfString:"bundle path=")
set theRange to current application's NSMakeRange((theLoc's location) + (theLoc's |length|), (theLength's location) - ((theLoc's location) + (theLoc's |length|)))
set bundleID to (entry's substringWithRange:theRange)
set bundleID to (bundleID's stringByTrimmingCharactersInSet:(NSCharacterSet's whitespaceAndNewlineCharacterSet()))
set bundleID to (bundleID's stringByTrimmingCharactersInSet:(NSCharacterSet's punctuationCharacterSet()))
set end of foregroundBundleIDs to bundleID as text
end if
end repeat
foregroundBundleIDs