Recently Written · git

sbm

dmenu bookmarks. Instantly fuzzy search thousands of bookmarks and plumb them. LOOKING FOR SUCKLESS SOFTWARE EDITION? GO TO sbm-suckless INSTEAD

git clone https://github.com/equwal/sbm

Log | Files | Refs


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(/&lt;/, "<", s); gsub(/&gt;/, ">", s); gsub(/&quot;/, "\"", s)
105         gsub(/&#39;|&#x27;|&apos;/, "\047", s); gsub(/&nbsp;/, " ", s)
106         gsub(/&amp;/, "\\&", 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):\/\//'