Apple Two-Factor Authentication for a Secondary Apple ID

Apple sent an email to developers, stating that later this months, two-factor authentication will be required for Apple IDs used for developer accounts.

If you, like me, use separate Apple IDs for your personal iCloud and your developer accounts, this will pose some kind of challenge. There is a solution, however Apple does not document it very well.

Two-factor authentication for the primary account

Assumption: you have two-factor authentication (2FA) enabled on your primary, personal Apple ID, and are logged in to that account on your Mac(s) and iOS devices. If you haven’t done that yet, do it now. 2FA does increase your account security significantly.

You can enable 2FA on any device logged in to the account in the iCloud Settings or preference pane. As part of the setup you can provide one or more phone numbers as a fall back mechanism. If no devices can be prompted through Apple’s built-in 2FA, it will send an SMS to the trusted phone numbers. You can use the same phone number for multiple Apple IDs, but there seems to be some limit on how often you can do that.

Enable 2FA for the secondary account

Assumption: The secondary account is your developer Apple ID, you don’t use it for iCloud storage, device backups, mail etc. You use it to log in to developer.apple.com and iTunes Connect, and to get all the certificates and other resources you need as a developer.

The challenge here is that you can only enable 2FA on the first account logged in to iCloud on a device. You could log out of your primary iCloud account, and the log in with the secondary, but this will disrupt a lot of things on your device. I’d rather avoid that.

On a Mac, you can have a separate iCloud account for each local user. So, it is easiest to create a second user account, log out of your first account, log in to the new second account and set up iCloud and 2FA for the developer Apple ID on this second local account.

You can sign in to the secondary Apple ID enable 2FA in System Preferences -> iCloud -> Account Details -> Security as described in Apple’s Support Article.

Follow the prompts to set up 2FA, you can re-use the same phone number as a trusted number. (There seem to be limits to how often you use the same phone number, but two accounts works fine for me.)

Once 2FA is set up, we don’t need the second user account on the Mac any more. Sign out of iCloud, log out of the second account and back in to your normal user account.

If you are ok with using SMS authentication (Apple calls this ‘two-step authentication’, rather than ‘two-factor authentication’) then you are done. However, many will argue codes over SMS are not good enough for secondary authentication, so we want go to ‘full’ 2FA.

Use the secondary Apple ID

As it turns out, you can be logged in to multiple iCloud accounts on the same device or account. Certain services, such as iCloud storage, or the Photo Library, will only work with the primary iCloud account, but other services, including 2FA, will work for all iCloud accounts.

On your iOS device go to Settings > Passwords & Accounts > Add Account, and choose to add another iCloud account. You probably want to turn off all services, like Mail, Calendar, etc. secondary account.

Second iCloud Account on iPhone
Second iCloud Account on iPhone

On the Mac you can do the same in System Preferences > Internet Accounts. You can use both your Mac and iOS devices for 2FA.

Second iCloud Account on macOS 
Second iCloud Account on macOS

Now the secondary Apple ID will prompt the devices you are logged in as for 2FA.

2FA prompt on the iPhone 
2FA prompt on the iPhone

PLIST Editor can Open Signed Mobileconfigs

Last year I posted about a useful app to read and edit property list files: PLIST Editor.

In its last update (1.15) the app gained a feature which is tremendously useful to MacAdmins: it can now open signed property lists.

MacAdmins regularly encounter signed property lists when they download configuration profiles (.mobileconfig) from a management server. Since the cryptographic signature is binary data wrapping the property list, most property list editors, such as Xcode or PlistEdit Pro, choke on the signature.

The new PLIST Editor (1.15) now detects a signed mobileconfig and will automatically unwrap the plist data from the signature and display a notification.

PLIST Editor Message

When you click on the notification, you will get more detailed information on the signature itself.

Once you edit and save the mobileconfig, the signature will obviously be removed. You can re-sign the edited mobileconfig file with a tool like Hancock or in the Terminal with:

$ security cms -S -N "Identity Name" -i profile.mobileconfig -o signed.mobileconfig

