Use scout to read Property Lists

I have written a few posts in the past about parsing data from property list files in scripts and Terminal. My usual tool for this is PlistBuddy. However, PlistBuddy’s syntax is… well… eccentric.

Recently, Alexis Bridoux, who is also the main developer on Octory, introduced a command line tool called scout which solves many of the issues I have with PlistBuddy.

For example, you can pipe the output of another command into scout, something you can only convince PlistBuddy to do with some major shell syntax hackery.

So instead of this:

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


With scout I can use this much clearer syntax:

> dscl -plist . read /Users/armin RealName | scout "dsAttrTypeStandard:RealName[0]"


The tool can also modify existing files, by changing, adding or deleting keys.

scout can also parse JSON and (non plist) XML files, so it can also stand in as a replacement for jq and xpath. It will also color-code output for property list, XML and JSON files.

I have been using scout interactively in the Terminal for a while now. So far, I have been refraining from using scout in scripts I use for deployment. To use a non-system tool in deployment scripts, you need to ensure the tool is deployed early in the setup process. Then you also have to write your scripts in a way that they will gracefully fail or fallback to PlistBuddy in the edge case where scout is not installed:

scout="/usr/local/bin/scout"
if [ ! -x "$scout"]; then
    echo "could not find scout, exiting..."
    exit 1
fi

realName=$( dscl -plist . read /Users/armin RealName | scout "dsAttrTypeStandard:RealName[0]" )


All of this overhead, adds extra burden to using a tool. The good news is that scout comes as a signed and notarized package installer, which minimizes deployment effort.

I wills be considering scout for future projects. If anyone at Apple is reading this: please hire Alexis and integrate scout or something like it in macOS.

Use Swift Package Manager and Swift’s ArgumentParser to build a Command Line Tool

Update 2023-08-28: There is a newer post with more, updated information on this topic.

Apple recently published the Swift Package ArgumentParser. As the name implies, this provides a library of commands to parse arguments in a command line tool.

For MacAdmins, Swift can be a useful choice to build tools, especially now that built-in Python is going away in some ‘future macOS.’ While you can (and should) ‘bring your own Python’ for MacAdmin tools, using Swift is interesting alternative.

I have talked about this in detail in my 2018 MacSysAdmin presentation “Swift for Apple Admins” and towards the end of my 2019 MacSysAdmin presentation “Moving to zsh.”

A powerful and ‘official’ library to parse arguments has been a glaring hole when writing Swift command line tools. Now, ArgumentParser can fill this gap. But, to use the ArgumentParser package, you need to use Swift Package Manager (SPM). Previously you could only use SPM from the command line. With Xcode 11 there is some support for Swift Package Manager in the UI. Either way, it is still an extra hurdle for a MacAdmin before they can build their tool.

In this post I will show how to build a simple command line tool using Swift Package Manager and the ArgumentParser package. And we will do it entirely from the command line, but also be able to use Xcode.

Preparation

You will need Xcode 11 or higher, preferably the latest version that works on your version of macOS. You can get Xcode from the Mac App Store or from the Apple Developer Portal.

Once you have downloaded Xcode, launch it to accept the license agreement and finish the additional installations, then we are ready to go.

What will we build?

I will build a simple command line to read user preferences. In macOS the value of a setting can come from a number of different ‘domains.’ A preference can be set by the user, for the computer, or from an MDM server with a configuration profile.

The built-in defaults command will read preferences, but only from the user level, or from a specified file path. This ignores one of the main features of macOS’ preferences system. The defaults tool will not realize or show if a value is being managed by a higher level, such as a configuration profile.

This is the simplified introduction into macOS Preferences and domains. You can learn all about preferences and profiles in my book “Property Lists, Preferences and Profiles for Apple Administrators.”

We will build a simple command line tool which can show the consolidated value of a setting, no matter where the value comes from.

I have built such a tool using python and eventually I will want to re-build that with Swift. But for our demo, we will just have a very simple subset of the functionality, so we will call our simple Swift tool just prf.

Create a Project with Swift Package Manager

Open Terminal. Change Directory for the place where you store your code projects. (For me that is ~/Projects.) Then create a new empty directory for the project and change into it.

> mkdir swift-prf
> cd swift-prf

Then we run the swift command to set us up with an template SPM project.

> swift package init --type executable --name prf

The set the type as executable because we want to build a command line tool. We also set the name to just prf. When you don’t give a name, it will use the name of the enclosing directory.

