app.plist - Plist Operations API

Read and write macOS Property List files.

keyPath Expression

Engine v1.3.0+ supports two keyPath forms:

  • string — a single literal key (no splitting; a . in the string is part of the key, not a separator)
  • array — nested path; elements are string keys or number indices (Lua is 1-based)

Why not "a.b.c" dot-separated? Because plist keys frequently contain . (reverse-domain naming like com.apple.WatchKit), and dot-splitting collides with the literal key contents. The new design separates “one key” from “multiple-step nesting” — safer and unambiguous.

-- Single key (a "." in the string is literal, works directly)
app.plist.getValue(path, "CFBundleIdentifier")
app.plist.getValue(path, "com.apple.developer.networking.wifi-info")

-- Nested (array)
app.plist.getValue(path, {"CFBundleURLTypes", 1, "CFBundleURLSchemes", 1})
--                              ^ 1st dict        ^ 1st scheme (Lua 1-based)

Methods

app.plist.read(path)

Read a plist file.

Parameters:

  • path (string) - Plist file path

Returns: table|nil, error

local info = app.plist.read("/Applications/Safari.app/Contents/Info.plist")
if info then
    app.log.info("Version: " .. info.CFBundleShortVersionString)
    app.log.info("Bundle ID: " .. info.CFBundleIdentifier)
end

app.plist.write(path, data)

Write a plist file (XML format). Any nil / NSNull values inside dicts or arrays are stripped automatically to avoid serialization failure.

Parameters:

  • path (string) - File path
  • data (table) - Data to write

Returns: boolean, error

app.plist.write("/path/to/config.plist", {
    AppName = "MyApp",
    Version = "1.0.0",
    Settings = {
        Theme = "dark",
        AutoSave = true
    }
})

app.plist.getValue(path, keyPath)

Read a value at the given path.

Parameters:

  • path (string) - Plist file path
  • keyPath (string | table) - See keyPath Expression above

Returns: any|nil, error — returns nil (not error) when the key is missing

-- Top-level
local version = app.plist.getValue(infoPath, "CFBundleShortVersionString")

-- Key containing "." (no escaping needed)
local wifi = app.plist.getValue(entitlements, "com.apple.developer.networking.wifi-info")

-- Nested (array keyPath, Lua 1-based)
local scheme = app.plist.getValue(infoPath, {"CFBundleURLTypes", 1, "CFBundleURLSchemes", 1})

app.plist.setValue(path, keyPath, value)

Set a value at the given path. When value is nil, the key is removed (prefer removeValue for clarity).

Parameters:

  • path (string) - Plist file path
  • keyPath (string | table) - See keyPath Expression above
  • value (any | nil) - Value

Returns: boolean, error

-- Top-level assignment
app.plist.setValue(configPath, "LastOpened", os.time())

-- Nested assignment (intermediate dicts are created as needed)
app.plist.setValue(configPath, {"Settings", "Theme"}, "light")

-- Delete a key (v1.3.0+ truly removes; older versions wrote NSNull instead)
app.plist.setValue(configPath, "OldKey", nil)

app.plist.removeValue(path, keyPath) v1.3.0+

Explicitly remove a key (preferred over setValue(path, k, nil) for readability).

Parameters:

  • path (string) - Plist file path
  • keyPath (string | table) - See keyPath Expression above

Returns: boolean, error

-- Remove ipa device restrictions
app.plist.removeValue(infoPath, "UISupportedDevices")
app.plist.removeValue(infoPath, "UIRequiredDeviceCapabilities")

-- Nested removal
app.plist.removeValue(configPath, {"Settings", "OldKey"})

app.plist.hasKey(path, keyPath) v1.3.0+

Check whether a key exists. Disambiguates “key absent” from “key value is nil” — getValue returns nil in both cases, but the semantics differ.

Parameters:

  • path (string) - Plist file path
  • keyPath (string | table) - See keyPath Expression above

Returns: boolean, error

if app.plist.hasKey(infoPath, "UISupportedDevices") then
    app.plist.removeValue(infoPath, "UISupportedDevices")
end

Write Semantics (setValue / removeValue / arrays)