(Learn more on in my book: ‘Property Lists, Preferences and Profiles for Apple Administrators’)

This addition will be very useful for my workflow and think for other MacAdmins as well.

PLIST Editor is available in the Mac App Store for US$3.99 (price will vary depending on region).

On macOS User Groups

User groups are easy, right? A user is either a member or they are not.

Once you start thinking about the deatils and want or need to automate some of the aspects of user and group management on macOS, there is a lot of devil in those details.

User Membership

You can easily list all groups a given user is a member of. The id command will show all the groups the current user is a member of. id -Gn will list just the groups. Add a username to the id command to see the information for a different user. The groups command does the same as id -Gn.

You can also run a command to check if a given user is a member of a group:

$ dseditgroup -o checkmember -m user staff
yes user is a member of staff
$ dseditgroup -o checkmember -m user wheel
no user is NOT a member of wheel

Group Membership

So far, so good.

A user is a member of a group when one of these applies:

  • the user’s PrimaryGroupID attribute matches the PrimaryGroupID of the group
  • the user’s UUID is listed in the group’s GroupMembers attribute and the user’s shortname is listed in the group’s GroupMembership
  • the user is a member of a group nested in the group

Note: you should not attempt to manipulate the GroupMembers or GroupMembership attributes directly. Use the dseditgroup -o edit command to manage group membership instead. dseditgroup syntax is weird, but it is a really useful tool. Study its man page.

Listing Group Members

Sometimes (mainly for security audits) you need to list all the members of a group. With the above information, it is easy enough to build a script that checks the PrimaryGroupID, the GroupMembership attribute and the recursively loops through the NestedGroups.

This is confused by the fact that PrimaryGroupID stores the numeric User ID, GroupMembership uses the shortname and NestedGroups uses UUIDs. Nevertheless, you can sort through it.

I have written exactly such a script here:

In most cases this script will work fine. But, (and you knew there would be a “but”) macOS has a very nasty wrench to throw in our wheels.

Calculated Groups

There are a few groups on macOS, that have neither GroupMembers, GroupMembership, nor NestedGroups, but still have members. This is because the system calculates membership dynamically. This is similar to Smart Playlists in iTunes, Smart Folders in Finder, or Smart Groups in Jamf Pro.

You can list all calculated groups on macOS with:

$ dscl . list /Groups Comment | grep "calc"

The most interesting calculated groups are everyone, localaccounts, and netaccounts.

These groups can be very useful in certain environments. For example in a DEP setup you could add localaccounts or everyone to the _lpadmin and _developer groups, before the user has even created their standard account. That way any user created on that Mac will can manage printers and use the developer tools.

However, since these groups are calculated magically, a script cannot list all the members of any of these groups. (My script above will show a warning, when it encounters one of these groups.)

While it would probably not be wise to nest the everybody group in the admin group, a malicious user could do that and hide from detection with the above script (or similar methods).

Other Solution

Instead of recursively listing all users, we can loop through all user accounts and check their member status with dseditgroup -checkmember. This script is actually much simpler and dseditgroup can deal with calculated groups.

This works well enough when run against all local users.

I strongly recommend against running this for all users in a large directory infrastructure. It’ll be very slow and generate a lot of requests to the directory server. Because of this the script above runs only on the local directory node by default.

Summary

  • on macOS users can be assigned to groups thorugh different means
  • you can check membership with dseditgroup -o checkmember
  • you can edit group membership with dseditgroup -o edit
  • macOS has a few groups which are dynamically calculated and difficult to process in scripts

Mojave Quick Action to Package Apps

One of the new macOS features in Mojave are “Finder Quick Actions.”

They show as action buttons in the Finder in Column View and the new Gallery View. You can also access Quick Actions in any view from an item’s context menu.

You can configure Quick Actions in the ‘Finder’ section of the ‘Extensions’ Preference Pane. The four sample actions that come with Mojave are ‘Rotate,’ ‘Markup,’ ‘Create PDF,’ and ‘Trim’. While these are certainly useful, they are oriented towards media.

