<>强>太长了,读不下去了,需要编辑Safari的^ {CD1>}以编程方式创建书签。查看下面的<em>“使用Python脚本”</em>部分。它需要在Bash脚本中使用XSLT样式表,并通过<code>.py</code>文件调用它。实现这一点所需的所有工具都内置在macOS上。在</p>
<p><strong>重要提示:</strong><em>使用macOS Mojave(10.14.x)<code>+</code>您需要执行下面“macOS Mojave限制”部分中的步骤1-10。这些更改允许对<code>Bookmarks.plist</code>进行修改。</em></p>
<p>在继续之前,请创建<code>Bookmarks.plist</code>的副本,该副本位于<code>~/Library/Safari/Bookmarks.plist</code>。您可以运行以下命令将其复制到您的<em>桌面</em>:</p>
<pre class="lang-none prettyprint-override"><code>cp ~/Library/Safari/Bookmarks.plist ~/Desktop/Bookmarks.plist
</code></pre>
<p>要在以后还原<code>Bookmarks.plist</code>,请运行:</p>
^{pr2}$
<hr/>
<h2>属性列表</h2>
<p>MacOS有内置的属性列表(<code>.plist</code>)相关的命令行工具,即<a href="https://www.manpagez.com/man/1/plutil/" rel="nofollow noreferrer">^{<cd9>}</a>,和<a href="https://ss64.com/osx/defaults.html" rel="nofollow noreferrer">^{<cd10>}</a>,它们有助于编辑通常包含平面数据结构的应用程序首选项。然而,Safari的<code>Bookmarks.plist</code>有一个深度嵌套的结构,这两个工具都不擅长编辑。在</p>
<p><strong>将<code>.plist</code>文件转换为XML</strong></p>
<p><code>plutil</code>提供了一个<code>-convert</code>选项来将<code>.plist</code>从二进制转换为XML。例如:</p>
^{3}$
<p>类似地,以下命令将转换为二进制:</p>
<pre class="lang-none prettyprint-override"><code>plutil -convert binary1 ~/Library/Safari/Bookmarks.plist
</code></pre>
<p>转换为XML可以使用<a href="https://developer.mozilla.org/en-US/docs/Web/XSLT" rel="nofollow noreferrer">XSLT</a>,这是转换复杂XML结构的理想选择。在</p>
<hr/>
<h2>使用XSLT样式表</h2>
<p>此自定义XSLT样式表转换<code>Bookmarks.plist</code>添加元素节点以创建书签:</p>
<p><strong>模板.xsl</strong></p>
<pre class="lang-xml prettyprint-override"><code><?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>
</code></pre>
<h2>运行转换:</h2>
<p>此<code>.xsl</code>需要指定每个所需书签属性的参数。在</p>
<ol>
<li><p>首先确保<code>Bookmarks.plits</code>是XML格式的:</p>
<pre class="lang-none prettyprint-override"><code>plutil -convert xml1 ~/Library/Safari/Bookmarks.plist
</code></pre></li>
<li><p>利用内置的<a href="http://xmlsoft.org/XSLT/xsltproc.html" rel="nofollow noreferrer">^{<cd19>}</a>将<code>template.xsl</code>应用到<code>Bookmarks.plist</code>。在</p>
<p>首先,<a href="https://ss64.com/osx/cd.html" rel="nofollow noreferrer">^{<cd22>}</a>到<code>template.xsl</code>所在的位置,然后运行以下复合命令:</p>
<pre class="lang-none prettyprint-override"><code>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
</code></pre>
<p>这将在您的<code>Desktop</code>上创建<code>result-plist.xml</code>,其中包含一个名为<code>QUUX</code>的新书签文件夹,其中包含两个新书签。</p></li>
<li><p>让我们进一步了解上述复合命令中的每个部分:</p>
<ul>
<li><p><a href="https://ss64.com/osx/uuidgen.html" rel="nofollow noreferrer">^{<cd27>}</a>生成新的<code>Bookmarks.plist</code>中需要的三个UUID(一个用于文件夹,一个用于每个书签条目)。我们预先生成它们并将它们传递给XSLT,因为:</p>
<ul>
<li>XSLT1.0没有生成UUID的功能。在</li>
<li><code>xsltproc</code>需要XSLT1.0</li>
</ul></li>
<li><p><code>xsltproc</code>的<code> stringparam</code>选项表示如下自定义参数:</p>
<ul>
<li><code> stringparam bkmarks-folder <value></code>-书签文件夹的名称。在</li>
<li><p>{cd33}每个书签的属性。在</p>
<p>每个书签规范都用逗号(<code>,</code>)分隔。每个分隔字符串有三个值:书签名称、URL和GUID。这3个值以空格分隔。</p></li>
<li><p><code> stringparam guid <value></code>-书签文件夹的GUID。</p></li>
</ul></li>
<li><p>最后一部分:</p>
<pre class="lang-none prettyprint-override"><code>./template.xsl - <~/Library/Safari/Bookmarks.plist > ~/Desktop/result-plist.xml
</code></pre>
<p>定义到的路径;<code>.xsl</code>、源XML和目标。</p></li>
</ul></li>
<li><p>要计算刚刚发生的转换,请使用<a href="https://ss64.com/osx/diff.html" rel="nofollow noreferrer">^{<cd37>}</a>来显示两个文件之间的差异。例如运行:</p>
<pre class="lang-none prettyprint-override"><code>diff -yb width 200 ~/Library/Safari/Bookmarks.plist ~/Desktop/result-plist.xml | less
</code></pre>
<p>然后按几次<kbd>F</kbd>键向前导航到每一页,直到在两列中间看到<code>></code>符号-它们指示添加新元素节点的位置。按<kbd>B</kbd>键向后移动一页,然后键入<kbd>Q</kbd>退出diff.</p></li>
</ol>
<hr/>
<h2>使用Bash脚本</h2>
<p>我们现在可以在Bash脚本中使用前面提到的<code>.xsl</code>。在</p>
<p><strong>脚本.sh</strong></p>
<pre class="lang-sh prettyprint-override"><code>#!/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 "$1" ] || [ -z "$2" ]; then
echo -e "${error_badge} Missing required arguments" >&2
exit 1
fi
bkmarks_folder_name=$1
bkmarks_spec=$2
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\0' "${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."
</code></pre>
<h2>解释</h2>
<p><code>script.sh</code>提供以下功能:</p>
<ol>
<li>简化的API在通过Python执行时非常有用。在</li>
<li>验证<code>.plist</code>未损坏。在</li>
<li>错误处理记录/记录。在</li>
<li>使用<code>template.xsl</code>内联,通过<code>xsltproc</code>转换{<cd8>}。在</li>
<li>根据给定参数中指定的书签数创建用于传递给XSLT的GUID。在</li>
<li>将<code>.plist</code>转换为XML,然后再转换回二进制。在</li>
<li>将新文件写入操作系统的<em>temp</em>文件夹,然后将其移动到<code>Bookmarks.plist</code>目录,有效地替换原始文件。在</li>
</ol>
<h2>运行shell脚本</h2>
<ol>
<li><p><code>cd</code>到<code>script.sh</code>所在的位置,并运行以下<a href="https://ss64.com/osx/chmod.html" rel="nofollow noreferrer">^{<cd49>}</a>命令使<code>script.sh</code>可执行:</p>
<pre class="lang-none prettyprint-override"><code>chmod +ux script.sh
</code></pre></li>
<li><p>运行以下命令:</p>
<pre class="lang-none prettyprint-override"><code>./script.sh "stackOverflow" "bash https://stackoverflow.com/questions/tagged/bash,python https://stackoverflow.com/questions/tagged/python"
</code></pre>
<p>然后将以下内容打印到CLI:</p>
<blockquote>
<p><code>✔ Successfully created Safari bookmarks.</code></p>
</blockquote>
<p>Safari现在有一个名为<code>stackOverflow</code>的书签文件夹,其中包含两个书签(<code>bash</code>和<code>python</code>)。</p></li>
</ol>
<hr/>
<h2>使用Python脚本</h2>
<p>有两种方法可以通过<code>.py</code>文件执行<code>script.sh</code>。在</p>
<h2>方法A:外部shell脚本</h2>
<p>下面的<code>.py</code>文件执行外部<code>script.sh</code>文件。让我们将文件命名为<code>create-safari-bookmarks.py</code>,并将其与<code>script.sh</code>保存在同一个文件夹中。在</p>
<p><strong>创建safari-书签.py</strong></p>
<pre class="lang-py prettyprint-override"><code>#!/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://stackoverflow.com/questions/tagged/bash'),
('python', 'https://stackoverflow.com/questions/tagged/python'),
('xslt', 'https://stackoverflow.com/questions/tagged/xslt'),
('xml', 'https://stackoverflow.com/questions/tagged/xml')
]
run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks))
run_script("stackOverflow", tuple_to_shell_arg(so_bkmarks))
</code></pre>
<p><strong>说明:</strong></p>
<ol>
<li><p>第一个<code>def</code>语句定义了一个<code>run-script</code>函数。它有两个参数:<code>folder_name</code>和<code>bkmarks</code>。<code>subprocess</code>模块<code>call</code>方法本质上使用所需参数执行<code>script.sh</code>。</p></li>
<li><p>第二条<code>def</code>语句定义了一个<code>tuple_to_shell_arg</code>函数。它有一个参数<code>tup</code>。String<code>join()</code>方法将元组列表转换为<code>script.sh</code>所需的格式。它本质上转换元组列表,例如:</p>
<pre class="lang-py prettyprint-override"><code>[
('foo', 'https://www.foo.com/'),
('quux', 'https://www.quux.com')
]
</code></pre>
<p>并返回一个字符串:</p>
<pre class="lang-none prettyprint-override"><code>foo https://www.foo.com/,quux https://www.quux.com
</code></pre></li>
<li><p>按如下方式调用<code>run_script</code>函数:</p>
<pre class="lang-py prettyprint-override"><code>run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks))
</code></pre>
<p>这将传递两个参数;<code>subreddit</code>(bookmarks文件夹的名称)和每个所需书签的规范(格式如前面第2点所述)。</p></li>
</ol>
<p><strong>正在运行<code>create-safari-bookmarks.py</code></strong></p>
<ol>
<li><p>使<code>create-safari-bookmarks.py</code>可执行:</p>
<pre class="lang-none prettyprint-override"><code>chmod +ux ./create-safari-bookmarks.py
</code></pre></li>
<li><p>然后调用它:</p>
<pre class="lang-none prettyprint-override"><code>./create-safari-bookmarks.py
</code></pre></li>
</ol>
<h2>方法B:内联shell脚本</h2>
<p>根据具体的用例,您可能需要考虑在<code>.py</code>文件中内联<code>script.sh</code>,而不是调用外部<code>.sh</code>文件。让我们将这个文件命名为<code>create-safari-bookmarks-inlined.py</code>,并将其保存到<code>create-safari-bookmarks.py</code>所在的同一个目录中。在</p>
<p><strong>重要:</strong></p>
<ul>
<li><p>您需要将<code>script.sh</code>中的所有内容复制并粘贴到<code>create-safari-bookmarks-inlined.py</code>中指定的位置。</p></li>
<li><p>将其粘贴到<code>bash_script = """\</code>部分后面的下一行。</p></li>
<li><code>create-safari-bookmarks-inlined.py</code>中的<code>"""</code>部分应该在粘贴的<code>script.sh</code>内容的最后一行后面的自己的行上。在</li>
<li><p>当在<code>.py</code>中内联时,<code>script.sh</code>的第31行必须使用另一个反斜杠转义<code>'%s\0'</code>部分(<code>\0</code>是空字符),即{<cd40>}的第31行应该如下所示:</p>
<pre class="lang-none prettyprint-override"><code>...
done < <(printf '%s\\0' "${bkmarks_spec[@]}")
^
...
</code></pre>
<p>这一行可能在<code>create-safari-bookmarks-inlined.py</code>中的第37行。</p></li>
</ul>
<p><strong>创建safari书签-内联.py</strong></p>
<pre class="lang-py prettyprint-override"><code>#!/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://stackoverflow.com/questions/tagged/bash'),
('python', 'https://stackoverflow.com/questions/tagged/python'),
('xslt', 'https://stackoverflow.com/questions/tagged/xslt'),
('xml', 'https://stackoverflow.com/questions/tagged/xml')
]
run_script(bash_script, "subreddit", tuple_to_shell_arg(reddit_bkmarks))
run_script(bash_script, "stackOverflow", tuple_to_shell_arg(so_bkmarks))
</code></pre>
<p><strong>说明</strong></p>
<ol>
<li><p>此文件的结果与<code>create-safari-bookmarks.py</code>相同。</p></li>
<li><p>这个修改过的<code>.py</code>脚本包含一个修改后的<code>run_script</code>函数,该函数利用Python的<code>tempfile</code>模块将内联shell脚本保存到临时文件中。</p></li>
<li><p>Python的<code>subprocess</code>模块<code>call</code>方法随后执行临时创建的shell文件。</p></li>
</ol>
<p><strong>正在运行<code>create-safari-bookmarks-inlined.py</code></strong></p>
<ol>
<li><p>使<code>create-safari-bookmarks-inlined.py</code>可执行:</p>
<pre class="lang-none prettyprint-override"><code>chmod +ux ./create-safari-bookmarks-inlined.py
</code></pre></li>
<li><p>然后通过运行以下命令调用它:</p>
<pre class="lang-none prettyprint-override"><code>./create-safari-bookmarks-inlined.py
</code></pre></li>
</ol>
<hr/>
<p><strong>附加说明:将书签附加到现有文件夹</strong></p>
<p>目前,每次再次运行上述脚本/命令时,我们都在有效地替换任何现有的命名Safari bookmark folder(它与给定的bookmark folder同名),使用一个全新的bookmark folder创建指定的书签。在</p>
<p>但是,如果您想将书签附加到现有文件夹,则<code>template.xsl</code>包含一个要传递给它的附加参数/参数。请注意第14行的部分内容:</p>
<pre class="lang-xml prettyprint-override"><code><xsl:param name="keep-existing" select="false" />
</code></pre>
<p>它的默认值是<code>false</code>。所以,如果我们要将<code>run_script</code>中的<code>run_script</code>函数改为如下。在</p>
<pre><code>def run_script(folder_name, bkmarks, keep_existing):
subprocess.call(["./script.sh", folder_name, bkmarks, keep_existing])
</code></pre>
<p>也就是说,添加一个名为<code>keep_existing</code>的第三个参数,并在<code>subprocess.call([...])</code>中包含对它的引用,即将其作为第三个参数传递给<code>script.sh</code>(…并随后传递到XSLT样式表)。在</p>
<p>{{cd110>然后我们可以调用一个额外的{cd110}函数<cd109},这样我们就可以调用一个<cd110}函数了:</p>
<pre class="lang-py prettyprint-override"><code>run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks), "true")
run_script("stackOverflow", tuple_to_shell_arg(so_bkmarks), "false")
</code></pre>
<p>但是,进行上述更改(即传入<code>"true"</code>以保留现有书签),确实有可能导致创建重复的书签。例如,当我们有一个退出的书签(名称和URL)时,就会出现重复的书签,然后在以后用相同的名称和URL重新提供该书签。在</p>
<p><strong>限制:</strong>当前为书签提供的任何名称参数都不能包含空格字符,因为它们被脚本用作分隔符。在</p>
<hr/>
<h2>马科斯莫哈韦限制</h2>
<p>由于macOS上更严格的安全策略,默认情况下不允许访问<code>~/Library/Safari/Bookmarks.plist</code>(如<a href="https://stackoverflow.com/questions/52657293/access-safari-bookmarks-in-macos-mojave-programmatically">this answer</a>中所述)。在</p>
<p>因此,有必要授予终端.app</em>(或其他首选的CLI工具,如<em>iTerm</em>)访问整个磁盘。为此,您需要:</p>
<ol>
<li>从Apple菜单中选择<em>系统首选项</em>。在</li>
<li>在<em>系统首选项</em>窗口中,单击<em>安全和策略</em>图标。在</li>
<li>在<em>安全和策略</em>窗格中,单击<em>隐私</em>选项卡。在</li>
<li>在左侧栏中选择<em>完整磁盘访问</em>。在</li>
<li>单击左下角的锁定图标以允许更改。在</li>
<li>输入管理员密码,然后单击<em>解锁</em>按钮。在</li>
<li>接下来单击加号图标(<kbd>+</kbd>)。在</li>
<li>选择<em>终端.app</em>,可位于<code>/Applications/Utilities/</code>,然后单击<em>打开</em>按钮。在</li>
<li><em>终端.app</em>将被添加到列表中。在</li>
<li>单击锁定图标以阻止任何进一步的更改,然后退出<em>系统首选项</em>。在</li>
</ol>
<p/>
<p>{a10}</p>