Intermediate levels

  • Missing intermediate levels are created automatically (type decided by the next keyPath segment: string key → dict, number index → array).
  • An intermediate that already exists but whose type conflicts with the keyPath (e.g. using a string key to traverse an array) → errors rather than silently overwriting or losing data.

Arrays

  • You may only replace an existing index or append at the end (index = current length + 1, Lua 1-based).
  • Out-of-bounds index → errors. Plist arrays cannot have gaps; no placeholder filling.

Data / Date

  • When setValue / removeValue edits one key, it does not corrupt other Data / Date fields in the file (internally it reads/modifies/writes the raw types; conversion to Base64 / ISO 8601 strings happens only when read / getValue returns to Lua).
-- Auto-create intermediate levels
app.plist.setValue(p, {"Settings", "Theme"}, "dark")          -- Settings is created if absent

-- Append at the end of an array (2 items → index 3 appends)
app.plist.setValue(p, {"URLSchemes", 3}, "myapp")

-- Out-of-bounds errors
local ok, err = app.plist.setValue(p, {"URLSchemes", 9}, "x")    -- ok = false

-- Type conflict errors (CFBundleName is a string, traversed as a dict)
local ok2, err2 = app.plist.setValue(p, {"CFBundleName", "x"}, "v")   -- ok2 = false

Type Mapping

Plist Type Lua Type
dict table (hash map)
array table (array, Lua 1-based)
string string
integer/real number
true/false boolean
data string (Base64 encoded)
date string (ISO 8601 format)

Examples

Read Application Info

function MyPlugin:getAppInfo(appPath)
    local infoPath = app.path.join(appPath, "Contents/Info.plist")
    if not app.path.exists(infoPath) then return nil end

    local info = app.plist.read(infoPath)
    if not info then return nil end

    return {
        name = info.CFBundleName or info.CFBundleDisplayName,
        version = info.CFBundleShortVersionString,
        build = info.CFBundleVersion,
        bundleId = info.CFBundleIdentifier,
        minOS = info.LSMinimumSystemVersion
    }
end

Remove ipa Device Restrictions

function MyPlugin:removeDeviceRestrictions(infoPath)
    for _, key in ipairs({"UISupportedDevices", "UIRequiredDeviceCapabilities"}) do
        if app.plist.hasKey(infoPath, key) then
            app.plist.removeValue(infoPath, key)
            app.log.info("Removed " .. key)
        end
    end
end

Toggle a Nested Setting

local enabled = app.plist.getValue(configPath, {"Settings", "Enabled"})
app.plist.setValue(configPath, {"Settings", "Enabled"}, not enabled)

Migration Notes (from older engines)

If your plugin previously used "a.b.c" strings to express nested paths (dot-split), change them to arrays:

-- Old (v1.2.0 and earlier, deprecated)
app.plist.getValue(p, "Settings.Theme")

-- New (v1.3.0+)
app.plist.getValue(p, {"Settings", "Theme"})

If you used setValue(p, k, nil) expecting deletion — older engines wrote NSNull instead of removing. v1.3.0+ truly removes the key. Prefer removeValue for explicit intent.

Developer Documentation
User Guide
Getting Started Script Menus FAQ
Script Development
Development Guide
Plugin Development
Quick Start Development Guide Example Plugins
API Reference
Overview API Query Plugin Info Logging Finder Context Plugin Settings Internationalization
UI & Interaction
Dialog Progress Notification Chooser WebView Status Bar Dock
Files & Paths
File Operations Path Utilities Finder Actions Trash Extended Attributes Metadata File Watcher
Data Formats
JSON Plist CSV XML PDF Image
Text & Encoding
String Regex Date & Time Color Crypto
System
Shell Commands Process Application System Info AppleScript Shortcuts
System Info
Network Power/Battery Screen/Appearance Audio Bluetooth Location
Network
HTTP WebSocket URL
Input & Clipboard
Keyboard Mouse Hotkey Clipboard Window
Storage
SQLite Keychain UserDefaults
Media
OCR QR Code
Utilities
Archive UTI Share Timer Wake Lock Thread