This will create a bunch of files. Our main code is in Sources/prf/main.swift. Right now, that is a single line print("Hello, world!').

You can build and run the project from Terminal with swift run:

> swift run
[3/3] Linking prf
Hello, world!

Adding the ArgumentParser Package

Before we can write our own code, we need to add the ArgumentParser package to the project. The configuration for this is in the Package.swift file in the root of the project. Open that file with your favored text editor.

In the dependencies array that starts in line 8, add a line for the swift-argument-parser package like this:

    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.5"),
    ],

Version 0.0.5 is the latest version as I write this post.

Then further down in the code there is .target named "prf" which contains another dependencies array. Add the ArgumentParser package there as well:

        .target(
            name: "prf",
            dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]),

Leave the rest of the Package.swift file as it is.

You can now run the tool again. You will see that swift will download and compile the package:

> swift run  
Fetching https://github.com/apple/swift-argument-parser
Cloning https://github.com/apple/swift-argument-parser
Resolving https://github.com/apple/swift-argument-parser at 0.0.5
[32/32] Linking prf
Hello, world!

We will still get the same output, because we haven’t changed the code of our tool yet. But now we can use the ArgumentParser package.

The Code for the Tool

Open Sources/prf/main.swift in you favorite text editor, delete the single print line and add the following:

// prf

// a simple tool to read preferences

import Foundation
import ArgumentParser

struct ReadPreference: ParsableCommand {
  
  @Argument()
  var domain: String
  
  @Argument()
  var key: String
  
  func run() throws {
    let plist = CFPreferencesCopyAppValue(key as CFString, domain as CFString)
    print(plist?.description ?? "<no value>")
  }
}

ReadPreference.main()

Then try to run it again:

> swift run  
[3/3] Linking prf
Error: Missing expected argument '<domain>'
Usage: read-preference <domain> <key>

At first glance this looks like an error, but when you look at it closely, you see that the tool is complaining that expected arguments are missing. Right now, the prf tool expects two positional arguments, the preference domain and the key. You can pass arguments into swift run but when you do, you need to provide the tool name as well;

> swift run prf com.apple.loginwindow lastUserName        
armin

Now, you might want to confirm this and read the same preference with defaults read com.apple.loginwindow and you will not find the lastUserName key and value there. This is because this value is not stored on the user level, but on the computer level, when you run defaults read /Library/Preferences/com.apple.loginwindow you see the value. So we see, that our simple prf tool properly finds and displays the consolidated value!

What the Code does

When we look at the code we see that the main work happens in the run() function of a ReadPreference struct that inherits most of its behavior from ParsableCommand struct from the ArgumentParser library.

In that function we use the CFPreferencesCopyAppValue function which gets a consolidated preference value for a key and identifier pair.

The two properties of this struct are labelled with Swift Property Wrappers: @Argument() this tells the ArgumentParser code that these are expected positional arguments that will be filled into these properties.

Everything else happens automatically.

Because we have two properties with the @Argument() wrapper, the command line tool will expect two arguments, otherwise it will return an error. This is what we saw earlier.

Get Xcode in the Mix

All we’ve needed so far is the swift command in Terminal and a text editor. Conceivably, this may be enough for building a decently complex tool. But, if you prefer to edit in Xcode, you can still do that. You can create an Xcode project file with:

> swift package generate-xcodeproj
generated: ./prf.xcodeproj

And then you can open that and edit, build, and run your tool in Xcode.

In Xcode you can provide arguments to the tool in the Scheme Editor. However, it is quite cumbersome to keep changing those for iterative testing. That’s when falling back to to swift run prf in the command line is very useful.

When you change the configuration in the Packages.swift files to remove or add more dependencies, you should re-generate the Xcode project file with swift package generate-xcodeproj.

Where to go from here

As you can tell from the version number, ArgumentParser is still very ‘young’ and it doesn’t have quite as deep a feature set as Python’s argparse. But there is, even now, much, much more to the ArgumentParser library than we are using in this simple example. There are verbs, options with long and short flags, and help messages and many other things. You can find the details in the package’s documentation.

There is also much more to Swift Package Manager and how to use it. You can learn more about SPM in its documentation.

Nevertheless, this should give you enough to get you started.

I am looking forward to the tools you build!

Build a macOS Application to Run a Shell Command with Xcode and SwiftUI

A few years ago, I published a post that described how to build a Mac application in Swift that would run a shell command. Surprisingly, this post still gets a lot of traffic. Xcode and Swift have progressed over the last three years and that old example is mostly useless now. It is time for an update!

In this post, I will describe how to build a simple Mac app which runs a shell command using SwiftUI.

SwiftUI is the new framework from Apple to build user interfaces across platforms. Applications built with Swift UI will require 10.15 Catalina and higher. As you will see, SwiftUI does simplify a lot of the work of creating the user interface. I believe that learning SwiftUI now will be a good investment into the future.

