Open Browser Bookmarks from the Terminal

Ever wanted to open your browser bookmarks from the terminal? Maybe not, but if you have a lot of bookmarks and you also happen to spend a lot of time in the terminal, this can be a great way to quickly launch your bookmarks straight from the terminal.

The bash script for doing this is actually fairly beefy, but I’ve broken into two parts:

  1. A script that parses browser bookmarks into a normalized JSON format.
  2. A script that takes the normalized bookmarks and manages the fuzzy-finding and opening of URLs.

The main reason for splitting the script into these two parts is so that it can support multiple browsers. All it takes to support a different browser is to create a shell script for that browser that parses into the normalized structure, then plug that into the main script and viola.

Normalizing Chrome bookmarks

Let’s get started with the script to normalize the Chrome bookmarks into a structure that our main script can read. We are using jq extensively in this entire process including most importantly parsing the Chrome bookmarks.

I won’t explain every detail, but in essence we are collapsing the recursive nature of Chrome bookmarks into a flat list and creating a slug for each entry (e.g. “My bookmark” becomes “my-bookmark”).

/usr/local/bin/chrome-bookmarks
#!/usr/bin/env bash

path="$HOME/Library/Application Support/Google/Chrome/Default/Bookmarks"

jq '.roots
  | to_entries[].value
  | recurse(.children[]?)
  | select(.type == "url")
  | {
      name,
      url,
      slug: .name
        | gsub("'\''"; "")
        | gsub("[^a-zA-Z0-9]"; "-")
        | gsub("-{2,}"; "-")
        | gsub("(^-|-$)"; "")
        | ascii_downcase
    }
  ' "$path"

The main script

Now that we have the script to read and normalize our Chrome bookmarks, we can create our main script to fuzzy-find and open our bookmarks. If called with an argument, the script will filter the results based on which results start with the given string and either open fzf if there are multiple results, or open the bookmark if it is an exact match.

/usr/local/bin/bm
#!/usr/bin/env bash

list_bookmarks() {
	chrome-bookmarks
}

filter_bookmarks() {
	jq -c --arg key "$1" --arg value "$2" 'select(.[$key] | startswith($value))'
}

find_bookmark() {
	jq -c --arg key "$1" --arg value "$2" 'select(.[$key] == $value)'
}

# If bm is called without argument, show all bookmarks
slug=${1:-}

# Print raw JSON when requested for completions
if [[ "$slug" == '--json' ]]; then
	list_bookmarks | jq -cM
	exit 0
fi

# Filter the bookmarks by the provided slug
bookmarks=$(list_bookmarks | filter_bookmarks 'slug' "$slug")

# If no bookmarks matched the query, exit early
if [[ -z "$bookmarks" ]]; then
	echo 'No bookmark found matching the provided slug'
	exit 1
fi

# If the slug matches an item exactly, open it without prompting to choose other
# options.
match=$(echo "$bookmarks" | find_bookmark 'slug' "$slug")
if [[ -n "$match" ]]; then
	echo "$match" | jq '.url' | xargs open
	exit 0
fi

# Get the exact name of the bookmark
name=$(echo -e "$bookmarks" | jq -r '.name' | fzf --no-info)

# Find the bookmark by name and open the URL in Chrome. If no bookmark is
# selected from the list, exit with an error.
if [[ -n "$name" ]]; then
	echo "$bookmarks" | find_bookmark 'name' "$name" | jq '.url' | xargs open
else
	exit 1
fi

Here are some examples of how we might call our script in our terminal:

bm
bm my-bookmark

Support for other browsers

As I mentioned, supporting other browsers is fairly trivial, one simply needs to create a shell script for the desired browser. I’ve created a version for the Arc browser which you can view in my dotfiles.

Shell completions

The script is setup in a way that makes shell completions very simple to add. Simply add the --json flag along with jq to format the results in a way that works for your shell’s completion mechanism.

Here is an example for the Fish shell:

~/.config/fish/completions/bm.fish
function __bm_complete_bookmarks
    bm --json | jq -r '.slug'
end

complete -c bm -f -n __fish_use_subcommand -a '(__bm_complete_bookmarks)'