【问题标题】:How to create and manage macOS Safari bookmarks programmatically?如何以编程方式创建和管理 macOS Safari 书签?
【发布时间】:2019-10-24 07:46:03
【问题描述】:

我正在编写一个脚本,它会更新我的 macOS Safari 上的书签,以始终将我所有订阅的子版块作为单独的书签保存在特定文件夹中。我已经到了一个地步,在 Python 中,我将所有 subreddits 作为元组的排序列表,所需的书签名称作为第一个元素,书签 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:

【问题讨论】:

    标签: python macos safari automation applescript


    【解决方案1】:

    tl;dr 有必要编辑 Safari 的 Bookmarks.plist 以编程方式创建书签。查看下面的“使用 Python 脚本” 部分。它需要在 Bash 脚本中使用 XSLT 样式表并通过您的 .py 文件调用它。实现这一点所需的所有工具都内置在 macOS 上。

    重要提示: 使用 macOS Mojave (10.14.x)+ 您需要执行下面“MacOS Mojave 限制”部分中的步骤 1-10。这些更改允许修改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 内置了与属性列表 (.plist) 相关的命令行工具,即 plutildefaults,它们有助于编辑通常包含平面数据结构的应用程序首选项。但是 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 &lt;value&gt; - 书签文件夹的名称。
        • --stringparam bkmarks &lt;value&gt; - 每个书签的属性。

          每个书签规范都用逗号 (,) 分隔。每个分隔字符串具有三个值;书签的名称、URL 和 GUID。这 3 个值是用空格分隔的。

        • --stringparam guid &lt;value&gt; - 书签文件夹的 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 键向前导航到每一页,直到您在两列中间看到&gt; 符号——它们表示新元素节点的添加位置。按 B 键后退一页,然后键入 Q 退出差异。


    使用 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 "$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."
    

    说明

    script.sh 提供以下功能:

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

    运行 shell 脚本

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

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

      ./script.sh "stackOverflow" "bash https://stackoverflow.com/questions/tagged/bash,python https://stackoverflow.com/questions/tagged/python"
      

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

      ✔ Successfully created Safari bookmarks.

      Safari 现在有一个名为 stackOverflow 的书签文件夹,其中包含两个书签(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://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))
    

    说明:

    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')
      ]
      

      并返回一个字符串:

      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\0' 部分(\0 是一个空字符)用另一个反斜杠转义,即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://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))
    

    说明

    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. 然后通过运行调用它:

      ./create-safari-bookmarks-inlined.py
      

    附加说明:将书签附加到现有文件夹

    目前,每次再次运行上述脚本/命令时,我们都会有效地将任何现有的命名 Safari 书签文件夹(与给定的书签文件夹名称相同)替换为全新的并创建指定的书签.

    但是,如果您想将书签附加到现有文件夹,则 template.xsl 包含一个要传递给它的附加参数/参数。请注意第 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("stackOverflow", tuple_to_shell_arg(so_bkmarks), "false")
    

    但是,进行上述更改(即传递 "true" 以保留现有书签)确实有可能导致创建重复的书签。例如;当我们有一个现有的书签(名称和 URL)时,会出现重复的书签,然后会在以后重新提供相同的名称和 URL。

    限制:目前为书签提供的任何名称参数都不能包含空格字符,因为它们被脚本用作分隔符。


    MacOS Mojave 限制

    由于 macOS Mojave (10.14.x) 上更严格的安全政策,默认情况下不允许访问 ~/Library/Safari/Bookmarks.plist(如 this answer 中所述)。

    因此,有必要授予 Terminal.app(或其他首选 CLI 工具,例如 iTerm)访问整个磁盘。为此,您需要:

    1. 从 Apple 菜单中选择 系统偏好设置
    2. System Preferences 窗口中单击 Security & Policy 图标。
    3. Security & Policy 窗格中,单击 Privacy 选项卡。
    4. 在左侧栏中选择全盘访问
    5. 单击左下角的锁定图标以允许更改。
    6. 输入管理员密码,然后点击解锁按钮。
    7. 接下来单击加号图标 (+)。
    8. 选择Terminal.app,它位于/Applications/Utilities/,然后点击打开按钮。
    9. Terminal.app 将被添加到列表中。
    10. 单击锁定图标以防止进一步更改,然后退出系统偏好设置

    【讨论】:

      【解决方案2】:

      我从未在 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 个子程序来管理书签。第一个检查书签是否存在

      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) 以查看其结构。

      希望对你有帮助!

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-12-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-06-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多