I have written this post with Xcode 11.2.1 on macOS Catalina 10.15.1. That means we are using Swift 5.1. You can download Xcode from the Mac App Store or Apple’s developer page.

In this post, we will be using the say command to make the Mac speak. You can test the command by opening the Terminal and entering

> say "Hello World"

The say command is a simple and fun stand in for other command line tools. It also provides instant feedback, so you know whether it is working or not. That said, when you want to provide text-to-speech functionality in a Swift app, you should probably use the text-to-speech frameworks, rather than sending out a shell command.

Nevertheless, for Mac Admin tasks, there are some that are only possible, or at least much easier, with shell tools.

First: Swift UI hello world

Before we get to more complicated things, let’s honor the classics and build a “Hello, World” application using Swift UI.

Open Xcode and from the project picker select “New Project.”

In the template chooser select ‘macOS’ and ‘Application’. Then click ‘Next.’

In the next pane, enter a name for the project: “SayThis.” Verify the other data and make sure the choice for ‘User Interface’ is ‘SwiftUI’. Then click ‘Next.’

The window with the new project will have four vertical panes. You can use the controller element in the top right of the toolbar to hide the right most “Inspector” pane as we will not need it.

Click it the “Play” button in the top left to build and run the template code, a window should open which show the place holder text “Hello World!”

When you return to Xcode the preview pane on the right should now be active and also display the “Hello World!” text. If it does not there should be a “Resume” button at the top of the preview pane (also called “Canvas”) which you can click to make Xcode resume live updating the preview. There are many situations that will stop Xcode from continuously updating the preview and you can click this button to resume.

The left column shows a list of files in this project. Select the ContentView.swift file. This file contains the Swift UI code that sets up the interface. You will see the code in the center pane. The relevant part of the code is:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

The ContentView contains a body which contains a Text element with the text Hello World!. at the end of the Text object, you a ‘modifier’ that sets the frame of the Text object to use all available space.

Hello, me!

In the code pane, change World to something else:

Text("Hello, Armin!")

You will see that the text in the preview pane updates as you change the code.

Note: there are several reasons Xcode might stop updating and when it does, you will have to hit the ‘Resume’ button above the preview pane.

The preview pane or canvas allows you to edit as well. When you (command) ⌘-click on the “Hello, …” text in the canvas, you will get an “Action menu.”

The first item in the action menu: “Show SwiftUI Inspector” will an inspector with the attributes or “modifiers” of the text object. Note that, even though there is no visual indication, the inspector view can be scrolled to reveal more options.

Change the font of the text to Body. The preview in the canvas will update, as well as the code. The code will now look like:

Text("Hello, Armin!")
    .font(.body)
    .frame(maxWidth: .infinity, maxHeight: .infinity)

I have broken the modifiers into their own lines for clarity.

Stacking it up… or down… or sideways…

The SwiftUI body can only contain one object. But there are SwiftUI objects that you can use to group multiple objects together. Command-Click on the text and choose “Embed in VStack” from the action menu.

The code will change to:

 VStack {
    Text("Hello, Armin!")
        .font(.body)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
}

The VStack stands for ‘vertical stack.’ The VStack and its sibling the HStack (horizontal) can contain multiple objects. So, you can add another Text over our “Hello” text:

VStack {
    Text("SayThis")
        .font(.largeTitle)
    Text("Hello, Armin!")
        .font(.body)
}.frame(maxWidth: .infinity, maxHeight: .infinity)

This adds the "SayThis" text above the "Hello, ..." text in a larger font. I have also moved the .frame(...) modifier to the VStack because that leads to a nicer output. Feel free to apply the .frame modifer to different objects to see how that affects the layout.

The text is a little bit close together, so we can add some .padding() modifiers:

VStack {
    Text("SayThis")
        .font(.largeTitle)
        .padding()
    Text("Hello, Armin!")
        .font(.body)
        .padding()
}.frame(maxWidth: .infinity, maxHeight: .infinity)

You can choose to change the properties using the inspector UI or in the code. Either way the changes should be reflected in the canvas and the code.

Adding some interaction…

Now we want to add some interaction. Eventually, we want the user to be able to enter some text, which will be sent to the say command. Before we get, we will add a field, to enter some text.

To add a TextField where the user can enter text to the layout, click on the + icon in the top right of the tool bar. A window will appear with SwiftUI objects. Enter TextField in the search area to find the TextField object and drag it between the two text items.

You will want to add a .padding() modifier to the text field as well.

We also need a local variable in our ContentView to store the value of the TextField. Add this variable declaration under the struct ContentView definition and before the body declaration:

