如何以编程方式创建和管理 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 需要指定每个所需书签属性的参数。

  1. 首先确保Bookmarks.plits是XML格式:

    plutil -convert xml1 ~/Library/Safari/Bookmarks.plist
    
  2. 利用内置函数 xsltproctemplate.xsl 应用于 Bookmarks.plist

    首先,cdtemplate.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 的新书签文件夹和两个新书签。

  3. 让我们进一步了解上述复合命令中的各个部分:

    • 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 和目的地。

  4. 要评估刚刚发生的转换,请使用 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 提供以下功能:

  1. 简化的 API 将在通过 Python.
  2. 执行时有益
  3. 验证 .plist 没有损坏。
  4. 错误handling/logging.
  5. 使用 template.xsl 内联通过 xsltproc 转换 .plist
  6. 根据编号创建 GUID 以传递给 XSLT。给定参数中指定的书签数。
  7. .plist 转换为 XML,然后返回二进制。
  8. 将新文件写入 OS 的 temp 文件夹,然后将其移动到 Bookmarks.plist 目录,有效地替换原始文件。

运行启用 shell 脚本

  1. cdscript.sh 所在的位置和 运行 以下 chmod 命令使 script.sh 可执行:

    chmod +ux script.sh
    
  2. 运行以下命令:

    ./script.sh "Whosebug" "bash https://whosebug.com/questions/tagged/bash,python https://whosebug.com/questions/tagged/python"
    

    然后将以下内容打印到您的 CLI:

    ✔ Successfully created Safari bookmarks.

    Safari 现在有一个名为 Whosebug 的书签文件夹,其中包含两个书签(bashpython)。


使用 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))

解释:

  1. 第一个 def 语句定义了一个 run-script 函数。它有两个参数; folder_namebkmarkssubprocess 模块 call 方法本质上是使用所需参数执行 script.sh

  2. 第二个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
    
  3. run_script函数调用如下:

    run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks))
    

    这传递了两个参数; subreddit(书签文件夹的名称),以及每个所需书签的规范(格式如前文第 2 点所述)。

运行宁create-safari-bookmarks.py

  1. 使 create-safari-bookmarks.py 可执行:

    chmod +ux ./create-safari-bookmarks.py
    
  2. 然后调用它:

    ./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))

说明

  1. 此文件与create-safari-bookmarks.py的结果相同。

  2. 此修改后的 .py 脚本包括修改后的 run_script 函数,该函数利用 Python 的 tempfile 模块保存内联 shell 脚本到一个临时文件。

  3. Python的subprocess模块call方法然后执行临时创建的shell文件。

运行宁create-safari-bookmarks-inlined.py

  1. 使 create-safari-bookmarks-inlined.py 可执行:

    chmod +ux ./create-safari-bookmarks-inlined.py
    
  2. 然后通过运行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), 访问整个磁盘。为此,您需要:

  1. Select Apple 菜单中的系统偏好设置
  2. 系统偏好设置 window中单击安全与策略图标。
  3. 安全与策略面板中点击隐私选项卡。
  4. 在left-hand列中选择全盘访问
  5. 单击左下角的锁定图标以允许更改。
  6. 输入管理员密码,然后点击解锁按钮。
  7. 接下来单击加号图标 (+)。
  8. 选择位于/Applications/Utilities/Terminal.app,然后点击打开按钮。
  9. Terminal.app将添加到列表中。
  10. 单击锁定图标以防止任何进一步的更改,然后退出 系统偏好设置