如何以编程方式创建和管理macOS Safari书签?

时间:2019-06-09 18:41:42

标签: python macos safari automation applescript

我正在制作一个脚本,该脚本更新macOS Safari上的书签,以始终将所有订阅的子reddit作为单个书签保存在特定文件夹中。我已经到了一个点,在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网址:

Bookmarks folder

2 个答案:

答案 0 :(得分:1)

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

希望对您有帮助!

答案 1 :(得分: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
    

    这将在您的result-plist.xml上创建Desktop,其中包含一个名为QUUX的新书签文件夹,其中包含两个新书签。

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

    • uuidgen生成新Bookmarks.plist中所需的三个UUID(一个用于文件夹,一个用于每个书签条目)。我们先生成它们,然后将它们传递给XSLT,因为:

      • XSLT 1.0没有用于生成UUID的功能。
      • xsltproc需要XSLT 1.0
    • xsltproc的{​​{1}}选项表示自定义参数,如下所示:

      • --stringparam-书签文件夹的名称。
      • --stringparam bkmarks-folder <value>-每个书签的属性。

        每个书签规范都以逗号(--stringparam bkmarks <value>)分隔。每个定界字符串具有三个值;书签的名称,URL和GUID。这3个值用空格分隔。

      • ,-书签文件夹的GUID。

    • 最后一部分:

      --stringparam guid <value>

      定义路径; ./template.xsl - <~/Library/Safari/Bookmarks.plist > ~/Desktop/result-plist.xml ,源XML和目标。

  4. 要评估刚刚发生的转换,请使用diff显示两个文件之间的差异。例如运行:

    .xsl

    然后几次按 F 键向前导航到每个页面,直到两列中间看到diff -yb --width 200 ~/Library/Safari/Bookmarks.plist ~/Desktop/result-plist.xml | less 符号-它们指示在何处添加了新的元素节点。按 B 键返回上一页,然后键入 Q 退出差异。


使用Bash脚本。

我们现在可以在Bash脚本中使用上述>

script.sh

.xsl

说明

#!/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." 提供以下功能:

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

运行shell脚本

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

    script.sh
  2. 运行以下命令:

    chmod +ux script.sh
    

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

      

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

    Safari现在有一个名为✔ Successfully created Safari bookmarks.的书签文件夹,其中包含两个书签(stackOverflowbash)。


使用Python脚本

有几种方法可以通过您的python文件执行script.sh

方法A:外部Shell脚本

以下.py文件执行外部.py文件。我们将文件命名为script.sh并将其保存在create-safari-bookmarks.py所在的文件夹中。

create-safari-bookmarks.py

script.sh

说明:

  1. 第一个#!/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)) 语句定义一个def函数。它有两个参数; run-scriptfolder_namebkmarks模块subprocess方法实际上是使用必需的参数执行call

  2. 第二条script.sh语句定义一个def函数。它具有一个参数tuple_to_shell_arg。字符串tup方法将元组列表转换为join()所需的格式。本质上,它转换了一个元组列表,例如:

    script.sh

    并返回一个字符串:

    [
        ('foo', 'https://www.foo.com/'),
        ('quux', 'https://www.quux.com')
    ]
    
  3. foo https://www.foo.com/,quux https://www.quux.com 函数的调用方式如下:

    run_script

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

运行subreddit

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

    create-safari-bookmarks.py
  2. 然后使用以下命令调用它:

    chmod +ux ./create-safari-bookmarks.py
    

方法B:内嵌shell脚本

根据您的确切用例,您可能需要考虑在./create-safari-bookmarks.py 文件中内联script.sh而不是调用外部.py文件。我们将此文件命名为.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'是空字符),即第31行\0中的应当显示为:

    script.sh

    此行可能位于... done < <(printf '%s\\0' "${bkmarks_spec[@]}") ^ ... 中的第37行。

create-safari-bookmarks-inlined.py

create-safari-bookmarks-inlined.py

说明

  1. 此文件可获得与#!/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)) 相同的结果。

  2. 此经过修改的create-safari-bookmarks.py脚本包括经过修改的.py函数,该函数利用Python的run_script模块将内嵌shell脚本保存到临时文件中。

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

运行call

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

    create-safari-bookmarks-inlined.py
  2. 然后通过运行以下命令来调用它:

    chmod +ux ./create-safari-bookmarks-inlined.py
    

其他说明:将书签添加到现有文件夹

当前,每次再次运行上述脚本/命令,我们都将用一个全新的文件夹有效地替换所有现有的命名Safari书签文件夹(其名称与给定的书签文件夹名称相同),并创建指定的书签

但是,如果您想将书签添加到现有文件夹中,则./create-safari-bookmarks-inlined.py 包括一个要传递给它的附加参数/参数。注意第14行的内容为:

template.xsl

其默认值为<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([...])(。 。然后是XSLT样式表。

然后我们可以调用script.sh函数并传递一个附加的String参数,run_script"true"如下:

"false"

但是,进行上述更改(即传递run_script('subreddit', tuple_to_shell_arg(reddit_bkmarks), "true") run_script("stackOverflow", tuple_to_shell_arg(so_bkmarks), "false") 来保留现有书签)确实有可能导致创建重复的书签。例如;当我们有一个退出的书签(名称和URL)时,将出现重复的书签,然后在以后再次提供相同的名称和URL。

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


MacOS Mojave限制

由于macOS Mojave (10.14.x)上的安全政策更加严格,因此默认情况下不允许访问"true"(如this answer中所述)。

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

  1. 从Apple菜单中选择系统偏好设置
  2. 系统偏好设置窗口中,单击安全和策略图标。
  3. 安全和策略窗格中,单击隐私标签。
  4. 在左侧列中选择完全磁盘访问
  5. 单击左下角的锁定图标以允许更改。
  6. 输入管理员密码,然后单击解锁按钮。
  7. 然后单击加号图标( + )。
  8. 选择位于~/Library/Safari/Bookmarks.plist Terminal.app ,然后单击 Open 按钮。
  9. Terminal.app 将添加到列表中。
  10. 单击锁定图标以防止进一步更改,然后退出系统偏好设置

screenshot