如何以编程方式创建和管理 macOS Safari 书签?
How to create and manage macOS Safari bookmarks programmatically?
我正在制作一个脚本,它会更新我的 macOS Safari 上的书签,以便始终将我订阅的所有 subreddits 作为特定文件夹中的单独书签。我已经到了这样的地步,我将所有子目录作为 Python 中元组的排序列表,其中想要的书签名称作为第一个元素,书签 url 作为第二个元素:
bookmarks = [
('r/Android', 'https://www.reddit.com/r/Android/'),
('r/Apple', 'https://www.reddit.com/r/Apple/'),
('r/Mac', 'https://www.reddit.com/r/Mac/'),
('r/ProgrammerHumor', 'https://www.reddit.com/r/ProgrammerHumor/')
]
如何在 Safari 中清除我的 subreddit 书签文件夹并在该文件夹中创建这些新书签?
到目前为止我一直使用 Python,但是从 Python 程序调用外部 AppleScript 或 Shell 脚本没有问题。
这是想要的结果的图片,每个书签都链接到各自的 subreddit url:
我从未在 Safari 中找到用于管理书签的 AS 命令(不在 AS 字典中)。所以我构建了自己的例程来使用 Safari 书签 plist 文件。但是,它们可能会受到 Apple 未来处理书签方式的意外更改的影响!到目前为止,它还在工作,但我还没有使用 10.14
首先你必须得到这个plist文件来改变它。这部分必须在您的主代码中。它为您提供了 plist 文件的补丁:
set D_Lib to ((path to library folder from user domain) as string) & "Safari"
set SafariPlistFile to D_Lib & ":Bookmarks.plist"
这里有2个sub-routine来管理书签。第一个检查书签是否存在
on Exist_BM(FPlist, BM_Name) -- Search bookmark named BM_Name in Plist file. returns number or 0 if not found. This search is limited to main bar, not sub menus
tell application "System Events"
set numBM to 0
set Main_Bar to property list item "Children" of property list item 2 of property list item "Children" of property list file FPlist
tell Main_Bar
set myBM to every property list item of Main_Bar
repeat with I from 1 to (count of myBM)
set myType to value of property list item "WebBookmarkType" of (item I of myBM)
if (myType = "WebBookmarkTypeLeaf") then
if (value of property list item "title" of property list item "URIDictionary" of (item I of myBM)) = BM_Name then
set numBM to I
exit repeat
end if
end if
end repeat
end tell
end tell
return numBM
end Exist_BM
您可以像下面这样调用此处理程序:
Set myAndroid to Exist_BM(SafariPlistFile,"r/Android")
if myAndroid >0 then -- set here the code to update : the bookmark already exists
else -- set here the code to add new bookmark "r/Android"
end if
第二个处理程序创建一个新书签:
on New_BM(FPlist, BM_Name, N_URL) -- create new bookmark at right end side of bookmarks and return its number
tell application "System Events"
set Main_Bar to property list item "Children" of property list item 2 of property list item "Children" of property list file FPlist
set numBM to count of property list item of Main_Bar
tell Main_Bar
set my_UUID to do shell script "uuidgen" -- create unique Apple UID
set myNewBM to make new property list item at the end with properties {kind:record}
tell myNewBM
set URIDict to make new property list item with properties {kind:record, name:"URIDictionary"}
tell URIDict to make new property list item with properties {name:"title", kind:string, value:BM_Name}
make new property list item with properties {name:"URLString", kind:string, value:N_URL}
make new property list item with properties {name:"WebBookmarkType", kind:string, value:"WebBookmarkTypeLeaf"}
make new property list item with properties {name:"WebBookmarkUUID", kind:string, value:my_UUID}
end tell -- myNewBM
end tell
end tell
return (numBM + 1)
end New_BM
我使用这些例程在我的书签右侧添加、查看和更改书签。在您的情况下,您需要使用书签子菜单,然后您必须调整此代码,但主要概念是相同的。
为了方便起见,我建议您在子菜单中有书签时开始查看 plist 文件 (Library/Safari/Bookmarks.plist) 以查看其结构。
希望对您有所帮助!
tl;dr 有必要编辑 Safari 的 Bookmarks.plist
以编程方式创建书签。查看下面的 "Using a Python script" 部分。它需要在 Bash 脚本中使用 XSLT 样式表并通过您的 .py
文件调用它。实现这一目标所需的所有工具都内置于 macOS.
重要提示: 使用 macOS Mojave (10.14.x)+
您需要执行步骤 1 -10 在下面的 "MacOS Mojave Restrictions" 部分。这些更改允许修改 Bookmarks.plist
.
在继续之前创建一个 Bookmarks.plist
的副本,可以在 ~/Library/Safari/Bookmarks.plist
找到它。您可以运行以下命令将其复制到您的桌面:
cp ~/Library/Safari/Bookmarks.plist ~/Desktop/Bookmarks.plist
稍后恢复Bookmarks.plist
运行:
cp ~/Desktop/Bookmarks.plist ~/Library/Safari/Bookmarks.plist
属性 列表
MacOS 内置了 属性 List (.plist
) 相关的命令行工具,即 plutil
, and defaults
,它们有助于编辑通常包含平面数据的应用程序首选项结构。然而 Safari 的 Bookmarks.plist
具有深层嵌套结构,这两种工具都不擅长编辑。
正在将 .plist
文件转换为 XML
plutil
提供了一个 -convert
选项来将 .plist
从二进制转换为 XML。例如:
plutil -convert xml1 ~/Library/Safari/Bookmarks.plist
同样,以下命令转换为二进制:
plutil -convert binary1 ~/Library/Safari/Bookmarks.plist
转换为 XML 可以使用 XSLT,这是转换复杂 XML 结构的理想选择。
使用 XSLT 样式表
此自定义 XSLT 样式表转换 Bookmarks.plist
添加元素节点以创建书签:
template.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:output
method="xml"
indent="yes"
doctype-system="http://www.apple.com/DTDs/PropertyList-1.0.dtd"
doctype-public="-//Apple//DTD PLIST 1.0//EN"/>
<xsl:param name="bkmarks-folder"/>
<xsl:param name="bkmarks"/>
<xsl:param name="guid"/>
<xsl:param name="keep-existing" select="false" />
<xsl:variable name="bmCount">
<xsl:value-of select="string-length($bkmarks) -
string-length(translate($bkmarks, ',', '')) + 1"/>
</xsl:variable>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template name="getNthValue">
<xsl:param name="list"/>
<xsl:param name="n"/>
<xsl:param name="delimiter"/>
<xsl:choose>
<xsl:when test="$n = 1">
<xsl:value-of select=
"substring-before(concat($list, $delimiter), $delimiter)"/>
</xsl:when>
<xsl:when test="contains($list, $delimiter) and $n > 1">
<!-- recursive call -->
<xsl:call-template name="getNthValue">
<xsl:with-param name="list"
select="substring-after($list, $delimiter)"/>
<xsl:with-param name="n" select="$n - 1"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="createBmEntryFragment">
<xsl:param name="loopCount" select="1"/>
<xsl:variable name="bmInfo">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bkmarks"/>
<xsl:with-param name="delimiter" select="','"/>
<xsl:with-param name="n" select="$loopCount"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmkName">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="1"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmURL">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="2"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmGUID">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="3"/>
</xsl:call-template>
</xsl:variable>
<xsl:if test="$loopCount > 0">
<dict>
<key>ReadingListNonSync</key>
<dict>
<key>neverFetchMetadata</key>
<false/>
</dict>
<key>URIDictionary</key>
<dict>
<key>title</key>
<string>
<xsl:value-of select="$bmkName"/>
</string>
</dict>
<key>URLString</key>
<string>
<xsl:value-of select="$bmURL"/>
</string>
<key>WebBookmarkType</key>
<string>WebBookmarkTypeLeaf</string>
<key>WebBookmarkUUID</key>
<string>
<xsl:value-of select="$bmGUID"/>
</string>
</dict>
<!-- recursive call -->
<xsl:call-template name="createBmEntryFragment">
<xsl:with-param name="loopCount" select="$loopCount - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="createBmFolderFragment">
<dict>
<key>Children</key>
<array>
<xsl:call-template name="createBmEntryFragment">
<xsl:with-param name="loopCount" select="$bmCount"/>
</xsl:call-template>
<xsl:if test="$keep-existing = 'true'">
<xsl:copy-of select="./array/node()|@*"/>
</xsl:if>
</array>
<key>Title</key>
<string>
<xsl:value-of select="$bkmarks-folder"/>
</string>
<key>WebBookmarkType</key>
<string>WebBookmarkTypeList</string>
<key>WebBookmarkUUID</key>
<string>
<xsl:value-of select="$guid"/>
</string>
</dict>
</xsl:template>
<xsl:template match="dict[string[text()='BookmarksBar']]/array">
<array>
<xsl:for-each select="dict">
<xsl:choose>
<xsl:when test="string[text()=$bkmarks-folder]">
<xsl:call-template name="createBmFolderFragment"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
<xsl:if test="not(./dict/string[text()=$bkmarks-folder])">
<xsl:call-template name="createBmFolderFragment"/>
</xsl:if>
</array>
</xsl:template>
</xsl:stylesheet>
运行转型:
这 .xsl
需要指定每个所需书签属性的参数。
首先确保Bookmarks.plits
是XML格式:
plutil -convert xml1 ~/Library/Safari/Bookmarks.plist
利用内置函数 xsltproc
将 template.xsl
应用于 Bookmarks.plist
。
首先,cd
到template.xsl
所在的位置,运行这个复合命令:
guid1=$(uuidgen) && guid2=$(uuidgen) && guid3=$(uuidgen) && xsltproc --novalid --stringparam bkmarks-folder "QUUX" --stringparam bkmarks "r/Android https://www.reddit.com/r/Android/ ${guid1},r/Apple https://www.reddit.com/r/Apple/ ${guid2}" --stringparam guid "$guid3" ./template.xsl - <~/Library/Safari/Bookmarks.plist > ~/Desktop/result-plist.xml
这会在您的 Desktop
上创建 result-plist.xml
,其中包含一个名为 QUUX
的新书签文件夹和两个新书签。
让我们进一步了解上述复合命令中的各个部分:
uuidgen
生成新 Bookmarks.plist
中所需的三个 UUID(一个用于文件夹,一个用于每个书签条目)。我们预先生成它们并将它们传递给 XSLT,因为:
- XSLT 1.0 没有生成 UUID 的功能。
xsltproc
需要 XSLT 1.0
xsltproc
的--stringparam
选项表示自定义参数如下:
--stringparam bkmarks-folder <value>
- 书签文件夹的名称。
--stringparam bkmarks <value>
- 每个书签的属性。
每个书签规范都用逗号分隔 (,
)。每个定界字符串都有三个值;书签名称、URL 和 GUID。这 3 个值是 space 分隔的。
--stringparam guid <value>
- 书签文件夹的 GUID。
最后部分:
./template.xsl - <~/Library/Safari/Bookmarks.plist > ~/Desktop/result-plist.xml
定义路径; .xsl
、来源 XML 和目的地。
要评估刚刚发生的转换,请使用 diff
显示两个文件之间的差异。例如 运行:
diff -yb --width 200 ~/Library/Safari/Bookmarks.plist ~/Desktop/result-plist.xml | less
然后多次按 F 键向前导航到每一页,直到您在两列中间看到 >
符号 - 它们表示新元素所在的位置已添加节点。按B键后退一页,输入Q退出diff.
使用 Bash 脚本。
我们现在可以在 Bash 脚本中使用前面提到的 .xsl
。
script.sh
#!/usr/bin/env bash
declare -r plist_path=~/Library/Safari/Bookmarks.plist
# ANSI/VT100 Control sequences for colored error log.
declare -r fmt_red='\x1b[31m'
declare -r fmt_norm='\x1b[0m'
declare -r fmt_green='\x1b[32m'
declare -r fmt_bg_black='\x1b[40m'
declare -r error_badge="${fmt_red}${fmt_bg_black}ERR!${fmt_norm}"
declare -r tick_symbol="${fmt_green}\xE2\x9C\x94${fmt_norm}"
if [ -z "" ] || [ -z "" ]; then
echo -e "${error_badge} Missing required arguments" >&2
exit 1
fi
bkmarks_folder_name=
bkmarks_spec=
keep_existing_bkmarks=${3:-false}
# Transform bookmark spec string into array using comma `,` as delimiter.
IFS=',' read -r -a bkmarks_spec <<< "${bkmarks_spec//, /,}"
# Append UUID/GUID to each bookmark spec element.
bkmarks_spec_with_uuid=()
while read -rd ''; do
[[ $REPLY ]] && bkmarks_spec_with_uuid+=("${REPLY} $(uuidgen)")
done < <(printf '%s[=19=]' "${bkmarks_spec[@]}")
# Transform bookmark spec array back to string using comma `,` as delimiter.
bkmarks_spec_str=$(printf '%s,' "${bkmarks_spec_with_uuid[@]}")
bkmarks_spec_str=${bkmarks_spec_str%,} # Omit trailing comma character.
# Check the .plist file exists.
if [ ! -f "$plist_path" ]; then
echo -e "${error_badge} File not found: ${plist_path}" >&2
exit 1
fi
# Verify that plist exists and contains no syntax errors.
if ! plutil -lint -s "$plist_path" >/dev/null; then
echo -e "${error_badge} Broken or missing plist: ${plist_path}" >&2
exit 1
fi
# Ignore ShellCheck errors regarding XSLT variable references in template below.
# shellcheck disable=SC2154
xslt() {
cat <<'EOX'
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:output
method="xml"
indent="yes"
doctype-system="http://www.apple.com/DTDs/PropertyList-1.0.dtd"
doctype-public="-//Apple//DTD PLIST 1.0//EN"/>
<xsl:param name="bkmarks-folder"/>
<xsl:param name="bkmarks"/>
<xsl:param name="guid"/>
<xsl:param name="keep-existing" select="false" />
<xsl:variable name="bmCount">
<xsl:value-of select="string-length($bkmarks) -
string-length(translate($bkmarks, ',', '')) + 1"/>
</xsl:variable>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template name="getNthValue">
<xsl:param name="list"/>
<xsl:param name="n"/>
<xsl:param name="delimiter"/>
<xsl:choose>
<xsl:when test="$n = 1">
<xsl:value-of select=
"substring-before(concat($list, $delimiter), $delimiter)"/>
</xsl:when>
<xsl:when test="contains($list, $delimiter) and $n > 1">
<!-- recursive call -->
<xsl:call-template name="getNthValue">
<xsl:with-param name="list"
select="substring-after($list, $delimiter)"/>
<xsl:with-param name="n" select="$n - 1"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="createBmEntryFragment">
<xsl:param name="loopCount" select="1"/>
<xsl:variable name="bmInfo">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bkmarks"/>
<xsl:with-param name="delimiter" select="','"/>
<xsl:with-param name="n" select="$loopCount"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmkName">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="1"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmURL">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="2"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmGUID">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="3"/>
</xsl:call-template>
</xsl:variable>
<xsl:if test="$loopCount > 0">
<dict>
<key>ReadingListNonSync</key>
<dict>
<key>neverFetchMetadata</key>
<false/>
</dict>
<key>URIDictionary</key>
<dict>
<key>title</key>
<string>
<xsl:value-of select="$bmkName"/>
</string>
</dict>
<key>URLString</key>
<string>
<xsl:value-of select="$bmURL"/>
</string>
<key>WebBookmarkType</key>
<string>WebBookmarkTypeLeaf</string>
<key>WebBookmarkUUID</key>
<string>
<xsl:value-of select="$bmGUID"/>
</string>
</dict>
<!-- recursive call -->
<xsl:call-template name="createBmEntryFragment">
<xsl:with-param name="loopCount" select="$loopCount - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="createBmFolderFragment">
<dict>
<key>Children</key>
<array>
<xsl:call-template name="createBmEntryFragment">
<xsl:with-param name="loopCount" select="$bmCount"/>
</xsl:call-template>
<xsl:if test="$keep-existing = 'true'">
<xsl:copy-of select="./array/node()|@*"/>
</xsl:if>
</array>
<key>Title</key>
<string>
<xsl:value-of select="$bkmarks-folder"/>
</string>
<key>WebBookmarkType</key>
<string>WebBookmarkTypeList</string>
<key>WebBookmarkUUID</key>
<string>
<xsl:value-of select="$guid"/>
</string>
</dict>
</xsl:template>
<xsl:template match="dict[string[text()='BookmarksBar']]/array">
<array>
<xsl:for-each select="dict">
<xsl:choose>
<xsl:when test="string[text()=$bkmarks-folder]">
<xsl:call-template name="createBmFolderFragment"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
<xsl:if test="not(./dict/string[text()=$bkmarks-folder])">
<xsl:call-template name="createBmFolderFragment"/>
</xsl:if>
</array>
</xsl:template>
</xsl:stylesheet>
EOX
}
# Convert the .plist to XML format
plutil -convert xml1 -- "$plist_path" >/dev/null || {
echo -e "${error_badge} Cannot convert .plist to xml format" >&2
exit 1
}
# Generate a UUID/GUID for the folder.
folder_guid=$(uuidgen)
xsltproc --novalid \
--stringparam keep-existing "$keep_existing_bkmarks" \
--stringparam bkmarks-folder "$bkmarks_folder_name" \
--stringparam bkmarks "$bkmarks_spec_str" \
--stringparam guid "$folder_guid" \
<(xslt) - <"$plist_path" > "${TMPDIR}result-plist.xml"
# Convert the .plist to binary format
plutil -convert binary1 -- "${TMPDIR}result-plist.xml" >/dev/null || {
echo -e "${error_badge} Cannot convert .plist to binary format" >&2
exit 1
}
mv -- "${TMPDIR}result-plist.xml" "$plist_path" 2>/dev/null || {
echo -e "${error_badge} Cannot move .plist from TMPDIR to ${plist_path}" >&2
exit 1
}
echo -e "${tick_symbol} Successfully created Safari bookmarks."
说明
script.sh
提供以下功能:
- 简化的 API 将在通过 Python.
执行时有益
- 验证
.plist
没有损坏。
- 错误handling/logging.
- 使用
template.xsl
内联通过 xsltproc
转换 .plist
。
- 根据编号创建 GUID 以传递给 XSLT。给定参数中指定的书签数。
- 将
.plist
转换为 XML,然后返回二进制。
- 将新文件写入 OS 的 temp 文件夹,然后将其移动到
Bookmarks.plist
目录,有效地替换原始文件。
运行启用 shell 脚本
cd
到 script.sh
所在的位置和 运行 以下 chmod
命令使 script.sh
可执行:
chmod +ux script.sh
运行以下命令:
./script.sh "Whosebug" "bash https://whosebug.com/questions/tagged/bash,python https://whosebug.com/questions/tagged/python"
然后将以下内容打印到您的 CLI:
✔ Successfully created Safari bookmarks.
Safari 现在有一个名为 Whosebug
的书签文件夹,其中包含两个书签(bash
和 python
)。
使用 Python 脚本
有几种方法可以通过 .py
文件执行 script.sh
。
方法 A:外部 shell 脚本
下面的 .py
文件执行外部 script.sh
文件。我们将文件命名为 create-safari-bookmarks.py
并将其保存在与 script.sh
.
相同的文件夹中
create-safari-bookmarks.py
#!/usr/bin/env python
import subprocess
def run_script(folder_name, bkmarks):
subprocess.call(["./script.sh", folder_name, bkmarks])
def tuple_to_shell_arg(tup):
return ",".join("%s %s" % t for t in tup)
reddit_bkmarks = [
('r/Android', 'https://www.reddit.com/r/Android/'),
('r/Apple', 'https://www.reddit.com/r/Apple/'),
('r/Mac', 'https://www.reddit.com/r/Mac/'),
('r/ProgrammerHumor', 'https://www.reddit.com/r/ProgrammerHumor/'),
('r/gaming', 'https://www.reddit.com/r/gaming/')
]
so_bkmarks = [
('bash', 'https://whosebug.com/questions/tagged/bash'),
('python', 'https://whosebug.com/questions/tagged/python'),
('xslt', 'https://whosebug.com/questions/tagged/xslt'),
('xml', 'https://whosebug.com/questions/tagged/xml')
]
run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks))
run_script("Whosebug", tuple_to_shell_arg(so_bkmarks))
解释:
第一个 def
语句定义了一个 run-script
函数。它有两个参数; folder_name
和 bkmarks
。 subprocess
模块 call
方法本质上是使用所需参数执行 script.sh
。
第二个def
语句定义了一个tuple_to_shell_arg
函数。它有一个参数tup
。 String join()
方法将元组列表转换为 script.sh
所需的格式。它实质上转换了一个元组列表,例如:
[
('foo', 'https://www.foo.com/'),
('quux', 'https://www.quux.com')
]
和returns一个字符串:
foo https://www.foo.com/,quux https://www.quux.com
run_script
函数调用如下:
run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks))
这传递了两个参数; subreddit
(书签文件夹的名称),以及每个所需书签的规范(格式如前文第 2 点所述)。
运行宁create-safari-bookmarks.py
使 create-safari-bookmarks.py
可执行:
chmod +ux ./create-safari-bookmarks.py
然后调用它:
./create-safari-bookmarks.py
方法 B:内联 shell 脚本
根据您的具体用例,您可能需要考虑在 .py
文件中内联 script.sh
,而不是调用外部 .sh
文件。我们将此文件命名为 create-safari-bookmarks-inlined.py
并将其保存到 create-safari-bookmarks.py
所在的同一目录中。
重要:
您需要将 script.sh
中的所有内容复制并粘贴到 create-safari-bookmarks-inlined.py
中指定的位置。
将其粘贴到 bash_script = """\
部分之后的下一行。
create-safari-bookmarks-inlined.py
中的 """
部分应该在粘贴的 script.sh
内容的最后一行之后单独一行。
script.sh
的第 31 行在 .py
中内联时必须使用另一个反斜杠转义 '%s[=124=]'
部分([=125=]
是一个空字符) ,即 script.sh
的第 31 行应如下所示:
...
done < <(printf '%s\0' "${bkmarks_spec[@]}")
^
...
这一行可能在 create-safari-bookmarks-inlined.py
中的第 37 行。
create-safari-bookmarks-inlined.py
#!/usr/bin/env python
import tempfile
import subprocess
bash_script = """\
# <--- Copy and paste content of `script.sh` here and modify its line 31.
"""
def run_script(script, folder_name, bkmarks):
with tempfile.NamedTemporaryFile() as scriptfile:
scriptfile.write(script)
scriptfile.flush()
subprocess.call(["/bin/bash", scriptfile.name, folder_name, bkmarks])
def tuple_to_shell_arg(tup):
return ",".join("%s %s" % t for t in tup)
reddit_bkmarks = [
('r/Android', 'https://www.reddit.com/r/Android/'),
('r/Apple', 'https://www.reddit.com/r/Apple/'),
('r/Mac', 'https://www.reddit.com/r/Mac/'),
('r/ProgrammerHumor', 'https://www.reddit.com/r/ProgrammerHumor/'),
('r/gaming', 'https://www.reddit.com/r/gaming/')
]
so_bkmarks = [
('bash', 'https://whosebug.com/questions/tagged/bash'),
('python', 'https://whosebug.com/questions/tagged/python'),
('xslt', 'https://whosebug.com/questions/tagged/xslt'),
('xml', 'https://whosebug.com/questions/tagged/xml')
]
run_script(bash_script, "subreddit", tuple_to_shell_arg(reddit_bkmarks))
run_script(bash_script, "Whosebug", tuple_to_shell_arg(so_bkmarks))
说明
此文件与create-safari-bookmarks.py
的结果相同。
此修改后的 .py
脚本包括修改后的 run_script
函数,该函数利用 Python 的 tempfile
模块保存内联 shell 脚本到一个临时文件。
Python的subprocess
模块call
方法然后执行临时创建的shell文件。
运行宁create-safari-bookmarks-inlined.py
使 create-safari-bookmarks-inlined.py
可执行:
chmod +ux ./create-safari-bookmarks-inlined.py
然后通过运行ning调用:
./create-safari-bookmarks-inlined.py
附加说明:将书签附加到现有文件夹
目前,每次上述 scripts/commands 再次 运行 时,我们实际上是在用一个全新的并创建指定的书签。
但是,如果您想将书签附加到现有文件夹,则 template.xsl
包括一个额外的 parameter/argument 以传递给它。请注意第 14 行的部分:
<xsl:param name="keep-existing" select="false" />
默认值为false
。因此,如果我们要将 run_script
函数更改为 create-safari-bookmarks.py
中的
def run_script(folder_name, bkmarks, keep_existing):
subprocess.call(["./script.sh", folder_name, bkmarks, keep_existing])
即添加名为 keep_existing
的第三个参数,并在 subprocess.call([...])
中包含对它的引用,即它作为第三个参数传递给 script.sh
( ...然后是 XSLT 样式表)。
然后我们可以调用 run_script
函数并传入一个额外的字符串参数,"true"
或 "false"
如下所示:
run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks), "true")
run_script("Whosebug", tuple_to_shell_arg(so_bkmarks), "false")
但是,进行上述更改(即传入 "true"
以保留现有书签)确实有可能导致创建重复的书签。例如;当我们有一个现有的书签(名称和 URL),然后在稍后重新提供相同的名称和 URL 时,将出现重复的书签。
限制: 目前,为书签提供的任何名称参数都不能包含 space 个字符,因为它们被脚本用作分隔符。
MacOS Mojave 限制
由于 mac 上更严格的安全策略OS Mojave (10.14.x) 默认情况下不允许访问 ~/Library/Safari/Bookmarks.plist
(如 ).
中所述
因此,有必要授予Terminal.app,(或其他首选的CLI工具,如iTer), 访问整个磁盘。为此,您需要:
- Select Apple 菜单中的系统偏好设置。
- 在系统偏好设置 window中单击安全与策略图标。
- 在安全与策略面板中点击隐私选项卡。
- 在left-hand列中选择全盘访问。
- 单击左下角的锁定图标以允许更改。
- 输入管理员密码,然后点击解锁按钮。
- 接下来单击加号图标 (+)。
- 选择位于
/Applications/Utilities/
的Terminal.app,然后点击打开按钮。
- Terminal.app将添加到列表中。
- 单击锁定图标以防止任何进一步的更改,然后退出 系统偏好设置。
我正在制作一个脚本,它会更新我的 macOS Safari 上的书签,以便始终将我订阅的所有 subreddits 作为特定文件夹中的单独书签。我已经到了这样的地步,我将所有子目录作为 Python 中元组的排序列表,其中想要的书签名称作为第一个元素,书签 url 作为第二个元素:
bookmarks = [
('r/Android', 'https://www.reddit.com/r/Android/'),
('r/Apple', 'https://www.reddit.com/r/Apple/'),
('r/Mac', 'https://www.reddit.com/r/Mac/'),
('r/ProgrammerHumor', 'https://www.reddit.com/r/ProgrammerHumor/')
]
如何在 Safari 中清除我的 subreddit 书签文件夹并在该文件夹中创建这些新书签?
到目前为止我一直使用 Python,但是从 Python 程序调用外部 AppleScript 或 Shell 脚本没有问题。
这是想要的结果的图片,每个书签都链接到各自的 subreddit url:
我从未在 Safari 中找到用于管理书签的 AS 命令(不在 AS 字典中)。所以我构建了自己的例程来使用 Safari 书签 plist 文件。但是,它们可能会受到 Apple 未来处理书签方式的意外更改的影响!到目前为止,它还在工作,但我还没有使用 10.14
首先你必须得到这个plist文件来改变它。这部分必须在您的主代码中。它为您提供了 plist 文件的补丁:
set D_Lib to ((path to library folder from user domain) as string) & "Safari"
set SafariPlistFile to D_Lib & ":Bookmarks.plist"
这里有2个sub-routine来管理书签。第一个检查书签是否存在
on Exist_BM(FPlist, BM_Name) -- Search bookmark named BM_Name in Plist file. returns number or 0 if not found. This search is limited to main bar, not sub menus
tell application "System Events"
set numBM to 0
set Main_Bar to property list item "Children" of property list item 2 of property list item "Children" of property list file FPlist
tell Main_Bar
set myBM to every property list item of Main_Bar
repeat with I from 1 to (count of myBM)
set myType to value of property list item "WebBookmarkType" of (item I of myBM)
if (myType = "WebBookmarkTypeLeaf") then
if (value of property list item "title" of property list item "URIDictionary" of (item I of myBM)) = BM_Name then
set numBM to I
exit repeat
end if
end if
end repeat
end tell
end tell
return numBM
end Exist_BM
您可以像下面这样调用此处理程序:
Set myAndroid to Exist_BM(SafariPlistFile,"r/Android")
if myAndroid >0 then -- set here the code to update : the bookmark already exists
else -- set here the code to add new bookmark "r/Android"
end if
第二个处理程序创建一个新书签:
on New_BM(FPlist, BM_Name, N_URL) -- create new bookmark at right end side of bookmarks and return its number
tell application "System Events"
set Main_Bar to property list item "Children" of property list item 2 of property list item "Children" of property list file FPlist
set numBM to count of property list item of Main_Bar
tell Main_Bar
set my_UUID to do shell script "uuidgen" -- create unique Apple UID
set myNewBM to make new property list item at the end with properties {kind:record}
tell myNewBM
set URIDict to make new property list item with properties {kind:record, name:"URIDictionary"}
tell URIDict to make new property list item with properties {name:"title", kind:string, value:BM_Name}
make new property list item with properties {name:"URLString", kind:string, value:N_URL}
make new property list item with properties {name:"WebBookmarkType", kind:string, value:"WebBookmarkTypeLeaf"}
make new property list item with properties {name:"WebBookmarkUUID", kind:string, value:my_UUID}
end tell -- myNewBM
end tell
end tell
return (numBM + 1)
end New_BM
我使用这些例程在我的书签右侧添加、查看和更改书签。在您的情况下,您需要使用书签子菜单,然后您必须调整此代码,但主要概念是相同的。
为了方便起见,我建议您在子菜单中有书签时开始查看 plist 文件 (Library/Safari/Bookmarks.plist) 以查看其结构。
希望对您有所帮助!
tl;dr 有必要编辑 Safari 的 Bookmarks.plist
以编程方式创建书签。查看下面的 "Using a Python script" 部分。它需要在 Bash 脚本中使用 XSLT 样式表并通过您的 .py
文件调用它。实现这一目标所需的所有工具都内置于 macOS.
重要提示: 使用 macOS Mojave (10.14.x)+
您需要执行步骤 1 -10 在下面的 "MacOS Mojave Restrictions" 部分。这些更改允许修改 Bookmarks.plist
.
在继续之前创建一个 Bookmarks.plist
的副本,可以在 ~/Library/Safari/Bookmarks.plist
找到它。您可以运行以下命令将其复制到您的桌面:
cp ~/Library/Safari/Bookmarks.plist ~/Desktop/Bookmarks.plist
稍后恢复Bookmarks.plist
运行:
cp ~/Desktop/Bookmarks.plist ~/Library/Safari/Bookmarks.plist
属性 列表
MacOS 内置了 属性 List (.plist
) 相关的命令行工具,即 plutil
, and defaults
,它们有助于编辑通常包含平面数据的应用程序首选项结构。然而 Safari 的 Bookmarks.plist
具有深层嵌套结构,这两种工具都不擅长编辑。
正在将 .plist
文件转换为 XML
plutil
提供了一个 -convert
选项来将 .plist
从二进制转换为 XML。例如:
plutil -convert xml1 ~/Library/Safari/Bookmarks.plist
同样,以下命令转换为二进制:
plutil -convert binary1 ~/Library/Safari/Bookmarks.plist
转换为 XML 可以使用 XSLT,这是转换复杂 XML 结构的理想选择。
使用 XSLT 样式表
此自定义 XSLT 样式表转换 Bookmarks.plist
添加元素节点以创建书签:
template.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:output
method="xml"
indent="yes"
doctype-system="http://www.apple.com/DTDs/PropertyList-1.0.dtd"
doctype-public="-//Apple//DTD PLIST 1.0//EN"/>
<xsl:param name="bkmarks-folder"/>
<xsl:param name="bkmarks"/>
<xsl:param name="guid"/>
<xsl:param name="keep-existing" select="false" />
<xsl:variable name="bmCount">
<xsl:value-of select="string-length($bkmarks) -
string-length(translate($bkmarks, ',', '')) + 1"/>
</xsl:variable>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template name="getNthValue">
<xsl:param name="list"/>
<xsl:param name="n"/>
<xsl:param name="delimiter"/>
<xsl:choose>
<xsl:when test="$n = 1">
<xsl:value-of select=
"substring-before(concat($list, $delimiter), $delimiter)"/>
</xsl:when>
<xsl:when test="contains($list, $delimiter) and $n > 1">
<!-- recursive call -->
<xsl:call-template name="getNthValue">
<xsl:with-param name="list"
select="substring-after($list, $delimiter)"/>
<xsl:with-param name="n" select="$n - 1"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="createBmEntryFragment">
<xsl:param name="loopCount" select="1"/>
<xsl:variable name="bmInfo">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bkmarks"/>
<xsl:with-param name="delimiter" select="','"/>
<xsl:with-param name="n" select="$loopCount"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmkName">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="1"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmURL">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="2"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmGUID">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="3"/>
</xsl:call-template>
</xsl:variable>
<xsl:if test="$loopCount > 0">
<dict>
<key>ReadingListNonSync</key>
<dict>
<key>neverFetchMetadata</key>
<false/>
</dict>
<key>URIDictionary</key>
<dict>
<key>title</key>
<string>
<xsl:value-of select="$bmkName"/>
</string>
</dict>
<key>URLString</key>
<string>
<xsl:value-of select="$bmURL"/>
</string>
<key>WebBookmarkType</key>
<string>WebBookmarkTypeLeaf</string>
<key>WebBookmarkUUID</key>
<string>
<xsl:value-of select="$bmGUID"/>
</string>
</dict>
<!-- recursive call -->
<xsl:call-template name="createBmEntryFragment">
<xsl:with-param name="loopCount" select="$loopCount - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="createBmFolderFragment">
<dict>
<key>Children</key>
<array>
<xsl:call-template name="createBmEntryFragment">
<xsl:with-param name="loopCount" select="$bmCount"/>
</xsl:call-template>
<xsl:if test="$keep-existing = 'true'">
<xsl:copy-of select="./array/node()|@*"/>
</xsl:if>
</array>
<key>Title</key>
<string>
<xsl:value-of select="$bkmarks-folder"/>
</string>
<key>WebBookmarkType</key>
<string>WebBookmarkTypeList</string>
<key>WebBookmarkUUID</key>
<string>
<xsl:value-of select="$guid"/>
</string>
</dict>
</xsl:template>
<xsl:template match="dict[string[text()='BookmarksBar']]/array">
<array>
<xsl:for-each select="dict">
<xsl:choose>
<xsl:when test="string[text()=$bkmarks-folder]">
<xsl:call-template name="createBmFolderFragment"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
<xsl:if test="not(./dict/string[text()=$bkmarks-folder])">
<xsl:call-template name="createBmFolderFragment"/>
</xsl:if>
</array>
</xsl:template>
</xsl:stylesheet>
运行转型:
这 .xsl
需要指定每个所需书签属性的参数。
首先确保
Bookmarks.plits
是XML格式:plutil -convert xml1 ~/Library/Safari/Bookmarks.plist
利用内置函数
xsltproc
将template.xsl
应用于Bookmarks.plist
。首先,
cd
到template.xsl
所在的位置,运行这个复合命令:guid1=$(uuidgen) && guid2=$(uuidgen) && guid3=$(uuidgen) && xsltproc --novalid --stringparam bkmarks-folder "QUUX" --stringparam bkmarks "r/Android https://www.reddit.com/r/Android/ ${guid1},r/Apple https://www.reddit.com/r/Apple/ ${guid2}" --stringparam guid "$guid3" ./template.xsl - <~/Library/Safari/Bookmarks.plist > ~/Desktop/result-plist.xml
这会在您的
Desktop
上创建result-plist.xml
,其中包含一个名为QUUX
的新书签文件夹和两个新书签。让我们进一步了解上述复合命令中的各个部分:
uuidgen
生成新Bookmarks.plist
中所需的三个 UUID(一个用于文件夹,一个用于每个书签条目)。我们预先生成它们并将它们传递给 XSLT,因为:- XSLT 1.0 没有生成 UUID 的功能。
xsltproc
需要 XSLT 1.0
xsltproc
的--stringparam
选项表示自定义参数如下:--stringparam bkmarks-folder <value>
- 书签文件夹的名称。--stringparam bkmarks <value>
- 每个书签的属性。每个书签规范都用逗号分隔 (
,
)。每个定界字符串都有三个值;书签名称、URL 和 GUID。这 3 个值是 space 分隔的。--stringparam guid <value>
- 书签文件夹的 GUID。
最后部分:
./template.xsl - <~/Library/Safari/Bookmarks.plist > ~/Desktop/result-plist.xml
定义路径;
.xsl
、来源 XML 和目的地。
要评估刚刚发生的转换,请使用
diff
显示两个文件之间的差异。例如 运行:diff -yb --width 200 ~/Library/Safari/Bookmarks.plist ~/Desktop/result-plist.xml | less
然后多次按 F 键向前导航到每一页,直到您在两列中间看到
>
符号 - 它们表示新元素所在的位置已添加节点。按B键后退一页,输入Q退出diff.
使用 Bash 脚本。
我们现在可以在 Bash 脚本中使用前面提到的 .xsl
。
script.sh
#!/usr/bin/env bash
declare -r plist_path=~/Library/Safari/Bookmarks.plist
# ANSI/VT100 Control sequences for colored error log.
declare -r fmt_red='\x1b[31m'
declare -r fmt_norm='\x1b[0m'
declare -r fmt_green='\x1b[32m'
declare -r fmt_bg_black='\x1b[40m'
declare -r error_badge="${fmt_red}${fmt_bg_black}ERR!${fmt_norm}"
declare -r tick_symbol="${fmt_green}\xE2\x9C\x94${fmt_norm}"
if [ -z "" ] || [ -z "" ]; then
echo -e "${error_badge} Missing required arguments" >&2
exit 1
fi
bkmarks_folder_name=
bkmarks_spec=
keep_existing_bkmarks=${3:-false}
# Transform bookmark spec string into array using comma `,` as delimiter.
IFS=',' read -r -a bkmarks_spec <<< "${bkmarks_spec//, /,}"
# Append UUID/GUID to each bookmark spec element.
bkmarks_spec_with_uuid=()
while read -rd ''; do
[[ $REPLY ]] && bkmarks_spec_with_uuid+=("${REPLY} $(uuidgen)")
done < <(printf '%s[=19=]' "${bkmarks_spec[@]}")
# Transform bookmark spec array back to string using comma `,` as delimiter.
bkmarks_spec_str=$(printf '%s,' "${bkmarks_spec_with_uuid[@]}")
bkmarks_spec_str=${bkmarks_spec_str%,} # Omit trailing comma character.
# Check the .plist file exists.
if [ ! -f "$plist_path" ]; then
echo -e "${error_badge} File not found: ${plist_path}" >&2
exit 1
fi
# Verify that plist exists and contains no syntax errors.
if ! plutil -lint -s "$plist_path" >/dev/null; then
echo -e "${error_badge} Broken or missing plist: ${plist_path}" >&2
exit 1
fi
# Ignore ShellCheck errors regarding XSLT variable references in template below.
# shellcheck disable=SC2154
xslt() {
cat <<'EOX'
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:output
method="xml"
indent="yes"
doctype-system="http://www.apple.com/DTDs/PropertyList-1.0.dtd"
doctype-public="-//Apple//DTD PLIST 1.0//EN"/>
<xsl:param name="bkmarks-folder"/>
<xsl:param name="bkmarks"/>
<xsl:param name="guid"/>
<xsl:param name="keep-existing" select="false" />
<xsl:variable name="bmCount">
<xsl:value-of select="string-length($bkmarks) -
string-length(translate($bkmarks, ',', '')) + 1"/>
</xsl:variable>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template name="getNthValue">
<xsl:param name="list"/>
<xsl:param name="n"/>
<xsl:param name="delimiter"/>
<xsl:choose>
<xsl:when test="$n = 1">
<xsl:value-of select=
"substring-before(concat($list, $delimiter), $delimiter)"/>
</xsl:when>
<xsl:when test="contains($list, $delimiter) and $n > 1">
<!-- recursive call -->
<xsl:call-template name="getNthValue">
<xsl:with-param name="list"
select="substring-after($list, $delimiter)"/>
<xsl:with-param name="n" select="$n - 1"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="createBmEntryFragment">
<xsl:param name="loopCount" select="1"/>
<xsl:variable name="bmInfo">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bkmarks"/>
<xsl:with-param name="delimiter" select="','"/>
<xsl:with-param name="n" select="$loopCount"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmkName">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="1"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmURL">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="2"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="bmGUID">
<xsl:call-template name="getNthValue">
<xsl:with-param name="list" select="$bmInfo"/>
<xsl:with-param name="delimiter" select="' '"/>
<xsl:with-param name="n" select="3"/>
</xsl:call-template>
</xsl:variable>
<xsl:if test="$loopCount > 0">
<dict>
<key>ReadingListNonSync</key>
<dict>
<key>neverFetchMetadata</key>
<false/>
</dict>
<key>URIDictionary</key>
<dict>
<key>title</key>
<string>
<xsl:value-of select="$bmkName"/>
</string>
</dict>
<key>URLString</key>
<string>
<xsl:value-of select="$bmURL"/>
</string>
<key>WebBookmarkType</key>
<string>WebBookmarkTypeLeaf</string>
<key>WebBookmarkUUID</key>
<string>
<xsl:value-of select="$bmGUID"/>
</string>
</dict>
<!-- recursive call -->
<xsl:call-template name="createBmEntryFragment">
<xsl:with-param name="loopCount" select="$loopCount - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="createBmFolderFragment">
<dict>
<key>Children</key>
<array>
<xsl:call-template name="createBmEntryFragment">
<xsl:with-param name="loopCount" select="$bmCount"/>
</xsl:call-template>
<xsl:if test="$keep-existing = 'true'">
<xsl:copy-of select="./array/node()|@*"/>
</xsl:if>
</array>
<key>Title</key>
<string>
<xsl:value-of select="$bkmarks-folder"/>
</string>
<key>WebBookmarkType</key>
<string>WebBookmarkTypeList</string>
<key>WebBookmarkUUID</key>
<string>
<xsl:value-of select="$guid"/>
</string>
</dict>
</xsl:template>
<xsl:template match="dict[string[text()='BookmarksBar']]/array">
<array>
<xsl:for-each select="dict">
<xsl:choose>
<xsl:when test="string[text()=$bkmarks-folder]">
<xsl:call-template name="createBmFolderFragment"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="@*|node()" />
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
<xsl:if test="not(./dict/string[text()=$bkmarks-folder])">
<xsl:call-template name="createBmFolderFragment"/>
</xsl:if>
</array>
</xsl:template>
</xsl:stylesheet>
EOX
}
# Convert the .plist to XML format
plutil -convert xml1 -- "$plist_path" >/dev/null || {
echo -e "${error_badge} Cannot convert .plist to xml format" >&2
exit 1
}
# Generate a UUID/GUID for the folder.
folder_guid=$(uuidgen)
xsltproc --novalid \
--stringparam keep-existing "$keep_existing_bkmarks" \
--stringparam bkmarks-folder "$bkmarks_folder_name" \
--stringparam bkmarks "$bkmarks_spec_str" \
--stringparam guid "$folder_guid" \
<(xslt) - <"$plist_path" > "${TMPDIR}result-plist.xml"
# Convert the .plist to binary format
plutil -convert binary1 -- "${TMPDIR}result-plist.xml" >/dev/null || {
echo -e "${error_badge} Cannot convert .plist to binary format" >&2
exit 1
}
mv -- "${TMPDIR}result-plist.xml" "$plist_path" 2>/dev/null || {
echo -e "${error_badge} Cannot move .plist from TMPDIR to ${plist_path}" >&2
exit 1
}
echo -e "${tick_symbol} Successfully created Safari bookmarks."
说明
script.sh
提供以下功能:
- 简化的 API 将在通过 Python. 执行时有益
- 验证
.plist
没有损坏。 - 错误handling/logging.
- 使用
template.xsl
内联通过xsltproc
转换.plist
。 - 根据编号创建 GUID 以传递给 XSLT。给定参数中指定的书签数。
- 将
.plist
转换为 XML,然后返回二进制。 - 将新文件写入 OS 的 temp 文件夹,然后将其移动到
Bookmarks.plist
目录,有效地替换原始文件。
运行启用 shell 脚本
cd
到script.sh
所在的位置和 运行 以下chmod
命令使script.sh
可执行:chmod +ux script.sh
运行以下命令:
./script.sh "Whosebug" "bash https://whosebug.com/questions/tagged/bash,python https://whosebug.com/questions/tagged/python"
然后将以下内容打印到您的 CLI:
✔ Successfully created Safari bookmarks.
Safari 现在有一个名为
Whosebug
的书签文件夹,其中包含两个书签(bash
和python
)。
使用 Python 脚本
有几种方法可以通过 .py
文件执行 script.sh
。
方法 A:外部 shell 脚本
下面的 .py
文件执行外部 script.sh
文件。我们将文件命名为 create-safari-bookmarks.py
并将其保存在与 script.sh
.
create-safari-bookmarks.py
#!/usr/bin/env python
import subprocess
def run_script(folder_name, bkmarks):
subprocess.call(["./script.sh", folder_name, bkmarks])
def tuple_to_shell_arg(tup):
return ",".join("%s %s" % t for t in tup)
reddit_bkmarks = [
('r/Android', 'https://www.reddit.com/r/Android/'),
('r/Apple', 'https://www.reddit.com/r/Apple/'),
('r/Mac', 'https://www.reddit.com/r/Mac/'),
('r/ProgrammerHumor', 'https://www.reddit.com/r/ProgrammerHumor/'),
('r/gaming', 'https://www.reddit.com/r/gaming/')
]
so_bkmarks = [
('bash', 'https://whosebug.com/questions/tagged/bash'),
('python', 'https://whosebug.com/questions/tagged/python'),
('xslt', 'https://whosebug.com/questions/tagged/xslt'),
('xml', 'https://whosebug.com/questions/tagged/xml')
]
run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks))
run_script("Whosebug", tuple_to_shell_arg(so_bkmarks))
解释:
第一个
def
语句定义了一个run-script
函数。它有两个参数;folder_name
和bkmarks
。subprocess
模块call
方法本质上是使用所需参数执行script.sh
。第二个
def
语句定义了一个tuple_to_shell_arg
函数。它有一个参数tup
。 Stringjoin()
方法将元组列表转换为script.sh
所需的格式。它实质上转换了一个元组列表,例如:[ ('foo', 'https://www.foo.com/'), ('quux', 'https://www.quux.com') ]
和returns一个字符串:
foo https://www.foo.com/,quux https://www.quux.com
run_script
函数调用如下:run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks))
这传递了两个参数;
subreddit
(书签文件夹的名称),以及每个所需书签的规范(格式如前文第 2 点所述)。
运行宁create-safari-bookmarks.py
使
create-safari-bookmarks.py
可执行:chmod +ux ./create-safari-bookmarks.py
然后调用它:
./create-safari-bookmarks.py
方法 B:内联 shell 脚本
根据您的具体用例,您可能需要考虑在 .py
文件中内联 script.sh
,而不是调用外部 .sh
文件。我们将此文件命名为 create-safari-bookmarks-inlined.py
并将其保存到 create-safari-bookmarks.py
所在的同一目录中。
重要:
您需要将
script.sh
中的所有内容复制并粘贴到create-safari-bookmarks-inlined.py
中指定的位置。将其粘贴到
bash_script = """\
部分之后的下一行。create-safari-bookmarks-inlined.py
中的"""
部分应该在粘贴的script.sh
内容的最后一行之后单独一行。script.sh
的第 31 行在.py
中内联时必须使用另一个反斜杠转义'%s[=124=]'
部分([=125=]
是一个空字符) ,即script.sh
的第 31 行应如下所示:... done < <(printf '%s\0' "${bkmarks_spec[@]}") ^ ...
这一行可能在
create-safari-bookmarks-inlined.py
中的第 37 行。
create-safari-bookmarks-inlined.py
#!/usr/bin/env python
import tempfile
import subprocess
bash_script = """\
# <--- Copy and paste content of `script.sh` here and modify its line 31.
"""
def run_script(script, folder_name, bkmarks):
with tempfile.NamedTemporaryFile() as scriptfile:
scriptfile.write(script)
scriptfile.flush()
subprocess.call(["/bin/bash", scriptfile.name, folder_name, bkmarks])
def tuple_to_shell_arg(tup):
return ",".join("%s %s" % t for t in tup)
reddit_bkmarks = [
('r/Android', 'https://www.reddit.com/r/Android/'),
('r/Apple', 'https://www.reddit.com/r/Apple/'),
('r/Mac', 'https://www.reddit.com/r/Mac/'),
('r/ProgrammerHumor', 'https://www.reddit.com/r/ProgrammerHumor/'),
('r/gaming', 'https://www.reddit.com/r/gaming/')
]
so_bkmarks = [
('bash', 'https://whosebug.com/questions/tagged/bash'),
('python', 'https://whosebug.com/questions/tagged/python'),
('xslt', 'https://whosebug.com/questions/tagged/xslt'),
('xml', 'https://whosebug.com/questions/tagged/xml')
]
run_script(bash_script, "subreddit", tuple_to_shell_arg(reddit_bkmarks))
run_script(bash_script, "Whosebug", tuple_to_shell_arg(so_bkmarks))
说明
此文件与
create-safari-bookmarks.py
的结果相同。此修改后的
.py
脚本包括修改后的run_script
函数,该函数利用 Python 的tempfile
模块保存内联 shell 脚本到一个临时文件。Python的
subprocess
模块call
方法然后执行临时创建的shell文件。
运行宁create-safari-bookmarks-inlined.py
使
create-safari-bookmarks-inlined.py
可执行:chmod +ux ./create-safari-bookmarks-inlined.py
然后通过运行ning调用:
./create-safari-bookmarks-inlined.py
附加说明:将书签附加到现有文件夹
目前,每次上述 scripts/commands 再次 运行 时,我们实际上是在用一个全新的并创建指定的书签。
但是,如果您想将书签附加到现有文件夹,则 template.xsl
包括一个额外的 parameter/argument 以传递给它。请注意第 14 行的部分:
<xsl:param name="keep-existing" select="false" />
默认值为false
。因此,如果我们要将 run_script
函数更改为 create-safari-bookmarks.py
中的
def run_script(folder_name, bkmarks, keep_existing):
subprocess.call(["./script.sh", folder_name, bkmarks, keep_existing])
即添加名为 keep_existing
的第三个参数,并在 subprocess.call([...])
中包含对它的引用,即它作为第三个参数传递给 script.sh
( ...然后是 XSLT 样式表)。
然后我们可以调用 run_script
函数并传入一个额外的字符串参数,"true"
或 "false"
如下所示:
run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks), "true")
run_script("Whosebug", tuple_to_shell_arg(so_bkmarks), "false")
但是,进行上述更改(即传入 "true"
以保留现有书签)确实有可能导致创建重复的书签。例如;当我们有一个现有的书签(名称和 URL),然后在稍后重新提供相同的名称和 URL 时,将出现重复的书签。
限制: 目前,为书签提供的任何名称参数都不能包含 space 个字符,因为它们被脚本用作分隔符。
MacOS Mojave 限制
由于 mac 上更严格的安全策略OS Mojave (10.14.x) 默认情况下不允许访问 ~/Library/Safari/Bookmarks.plist
(如
因此,有必要授予Terminal.app,(或其他首选的CLI工具,如iTer), 访问整个磁盘。为此,您需要:
- Select Apple 菜单中的系统偏好设置。
- 在系统偏好设置 window中单击安全与策略图标。
- 在安全与策略面板中点击隐私选项卡。
- 在left-hand列中选择全盘访问。
- 单击左下角的锁定图标以允许更改。
- 输入管理员密码,然后点击解锁按钮。
- 接下来单击加号图标 (+)。
- 选择位于
/Applications/Utilities/
的Terminal.app,然后点击打开按钮。 - Terminal.app将添加到列表中。
- 单击锁定图标以防止任何进一步的更改,然后退出 系统偏好设置。