Identifying Executables via Command Line

The_Black_Rose
New Contributor II

Fellow Admins,

Not sure if there's already a thread for this, but I wanted to post this method in case there are any of you with a similar problem to solve. Hope this helps and feedback is certainly welcome.

Overview

Here, the problem we've been asked to solve is finding a way to identify the applications on production workstations built from Intel architecture as well as those built from the ARM architecture. We want to collect this information as a way to see whether MacOS computers sporting the new M1 chipset is appropriate for our environment given the applications we're using in production. My solution approaches this question with a four-step workflow: 1. Get a list of installed applications that we can iterate over
2. Get the name of the executable file for each installed app
3. For each executable, figure out what architecture it's using
4. Report our results to the console somehow

Phase 1: Installed Apps & Executables

After messing around with ls and mdls for a while, I finally settled on a simple method for working with the installed applications...planting our session in the /Applications directory. This way we can use a wildcard character in the upcoming for loop

cd /Applications

From here I elected to get the executable name from each .app bundle by interrogating the value of the CFBundleExecutable key from their respective info.plist files. With these two in hand, the essential iteration was revealed:

for app in *;do
defaults read /Applications/[exampleApp.app]/Contents/info.plist CFBundleExectuable

This method works for all of the .app bundles in /Applications that are not contained within a subfolder. Items like the Jamf Pro apps and Python can exist as directories housing .app bundles. For these cases I chose to deal with them manually, but I'm confident that there's a function design that would allow for a more automated approach. Note also that I do not handle Utilities at all. I'm assuming that the applications living in /System/Library/CoreServices/Applications and and /System/Applications will be compliant. In any case my resultant function for this operation was this:

function getAppExecutable(){
defaults read /Applications/$1/Contents/info.plist CFBundleExecutable

Phase 2: Getting Executable Architectures

Once I had a way to get each executable, I then needed to know what architecture they used. For this the file binary provided a ready solution via this syntax:

file /Applications/[exampleApp.app/Contents/MacOS/[exampleAppExecutable]

In most cases what we're looking for is the very end of line returned to the console, as that is where the resultant architecture type is listed. We can pipe this to awk to achieve this:

file /Applications/[exampleApp.app}/Contents/MacOS/[exampleAppExecutable] | awk '{ print $(NF) }'

There are cases where an executable uses multiple architectures (presumably because the developers have already redesigned the app for backwards compatibility between M1 and Intel based Macs), and this "look at the end of the line" method does not yield actionable results. This is handled by a test outlined in the next section. Like the getExecutableName process, we can make a function for these file commands:

function getExecutableType(){
file /Applications/$1/Contents/MacOS/$2 | awk '{ print $(NF) }'

Phase 3: Putting It All Together Now

For better or worse I chose to go with a large for loop as the structure for this job:

cd /applications

function getExecutableName(){
    defaults read /Applications/$1/Contents/info.plist CFBundleExecutable
}

function getExecutableType(){
    file /Applications/$1/Contents/MacOS/$2 | awk '{ print $(NF) }'
}

resultInventoryUser=()

# P2: Iterate Over Installed Applications to find the type
for app in *;do
    if [[ $app == "Python 3.9" ]];then
        idleAppExecutableName=$(defaults read /Applications/Python 3.9/IDLE.app/Contents/info.plist CFBundleExecutable)
        idleAppExecutableType=$(file /Applications/Python 3.9/IDLE.app/Contents/MacOS/IDLE | awk '{ print $(NF) }')
        echo $idleAppExecutableType
        if [[ $idleAppExecutableType = "x86_64" || $idleAppExecutableType = "i386" ]];then
            execTypeReport=$(echo "$idleAppExecutableName is not compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        elif [[ $idleAppExecutableType = "arm64" || $idleAppExecutableType = "arm64e" ]];then
            execTypeReport=$(echo "idleAppExecutableName is compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        else
            backwardsCompTest=$(file /Applications/Python 3.9/IDLE.app/Contents/MacOS/IDLE | grep "architectures")
            if [[ $backwardsCompTest != "" ]];then
                execTypeReport=$(echo "$idleAppExecutableName is backwards compatible
            ")
            else
                execTypeReport=$(echo "$idleAppExecutableName could not be identified
            ")
            fi
            resultInventoryUser+=("$execTypeReport")
        fi
        launchAppExecutableName=$(defaults read /Applications/Python 3.9/Python Launcher.app/Contents/info.plist CFBundleExecutable)
        launchAppExecutableType=$(file /Application/Python 3.9/Python Launcher.app/Contents/MacOS/Python Launcher | awk '{ print $(NF) }')
        echo $launchAppExecutableType
        if [[ $launchAppExecutableType = "x86_64" || $launchAppExecutableType = "i386" ]];then
            execTypeReport=$(echo "$launchAppExecutableName is not compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        elif [[ $launchAppExecutableType = "arm64" || $launchAppExecutableType = "arm64e" ]];then
            execTypeReport=$(echo "$launchAppExecutableName is compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        else
            backwardsCompTest=$(file /Applications/Python 3.9/Python Launcher.app/Contents/MacOS/Python Launcher | grep "architectures")
            if [[ $backwardsCompTest != "" ]];then
                execTypeReport=$(echo "$idleAppExecutableName is backwards compatible
            ")
            else
                execTypeReport=$(echo "$idleAppExecutableName could not be identified
            ")
            fi
            resultInventoryUser+=("$execTypeReport")
        fi
    elif [[ $app == "Jamf Pro" ]];then
        #Composer
        composerAppExecutableName=$(defaults read /Applications/Jamf Pro/Composer.app/Contents/info.plist CFBundleExecutable)
        composerAppExecutableType=$(file /Applications/Jamf Pro/Composer.app/Contents/MacOS/Composer | awk '{ print $(NF) }')
        if [[ $composerAppExecutableType = "x86_64" || $composerAppExecutableType = "i386" ]];then
            execTypeReport=$(echo "$composerAppExecutableName is not compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        elif [[ $composerAppExecutableType = "arm64" || $composerAppExecutableType = "arm64e" ]];then
            execTypeReport=$(echo "$composerAppExecutableName is compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        else
            backwardsCompTest=$(file /Applications/Jamf Pro/Composer.app/Contents/MacOS/Composer | grep "architectures")
            if [[ $backwardsCompTest != "" ]];then
                execTypeReport=$(echo "$composerAppExecutableName is backwards compatible
            ")
            else
                execTypeReport=$(echo "$composerAppExecutableName could not be identified
            ")
            fi
            resultInventoryUser+=("$execTypeReport")
        fi
        #Recon
        reconAppExecutableName=$(defaults read /Applications/Jamf Pro/Recon.app/Contents/info.plist CFBundleExecutable)
        reconAppExecutableType=$(file /Applications/Jamf Pro/Recon.app/Contents/MacOS/Recon | awk '{ print $(NF) }')
        if [[ $reconAppExecutableType = "x86_64" || $reconAppExecutableType = "i386" ]];then
            execTypeReport=$(echo "$reconAppExecutableName is not compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        elif [[ $reconAppExecutableType = "arm64" || $reconAppExecutableType = "arm64e" ]];then
            execTypeReport=$(echo "$reconAppExecutableName is compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        else
            backwardsCompTest=$(file /Applications/Jamf Pro/Recon.app/Contents/MacOS/Recon | grep "architectures")
            if [[ $backwardsCompTest != "" ]];then
                execTypeReport=$(echo "$reconAppExecutableName is backwards compatible
            ")
            else
                execTypeReport=$(echo "$reconAppExecutableName could not be identified
            ")
            fi
            resultInventoryUser+=("$execTypeReport")
        fi
        #Imaging
        imagingAppExecutableName=$(defaults read /Applications/Jamf Pro/Jamf Imaging.app/Contents/info.plist CFBundleExecutable)
        imagingAppExecutableType=$(file /Applications/Jamf Pro/Jamf Imaging.app/Contents/MacOS/Jamf Imaging | awk '{ print $(NF) }')
        if [[ $imagingAppExecutableType = "x86_64" || $imagingAppExecutableType = "i386" ]];then
            execTypeReport=$(echo "$imagingAppExecutableName is not compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        elif [[ $imagingAppExecutableType = "arm64" || $imagingAppExecutableType = "arm64e" ]];then
            execTypeReport=$(echo "$imagingAppExecutableName is compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        else
            backwardsCompTest=$(file /Applications/Jamf Pro/Jamf Imaging.app/Contents/MacOS/Jamf Imaging | grep "architectures")
            if [[ $backwardsCompTest != "" ]];then
                execTypeReport=$(echo "$imagingAppExecutableName is backwards compatible
            ")
            else
                execTypeReport=$(echo "$imagingAppExecutableName could not be identified
            ")
            fi
            resultInventoryUser+=("$execTypeReport")
        fi
        #Admin
        adminAppExecutableName=$(defaults read /Applications/Jamf Pro/Jamf Admin.app/Contents/info.plist CFBundleExecutable)
        adminAppExecutableType=$(file /Applications/Jamf Pro/Jamf Admin.app/Contents/MacOS/Jamf Admin | awk '{ print $(NF) }')
        if [[ $adminAppExecutableType = "x86_64" || $adminAppExecutableType = "i386" ]];then
            execTypeReport=$(echo "$adminAppExecutableName is not compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        elif [[ $adminAppExecutableType = "arm64" || $adminAppExecutableType = "arm64e" ]];then
            execTypeReport=$(echo "$adminAppExecutableName is compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        else
            backwardsCompTest=$(file /Applications/Jamf Pro/Jamf Admin.app/Contents/MacOS/Jamf Admin | grep "architectures")
            if [[ $backwardsCompTest != "" ]];then
                execTypeReport=$(echo "$adminAppExecutableName is backwards compatible
            ")
            else
                execTypeReport=$(echo "$adminAppExecutableName could not be identified
            ")
            fi
            resultInventoryUser+=("$execTypeReport")
        fi
        #Remote
        remoteAppExecutableName=$(defaults read /Applications/Jamf Pro/Jamf Remote.app/Contents/info.plist CFBundleExecutable)
        remoteAppExecutableType=$(file /Applications/Jamf Pro/Jamf Remote.app/Contents/MacOS/Jamf Remote | awk '{ print $(NF) }')
        if [[ $remoteAppExecutableType = "x86_64" || $remoteAppExecutableType = "i386" ]];then
            execTypeReport=$(echo "$remoteAppExecutableName is not compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        elif [[ $remoteAppExecutableType = "arm64" || $remoteAppExecutableType = "arm64e" ]];then
            execTypeReport=$(echo "$remoteAppExecutableName is compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        else
            backwardsCompTest=$(file /Applications/Jamf Pro/Jamf Remote.app/Contents/MacOS/Jamf Remote | grep "architectures")
            if [[ $backwardsCompTest != "" ]];then
                execTypeReport=$(echo "$remoteAppExecutableName is backwards compatible
            ")
            else
                execTypeReport=$(echo "$remoteAppExecutableName could not be identified
            ")
            fi
            resultInventoryUser+=("$execTypeReport")
        fi
    else 
        appExecutableName=$(getExecutableName $app)
        appExecutableType=$(getExecutableType $app $appExecutableName)
        if [[ $appExecutableType = "x86_64" || $appExecutableType = "i386" ]];then
            execTypeReport=$(echo "$appExecutableName is not compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        elif [[ $appExecutableType = "arm64" || $appExecutableType = "arm64e" ]];then
            execTypeReport=$(echo "$appExecutableName should be compliant for M1 Devices
            ")
            resultInventoryUser+=("$execTypeReport")
        else
            backwardsCompTest=$(file /Applications/$app/Contents/MacOS/$appExecutableName | grep "architectures")
            if [[ $backwardsCompTest != "" ]];then
                execTypeReport=$(echo "$appExecutableName is backwards compatible
            ")
            else
                execTypeReport=$(echo "$appExecutableName could not be identified
            ")
            fi
            resultInventoryUser+=("$execTypeReport")
        fi
    fi
done

As mentioned before, I chose to handle both Jamf Pro and Python manually, as I was interested in what dealing with structures like this would be like. That said, if you can pull apps out of a containing folder into /Applications just do it. Huge time save. Additionally for the case of an executable using multiple architectures, I devised a simplistic test:

backwardsCompTest=$(file /Applications/Python 3.9/IDLE.app/Contents/MacOS/IDLE | grep "architectures")
            if [[ $backwardsCompTest != "" ]];then
                execTypeReport=$(echo "$idleAppExecutableName is backwards compatible
            ")
            else
                execTypeReport=$(echo "$idleAppExecutableName could not be identified
            ")
            fi

When evaluating executable files with file commands, I noticed that executables with multiple architectures returned output like this:

/Applications/[exampleApp.app]/Contents/MacOS/[exampleAppExectuable]: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]

In this case I elected to pipe the command output to grep searching for the string "architectures". The if statement is checking to see whether anything comes out of the operation after we've piped it into grep because apps with a single architecture come back blank. If there's something there the script assumes that a message like the above was the output and writes "this is backwards compatible" to the resultInventory array.

Phase 4: Outputting Results

To post the output I chose to append the result of each evalutation to an array that starts out with no data. I purposefully included spaces in the echo commands so that the output would look like a list instead of a long series of lines. This is certainly not the best way to do this so suggestions here would be useful for anyone looking to use this. In my environment I have the results posting in a form usable for a Computer Extension Attribute but you can cut these methods up and rearrange them to suit your particular needs.

2 REPLIES 2

mm2270
Legendary Contributor III

Hi. While I don't have a specific need for this myself, at least right now, I just wanted to make one suggestion on pulling the applications list.
You might want to try the following to see if it works better for you.

find /Applications /System/Applications -name "*.app" -maxdepth 3

The above should list out all applications in the main Applications folder up to 3 levels deep as well as all the applications that are part of /System/Applications/ which are the built in ones.
This would avoid needing special loops for any known apps that are within any folders like with the Jamf Pro apps. It should handle any others ones as well that might be within their own directory, like Cisco AnyConnect for example.

The only caveat I'll say is that in some cases this might end up pulling apps into the mix that you don't necessarily care about. For example, Xcode tends to have a large number of smaller applications inside its own app bundle. That's why I limited the find to only 3 levels deep. You can play with that number as needed to get the results you're looking for. If you don't tell it where to stop, find will drill down as far as it can until it reaches the end, even if it ends up going 6 or 7 levels down to do so.

One other tip to check on. mdls, aka Spotlight's listing tool has the ability to show a bundle's architectures, like x86_64, etc.

mdls "$app" -name kMDItemExecutableArchitectures

Here's a very basic script to find all applications within those 3 levels mentioned and print back the architecture listed for the application bundle. Some of them return null, but these are mostly embedded launcher apps inside other applications.

#!/bin/sh

all_apps=$(find /Applications /System/Applications -name "*.app" -maxdepth 3)

echo "$all_apps" | while read app; do
    architecture=$(mdls -raw "$app" -name kMDItemExecutableArchitectures | xargs)
    echo "$app architecture is $architecture"
done

jhuls
Contributor III

I'm probably reading this wrong but does "system_profiler SPApplicationsDataType" get you what you want?

I'm still a rookie at this stuff but this seems like it would get what you want but might need a little more massaging.

system_profiler SPApplicationsDataType | grep -E 'Kind:|Location:'

Edit: I was playing around with it and came up with this version instead that might be more readable and only covers /Applications. I'd personally prefer to flip the location with kind but that's beyond my skills at the moment.

system_profiler SPApplicationsDataType | grep -E -B2 'Location: /Applications/' |grep -E -v 'Signed by:|Last Modified:'