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 pathdata(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 pathkeyPath(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 pathkeyPath(string | table) - See keyPath Expression abovevalue(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 pathkeyPath(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 pathkeyPath(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/removeValueedits 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 whenread/getValuereturns 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.