It’s common for apps to store user preferences and settings, the same is true for scripts:
ssh needs a place to store the user keys, git a place to store the user name and email, etc.

While many CLI tools store such data in dot files, in this article let’s explore how we can use Foundation’s UserDefaults!

UserDefaults

In the surface there’s no real difference between using UserDefaults in a script or an app, for example:

  • we can read and set any data in the UserDefault’s standard object:
import Foundation

let userDefaults = UserDefaults.standard

userDefaults.set(5, forKey: "stars")
print(userDefaults.integer(forKey: "stars")) // prints "5"

The main.swift content.

  • we can create and use different suites:
import Foundation

guard 
  let userDefaults = UserDefaults(suiteName: "five.stars") 
  else { exit(EXIT_FAILURE) }

userDefaults.set(5, forKey: "stars")
print(userDefaults.integer(forKey: "stars")) // prints "5"

The main.swift content.

  • we can even observe for UserDefaults changes:
extension UserDefaults {
  @objc dynamic public var stars: Int {
    get { integer(forKey: #function) }
    set { set(newValue, forKey: #function) }
  }
}

guard 
  let userDefaults = UserDefaults(suiteName: "five.stars") 
  else { exit(EXIT_FAILURE) }

let token: NSKeyValueObservation = userDefaults.observe(
  \.stars,
  options: [.initial, .new]
) { defaults, _ in
  let rating = defaults.stars
  let fullStars = String(repeating: "★", count: rating)
  let emptyStars = String(repeating: "☆", count: 5 - rating)
  let stars: String = fullStars + emptyStars
  print(stars)
  // 👆🏻 prints the initial value and whenever a new change is made.
}

RunLoop.current.run()
// 👆🏻 stops the script from terminating

The main.swift content.

The script in action

UserDefault Location

Regardless of the platform we’re running on, UserDefaults are always stored in Property List files, which are XML files in disguise, where the elements alternate between key tags and other elements types.

This explains why we can store only just a few handful types in UserDefaults.

In iOS apps, the standard UserDefaults plist is stored in the app home directory under the Library/Preferences folder: the name of the file is the app bundle identifier, for example blog.fivestars.app.plist.

On macOS, all user applications UserDefaults are stored at ~/Library/Preferences, therefore this folder not only contains our app plist file, but all other apps as well.

This is one reason why our apps need to have an unique bundle identifiers.

This works great for apps, but our scripts don’t have any bundle identifier: where the UserDefaults data of our tools are stored? It turns out that scripts also use the same folder: instead of using a bundle identifier, the script standard UserDefaults plist is stored under the name of the script.

e.g. a Swift executable named hello will have its standard UserDefaults stored at ~/Library/Preferences/hello.plist.

What about UserDefaults suites? In this case the suite name will be the name of the plist file.

e.g. a script using UserDefaults(suiteName: "five.stars") will have this suite data stored at ~/Library/Preferences/five.stars.plist.

Scripts are unsandboxed processes, hence they can read/write any UserDefaults file located in ~/Library/Preferences/, all it takes is to know the plist name (a.k.a. the app bundle id):
want to read/edit the user preferences for…

  • Xcode? Use UserDefaults(suiteName: "com.apple.dt.Xcode")

  • Finder? Use UserDefaults(suiteName: "com.apple.finder").

  • Etc.

For an easy way to explore even more preferences of both system and 3rd party apps, I suggest to use the free Prefs Editor app.

Conclusions

In this article we’ve explored the behind the scenes of our scripts UserDefaults preferences, where they’re persisted in our machines, and how scripts can actually access to all apps UserDefaults.

Do your CLI tools store any preferences? What kind of preferences do you store? Do you use UserDefaults? Please let me know!

As always, thank you for reading and stay tuned for more articles!

⭑⭑⭑⭑⭑