You can, however, build your own Quick Actions with Automator!

Custom Quick Action

When you look at it closely, Quick Actions are re-branded Automator Service Workflows. They are even stored in ~/Library/Services.

Let’s build a useful Quick Action for admins.

Quick Packaging

Recap: You can quickly build an installer package from an application bundle with pkgbuild:

$ pkgbuild --component /Applications/Firefox.app Firefox-63.0.pkg

This even works when the application is not installed on the current system, but then you have to add the --install-location argument:

$ pkgbuild --component /Volumes/Firefox/Firefox.app/ --install-location /Applications Firefox-63.0.pkg

This allows you to build an installer package from a disk image without having to install it.

Note: this method works well with ‘drag to install’ type applications. For many other applications, re-packaging is more elaborate. Learn all the details of packaging in my book: “Packaging for Apple Adminstrators

To make this simple process even simpler, I wrote a small script a while back called quickpkg, which simplifies this even further:

$ quickpkg "~/Downloads/Firefox 63.0.dmg"

(For help with downloading and installing scripts like quickpkg see this post.)

This seems like a good candidate for a Quick Action.

Bring in the Robot

Open the Automator application and create a new Workflow. Choose “Quick Action” as the type of Workflow.

New Workflow Window for a Quick Action in Finder

This will present an empty workflow window with a list of categories and actions on the left and the area where you assemble the workflow on the right. Above the workflow area is a panel where you define the input for our Quick Action. Change the popups to match this image:

Input for our QuickAction
Input for our QuickAction

Then add a ‘Run Shell Script’ action from the list on the left. The easiest way to locate the action is with the search box, or you can find the ‘Run Shell Script’ action in the ‘Utilities’ category. Drag it to the workflow area, labelled ‘Drag actions or file here to build your Workflow.’

Make sure that the shell popup is set to /bin/bash and the ‘Pass input’ popup is set to ‘to stdin’. With these settings, the selected file(s) will be passed as a list of paths, one per line to the stdin stream of the bash code we enter in the text area.

Add the following code:

destination="$HOME/Documents/"

while read -r file; do
    /usr/local/bin/quickpkg --output "$destination" "$file"
done

Your action should look like this:

First, we set a bash variable for the destination folder. You can change this to another path if you want to, but the destination folder has to exist before the workflow runs, otherwise you’ll get an error.

Then we use a while loop with the read command to read the stdin input line by line. Then we run the quickpkg tool once for each line.

You can now save the Workflow (it will be saved in ~/Library/Services/) and then ‘QuickPkg’ (or whatever name you chose) will appear in Finder, for any selected item. Unfortunately, the Automator input controls don’t allow to filter for file types other than the few given ones.

Select a dmg with an application in it, such as the dmg downloaded from the Firefox website and wait a moment. Then check the ~/Documents folder for the result.

(A rotating gear wheel will appear in the menu bar while the action is running. This is all the feedback you can get.)

Revealing the Result

It is annoying that we have to manually open the destination folder to see our result. But the nice thing is that we can let the workflow take care of that. In the action list on the left, search for ‘Reveal Finder Items’ or locate this action in the ‘Files & Folders’ category. You can drag it to the end of your workflow, below the ‘Run Shell Script’ action or just double-click in the list to add to the end of your workflow.

The extended Workflow

Save and run again from the Finder. It should now reveal the pkg file automatically.

You can add more actions to the workflow. For example, you can add actions to

  • open with Pacifist or Suspicious Package
  • tag the pkg file
  • add a comment
  • append the date to the file name
  • copy the pkg to a file share

Improving the Workflow

You may have noticed during testing that in its current form the workflow doesn’t really react well when something goes wrong.

quickpkg can work with .app, .dmg, and .zip files. Unfortunatly, Automator does not let us filter for just those file types in the input setup. The script will report an error when you try to run it against a different file type, but the error is not displayed in Finder.

