app.plist - Plist 操作 API

读写 macOS Property List 文件。

keyPath 表达

新版(引擎 v1.3.0+)keyPath 支持两种形式:

  • 字符串 —— 单一 key 字面值,不做任何切分(含 . 也直接当 key 的一部分)
  • 数组 —— 嵌套路径,元素是 string key 或 number index(Lua 1-based

为什么不用 "a.b.c" 这种点分隔?因为 plist 的 key 字面值经常含 .(反域名命名约定如 com.apple.WatchKit),dot-split 跟 key 字面值直接冲突。新设计把”一段 key”和”多段嵌套”两件事解耦,更安全。

-- 单一 key(含 dot 也直接 work)
app.plist.getValue(path, "CFBundleIdentifier")
app.plist.getValue(path, "com.apple.developer.networking.wifi-info")

-- 嵌套(数组)
app.plist.getValue(path, {"CFBundleURLTypes", 1, "CFBundleURLSchemes", 1})
--                              ^ 第 1 个 dict ^ 第 1 个 scheme(Lua 1-based)

方法

app.plist.read(path)

读取 plist 文件。

参数:

  • path (string) - plist 文件路径

返回值: table|nil, error

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

app.plist.write(path, data)

写入 plist 文件(XML 格式)。data 内 dict/array 中的 nil/NSNull 会被自动 strip,避免 serialization 失败。

参数:

  • path (string) - 文件路径
  • data (table) - 要写入的数据

返回值: 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)

读取指定路径的值。

参数:

  • path (string) - plist 文件路径
  • keyPath (string | table) - 见上方 keyPath 表达

返回值: any|nil, error —— 找不到返回 nil(不是 error)

-- 顶层
local version = app.plist.getValue(infoPath, "CFBundleShortVersionString")

-- 含 dot 的 key(无需转义)
local wifi = app.plist.getValue(entitlements, "com.apple.developer.networking.wifi-info")

-- 嵌套(数组 keyPath,Lua 1-based)
local scheme = app.plist.getValue(infoPath, {"CFBundleURLTypes", 1, "CFBundleURLSchemes", 1})

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

设置指定路径的值。valuenil 时删除该 key(推荐用 removeValue 更显式)。

参数:

  • path (string) - plist 文件路径
  • keyPath (string | table) - 见上方 keyPath 表达
  • value (any | nil) - 值

返回值: boolean, error

-- 顶层赋值
app.plist.setValue(configPath, "LastOpened", os.time())

-- 嵌套赋值(中间路径不存在会自动创建)
app.plist.setValue(configPath, {"Settings", "Theme"}, "light")

-- 删除 key(v1.3.0 起真正删除;旧版本会写入 NSNull)
app.plist.setValue(configPath, "OldKey", nil)

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

显式删除 key(推荐写法,比 setValue(path, k, nil) 更可读)。

参数:

  • path (string) - plist 文件路径
  • keyPath (string | table) - 见上方 keyPath 表达

返回值: boolean, error

-- 移除 ipa 的支持设备限制
app.plist.removeValue(infoPath, "UISupportedDevices")
app.plist.removeValue(infoPath, "UIRequiredDeviceCapabilities")

-- 嵌套删除
app.plist.removeValue(configPath, {"Settings", "OldKey"})

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

判断 key 是否存在。消除”key 不存在”与”key 值为 nil”的歧义——getValue 都返回 nil,但语义不同。

参数:

  • path (string) - plist 文件路径
  • keyPath (string | table) - 见上方 keyPath 表达

返回值: boolean, error

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

写入语义(setValue / removeValue / 数组)

中间层

  • 缺失的中间层会自动创建(类型由下一段 keyPath 决定:string key → dict,number index → array)。
  • 中间层已存在但类型与 keyPath 不符(如用字符串 key 去穿一个数组)→ 报错,不会静默覆盖、不丢数据。

数组

  • 只能改写已有下标,或在末尾追加(下标 = 当前长度 + 1,Lua 1-based)。
  • 下标越界 → 报错。plist 数组不允许空洞,不会用占位值填充。

Data / Date

  • setValue / removeValue 改某个 key 时,不会损坏文件里其他的 Data / Date 字段(内部按原始类型读改写;只有 read / getValue 返回给 Lua 时才转成 Base64 / ISO 8601 字符串)。
-- 自动创建中间层
app.plist.setValue(p, {"Settings", "Theme"}, "dark")          -- Settings 不存在则自动建

-- 数组末尾追加(已有 2 项 → 下标 3 追加)
app.plist.setValue(p, {"URLSchemes", 3}, "myapp")

-- 越界报错
local ok, err = app.plist.setValue(p, {"URLSchemes", 9}, "x")    -- ok = false

-- 类型冲突报错(CFBundleName 是字符串,却当 dict 穿过)
local ok2, err2 = app.plist.setValue(p, {"CFBundleName", "x"}, "v")   -- ok2 = false

类型映射

Plist 类型 Lua 类型
dict table(哈希表)
array table(数组,Lua 1-based
string string
integer/real number
true/false boolean
data string(Base64 编码)
date string(ISO 8601 格式)

示例

读取应用信息

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

移除 ipa 支持设备限制

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

修改嵌套配置

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

升级须知(旧插件迁移)

如果你的插件之前用 "a.b.c" 字符串表达嵌套路径(dot-split),需要改成数组:

-- 旧(v1.2.0 及之前,已废弃)
app.plist.getValue(p, "Settings.Theme")

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

如果之前用 setValue(p, k, nil) 期待删除——旧版会写入 NSNull(实际不删),v1.3.0+ 真正删除。推荐改用 removeValue 让语义更显式。

开发者文档
使用帮助
使用说明 脚本菜单 常见问题
脚本开发
开发指南
插件开发
快速开始 开发指南 示例插件
API 参考
概览 API 查询 插件信息 日志 Finder 上下文 插件设置 国际化
UI 与交互
对话框 进度条 系统通知 选择器 WebView 状态栏 Dock
文件与路径
文件操作 路径工具 Finder 操作 废纸篓 扩展属性 元数据 文件监听
数据格式
JSON Plist CSV XML PDF 图片
文本与编码
字符串 正则表达式 日期时间 颜色 加密编码
系统
Shell 命令 进程管理 应用管理 系统信息 AppleScript 快捷指令
系统信息
网络信息 电源/电池 屏幕/外观 音频控制 蓝牙设备 位置服务
网络
HTTP 请求 WebSocket URL 工具
输入与剪贴板
键盘模拟 鼠标模拟 全局热键 剪贴板 窗口管理
存储
SQLite Keychain UserDefaults
媒体
文字识别 二维码
工具
归档 类型标识 分享 定时器 防休眠 并发/协程