I'm making a script, which updates bookmarks on my macOS Safari to always have all of my subscribed subreddits as individual bookmarks in a specific folder. I've gotten to a point where I have all the subreddits as a sorted list of tuples in Python, with the wanted bookmark name as the first element and the bookmark url as the second element:
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/')
]
How could I clear my subreddit bookmarks folder in Safari and create these new bookmarks in that folder?
I've used Python till this point, but calling an external AppleScript or Shell script from the Python program would be no problem.
Here's an image of the wanted result, every bookmark links to their respective subreddit url:
tl;dr It's necessary to edit Safari's
Bookmarks.plist
to create bookmarks programmatically. Checkout the "Using a Python script" section below. It entails utilizing a XSLT stylesheet in a Bash script and invoking it via your.py
file. All tooling required to achieve this are builtin on macOS.Important: Using macOS Mojave (10.14.x)
+
you need to perform steps 1-10 in the "MacOS Mojave Restrictions" section below. Those changes permit modifications toBookmarks.plist
.Before proceeding create a copy of
Bookmarks.plist
which can be found at~/Library/Safari/Bookmarks.plist
. You can run the following command to copy it to your Desktop:To restore
Bookmarks.plist
later run:Property Lists
MacOS has builtin Property List (
.plist
) related command line tools, namelyplutil
, anddefaults
, which lend themselves to editing application preferences that typically contain flat data structures. However Safari'sBookmarks.plist
has a deeply nested structure, which neither of these tools are good at editing.Tranforming
.plist
files to XMLplutil
provides a-convert
option to transform.plist
from binary to XML. For instance:Similarily, the following command transforms to binary:
Converting to XML enables use of XSLT which is ideal for transforming complex XML structures.
Using a XSLT Stylesheet
This custom XSLT stylesheet transforms
Bookmarks.plist
adding element nodes to create bookmarks:template.xsl
Running a transformation:
This
.xsl
requires parameters that specify the properties of each required bookmark.Firstly ensure that
Bookmarks.plits
is XML formatted:Utilize the builtin
xsltproc
to applytemplate.xsl
toBookmarks.plist
.Firstly,
cd
to wheretemplate.xsl
resides, and run this compound command:This creates
result-plist.xml
on yourDesktop
containing a new bookmarks folder namedQUUX
with two new bookmarks.Let's further understand each part in the aforementioned compound command:
uuidgen
generates three UUID's that are required in the newBookmarks.plist
(one for the folder, and one for each bookmark entry). We generate them upfront and pass them to the XSLT because:xsltproc
requires XSLT 1.0xsltproc
's--stringparam
option denotes custom arguments as follows:--stringparam bkmarks-folder <value>
- Name of the bookmark folder.--stringparam bkmarks <value>
- Properties for each bookmark.Each bookmark spec is delimited with a comma (
,
). Each delimited string has three values; the name of bookmark, the URL, and GUID. These 3 values are space delimited.--stringparam guid <value>
- GUID for the bookmark folder.The last parts:
define paths to; the
.xsl
, source XML, and destination.To evaluate the transformation that just occurred utilize
diff
to display differences between the two files. For instance run:Then press the F key several times to navigate forward to each page until you see
>
symbols in the middle of the two columns - they indicate where new element nodes have been addede. Press the B key to move back a page, and type Q to exit diff.Using a Bash script.
We can now utilize the aforementioned
.xsl
in a Bash script.script.sh
Explanation
script.sh
provides the following features:.plist
is not broken..plist
viaxsltproc
usingtemplate.xsl
inlined..plist
to XML, and back to binary.Bookmarks.plist
directory, effectively replacing the orginal.Running the shell script
cd
to wherescript.sh
resides and run the followingchmod
command to makescript.sh
executable:Run the following command:
The following is then printed to your CLI:
Safari now has a bookmarks folder named
stackOverflow
containing two bookmarks (bash
andpython
).Using a Python script
There's a couple of ways to execute
script.sh
via your.py
file.Method A: External shell script
The following
.py
file executes the externalscript.sh
file. Let's name the filecreate-safari-bookmarks.py
and save it in the same folder asscript.sh
.create-safari-bookmarks.py
Explanation:
The first
def
statement defines arun-script
function. It has two parameters;folder_name
andbkmarks
. Thesubprocess
modulescall
method essentially executesscript.sh
with the required arguments.The second
def
statement defines atuple_to_shell_arg
function. It has one parametertup
. The Stringjoin()
method transforms a list of tuples into a format required byscript.sh
. It essentially transforms a list of tuples such as:and returns a string:
The
run_script
function is invoked as follows:This passes two arguments;
subreddit
(the name of the bookmarks folder), and the spec for each required bookmark (formatted as previously described in point no. 2).Running
create-safari-bookmarks.py
Make
create-safari-bookmarks.py
executable:Then invoke it with:
Method B: Inline shell script
Depending on your exact use case, you may want to consider inlining
script.sh
in your.py
file instead of calling an external.sh
file. Let's name this filecreate-safari-bookmarks-inlined.py
and save it to the same directory wherecreate-safari-bookmarks.py
resides.Important:
You'll need to copy and paste all the content from
script.sh
intocreate-safari-bookmarks-inlined.py
where indicated.Paste it on the next line following the
bash_script = """\
part."""
part increate-safari-bookmarks-inlined.py
should be on it's own line following the last line of the pastedscript.sh
content.Line 31 of
script.sh
when inlined in.py
must have the'%s\0'
part (\0
is a null character) escaped with another backslash, i.e. line 31 ofscript.sh
should appear like this:This line will probably be on line 37 in
create-safari-bookmarks-inlined.py
.create-safari-bookmarks-inlined.py
Explanation
This file achieves the same result as
create-safari-bookmarks.py
.This modified
.py
script includes a modifiedrun_script
function that utilizes Python'stempfile
module to save the inline shell script to a temporary file.Python's
subprocess
modulescall
method then executes the temporary created shell file.Running
create-safari-bookmarks-inlined.py
Make
create-safari-bookmarks-inlined.py
executable:Then invoke it by running:
Additional Note: Appending bookmarks to an existing folder
Currently, each time the aforementioned scripts/commands are run again we are effectively replacing any existing named Safari bookmark folder, (which has the same name as the given bookmark folder name), with a completely new one and creating the specified bookmarks.
However, if you wanted to append bookmarks to an exiting folder then
template.xsl
includes one additional parameter/argument to be passed to it. Note the part on line 14 that reads:It's default value is
false
. So, if we were to change therun_script
function in let's saycreate-safari-bookmarks.py
to the following.That is to add a third parameter named
keep_existing
, and include a reference to it in thesubprocess.call([...])
, i.e. so that it gets passed as the third argument toscript.sh
(...and subsequently to the XSLT stylesheet).We can then invoke the
run_script
function and pass in an additional String argument, either"true"
or"false"
like so:However, making the changes above, (i.e. passing in
"true"
to keep existing bookmarks), does have potential to result in duplicate bookmarks being created. For example; duplicate bookmarks will occur when we have an exiting bookmark (name and URL) which is then reprovided with the same name and URL at a later time.Limitations: Currently any name argument provided for a bookmark cannot include a space character(s) because they are used as delimiters by the script(s).
MacOS Mojave Restrictions
Due to stricter security policies on macOS Mojave (10.14.x) access to
~/Library/Safari/Bookmarks.plist
is not permitted by default (as mentioned in this answer).Therefore, it is necessary to grant the Terminal.app, (or other preferred CLI tool such as iTerm), access to your whole disk. To do this you'll need to:
/Applications/Utilities/
, then click the Open button.I never found the AS commands to manage bookmarks in Safari (not in AS dictionary). So I built my own routines to play with Safari bookmark plist file. However, they are subject to unexpected changes made by Apple in the way bookmarks will be handled in future ! up to now , it is still working, but I do not use yet the 10.14
First you must get this plist file to change it. This part must be in your main code. it gives you the patch to your plist file :
Here are 2 sub-routine to manage bookmarks. The first one checks if a bookmark exists
You can call this handler like bellow :
The second handler creates a new bookmark :
I used these routines to add, check and change a bookmark at right side of my bookmarks. In your case, you need to play with bookmark sub menu, and then you have to adjust this code, but the main concept is the same.
To make it easier, I recommend you to start looking your plist file (Library/Safari/Bookmarks.plist) to see its structure when you have your bookmarks in sub menu.
I hope it helps !