It is not that difficult to extend our short script to make it a bit more resilient. Change the code in the ‘Run Shell Script’ action to this:

destination="$HOME/Documents/"

while read -r file; do
    result=$(/usr/local/bin/quickpkg --output "$destination" "$file")

    if [[ $? != 0 ]]; then
        osascript -e "display alert \"QuickPkg: An error occured: $result\""
    else
        echo "$result"
    fi
done

With this code we check result code $? of the quickpkg command for an error. If the code is non-zero we display an alert with osascript. If all went well, we echo the result (the path to the new pkg) to stdout, so that Automator can pass that into the following actions.

This is still a fairly crude error handling, but much better than nothing.

Summary

It is quite easy to turn simple shell commands into Automator workflows and Finder Quick Actions. Keep this in mind, when you find yourself performing repetetive tasks in Finder or in Terminal.

Include Assets in External macOS Installer Drives

Apple has included a tool to build a bootable external installer drive with the macOS Installer application for a while now. Apple actually has documentation for this tool.

The tool is called createinstallmedia and can be found in /Applications/Install macOS [[High ]Sierra | Mojave].app/Contents/Resources/.

When run, the tool requires a path to an external volume or partition, which will be erased and replaced with a bootable installer volume.

Note: Secure Boot Macs with the T2 chip cannot boot from external drives in the default configuration. As of this writing this affects the iMac Pro and the 2018 MacBook Pro. But it is expected that any new Macs released from now on (as in maybe at the Apple Event tomorrow?) will also have Secure Boot.
Nevertheless, having an bootable external installer is still every useful for ‘legacy’ (i.e. non-secure boot) Macs. Also, while it not a good general configuration, it can be very useful to enable external boot on machines that you frequently re-install for testing.

While the support article covers the basics, the tool gained a new feature in Mojave which is not documented in the article.

When you run the Mojave createinstallmedia tool without arguments you get the usage documentation:

$ /Applications/Install\ macOS\ Mojave.app/Contents/Resources/createinstallmedia 
Usage: createinstallmedia --volume <path to volume to convert>

Arguments
--volume, A path to a volume that can be unmounted and erased to create the install media.
--nointeraction, Erase the disk pointed to by volume without prompting for confirmation.
--downloadassets, Download on-demand assets that may be required for installation.

Example: createinstallmedia --volume /Volumes/Untitled

This tool must be run as root.

The new argument in the Mojave is called --downloadassets. The description is a bit sparse, but from what I gather this is download additional assets, like firmware installers and bundle them with the other installer files on the installer drive instead of downloading them on-demand during installation.

This will not remove the requirement for the Mac to be connected to the internet during the installation process but it should speed up the process quite a bit.

If you want to learn more about how to create external installers and how to use the macOS Installer app most effectively in your workflows, you can buy my book ‘macOS Installation for Apple Administrators

Changing a User’s Login Picture

Nick asked a question in the comments recently:

Now only if there was as simple a tool for setting the profile pic!

There is no simple tool, but it is not that hard really.

When an individual user wants to change their login picture, they open the Users & Groups preference pane. But if want to pre-set or change it for multiple Computers or Users, then we need to script.

Where is it stored?

The data for the user picture is stored in the user record in the directory. If the user is stored locally the directory is just a bunch of property list files in /var/db/dslocal/nodes. However, we do not want to, nor should we manipulate them directly. The tool to interface with directory data is dscl (directory service command line, pronounced diskel)

You can get a user’s record data like this:

$ dscl . read /Users/username

This will dump a lot of data, some of it binary. When you look more closely at the data, you can see that the binary data is in an attribute JPEGPhoto. This is promising, but converting an image file into some binary code and writing it into the attribute does not sound fun.

When you look around the user record some more, you can find another attribute labeled Picture which contains a decent file path (it may be empty on your machine). When JPEGPhoto contains data, Picture will be ignored. But when we delete the JPEGPhoto attribute, then the system will use the file path set in the Picture attribute.

Let’s Change It!