@State var message = "Hello, World!"

This adds and initializes a variable named message. The @State marker tells SwiftUI this ‘State’ variable will be used with the view. The variable is part of the ‘state’ of the view. Changes to the variable will immediately update the UI and vice versa.

Use the message variable for the TextField and Text objects:

@State var message = "Hello, World!"

var body: some View {
    VStack {
        Text("SayThis")
            .font(.largeTitle)
            .padding()
        TextField("Message", text: $message)
            .padding()
        Text(message)
            .font(.body)
            .padding()
    }.frame(maxWidth: .infinity, maxHeight: .infinity)
}

When you look at this interface in the preview pane, you will see that the contents of the message variable are reflected in the TextField and Text. To test the interactivity, you need to either ‘build and run’ the application or hit the ‘Play’ button in the preview. Then you can change the text in the text field and immediately see the changes displayed in the Text below.

Declaring the message variable as a @State will automatically set up all the notifications for these seamless updates.

Buttons and Actions

While updating another object is quite cool, it is not what we set out to do. Let’s get back on track.

  • Remove the last Text(message) and all its modifiers.
  • Command-click on the TextField and use the action menu to embed it in an HStack (horizontal stack)
  • Drag a Button from the object Library (you can open this with the ‘+’ button in the top right of the window) next to the text field

The code you generated should look like this:

HStack {
    TextField("Message", text: $message)
        .padding()
    Button(action: {}) {
        Text("Button")
    }
}

You can change the label of the button by changing the Text object inside of it. Change "Button" to "Say". You also want to add a .padding() modifier to the button.

The preview should now look like this:

The action of the Button is a code block that will be run when the button is clicked. Add the code between the action closure brackets:

Button(action: {
    let executableURL = URL(fileURLWithPath: "/usr/bin/say")
    try! Process.run(executableURL,
                     arguments: [self.message],
                     terminationHandler: nil)
}) {
    Text("Say")
}.padding(.trailing)

This uses the run() convenience method of the Process class. You need to provide the full path to the command line tool. You can determine the full path in Terminal with the which command:

> which say
/usr/bin/say

The arguments to the say command need to be provided as an Array of Strings. The process is launched asynchronously, so the main thread continues immediately. We can also provide a code block that will be executed when the Process terminates. For now, we have no code that needs to be run on termination, so we set the terminationHandler to nil.

Note: using the try! statement to crash when this method throws an exception is lazy. I use it here to simplify the code drastically. Process.run() will throw an error when the file at exectableURL does not exist or is not executable. We can be fairly certain the /usr/bin/say executable exists, so the try! statement is justifiable. You should probably add proper error prevention and handling in production code.

Updating the Interface

The say command is executed asynchronously. This means that it will be running in the background and our user interface remains responsive. However, you may want to provide some feedback to the user while the process is running. One way of doing that, is to disable the ‘Say’ button while the process is running. Then you can re-enable it when the process is done.

Add a second @State variable to track whether our process is running:

@State var isRunning = false

Add a .disabled(isRunning) modifier to the Button. This will disable the button when the value of the isRunning variable changes to true.

Then we add a line in the Button action code to set that variable to true. We also add a code block to the terminationHandler which sets its value back to false:

 Button(action: {
    let executableURL = URL(fileURLWithPath: "/usr/bin/say")
    self.isRunning = true
    try! Process.run(executableURL,
                     arguments: [self.message],
                     terminationHandler: { _ in self.isRunning = false })
}) {
    Text("Say")
}.disabled(isRunning)
    .padding(.trailing)

Now, when you press the button, it will disable until the say process is done speaking.

Sample Code

For your convenience, here is the finished code for the ContentView.swift class:

//
//  ContentView.swift
//  SayThis
//

import SwiftUI

struct ContentView: View {
    @State var message = "Hello, World!"
    @State var isRunning = false
    
    var body: some View {
        VStack {
            Text("SayThis")
                .font(.largeTitle)
                .padding()
            HStack {
                TextField("Message", text: $message)
                    .padding(.leading)
                Button(action: {
                    let executableURL = URL(fileURLWithPath: "/usr/bin/say")
                    self.isRunning = true
                    try! Process.run(executableURL,
                                     arguments: [self.message],
                                     terminationHandler: { _ in self.isRunning = false })
                }) {
                    Text("Say")
                }.disabled(isRunning)
                    .padding(.trailing)
            }
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

More Tutorials

If you want to learn more about SwiftUI and how it works, Apple has some excellent Tutorials on their developer page:

Update 2023-08-16: nearly five years later, I wrote a second part to this tutorial.