bm-import (5291 bytes)
1 #!/bin/sh 2 3 # bm-import: turn a browser's bookmarks into bm's format, on stdout. 4 # 5 # bm-import bookmarks.html | bm --merge 6 # bm-import brave | bm --merge 7 # 8 # Reads the Netscape HTML that every browser exports, or the JSON file that 9 # Chromium style browsers keep (this needs jq). Folder names become tags: 10 # "Dev Tools" is dev-tools. Only http, https, ftp and file URLs are kept. 11 # A browser name reads the JSON files of all profiles of that browser, in 12 # the directory that the browser uses on Linux, macOS or Windows (Cygwin). 13 # --files prints the names of these files, for bm-watch. 14 15 usage () { 16 cat <<'EOF2' 17 usage: bm-import <file> | vivaldi | chrome | brave | edge | chromium 18 bm-import --files vivaldi | chrome | brave | edge | chromium 19 EOF2 20 } 21 22 die () { 23 printf 'bm-import: %s\n' "$*" >&2 24 exit 1 25 } 26 27 files= 28 case ${1-} in 29 -h|--help) usage; exit 0 ;; 30 --files) files=1; shift ;; 31 esac 32 if [ $# -ne 1 ]; then 33 usage >&2 34 exit 2 35 fi 36 37 # browser_dir <linux> <macos> <windows>: the first directory of the browser 38 # that exists. The names are relative to ~/.config, to ~/Library/Application 39 # Support and to %LOCALAPPDATA%. The directory holds one folder per profile. 40 browser_dir () { 41 appdata=${LOCALAPPDATA-} 42 if [ -n "$appdata" ] && command -v cygpath >/dev/null 2>&1; then 43 appdata=$(cygpath -u "$appdata") 44 fi 45 for dir in "${XDG_CONFIG_HOME:-$HOME/.config}/$1" \ 46 "$HOME/Library/Application Support/$2" \ 47 ${appdata:+"$appdata/$3/User Data"}; do 48 if [ -d "$dir" ]; then 49 printf '%s\n' "$dir" 50 return 0 51 fi 52 done 53 return 1 54 } 55 56 case $1 in 57 vivaldi) dir=$(browser_dir vivaldi Vivaldi Vivaldi) ;; 58 chrome) dir=$(browser_dir google-chrome Google/Chrome Google/Chrome) ;; 59 brave) dir=$(browser_dir BraveSoftware/Brave-Browser \ 60 BraveSoftware/Brave-Browser BraveSoftware/Brave-Browser) ;; 61 edge) dir=$(browser_dir microsoft-edge 'Microsoft Edge' Microsoft/Edge) ;; 62 chromium) dir=$(browser_dir chromium Chromium Chromium) ;; 63 *) dir= ;; 64 esac || die "found no profiles of $1" 65 if [ -n "$files" ]; then 66 if [ -z "$dir" ]; then 67 usage >&2 68 exit 2 69 fi 70 for file in "$dir"/*/Bookmarks; do 71 if [ -r "$file" ]; then printf '%s\n' "$file"; fi 72 done 73 exit 0 74 fi 75 if [ -n "$dir" ]; then 76 command -v jq >/dev/null 2>&1 || die "reading $1 needs jq" 77 else 78 [ -r "$1" ] || die "cannot read $1" 79 fi 80 81 # Chromium JSON. The trash (Vivaldi has one) is left out. 82 from_json () { 83 command -v jq >/dev/null 2>&1 || die 'a Chromium bookmark file needs jq' 84 jq -r ' 85 def slug: ascii_downcase | gsub("[^a-z0-9]+"; "-") 86 | sub("^-"; "") | sub("-$"; ""); 87 def emit(path): 88 if .type == "url" then 89 [.url, (.name | gsub("[\t\r\n]+"; " ")), 90 (path | unique | join(" "))] | join("\t") 91 elif (.children? | type) == "array" then 92 (path + ([.name | slug] | map(select(. != "")))) as $sub 93 | .children[] | emit($sub) 94 else empty end; 95 .roots | to_entries[] 96 | select(.key != "trash" and (.value | type) == "object") 97 | .value.children[]? | emit([])' "$1" 98 } 99 100 # Netscape HTML. <H3> opens a folder, </DL> closes it. 101 from_html () { 102 awk ' 103 function decode(s) { 104 gsub(/</, "<", s); gsub(/>/, ">", s); gsub(/"/, "\"", s) 105 gsub(/'|'|'/, "\047", s); gsub(/ /, " ", s) 106 gsub(/&/, "\\&", s) 107 gsub(/[ \t\r]+/, " ", s); sub(/^ /, "", s); sub(/ $/, "", s) 108 return s 109 } 110 function slug(s) { 111 s = tolower(s) 112 gsub(/[^a-z0-9]+/, "-", s); sub(/^-/, "", s); sub(/-$/, "", s) 113 return s 114 } 115 { 116 upper = toupper($0) 117 if ((at = index(upper, "<H3")) > 0) { 118 name = substr($0, at) 119 name = substr(name, index(name, ">") + 1) 120 end = index(toupper(name), "</H3") 121 if (end) name = substr(name, 1, end - 1) 122 folder[++depth] = slug(decode(name)) 123 } 124 if ((at = index(upper, "HREF=\"")) > 0) { 125 url = substr($0, at + 6) 126 rest = substr(url, index(url, "\"") + 1) 127 url = decode(substr(url, 1, index(url, "\"") - 1)) 128 name = substr(rest, index(rest, ">") + 1) 129 end = index(toupper(name), "</A") 130 if (end) name = substr(name, 1, end - 1) 131 tags = "" 132 for (i = 1; i <= depth; i++) 133 if (folder[i] != "" && index(" " tags " ", " " folder[i] " ") == 0) 134 tags = tags (tags == "" ? "" : " ") folder[i] 135 printf "%s\t%s\t%s\n", url, decode(name), tags 136 } 137 if (index(upper, "</DL>") > 0 && depth > 0) depth-- 138 }' "$1" 139 } 140 141 # rows <file>: the rows of the file, or of every profile of the browser. 142 rows () { 143 if [ -n "$dir" ]; then 144 for file in "$dir"/*/Bookmarks; do 145 if [ -r "$file" ]; then from_json "$file"; fi 146 done 147 return 148 fi 149 case $(LC_ALL=C tr -d ' \t\r\n' < "$1" | dd bs=1 count=1 2>/dev/null) in 150 '{') from_json "$1" ;; 151 *) from_html "$1" ;; 152 esac 153 } 154 155 rows "$1" | awk -F'\t' '$1 ~ /^(https?|ftp|file):\/\//'