Deleting the JPEGPhoto attribute is easy:

$ dscl . delete /Users/username JPEGPhoto

And so is setting the Picture attribute to a new value:

$ dscl . create /Users/username Picture "/Library/User Pictures/Flowers/Sunflower.tif"

With this you can create a script that resets all user pictures by looping through all the available pictures in the /Library/User Pictures folder.

(Since you are affecting other users’ records, this script needs to be run as root.)

Custom Images

Of course, you don’t have to use the pre-installed User Picture images, but can install your own.

To demonstrate how this would work, I conceived of a little fun exercise. I wanted to write a script that sets the user picture to an image from the ‘User Pictures’ folder which starts with the same letter as the username.

The set of images in the default cover 19 of the 26 letters in the latin alphabet. I created images for the seven missing letters (A, I, J, K, Q, U, and X).

To run the script at login, I created a LaunchAgent. And finally a script which will set the Picture to the appropriate path.

Since LaunchAgents run as the user, we need to be a bit more careful when changing the attributes. While a user has privileges to modify and delete the JPEGPhoto and Picture attribute, they cannot create the attributes, so our sledgehammer method to overwrite any existing value from the script above will not work.

The dscl . change verb, which modifies an attribute has a weird syntax which requires you to pass the previous value as well as the new value. To get the previous value, which may contain spaces, I use the same PlistBuddy trick from this post.

Finally, I built an installer package which installs all the parts (the additional images, the LaunchAgent plist and the script) in the right places. You can get the project here. Run the buildAlphabetUserPkg.sh script to build an installer package.

Since the LaunchAgent will trigger at a user’s login, you will have to logout and back in, before you can see any changes. You could add a postinstall script that loads the launchagent for the current user (when a user is logged in), but I will leave that as an exercise for the attentive student.

You can get all the pointers on how to build installer packages with postinstall scripts in my book: “Packaging for Apple Administrators

EraseInstall Application

The consulting team at Pro Warehouse has been working on an application. I mentioned this application in my talk at MacSysAdmin. The application is called ‘EraseInstall’ and provides a user interface which runs the startosinstall --eraseinstall command, which is part of the macOS Installer application.

Why?

The startosinstall --eraseinstall command with all its options is fairly accessible for an administrator. There have been some attempts to make the command more accessible to end users.

With Mojave, Apple is enforcing the requirement to have an active internet connection before you start the installation. The startosinstall command will fail if it cannot reach Apple’s servers. Also on Secure Boot Macs, you really want a user to have Find My Mac disabled before a system is wiped.

We chose to build an application with an interface that runs the necessary checks and displays a summary, before the startosinstall --eraseinstall is launched.

This will provide end users, techs and admins easy access to a tool which wipes the system. This will close the lifecycle loop of a Mac, from on-boarding to ‘off-boarding.’

Enter EraseInstall

EraseInstall will show three screens, the first will explain what the application does (wipe everything!) and then you will get a summary of the checks. In this initial version we check whether the system has APFS, if Find My Mac is enabled and if there is an internet connection.

EraseInstall also locates a suitable “Install macOS” application, either “Install macOS High Sierra” for 10.13.4 and higher or “Install macOS Mojave.” It is your responsibility to have the install app on the system before the EraseInstall is run. The app does not have to be installed in /Applications. (EraseInstall uses a spotlight query to locate available installer applications. It may take a few minutes after an installer app has been copied to a system for spotlight to pick it up.)

WARNING: the app as we have posted it is fully functional and will erase and install the system on which it is run. Please only run this on a test machine!

You can watch a video of the installation and workflow here:

How to get it

You can download an installer and the source code for the EraseInstall application here.

The installer will put the EraseInstall.app in /Applications/Utilities/.

You need to install, copy or download the “Install macOS” application (for 10.13.4 or higher) through your management system, VPP or manually.

Managing the Desktop Picture on macOS

Many organisations like to set or pre-set the Desktop Picture of managed Macs. There are a few options for Mac Admins.

