commit ee81288af3c599b8d3f4a9e9a8fc770ab5903193 Spenser Truex <truex@equwal.com> 2026-09-20 22:29:47 -0700 bm-import: read all profiles of a browser, on Linux, macOS and Windows bm-import read only the Default profile, and only from ~/.config. Now a browser name reads the Bookmarks file of every profile. bm-import looks for the browser in ~/.config, in ~/Library/Application Support (macOS), and in %LOCALAPPDATA% (Windows, through cygpath under Cygwin). bm --merge skips the known bookmarks, so the same command again syncs the new browser bookmarks into bm.
README | 9 ++++++--- bm-import | 66 +++++++++++++++++++++++++++++++++++++++++++++---------------- test/run.sh | 28 ++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 20 deletions(-)
diff --git a/README b/README index 30f3842..2b0d7fe 100644 --- a/README +++ b/README @@ -153,9 +153,12 @@ Links that would run script (javascript:, data:) are never written. bm-import vivaldi | bm --merge # or chrome, brave, edge, chromium bm-import path/to/Bookmarks | bm --merge # a Chromium JSON file (needs jq) -Folder names become tags ("Dev Tools" becomes dev-tools). bm --merge skips -what you already have, so importing twice adds nothing. Look before you -merge: bm-import prints plain bookmark lines. +A browser name imports the bookmarks of all profiles of that browser. The +profiles are in ~/.config, in ~/Library/Application Support on macOS, or in +%LOCALAPPDATA% on Windows. Folder names become tags ("Dev Tools" becomes +dev-tools). bm --merge skips what you already have, so importing twice adds +nothing. To sync new browser bookmarks into bm, run the same command again. +Look before you merge: bm-import prints plain bookmark lines. == Dead links == bm-check diff --git a/bm-import b/bm-import index bea348d..3ceaa9b 100755 --- a/bm-import +++ b/bm-import @@ -3,10 +3,13 @@ # bm-import: turn a browser's bookmarks into bm's format, on stdout. # # bm-import bookmarks.html | bm --merge +# bm-import brave | bm --merge # # Reads the Netscape HTML that every browser exports, or the JSON file that # Chromium style browsers keep (this needs jq). Folder names become tags: # "Dev Tools" is dev-tools. Only http, https, ftp and file URLs are kept. +# A browser name reads the JSON files of all profiles of that browser, in +# the directory that the browser uses on Linux, macOS or Windows (Cygwin). usage () { cat <<'EOF2' @@ -27,21 +30,39 @@ case $1 in -h|--help) usage; exit 0 ;; esac -src=$1 -case $src in - vivaldi) src=vivaldi ;; - chrome) src=google-chrome ;; - brave) src=BraveSoftware/Brave-Browser ;; - edge) src=microsoft-edge ;; - chromium) src=chromium ;; - *) src= ;; -esac -if [ -n "$src" ]; then - src=${XDG_CONFIG_HOME:-$HOME/.config}/$src/Default/Bookmarks +# browser_dir <linux> <macos> <windows>: the first directory of the browser +# that exists. The names are relative to ~/.config, to ~/Library/Application +# Support and to %LOCALAPPDATA%. The directory holds one folder per profile. +browser_dir () { + appdata=${LOCALAPPDATA-} + if [ -n "$appdata" ] && command -v cygpath >/dev/null 2>&1; then + appdata=$(cygpath -u "$appdata") + fi + for dir in "${XDG_CONFIG_HOME:-$HOME/.config}/$1" \ + "$HOME/Library/Application Support/$2" \ + ${appdata:+"$appdata/$3/User Data"}; do + if [ -d "$dir" ]; then + printf '%s\n' "$dir" + return 0 + fi + done + return 1 +} + +case $1 in + vivaldi) dir=$(browser_dir vivaldi Vivaldi Vivaldi) ;; + chrome) dir=$(browser_dir google-chrome Google/Chrome Google/Chrome) ;; + brave) dir=$(browser_dir BraveSoftware/Brave-Browser \ + BraveSoftware/Brave-Browser BraveSoftware/Brave-Browser) ;; + edge) dir=$(browser_dir microsoft-edge 'Microsoft Edge' Microsoft/Edge) ;; + chromium) dir=$(browser_dir chromium Chromium Chromium) ;; + *) dir= ;; +esac || die "found no profiles of $1" +if [ -n "$dir" ]; then + command -v jq >/dev/null 2>&1 || die "reading $1 needs jq" else - src=$1 + [ -r "$1" ] || die "cannot read $1" fi -[ -r "$src" ] || die "cannot read $src" # Chromium JSON. The trash (Vivaldi has one) is left out. from_json () { @@ -103,7 +124,18 @@ from_html () { }' "$1" } -case $(LC_ALL=C tr -d ' \t\r\n' < "$src" | dd bs=1 count=1 2>/dev/null) in - '{') from_json "$src" ;; - *) from_html "$src" ;; -esac | awk -F'\t' '$1 ~ /^(https?|ftp|file):\/\//' +# rows <file>: the rows of the file, or of every profile of the browser. +rows () { + if [ -n "$dir" ]; then + for file in "$dir"/*/Bookmarks; do + if [ -r "$file" ]; then from_json "$file"; fi + done + return + fi + case $(LC_ALL=C tr -d ' \t\r\n' < "$1" | dd bs=1 count=1 2>/dev/null) in + '{') from_json "$1" ;; + *) from_html "$1" ;; + esac +} + +rows "$1" | awk -F'\t' '$1 ~ /^(https?|ftp|file):\/\//' diff --git a/test/run.sh b/test/run.sh index d3755b4..b83ecc8 100755 --- a/test/run.sh +++ b/test/run.sh @@ -493,6 +493,34 @@ if command -v jq >/dev/null 2>&1; then "https://blog.example/feed${TAB}A blog${TAB}news-blogs" \ "https://deep.example/${TAB}Deep${TAB}news-blogs tech" \ "http://www.bar.example${TAB}Bar dupe${TAB}")" + + # profiles <dir> <folder>...: a browser directory with these profiles, + # each with the fixture as its bookmarks. + profiles () { + dir=$1 + shift + for folder; do + mkdir -p "$dir/$folder" + cp "$here/fixtures/chromium.json" "$dir/$folder/Bookmarks" + done + } + json_rows=$(printf '%s\n' \ + "https://bar.example/${TAB}Bar site${TAB}" \ + "https://blog.example/feed${TAB}A blog${TAB}news-blogs" \ + "https://deep.example/${TAB}Deep${TAB}news-blogs tech" \ + "http://www.bar.example${TAB}Bar dupe${TAB}") + profiles "$work/linux/.config/BraveSoftware/Brave-Browser" Default 'Profile 1' + eq 'bm-import brave reads the bookmarks of every profile' \ + "$(HOME=$work/linux XDG_CONFIG_HOME='' LOCALAPPDATA='' $IMPORT brave)" \ + "$(printf '%s\n%s\n' "$json_rows" "$json_rows")" + profiles "$work/mac/Library/Application Support/Google/Chrome" Default 'Profile 2' + eq 'bm-import finds a browser in ~/Library/Application Support' \ + "$(HOME=$work/mac XDG_CONFIG_HOME='' LOCALAPPDATA='' $IMPORT chrome | wc -l | tr -d ' ')" '8' + profiles "$work/win/Microsoft/Edge/User Data" Default + eq 'bm-import finds a browser in LOCALAPPDATA' \ + "$(HOME=$work/nohome XDG_CONFIG_HOME='' LOCALAPPDATA=$work/win $IMPORT edge)" "$json_rows" + HOME=$work/nohome XDG_CONFIG_HOME='' LOCALAPPDATA='' $IMPORT brave 2>/dev/null + eq 'bm-import fails when it finds no profiles of the browser' "$?" '1' else printf 'skip bm-import json: no jq\n' fi