Retrieving properties of applications

The current handler, appInfo, returns properties of one or more applications efficiently. The applications may be running or non-running. If non-running, properties will be retrieved without launching the application.

The handler’s input argument is an application reference, or a list of application references. Each application reference may be in any of the following forms:
- Application name with or without a “.app” extension (e.g., “Safari” or “Safari.app”)
- Bundle identifier (e.g., “com.apple.Safari”)
- Full HFS path to the application bundle (e.g., “[startup disk name]:Applications:Safari.app”)
- Full POSIX path to the application bundle (e.g., “/Applications/Safari.app”)
- AppleScript alias to the application bundle (e.g., alias “[startup disk name]:Applications:Safari.app:”)
- «class furl» file reference to the application bundle (e.g., “/Applications/Safari.app” as POSIX file)
- NSURL object referring to the application bundle (e.g., current application’s |NSURL|'s fileURLWithPath:“/Applications/Safari.app”)
- Unix process ID as an integer (e.g., 60839) or text string (e.g., “60839”), if the application is running

The handler’s return value is an AppleScript record of application properties of the form {appName:…, executableName:…, bundleIdentifier:…, posixPath:…, hfsPath:…, bundleURL:…, versionString:…, isAppleScriptEnabled:…, allowsUserInteraction:…, isBackgroundOnly:…, isRunning:…, processID:…, processIDs:…}
appName
→ application bundle name = [app name].app
executableName
→ executable name = /…/[app name].app/Contents/MacOS/[executableName]
bundleIdentifier
→ Apple’s bundle identifier in reverse DNS notation, e.g., “com.apple.Safari”
posixPath
→ POSIX path to .app bundle = “/…/[app name].app”
hfsPath
→ HFS path to .app bundle = “[startup disk name]:…:[app name].app”
bundleURL
→ NSURL object referring to [app name].app bundle
versionString
→ application version as text string (for some applications, this value is available only if the application is running)
isAppleScriptEnabled
→ boolean true value if the application is AppleScript-enabled (i.e., if its Info.plist item NSAppleScriptEnabled = YES or 1 or true)
allowsUserInteraction
→ boolean true value if the application runs in the background but allows user interaction (i.e., if its Info.plist item LSUIElement = YES or 1 or true)
isBackgroundOnly
→ boolean true value if the application runs in the background and does not allow user interaction (i.e., if its Info.plist item LSBackgroundOnly = YES or 1 or true)
isRunning
→ boolean true value if the application is running
processID
→ Unix ID of the newest running instance of the application, or the missing value if the application is not running
processIDs
→ list of the Unix IDs of all running instances of the application, or the empty list if the application is not running
NOTES:
- If the input argument is a single application reference, the output property values are the values themselves; if the input argument is a list of application references, the output record properties are lists of values corresponding to the input application references.
- Any missing output property values will be set to missing value instead.
- If the application can’t be found, all its output properties will be set to missing value.

The details of how the handler works are described in the handler’s comments. Some highlights are as follows:
- The handler retrieves an application’s properties efficiently, generally about 0.1 seconds per application, whether the application is running or not
- The bundle identifier is the first application property to be retrieved, from which the remaining properties are obtained
- If the application reference is in the form of a running application’s Unix process ID, the bundle identifier is retrieved using NSRunningApplication’s runningApplicationWithProcessIdentifier: method
- If the application reference is in any other form, the bundle identifier is retrieved from the application’s id property
- Retrieval via the id property allows efficient retrieval with the added benefit of not launching the application if it isn’t running
- The retrieval is performed within an osascript command wrapper, which has the benefit of preventing a choose application… dialog window from opening if the application isn’t found
- If the application reference is in the form of a bundle identifier, the bundle identifier is retrieved (i.e., validated) with the construct: application id [application reference]'s id
- If the application reference is in any other form (i.e., neither a process ID nor a bundle identifier), the bundle identifier is retrieved (after converting the reference to a text string if necessary) with the construct: application [application reference]'s id

Here’s an example of handler usage with a single application reference as the input argument:


	appInfo("Finder")
	-->
	{
	appName:"Finder", 
	executableName:"Finder", 
	bundleIdentifier:"com.apple.finder", 
	posixPath:"/System/Library/CoreServices/Finder.app", 
	hfsPath:"[startup disk name]:System:Library:CoreServices:Finder.app", 
	bundleURL:(NSURL) file:///System/Library/CoreServices/Finder.app/, 
	versionString:"10.13.6", 
	isAppleScriptEnabled:true, 
	allowsUserInteraction:false, 
	isBackgroundOnly:false, 
	isRunning:true, 
	processID:1438, 
	processIDs:{1438}
	}

Here’s an example of handler usage with multiple application references as the input argument:


appInfo({"com.apple.Dock", "/Applications/QuickTime Player.app", "Nonexistent App"})
-->
	{
		appName:{
			"Dock", 
			"QuickTime Player", 
			missing value
		},
		executableName:{
			"Dock", 
			"QuickTime Player", 
			missing value
		}, 
		bundleIdentifier:{
			"com.apple.dock", 
			"com.apple.QuickTimePlayerX", 
			missing value
		}, 
		posixPath:{
			"/System/Library/CoreServices/Dock.app", 
			"/Applications/QuickTime Player.app", 
			missing value
		}, 
		hfsPath:{
			"[startup disk name]:System:Library:CoreServices:Dock.app", 
			"[startup disk name]:Applications:QuickTime Player.app", 
			missing value
		}, 
		bundleURL:{
			(NSURL) file:///System/Library/CoreServices/Dock.app/, 
			(NSURL) file:///Applications/QuickTime%20Player.app/, 
			missing value
		}, 
		versionString:{
			"1.8", 
			"10.4", 
			missing value
		}, 
		isAppleScriptEnabled:{
			false, 
			true, 
			missing value
		}, 
		allowsUserInteraction:{
			true, 
			false, 
			missing value
		}, 
		isBackgroundOnly:{
			false, 
			false, 
			missing value
		}, 
		isRunning:{
			true, 
			false, 
			missing value
		}, 
		processID:{
			1423, 
			missing value, 
			missing value
		}, 
		processIDs:{
			{1423},
			{},
			missing value
		}
	}

Here is the handler:


on appInfo(appRef)
	-- Returns properties of running and non-running applications
	-- Wrap the code in a try block to capture any errors
	try
		-- Constants
		set classNSURL to current application's |NSURL|'s |class|()
		set sharedWorkspace to current application's NSWorkspace's sharedWorkspace()
		set noValueToken to "¦NO¦VALUE¦"
		---- Be sure the input argument is in the form of a list
		tell appRef to if its class ≠ list then set appRef to {it}
		-- Create the shell script that will return the application bundle identifiers, one bundle identifier per line of returned text
		set theScript to ""
		repeat with currRef in appRef
			tell currRef's contents to set {currRef, currClass} to {it, its class}
			try
				-- If the application reference is in the form of a process ID, be sure it is in the form of an integer
				set currRef to currRef as integer
			end try
			tell currRef
				try
					-- Create the shell comnmands that will return the current reference's bundle identifier, or, if the bundle identifier can't be found, a "no value" token instead
					if its class = integer then
						-- If the application reference is in the form of a process ID, get the bundle identifier from Cocoa's NSRunningApplication class
						-- If the bundle identifier can't be found, an error will be thrown, thereby setting the value to the "no value" token
						tell ((current application's NSRunningApplication's runningApplicationWithProcessIdentifier:it)'s bundleIdentifier())
							if it = missing value then error
							set theScript to theScript & linefeed & "echo " & (it as text)'s quoted form
						end tell
						-- The following is an alternative Bash solution that works by reading the bundle identifier directly from the application's Info.plist file; however, it runs about 1.7x slower than the NSRunningApplication solution above
						-- set theScript to theScript & linefeed & "/usr/libexec/PlistBuddy -c \"Print :CFBundleIdentifier\" \"$(ps -p " & it & " -o comm= | egrep -o -m 1 '^.+\\.app\\/Contents\\/')Info.plist\""
					else if {its class} is in {text, alias, «class furl», classNSURL} then
						-- If the application reference is supplied in any other form, coerce it to a text string, then get the bundle identifier from the application "id" property
						-- Wrap these actions inside an osascript command to prevent a "choose application" dialog window from opening if the application isn't be found
						-- If the bundle identifier can't be found, a "no value" token is returned instead
						-- Note that the first construct, "application ...'s id", handles all application reference forms except bundle identifer, which is handled by the second construct, "application id ...'s id", and Unix process ID, which is handled separately by Cocoa's NSRunningApplication class
						set theScript to theScript & linefeed & "osascript -e \"application \\\"" & it & "\\\"'s id\" || osascript -e \"application id \\\"" & it & "\\\"'s id\" || echo " & noValueToken
					else
						-- For any invalid application reference construct, force an error, thereby setting the value to the "no value" token
						error
					end if
				on error
					set theScript to theScript & linefeed & "echo " & noValueToken
				end try
			end tell
		end repeat
		-- Execute the shell script, and capture the bundle identifiers (or "no value" tokens for any bundle identifiers that can't be found) from the returned lines of text
		set bundleIdentifierCandidates to (do shell script theScript)'s paragraphs
		-- Derive the remaining output properties from the bundle identifiers
		set {bundleIdentifier, bundleURL, posixPath, hfsPath, appName, executableName, versionString, isAppleScriptEnabled, allowsUserInteraction, isBackgroundOnly, isRunning, processID, processIDs} to {{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}
		repeat with currBundleIdentifier in bundleIdentifierCandidates
			-- Set the baseline value of all properties of the current application reference (except the bundle identifier property) to the missing value
			set currBundleIdentifier to currBundleIdentifier's contents
			set {currBundleURL, currPOSIXPath, currHFSPath, currAppName, currExecutableName, currVersionString, currIsAppleScriptEnabled, currAllowsUserInteraction, currIsBackgroundOnly, currIsRunning, currProcessID, currProcessIDs} to {missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value, missing value}
			if currBundleIdentifier = noValueToken then
				-- If a bundle identifier could not be found for the current application reference, change its bundle identifier property value from the "no value" token to the missing value, and abort any further processing
				set currBundleIdentifier to missing value
			else
				-- If the current application reference has a valid bundle identifier, get its remaining properties
				-- To get properties stored in the application's Info.plist file, convert to plist file to an NSDictionary, and retrieve properties via NSDictionary's objectForKey: method
				try
					set currBundleURL to (sharedWorkspace's URLForApplicationWithBundleIdentifier:currBundleIdentifier)
				end try
				try
					tell ((currBundleURL)'s |path|()) to if it ≠ missing value then set currPOSIXPath to it as text
				end try
				try
					set currHFSPath to currPOSIXPath as POSIX file as text
				end try
				try
					set currInfoPlistDictionary to (current application's NSDictionary's dictionaryWithContentsOfFile:(currPOSIXPath & "/Contents/Info.plist"))
					try
						tell (currInfoPlistDictionary's objectForKey:"CFBundleName") to if it ≠ missing value then set currAppName to it as text
					end try
					try
						tell (currInfoPlistDictionary's objectForKey:"CFBundleExecutable") to if it ≠ missing value then set currExecutableName to it as text
					end try
					try
						tell (currInfoPlistDictionary's objectForKey:"CFBundleShortVersionString")
							if it = missing value then error
							set currVersionString to it as text
						end tell
					on error
						try
							tell (currInfoPlistDictionary's objectForKey:"CFBundleVersion") to if it ≠ missing value then set currVersionString to it as text -- handles the occasional application without a short version string
						end try
					end try
					try
						tell (currInfoPlistDictionary's objectForKey:"NSAppleScriptEnabled") to set currIsAppleScriptEnabled to {it as text} is in {"true", "1", "YES"}
					end try
					try
						tell (currInfoPlistDictionary's objectForKey:"LSUIElement") to set currAllowsUserInteraction to {it as text} is in {"true", "1", "YES"}
					end try
					try
						tell (currInfoPlistDictionary's objectForKey:"LSBackgroundOnly") to set currIsBackgroundOnly to {it as text} is in {"true", "1", "YES"}
					end try
				end try
				-- Get the Unix process IDs of all running instances of the application
				try
					tell ((current application's NSRunningApplication's runningApplicationsWithBundleIdentifier:currBundleIdentifier) as list)
						set currProcessIDs to {}
						repeat with currObj in it
							try
								set end of currProcessIDs to currObj's processIdentifier() as integer
							end try
						end repeat
						if currProcessIDs = {} then error
						-- Save the newest running instance of the application in a separate return property from the property containing the full list of running application instances
						set {currIsRunning, currProcessID} to {true, currProcessIDs's last item}
					end tell
				on error
					set currIsRunning to false
				end try
			end if
			-- Add the current application's values to the output variable lists
			set {end of bundleIdentifier, end of bundleURL, end of posixPath, end of hfsPath, end of appName, end of executableName, end of versionString, end of isAppleScriptEnabled, end of allowsUserInteraction, end of isBackgroundOnly, end of isRunning, end of processID, end of processIDs} to {currBundleIdentifier, currBundleURL, currPOSIXPath, currHFSPath, currAppName, currExecutableName, currVersionString, currIsAppleScriptEnabled, currAllowsUserInteraction, currIsBackgroundOnly, currIsRunning, currProcessID, currProcessIDs}
		end repeat
		-- Return the results; if the input argument consisted of only one application reference, delist the output variable values before returning them to the calling program
		if bundleIdentifier's length = 1 then set {appName, executableName, bundleIdentifier, posixPath, hfsPath, bundleURL, versionString, isAppleScriptEnabled, allowsUserInteraction, isBackgroundOnly, isRunning, processID, processIDs} to {appName's first item, executableName's first item, bundleIdentifier's first item, posixPath's first item, hfsPath's first item, bundleURL's first item, versionString's first item, isAppleScriptEnabled's first item, allowsUserInteraction's first item, isBackgroundOnly's first item, isRunning's first item, processID's first item, processIDs's first item}
		return {appName:appName, executableName:executableName, bundleIdentifier:bundleIdentifier, posixPath:posixPath, hfsPath:hfsPath, bundleURL:bundleURL, versionString:versionString, isAppleScriptEnabled:isAppleScriptEnabled, allowsUserInteraction:allowsUserInteraction, isBackgroundOnly:isBackgroundOnly, isRunning:isRunning, processID:processID, processIDs:processIDs}
	on error m number n
		if n = -128 then error number -128
		if n ≠ -2700 then set m to "(" & n & ")  " & m
		error ("Problem with handler appInfo:" & return & return & m)
	end try
end appInfo

Edit note: A minor cosmetic change was made to the originally submitted version for the code that retrieves the value of the Info.plist item CFBundleShortVersionString.
Edit note 2: An error in the «class furl» file reference example in the introductory discussion (not the handler code) was corrected.