One of the commonly used methods may break in macOS Mojave because of the new security and privacy controls for AppleEvents, also known as TCC.

Getting the Image File to the Mac

Unless you want to use one of the default macOS Desktop Picture images, you will first need to get the image file on to the client. The best way of doing that is with an installer package.

Note: installing a single image file to /Library/Desktop Pictures is actually the first exercise/example in my book “Packaging for Apple Administrators.” Get that for a more detailed description.

I will use the same example desktop picture as in the book. You can use your own desktop picture or download BoringBlueDesktop.png from the book’s resources.

Note: Desktop pictures on macOS can be many file formats. PNG and JPEG are most commonly used. The new dynamic desktop pictures of macOS Mojave have the heic file extension.

First create a project folder, with a payload folder inside:

$ mkdir -p BoringDesktop/payload
$ cd BoringDesktop

Then copy the image to the payload folder.

$ cp /path/to/BoringBlueDesktop.png payload

Then you can build a pkg with

$ pkgbuild --root payload --install-location "/Library/Desktop Pictures/" --identifier com.example.BoringDesktop --version 1 BoringDesktop.pkg

The resulting pkg file will install the image file in /Library/Desktop Picture.

Note: the pkgbuild command has many options and arguments. If you get one of them slightly wrong it can lead to unexpected behavior or even break the installation. I recommend using a script to run the pkgbuild command to avoid errors. You can find a sample build script here. Read the book for a more detailed explanation of pkgbuild and the build script. If you prefer, you can use munkipkg, which also simplifies and automates the process of building pkg installers.

This will provide the image in a location that the user might look for. However, for better management you want to set the desktop picture as well.

Lock Down the Desktop Picture

Once the image file is in place. You can set the desktop picture with a configuration profile. Many management systems will have an option in the ‘Restrictions’ payload where you can set the path to the desktop picture.

You can also use this custom profile:

You can learn all the ways to manage and install profiles in my other book ‘Property Lists, Preferences and Profiles for Apple Administrators’

With this configuration profile in place, the desktop picture is locked. The user can still open the Desktop preference pane but the selection will be ignored. You would need to be able to remove the profile to change the desktop picture again.

This is very useful in education and other strictly managed environments.

Suggesting a Desktop Picture

In other, less tightly managed environments, you might prefer to set an initial desktop picture, but allow the user to change it later.

The common means to do this has been to use an AppleScript command:

tell application "Finder" to set desktop picture to POSIX file "/Library/Desktop Pictures/BoringBlueDesktop.png"

If you want to run this from a shell script you would execute it with osascript:

osascript -e 'tell application "Finder" to set desktop picture to POSIX file "/Library/Desktop Pictures/Sierra.jpg"'

Note that this sets a user preference so it should be run as the user. See this post for details.

However, with macOS Mojave, Apple is introducing new Privacy and Security measures which require user approval for processes to send AppleEvents. This will put a severe limit on the use of osascript for admin scripts.

One solution would be to whitelist your management system’s agent which allows it to send Apple Events to the Finder. This requires managing the client with a user-approved MDM.

Another solution is to avoid AppleScript and Apple Events entirely.

Here comes the desktoppr!

To do this, I wrote a simple command line tool which can read and set the desktop picture. Neil Martin had the brilliant idea to call it desktoppr.

You can read the current desktop picture with:

$ desktoppr
/Library/Desktop Pictures/High Sierra.jpg

and set the desktop picture with

$ desktoppr "/Library/Desktop Pictures/BoringBlueDesktop.png"

When you have multiple displays, desktoppr will list all desktop pictures:

$ desktoppr
/Library/Desktop Pictures/HotStepper.jpg
/Library/Desktop Pictures/LyricalGangster.jpg
/Library/Desktop Pictures/MrOfficer.jpg

When you pass a file desktoppr will set it as the desktop picture for all screens:

$ desktoppr /Library/Desktop Pictures/NaahNananah.jpg
$ desktoppr
/Library/Desktop Pictures/NaahNananah.jpg
/Library/Desktop Pictures/NaahNananah.jpg
/Library/Desktop Pictures/NaahNananah.jpg

You can also set a specific desktop picture for a specific screen: (index starts at zero)

$ desktoppr 0 /Library/Desktop Pictures/HotStepper.jpg
$ desktoppr 1 /Library/Desktop Pictures/LyricalGangster.jpg
$ desktoppr 2 /Library/Desktop Pictures/MrOfficer.jpg

Managing with desktoppr

You can get the code for desktoppr on Github and an installer package here. The installer pkg will install desktoppr in /usr/local/bin. When you want to run it from a management script it is safest to include the entire path:

/usr/local/bin/desktoppr "/Library/Desktop Pictures/BoringBlueDesktop.png"

Since the desktoppr tool also sets user preferences, you still need to pay attention that it runs as the user.

For example, you could run desktoppr from a LaunchAgent (deployed in /Library/LaunchAgents so it affects all users:

This LaunchAgent will reset the Desktop Picture at every login.

If you want to set the Desktop Picture just once from a management or postinstall script (probably running as root) you can use the following to be safe:

When you find yourself building LaunchAgents or LaunchDaemons often (i.e. more than once) you should really consider using outset.

If you wanted to build an installer package that drops both the picture file and the LaunchAgent, you can do the following:

$ mkdir -p DesktopAndAgent/payload/
$ cd DesktopAndAgent
$ mkdir -p "payload/Library/Desktop Pictures/"
$ cp /path/to/BoringBlueDesktop.png "payload/Library/Desktop Pictures/"
$ mkdir -p payload/Library/LaunchAgents/
$ cp /path/to/com.scriptingosx.setdesktop.plist payload/Library/LaunchAgents
$ pkgbuild --root payload --install-location / --version 1 --identifier com.scriptingosx.desktopandagent DesktopAndAgent-1.pkg

Parsing dscl Output in Scripts

On macOS dscl is a very useful to access data in the local user directory or another directory the Mac is bound to. For example you can read a user’s UID with:

$ dscl /Search read /Users/armin UniqueID
UniqueID: 501

This output looks easy enough to parse, you can just use cut or awk:

$ dscl /Search read /Users/armin UniqueID | cut -d ' ' -f 2
501
$ dscl /Search read /Users/armin UniqueID | awk '{print $2;}'
501

However, dscl is a treacherous. Its output format changes, depending on the contents of an attribute. When an attribute value contains whitespace, the format of the output has two lines:

$ dscl /Search read /Users/armin RealName
RealName:
 Armin Briegel

With attributes like the UID, it is fairly safe safe to assume that there will be no whitespace in the value. With other attributes, such as RealName or NFSHomeDirectory, you cannot make that prediction with certainty. Real names may or may not have been entered with a space. A user (or management script) may have changed their home directory to something starting with /Volumes/User HD/... and your script may fail.

To remove this output ambiguity, dscl has a -plist option which will print the output as a property list:

 $ dscl -plist . read /Users/armin RealName
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>dsAttrTypeStandard:RealName</key>
    <array>
        <string>Armin Briegel</string>
    </array>
</dict>
</plist>

The resulting property list is a dict containing a key with the native attribute name and an array containing the values, even when there is only one value.

Having a property list is nice, but parsing property lists in a shell script is challenging. I have found two solutions

Xpath

You can use the xpath tool extract data from the XML output:

$ dscl -plist . read /Users/armin RealName | xpath "//string[1]/text()" 2>/dev/null
Armin Briegel

Note that the xpath output does not include a final new line character, which makes it look a bit strange.

The xpath argument in detail means:

  • //string[1]: the first of any string element
  • /text() the text contents of that stringobject

This syntax makes a lot of assumptions about the property list input. I believe they are safe with the dscl output. (Please test)

If you want to play around with xpath syntax, I recommend using an interactive tool. I used this one from Code Beautify which worked well enough, but frankly I just randomly chose one from the list of search results for ‘xpath tester’. (If you can recommend a great one, let us know in the comments.)

PlistBuddy

As I said, the xpath solution makes a lot of assumptions about the layout of the property list. A safer way of parsing property lists would be a dedicated tool, such as PlistBuddy. However, PlistBuddy does not read from stdin. At least not voluntarily.

A few weeks ago Erik Berglund shared this trick on Mac Admins Slack which makes PlistBuddy read the output from another command. We can adapt this for our use case:

$ /usr/libexec/PlistBuddy -c "print :dsAttrTypeStandard\:RealName:0" /dev/stdin <<< $(dscl -plist . read /Users/armin RealName)
Armin Briegel

Note that you have to escape the : in the attribute name, since PlistBuddy uses the colon as a path separator.

You can use this in scripts to assign the value to a variable with

realName=$(/usr/libexec/PlistBuddy -c "print :dsAttrTypeStandard\:RealName:0" /dev/stdin <<< $(dscl -plist . read /Users/$username RealName))

This uses nested command substitution with the $(... $(...) ...) syntax which is not possible using backticks.

Either way, you can get a safe value from dscl in shell script, whether it contains whitespace or not.

Prefs Tool

Preferences or defaults on macOS seem easy, but their subtleties can grow complex very quickly.

The main reason for confusion is that preferences can be stored in many places and on many levels. The defaults system composites all of the keys and values from all locations to a process or application.

In my book “Property Lists, Preferences and Profiles for Apple Administrators”, I list 17 possible domains or levels where preferences can be stored and read from. The most common domains are:

Domain Location
User/Application ~/Library/Preferences/identifier.plist
User/Application/Computer ~/Library/Preferences/ByHost/identifier.xyz.plist
Computer/Application /Library/Preferences/identifier.plist
Configuration Profile n/a

To add to this confusion, Apple’s documentation keeps mixing up terms like ‘domain’ and ‘identifier’. I use the term ‘domain’ to designate the level or location a setting is stored in, and ‘identifier’ for the name of the preference (i.e. com.apple.screensaver).

The defaults command, which is the proper tool to interact with preferences files, does not properly work with different levels or domains. When you run

$ defaults read com.apple.screensaver

The output will be from the User/Application domain only, i.e. the data stored in the file ~/Library/Preferences/com.apple.screensaver.plist.

But the ScreenSaver process stores more data in the ByHost domain. You can read this domain or location with defaults as well:

$ defaults -currentHost read com.apple.screensaver

However, you must remember to check and read the ByHost domain as well as the standard domain. To access the computer level domain you have to use

$ defaults read /Library/Preferences/com.apple.screensaver

(The ScreenSaver process does not use this domain, so you will get an error saying that it does not exist. However, you won’t know this domain is empty until you try.)

Defaults cannot tell you when a setting is set or overridden by a configuration profile, or what its value is in that case. You cannot get the full composited view of defaults with the defaults command.

Greg Neagle wrote a short python script a while back which could give you the effective result for an identifier and a specific key. His script will also show where the value is coming from.

I have found Greg’s script to be very useful, but I wanted it to do a bit more. My version, Prefs Tool, can now show you all keys set for a specific application identifier, including those managed by configuration profiles.

$ ./prefs.py com.apple.screensaver
idleTime <int>: 0L (User/ByHost)
CleanExit <string>: u'YES' (User/ByHost)
askForPassword <bool>: True (Managed)
askForPasswordDelay <int>: 0L (Managed)
moduleDict <dict>: {
    moduleName = iLifeSlideshows;
    path = "/System/Library/Frameworks/ScreenSaver.framework/Resources/iLifeSlideshows.saver";
    type = 0;
} (User/ByHost)
showClock <bool>: True (User/ByHost)
PayloadUUID <string>: u'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' (Managed)
tokenRemovalAction <int>: 0L (User/ByHost)
PrefsVersion <int>: 100L (User/ByHost)

The script has a few more tricks up its sleeve. There is also still lots of work to be done. See the Github repository and its ReadMe for details.