-
-
Save apathor/2128927a471224d09330e6a33c7794ab to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# cz - Line selection abstraction (plus utilities!) | |
# (C) 2020 by Mike Lalumiere | |
# TODO optional prompt (see 'dmenu -p foo') | |
# TODO cached input selection | |
### | |
req() { | |
local x="" | |
for x in "$@"; do | |
if ! hash "$x" &>/dev/null; then | |
printf "%s -- %s is required!\n" "${FUNCNAME[1]}" "$x" >&2 | |
return 1 | |
fi | |
done | |
} | |
rleval() { | |
local a="${READLINE_LINE:0:$READLINE_POINT}" | |
local b="${READLINE_LINE:$READLINE_POINT}" | |
local s; s=$(eval "$@") | |
READLINE_LINE="${a}${s}${b}" | |
((READLINE_POINT += ${#s})) | |
} | |
nth() { | |
read -r -d '' use <<EOF | |
nth TEMPLATE [ARG ...] | |
Render a string from TEMPLATE replacing variables of the following format: | |
{X} - argument X | |
{X:} - arguments X through end of arguments | |
{X:Y} - arguments X through X + Y | |
{X,Y,Z} - arguments X, Y, and Z | |
EOF | |
# accept options | |
local want=0 | |
local opt OPTIND OPTARG | |
while getopts ":hpq" opt; do | |
case "$opt" in | |
p) | |
want=0 | |
;; | |
q) | |
want=1 | |
;; | |
*) | |
printf "%s\n" "$use" >&2 | |
return 1 | |
;; | |
esac | |
done | |
shift $((OPTIND - 1)) | |
# require template argument | |
local tmpl="$1" | |
shift | |
if [ -z "$tmpl" ]; then | |
printf "%s\n" "$use" >&2 | |
return 1 | |
fi | |
# remaining arguments are tokens | |
local toks=("$@") | |
local segs=() | |
# parse field selection | |
local str="" fld="" idx=0 suf=0 mode="text" | |
tmpl+=" " | |
while (( idx < "${#tmpl}" )); do | |
# consider each character | |
char="${tmpl:$(( idx++ )):1}" | |
# switch on parser mode | |
if [[ "$mode" == "text" ]]; then | |
# literal text mode | |
case "$char" in | |
'{') | |
# start parsing a variable | |
mode="var" | |
;; | |
' ') | |
# append a suffix or a new field | |
if [ -n "$str" ]; then | |
if ((suf)); then | |
segs[-1]+="$str" | |
else | |
segs+=("$str") | |
fi | |
fi | |
# reset for next string | |
str="" | |
suf=0 | |
;; | |
*) | |
# accumulate characters | |
str+="$char" | |
;; | |
esac | |
elif [[ "$mode" == "var" ]]; then | |
# variable mode | |
case "$char" in | |
'{') # escaped literal curly brace | |
str+="$char" | |
;; | |
[,\}]) # field separator | |
# match and validate the field | |
if ! [[ "$fld" =~ ^([[:digit:]]+)(:([[:digit:]]+)?)?$ ]]; then | |
printf "Invalid field specifier '%s'\n" "$fld" >&2 | |
return 3 | |
fi | |
# get matched groups - field and range | |
local sel len | |
sel="${BASH_REMATCH[1]}" | |
len="${BASH_REMATCH[3]:-${BASH_REMATCH[2]}}" | |
if [ -z "$len" ]; then | |
# N - one specific field | |
segs+=("$(printf "%q" "${str}${toks[$sel]}")") | |
elif [ "$len" == ":" ]; then | |
# N: - a slice of fields | |
segs+=("$(printf "%s%q" "$str" "${toks[@]:${sel}:1}")") | |
while read -r l; do | |
if [ -z "$l" ]; then continue; fi | |
segs+=("$(printf "%q" "$l")") | |
done < <(printf "%s\n" "${toks[@]:$((sel + 1))}") | |
else | |
# N:X - a range of fields | |
segs+=("$(printf "%s%q" "$str" "${toks[@]:${sel}:1}")") | |
while read -r l; do | |
if [ -z "$l" ]; then continue; fi | |
segs+=("$(printf "%q" "$l")") | |
done < <(printf "%s\n" "${toks[@]:$((sel + 1)):$((len - 1))}") | |
fi | |
# maybe done with this variable | |
if [[ "$char" == '}' ]]; then | |
mode="text" | |
suf=1 | |
fi | |
# reset field | |
fld="" | |
str="" | |
;; | |
[[:digit:]:]) # valid field chars | |
fld+="$char" | |
;; | |
*) # invalid | |
printf "Could not parse character '%s' at index %d.\n\n%s\n" "$char" "$idx" "$use" >&2 | |
return 3 | |
;; | |
esac | |
fi | |
done | |
# check if parsing ended cleanly | |
if [[ "$mode" == "var" ]]; then | |
printf "Unclosed variable delimeter?\n%s\n" "$use" >&2 | |
return 3 | |
fi | |
# generate output string maybe with shell quotes | |
local out | |
out="$(eval printf "%s\ " "${segs[@]}")" | |
if ((want)); then | |
out="$(printf "%s " "${segs[@]}")" | |
fi | |
# strip trailing whitespace | |
out="${out%${out##*[![:space:]]}}" | |
# write it | |
printf "%s\n" "$out" | |
} | |
chuz() { | |
# usage | |
read -r -d '' use <<EOF | |
chuz < INPUT > LINE | |
Select one line from input. | |
EOF | |
# accept options | |
local opt OPTIND OPTARG | |
while getopts ":h" opt; do | |
case "$opt" in | |
*) printf "%s\n" "$use" >&2 | |
return 1 | |
;; | |
esac | |
done | |
shift $((OPTIND - 1)) | |
# prepend a number to each line of input | |
local ind lines | |
mapfile -t lines | |
for ((ind=0; ind < "${#lines[@]}"; ind++)); do | |
printf "% 5d %s\n" "$ind" "${lines[$ind]}" >&2 | |
done < <(printf "%s\n" "${lines[@]}") | |
# prompt interactively to select a line number | |
shopt -s nocasematch | |
while read -p "#? " -r sel < /dev/tty; do | |
if ! [[ "$sel" =~ ^[0-9]+$ ]]; then | |
# MAYBE tput reset >&2 | |
# given a non-number perform a string search | |
for ((ind=0; ind < "${#lines[@]}"; ind++)); do | |
if [[ "${lines[$ind]}" =~ $sel ]]; then | |
printf "% 5d %s\n" "$ind" "${lines[$ind]}" >&2 | |
fi | |
done < <(printf "%s\n" "${lines[@]}") | |
else | |
# line number must be within boundaries | |
if [ "$sel" -lt "${#lines[@]}" ]; then | |
break | |
fi | |
fi | |
done | |
shopt -u nocasematch | |
# needs a selection | |
if [ -z "$sel" ]; then return 2; fi | |
# print the selected line | |
printf "%s\n" "${lines[$sel]}" | |
} | |
cz() { | |
# version | |
local ver="0.1" | |
# usage | |
read -r -d '' use <<EOF | |
USAGE | |
cz [OPTIONS] < LINES | |
Select a line from input with an appropriate tool. | |
cz [OPTIONS] [PLUGIN] [ARGS ...] | |
Run a plugin. | |
OPTIONS | |
Set program mode. Select a line then... | |
-p : Print the line. This is the default mode. | |
-q : Print extracted FIELDS from the line. | |
-r : Run the string formatted by TEMPLATE using the line. | |
-s : Print the string formatted by TEMPLATE using the line. | |
General options: | |
-e TEMPLATE : Template to format selected line. | |
-d DELIM : Split selected line on given delimeter. | |
-f FIELDS : Extract fields from selected line. | |
-i IN-FILE : Read selections from file instead of stdin. | |
-x : Use a graphical line selection program. | |
-y : Use a text terminal line selection program. | |
-0 : Read null terminated lines. | |
Print some information and exit: | |
-h : Help! | |
-v : Version | |
-k : List supported line selection tools. | |
-l : List detected plugins. | |
ENVIRONMENT | |
CZ_GUI : preferred interface - 1=graphical 0=terminal | |
CZ_BINS_GUI : list of graphical utilities in order of preference | |
CZ_BINS_TTY : list of terminal utilities in order of preference | |
CZ_DMENU_COLOR : Colon separated colors for dmenu (NF:NB:SF:SB) | |
TEMPLATES | |
Substrings of TEMPLATE in the following formats are replaced with | |
one or more fields from a selected line split by DELIM. | |
{X} - argument X | |
{X:} - arguments X through end of arguments | |
{X:Y} - arguments X through X + Y | |
{X,Y,Z} - arguments X, Y, and Z | |
FIELDS consists of one of the above without enclosing '{}'. | |
EOF | |
# examples | |
read -r -d '' hows <<EOF | |
$ printf "%s\n" foo bar qux | cz | |
$ find . -name '*.yml' -print0 | cz -0 | |
$ cz -f 0,5 -d : < /etc/passwd | |
$ cd \$(cz find dir) | |
$ cz \$(cz -l | cz) | |
$ cz -e 'dig {0} MX' host | |
$ cz xclip out | cz -e 'firefox {0}' uri | |
$ cz -e 'cz jq json {0}' locate *.json | |
EOF | |
# determine appropriate type of executable | |
local gfx="${CZ_GUI:-1}" | |
if [ -z "$DISPLAY" ]; then gfx=0; fi | |
# figure available graphical executable | |
local gbins | |
read -r -a gbins <<< "${CZ_BINS_GUI:-dmenu rofi}" | |
for it in "${gbins[@]}"; do | |
if type "$it" &>/dev/null; then gbin="$it"; break; fi | |
done | |
# figure available terminal executable | |
local tbins | |
read -r -a tbins <<< "${CZ_BINS_TTY:-fzf pick pipedial sentaku iselect vis-menu}" | |
for it in "${tbins[@]}"; do | |
if type "$it" &>/dev/null; then tbin="$it"; break; fi | |
done | |
# accept options | |
local tpl="${CZ_TEMPLATE}" # string template | |
local fld="${CZ_FIELDS}" # string field selection | |
local mode="${CZ_MODE}" # integer program mode | |
local inp="/dev/stdin" # string file from which to select a line | |
local run="" # integer program mode implied by another option | |
local nul=0 # boolean reading null separated lines | |
local dlm="$IFS" # string delimeter to split selected line | |
local opt OPTIND OPTARG | |
while getopts ":d:e:f:hi:lkpqrsvxy0" opt; do | |
case "$opt" in | |
d) # delimeter | |
dlm="$OPTARG" | |
;; | |
e) # template to be executed | |
tpl="${tpl:-$OPTARG}" | |
run=2 | |
;; | |
f) # fields to be extracted | |
fld="${fld:-{${OPTARG}\}}" | |
run=1 | |
;; | |
h) # write some help text | |
printf "%s\n" "$use" >&2 | |
return 0 | |
;; | |
i) # input file from which to read selections | |
inp="$OPTARG" | |
if ! [ -r "$inp" ]; then | |
printf "%s\n" "Bad input file" >&2 | |
return 2 | |
fi | |
;; | |
l) # list available plugins | |
while read -r f; do | |
if [[ "$f" == cz_* ]]; then | |
printf "%s\n" "${f##cz_}" | |
fi | |
done < <(compgen -c) | sort | |
return 0 | |
;; | |
k) # list supported executables | |
read -r -d '' exes <<EOF | |
dmenu https://tools.suckless.org/dmenu | |
fzf https://github.com/junegunn/fzf | |
iselect http://www.ossp.org/pkg/tool/iselect | |
pick https://github.com/mptre/pick | |
pipedial https://code.reversed.top/user/xaizek/pipedial | |
rofi https://github.com/davatorium/rofi | |
sentaku https://github.com/rcmdnk/sentaku | |
slmenu https://bitbucket.org/rafaelgg/slmenu | |
vis-menu https://github.com/martanne/vis | |
EOF | |
printf "%s\n" "$exes" | |
return 0 | |
;; | |
p) # print selected line instead of running anything | |
mode=0 | |
;; | |
q) # print fields from extrated line provided by '-f' option | |
mode=1 | |
;; | |
r) # run template provided by by '-e' option | |
mode=2 | |
;; | |
s) # print template provided by by '-e' option | |
mode=3 | |
;; | |
0) # handle null separated input | |
nul=1 | |
;; | |
v) # write version | |
printf "cz %s\n" "$ver" | |
return 0 | |
;; | |
x) # try to use a graphical executable for line selection | |
gfx=1 | |
;; | |
y) # try to use a terminal executable for line selection | |
gfx=0 | |
;; | |
\?) # invalid option? | |
printf "Bad option: -%s\n%s\n" "$OPTARG" "$use" >&2 | |
return 2 | |
;; | |
esac | |
done | |
shift $((OPTIND - 1)) | |
# program mode might have been implied | |
mode="${mode:-$run}" | |
# plugins - any command in the format 'cz_$WORD' | |
if [ -n "$*" ]; then | |
# determine if a known plugin is given | |
local plug="" | |
local args=("$@") | |
local word=0 | |
for ((word=${#args[@]}; word>0; word--)); do | |
plug="${args[*]:0:$word}" | |
plug="cz_${plug// /_}" | |
if type "$plug" >&/dev/null; then break; fi | |
plug="" | |
done | |
# bail out for invalid plugins | |
if [ -z "$plug" ]; then | |
printf "No plugin matching '%s'.\n" "$*" >&2 | |
return 2 | |
fi | |
shift "$word" | |
# run the plugin with some override environment variables | |
CZ_FIELDS="$fld" CZ_GUI="$gfx" CZ_MODE="$mode" CZ_TEMPLATE="$tpl" \ | |
"$plug" "$@" | |
return $? | |
fi | |
# read selection file | |
declare -a lines | |
if ((nul)); then | |
mapfile -d '' -t lines < "$inp" | |
else | |
mapfile -t lines < "$inp" | |
fi | |
# require at least one non-empty line | |
if ! (("${#lines[@]}")); then | |
return 3 | |
fi | |
# buffer stdin if reading selection input from another file | |
declare -a stdin | |
if [[ "$inp" != "/dev/stdin" ]] && ! [ -t 0 ]; then | |
mapfile -t stdin | |
# require some input bytes unless attached to /dev/null | |
if [ -z "${stdin[*]}" ] && | |
type readlink &>/dev/null && | |
[ "$(readlink -f /dev/stdin)" != "/dev/null" ]; then | |
return 3 | |
fi | |
fi | |
# pick an appropriate line selection program | |
if ((gfx)); then | |
app="$gbin" | |
else | |
app="$tbin" | |
fi | |
# cz a line using a known program | |
local out | |
out=$(case "$app" in | |
dmenu ) | |
local color="${CZ_DMENU_COLOR:-white:black:black:white}" | |
IFS=':' read -r nf nb sf sb <<< "$color" | |
dmenu -i -l 30 -nf "$nf" -nb "$nb" -sf "$sf" -sb "$sb" | |
;; | |
fzf ) fzf ;; | |
iselect ) iselect -a ;; | |
pick ) pick ;; | |
rofi ) rofi -i -dmenu -p "" ;; | |
pipedial ) pipedial ;; | |
sentaku ) sentaku -s $'\n' | head -n1 ;; | |
slmenu ) slmenu -i -l 30 ;; | |
vis-menu ) vis-menu -i -l 30 ;; | |
* ) chuz ;; | |
esac < <(if ((nul)); then printf "%q\n" "${lines[@]}" | |
else printf "%s\n" "${lines[@]}"; fi) | |
) | |
# rectify shell quoted line | |
if ((nul)); then | |
out=$(eval printf "%s" "$out") | |
fi | |
# terminate if the line is empty | |
if [ -z "$out" ]; then | |
return 3 | |
fi | |
# tokenize selected line for use in templates | |
local cmd="" | |
IFS="$dlm" read -r -a toks <<< "$out" | |
# set harmless defaults for template arguments | |
tpl="${tpl:-echo {0:\}}" | |
fld="${fld:-{0:\}}" | |
# do something with the selection depending on mode | |
case "${mode:-0}" in | |
0) # print selection | |
printf "%s\n" "$out" | |
;; | |
1) # print some fields extracted from the line | |
if ! out="$(nth -p "${fld}" "${toks[@]}" 2>/dev/null)"; then | |
printf "Failed to parse template.\n%s\n" "$use" >&2 | |
return 4 | |
fi | |
printf "%s\n" "$out" | |
;; | |
2) # execute templated string | |
if ! cmd="$(nth -q "${tpl}" "${toks[@]}" 2>/dev/null)"; then | |
printf "Failed to parse template.\n%s\n" "$use" >&2 | |
return 4 | |
fi | |
if [ -n "${stdin[*]}" ]; then | |
# input for filter commands | |
CZ_MODE="" CZ_FIELDS="" CZ_TEMPLATE="" CZ_GUI="$gfx" \ | |
eval "$cmd" < <(printf "%s\n" "${stdin[@]}") | |
else | |
# no input for commands that need an attached terminal | |
CZ_MODE="" CZ_FIELDS="" CZ_TEMPLATE="" CZ_GUI="$gfx" \ | |
eval "$cmd" | |
fi | |
return $? | |
;; | |
3) # print templated string | |
if ! cmd="$(nth -p "${tpl}" "${toks[@]}" 2>/dev/null)"; then | |
printf "Failed to parse template.\n%s\n" "$use" >&2 | |
return 4 | |
fi | |
printf "%s\n" "$cmd" | |
;; | |
esac | |
} | |
# cz command completion | |
function _cz() { | |
# get current and previous word | |
local pre="" | |
if ((COMP_CWORD)); then pre="${COMP_WORDS[COMP_CWORD - 1]}"; fi | |
local cur="${COMP_WORDS[COMP_CWORD]}" | |
# maybe the previous word is an option that expects an argument | |
case "$pre" in | |
-d) COMPREPLY=(); return ;; | |
-e) COMPREPLY=($(compgen -c -- "$cur")); return ;; | |
-f) COMPREPLY=($(compgen -W "0 1 2 3 4 5 6 7 8 9" -- "$cur")); return ;; | |
-i) COMPREPLY=($(compgen -f -- "$cur")); return ;; | |
esac | |
# cz has some options | |
declare -A opts | |
for opt in -{d,e,f,h,i,l,k,p,q,r,s,v,x,y,0}; do opts["$opt"]=0; done | |
# cz has some plugins | |
declare -a exts | |
exts=($(cz -l)) | |
# scan all previous words | |
local base="" | |
local last word | |
for ((i=1; i < COMP_CWORD; i++)); do | |
word="${COMP_WORDS[$i]}" | |
last="${COMP_WORDS[$i-1]}" | |
# have options ended? | |
if [[ "$last" =~ ^-[[:alnum:]]^ ]]; then | |
# record options already seen | |
unset opts["$word"] | |
elif [[ "$word" =~ ^[[:alnum:]]+ ]]; then | |
# check if options have ended | |
if [[ "$last" =~ ^-[defi]$ ]]; then continue; fi | |
opts=() | |
# concat command prefix | |
base="${base}_${word}" | |
base="${base##_}" | |
fi | |
done | |
# first complete initial command token and options | |
if [ -z "$base" ]; then | |
COMPREPLY=($(compgen -W "${!opts[*]} ${exts[*]%%_*}" -- "$cur")) | |
return | |
fi | |
# determine next command token | |
local next=() | |
local got="" | |
local pat="^${base}_([[:alnum:]]+)" | |
for ext in "${exts[@]}"; do | |
if [[ "$ext" =~ $pat ]]; then | |
next+=("${BASH_REMATCH[1]%%_*}" "${got}") | |
fi | |
done | |
# maybe complete next command token | |
if (("${#next[@]}")); then | |
COMPREPLY=($(compgen -W "${next[*]}" -- "$cur")) | |
return | |
fi | |
# maybe complete something specific for a plugin | |
case "$base" in | |
*) return ;; | |
esac | |
} | |
complete -F _cz cz | |
#### | |
cz_meta() { | |
cz -f 0 -e "cz {0}" -i <(cz -l) | |
} | |
cz_value() { | |
cz -i <(printf "%s\n" "$@") | |
} | |
cz_lineno() { | |
req grep || return 255 | |
cz -d : -f 0 < <(grep -n .) | |
} | |
cz_command() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-black:dark green:white:black}" | |
cz -f 0 -e "{0}" -i <(compgen -c) | |
} | |
cz_file() { | |
cz -i <(compgen -f) | |
} | |
cz_dir() { | |
cz -i <(compgen -d) | |
} | |
cz_bash_host() { | |
cz -i <(compgen -A hostname | sort -u) | |
} | |
cz_nss_passwd() { | |
req getent || return 255 | |
cz -f 0 -e '{0}' -d : -i <(getent passwd) | |
} | |
cz_nss_group() { | |
req getent || return 255 | |
cz -f 0 -d : -i <(getent group) | |
} | |
cz_nss_host() { | |
req getent || return 255 | |
cz -f 0 -i <(getent hosts) | |
} | |
cz_nss_network() { | |
req getent || return 255 | |
cz -f 0 -i <(getent networks) | |
} | |
cz_nss_protocol() { | |
req getent || return 255 | |
cz -f 1 -i <(getent protocols) | |
} | |
cz_nss_service() { | |
req getent || return 255 | |
cz -f 1 -i <(getent services) | |
} | |
cz_bash_help() { | |
mapfile -t m < <(help -s '*') | |
cz -f 0 -e 'help {0}' -d : -i <(printf "%s\n" "${m[@]:2}") | |
} | |
cz_bash_completion() { | |
cz -i <(complete -p) | |
} | |
cz_bash_var() { | |
cz -i <(compgen -v) | |
} | |
cz_bash_job() { | |
cz -f 1 -i <(jobs -l) | |
} | |
cz_bash_bind_function() { | |
cz -i <(bind -l) | |
} | |
cz_bash_bind_command() { | |
cz -d : -f 1: -i <(bind -X) | |
} | |
cz_bash_bind_key() { | |
cz -d : -f 0 -i <({ bind -p; bind -X; bind -s; } | grep -v '^#') | |
} | |
cz_bash_bind_var() { | |
cz -i <(bind -v) | |
} | |
cz_bash_history() { | |
req sort || return 255 | |
cz -f 3: -e "eval {3:}" -i <(HISTTIMEFORMAT="%F %T " history | sort -r -n) | |
} | |
cz_grep() { | |
req grep || return 255 | |
cz -d : -f 0:1 -i <(grep -rn "$@" .) | |
} | |
cz_find_file() { | |
req find || return 255 | |
cz -0 -i <(find "${1:-.}" -iwholename "${2:-*}" -type f -print0) | |
} | |
cz_find_dir() { | |
req find || return 255 | |
cz -0 -i <(find "${1:-.}" -iwholename "${2:-*}" -type d -print0) | |
} | |
cz_apparix() { | |
req awk apparix || return 255 | |
cz -f 1 -e "cd {1}" -i <(apparix | awk '/^j/ { print $2, $3 }') | |
} | |
cz_mpd_track() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-white:blue:blue:white}" | |
req mpc || return 255 | |
cz -f 0 -e "mpc play {0}" \ | |
-i <(mpc -f '%position% [%artist% - %album% - %title%] -- %file%' playlist) | |
} | |
cz_mpd_output() { | |
req mpc || return 255 | |
cz -e "mpc enable {1}" -i <(mpc outputs) | |
} | |
declare t="" f="" | |
for t in artist album genre composer; do | |
read -r -d '' f <<EOF | |
cz_mpd_$t() { | |
req mpc || return 255 | |
cz -e 'mpc search $t {0:}' -i <(mpc list $t) | |
} | |
EOF | |
eval "$f" | |
done | |
unset t f | |
cz_uri() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-white:black:black:green}" | |
if type urifind &>/dev/null; then | |
cz -e 'sensible-browser {0}' < <(urifind -su) | |
elif type xurls &>/dev/null; then | |
cz -e 'sensible-browser {0}' < <(xurls -r) | |
else | |
req xurls urifind || return 255 | |
fi | |
} | |
cz_pass() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-black:purple:black:red}" | |
req pass find sed || return 255 | |
local pdir="${PASSWORD_STORE_DIR:-$HOME/.password-store}" | |
cz -f 0 -e 'pass show {0}' \ | |
-i <(find "$pdir" -type f -name '*.gpg' | sed "s#\.gpg##; s#$pdir##") | |
} | |
cz_lastpass() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-black:purple:red:black}" | |
req lpass || return 255 | |
local menu=() | |
local pat='^(.*[^/]) \[id: ([[:digit:]]+)\]$' | |
while read -r item; do | |
if [[ "$item" =~ $pat ]]; then | |
menu+=("${BASH_REMATCH[2]} ${BASH_REMATCH[1]}") | |
fi | |
done < <(lpass ls) | |
cz -e "lpass show --password {0}" < <(printf "%s\n" "${menu[@]}") | |
} | |
xclip_brief() { | |
local mod buf lns | |
for mod in clipboard primary secondary; do | |
buf=$(xclip -o -selection "$mod" 2>/dev/null) | |
mapfile lns <<< "$buf" | |
printf "%s %s %s\n" "$mod" "${#buf}" "${#lns[@]}" | |
done | |
} | |
cz_xclip_in() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-black:dark gray:black:red}" | |
req xclip || return 255 | |
cz -e 'xclip -selection {0} -i' -i <(xclip_brief) | |
} | |
cz_xclip_out() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-black:dark gray:black:green}" | |
req xclip || return 255 | |
cz -e 'xclip -selection {0} -o' -i <(xclip_brief) | |
} | |
cz_info() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-white:blue:black:green}" | |
req info || return 255 | |
cz -e 'info {0}' -i <(info -k .) | |
} | |
cz_man() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-black:white:yellow:black}" | |
req man sed || return 255 | |
cz -f 0 -e 'man {0}' -i <(man -k . | sed 's/ (/./;s/)//') | |
} | |
cz_process() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-white:blue:black:orange}" | |
req ps || return 255 | |
mapfile table < <(ps ux) | |
read -r -a cols <<< "${table[0]}" | |
for ((ind=1; ind <= "${#cols[@]}"; ind++)) ; do | |
if [[ "${cols[$ind]}" =~ ^(pid|PID)$ ]]; then break; fi | |
done | |
cz -f "$ind" <<< "${table[@]:1}" | |
} | |
cz_signal() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-white:blue:black:red}" | |
local name line | |
declare -a signals | |
for x in {1..64}; do | |
name=$(kill -l "$x") | |
if [ -z "$name" ]; then continue; fi | |
line=$(printf "%02d %s\n" "$x" "$name") | |
signals+=("$line") | |
done | |
cz -f 0 < <(printf "%s\n" "${signals[@]}") | |
} | |
cz_kill() { | |
CZ_DMENU_COLOR="${CZ_DMENU_COLOR:-black:red:green:black}" | |
local pid | |
if ! pid=$(cz process); then return 2; fi | |
cz -e "kill -s {0} $pid" signal | |
} | |
cz_word() { | |
req grep sort || return 255 | |
cz < <(grep -Eo '([[:alnum:]])+' | sort -u) | |
} | |
cz_hg_status() { | |
req hg || return 255 | |
cz -f 1 -i <(hg -R "${1:-.}" status) | |
} | |
cz_hg_branch() { | |
req hg || return 255 | |
cz -f 0 -i <(hg -R "${1:-.}" branches) | |
} | |
cz_hg_revision() { | |
req hg || return 255 | |
cz -f 0 -i <(hg log -T '{rev} {branch} {user} {date} {desc|firstline}\n' "${1:-.}") | |
} | |
cz_git() { | |
cz git status | |
} | |
cz_git_status() { | |
req git grep || return 255 | |
local repo="${1:-.}" | |
cz -f 1 -e 'git diff {1}' -i <(git -C "$repo" status -sbu | grep -v ^#) | |
} | |
cz_git_file() { | |
req git grep || return 255 | |
local repo="${1:-.}" | |
cz -f 4: -i <(git -C "$repo" ls-tree -rl HEAD) | |
} | |
cz_git_branch() { | |
req git sed || return 255 | |
local repo="${1:-.}" | |
cz -f 0 -i <(git -C "$repo" branch -vv | sed 's/^[* ] //') | |
} | |
cz_git_tag() { | |
req git || return 255 | |
local repo="${1:-.}" | |
cz -f 0 -i <(git -C "$repo" tag) | |
} | |
cz_git_commit() { | |
req git || return 255 | |
local repo="${1:-.}" | |
cz -f 0 -e "git -C $repo show {0}" \ | |
-i <(git -C "$repo" log --pretty=format:'%h %cI %s (%ce)' --abbrev-commit) | |
} | |
cz_git_remote() { | |
req git || return 255 | |
local repo="${1:-.}" | |
cz -f 0 -i <(git -C "$repo" remote -v) | |
} | |
cz_systemd_unit() { | |
req systemctl || return 255 | |
cz -f 0 -e 'systemctl status {0}' -i <(systemctl list-units -l --plain --no-legend) | |
} | |
cz_systemd_service() { | |
req systemctl || return 255 | |
cz -f 0 -e 'systemctl status {0}' -i <(systemctl list-units -l --type=service --plain --no-legend) | |
} | |
cz_systemd_timer() { | |
req systemctl || return 255 | |
cz -f 0 -e 'systemctl status {0}' -i <(systemctl list-units -l --type=timer --plain --no-legend) | |
} | |
cz_systemd_socket() { | |
req systemctl || return 255 | |
cz -f 0 -e 'systemctl status {0}' -i <(systemctl list-units -l --type=socket --plain --no-legend) | |
} | |
cz_tmux_command() { | |
req tmux || return 255 | |
cz -f 0 -i <(tmux list-commands) | |
} | |
cz_tmux_session() { | |
req tmux || return 255 | |
cz -d : -f 0 -e 'tmux attach-session -t {0}' -i <(tmux list-sessions) | |
} | |
cz_tmux_pane() { | |
req tmux || return 255 | |
cz -d : -f 0 -e 'tmux select-pane -t {0}' -i <(tmux list-panes) | |
} | |
cz_tmux_window() { | |
req tmux || return 255 | |
cz -d : -f 0 -e 'tmux select-window -t {0}' -i <(tmux list-windows) | |
} | |
cz_screen_session() { | |
req screen || return 255 | |
cz -f 0 -e 'screen -rd {0}' -i <(screen -ls | grep pts) | |
} | |
cz_apt_package() { | |
req apt-cache || return 255 | |
local qry="${*:-.}" | |
cz -f 0 -e 'apt-cache show {0}' -i <(apt-cache search "$qry") | |
} | |
cz_pulseaudio_sink() { | |
req pactl || return 255 | |
cz -f 0 -e 'pactl set-sink-mute {0} toggle' -i <(pactl list short sinks) | |
} | |
cz_pulseaudio_source() { | |
req pactl || return 255 | |
cz -f 0 -e 'pactl set-source-mute {0} toggle' -i <(pactl list short sources) | |
} | |
cz_pulseaudio_sink_volume() { | |
req pactl || return 255 | |
local sink | |
if ! sink=$(cz -p pulseaudio sink); then return 2; fi | |
cz -f 0 -e "pactl set-sink-volume $sink {0}" -i <(printf "%s%%\n" {100..0..5}) | |
} | |
cz_ip_route4() { | |
req ip || return 255 | |
cz -f 0 -i <(ip -4 route) | |
} | |
cz_ip_route6() { | |
req ip || return 255 | |
cz -f 0 -i <(ip -6 route) | |
} | |
cz_ip_addr4() { | |
req ip || return 255 | |
cz -f 2 -i <(ip -br -4 addr) | |
} | |
cz_ip_addr6() { | |
req ip || return 255 | |
cz -f 2 -i <(ip -br -6 addr) | |
} | |
cz_ispell() { | |
req ispell || return 255 | |
# parse ispell spellcheck output | |
mapfile -t lines | |
local line="" index=0 fix="" | |
while read -r symbol rest; do | |
line="${lines[$index]}" | |
case "$symbol" in | |
'') # next | |
let index++ | |
continue | |
;; | |
'?') # guess | |
: ;; | |
'&') # miss | |
read -r bad _ _ sug <<< "$rest" | |
IFS=', ' read -r -a fixes <<< "$sug" | |
fix=$(printf "%s\n" "$bad" "${fixes[@]}" | cz) | |
if [ -n "$fix" ]; then | |
lines["$index"]="${line/$bad/$fix}" | |
fi | |
;; | |
'#') # no guess | |
continue | |
;; | |
'*') # okay | |
continue | |
;; | |
'+') # root | |
continue | |
;; | |
esac | |
done < <(printf "%s\n" "${lines[@]}" | ispell -a) | |
printf "%s\n" "${lines[@]}" | |
} | |
cz_perldoc() { | |
req perldoc perldoc-search || return 255 | |
cz -f 0 -e "perldoc {0}" < <(perldoc-search .) | |
} | |
cz_pydoc() { | |
req pydoc || return 255 | |
cz -f 0 -e "pydoc {0}" < <(pydoc -k .) | |
} | |
cz_ansibledoc() { | |
req ansible-doc || return 255 | |
cz -f 0 -e "ansible-doc {0}" < <(ansible-doc -l) | |
} | |
cz_surfraw() { | |
req surfraw || return 255 | |
cz -f 0 -e "surfraw {0} \"$1\"" < <(surfraw -elvi | sed 1d) | |
} | |
cz_gcloud_bq_dataset() { | |
req jq bq || return 255 | |
local proj="${1:-$(CZ_FIELDS="" cz -q gcloud project)}" | |
cz -f 0 -i <(bq --format=json --project_id "$proj" ls | \ | |
jq -r '.[] | [.id, (.datasetReference | (.datasetId, .projectId)), .location] | @tsv') | |
} | |
cz_gcloud_bq_table() { | |
req jq bq || return 255 | |
local proj="$1" data="$2" | |
if [ -z "$proj" ] || [ -z "$data" ]; then | |
read -r data proj < <(cz -f 1,2 -q gcloud bq dataset) | |
fi | |
cz -f 0 -i <(bq --format=json --project_id "$proj" ls "$data" | \ | |
jq -r '.[] | [.id, (.tableReference | (.tableId, .datasetId, .projectId)), .type] | @tsv') | |
} | |
cz_gcloud_dns_zone() { | |
req jq gcloud || return 255 | |
local proj="${1:-$(CZ_FIELDS="" cz -q gcloud project)}" | |
if [ -z "$proj" ]; then return 2; fi | |
cz -f 0 -e "gcloud --project='$proj' dns managed-zones describe {0}" \ | |
-i <(gcloud --project="$proj" --format=json dns managed-zones list | \ | |
jq -r '.[] | [.name, .dnsName, .visibility, .description] | @tsv') | |
} | |
cz_gcloud_dns_record() { | |
req jq gcloud || return 255 | |
local proj="${1:-$(CZ_FIELDS="" cz -q gcloud project)}" | |
local zone="${2:-$(CZ_FIELDS="" cz -q gcloud dns zone)}" | |
if [ -z "$proj" ] || [ -z "$zone" ]; then return 2; fi | |
cz -f 0,1 \ | |
-i <(gcloud --project="$proj" --format=json dns record-sets list -z "$zone" | \ | |
jq -r '.[] | [.type, .name, .ttl, .rrdatas[]] | @tsv') | |
} | |
cz_gcloud_project() { | |
req gcloud jq || return 255 | |
cz -f 0 -e 'gcloud projects describe {0}' \ | |
-i <(gcloud --format=json projects list | jq -r '.[] | [.projectId, .name] | @tsv') | |
} | |
cz_gcloud_role() { | |
req gcloud jq || return 255 | |
local proj="${1:-$(cz -q gcloud project)}" | |
if [ -z "$proj" ]; then return 2; fi | |
cz -f 0 -e "gcloud --project='$proj' iam roles describe {0}" \ | |
-i <(gcloud --project="$proj" --format=json iam roles list | jq -r '.[] | [.name, .description] | @tsv') | |
} | |
cz_gcloud_compute_instance() { | |
req gcloud jq || return 255 | |
local proj="${1:-$(cz -q gcloud project)}" | |
if [ -z "$proj" ]; then return 2; fi | |
cz -f 1 -e "gcloud --project='$proj' compute instances describe --zone {2} {1}" \ | |
-i <(gcloud --project="$proj" --format=json compute instances list \ | |
| jq -r '.[] | [.id, .name, (.zone | split("/")[-1]), .networkInterfaces[0].networkIP // "-", .accessConfigs[0].natIP // "-", .status] | @tsv') | |
} | |
cz_gcloud_serviceaccount() { | |
req gcloud jq || return 255 | |
local proj="${1:-$(cz -q gcloud project)}" | |
if [ -z "$proj" ]; then return 2; fi | |
cz -f 0 -e "gcloud --project='$proj' iam service-accounts describe {0}" \ | |
-i <(gcloud --project="$proj" --format=json iam service-accounts list | jq -r '.[] | [.email, .displayName, .email] | @tsv') | |
} | |
cz_gcloud_auth() { | |
req gcloud jq || return 255 | |
cz -f 0 -i <(gcloud auth list --format=json | jq -r '.[] | [.account, .status] | @tsv') | |
} | |
cz_gcloud_bucket() { | |
req gsutil || return 255 | |
cz -i <(gsutil ls) | |
} | |
cz_gcloud_bucket_file() { | |
req gsutil || return 255 | |
local b; b="$(cz gcloud bucket)" | |
if [ -z "$b" ]; then return 2; fi | |
cz -f 2 -i <(gsutil ls -lr "$b"'**' | sed '$d') | |
} | |
cz_mimetype() { | |
req grep /etc/mime.types || return 255 | |
cz -f 0 -i <(grep -v -e '^#' -e '^$' /etc/mime.types) | |
} | |
cz_locate() { | |
req locate || return 255 | |
cz -0 -i <(locate -i -0 "${@:-$PWD}") | |
} | |
cz_locate_mimetype() { | |
req locate || return 255 | |
local e=($(cz -q -f 1: mimetype)) | |
if ! ((${#e})); then return 2; fi | |
cz locate "${e[@]/#/*.}" | |
} | |
cz_locate_regex() { | |
req locate || return 255 | |
cz -0 -i <(locate -i -0 -r "${@:-.}") | |
} | |
cz_xrandr_monitor() { | |
req xrandr sed || return 255 | |
cz -f 0 -i <(xrandr --listmonitors | sed '1d') | |
} | |
cz_xrandr_provider() { | |
req xrandr sed || return 255 | |
cz -f 0 -i <(xrandr --listproviders | sed '1d') | |
} | |
cz_jq_json() { | |
req jq || return 255 | |
if ! [ -f "$1" ]; then return 2; fi | |
cz -e "jq -r {0} $1" -i <(jq -r '"." + (path(..) | map("[\(tojson)]") | join(""))' "$1") | |
} | |
cz_jq_yaml() { | |
req jq python3 || return 255 | |
local f="$1" | |
if ! [ -f "$f" ]; then return 2; fi | |
local t="/tmp/${f##*/}.json" | |
python3 -c 'import sys,json,yaml; print(" ".join([json.dumps(x) for x in yaml.load_all(sys.stdin, Loader=yaml.SafeLoader)]))' < "$f" > "$t" | |
cz jq json "$t" | |
} | |
cz_xml_element() { | |
req xmlstarlet || return 255 | |
if ! [ -f "$1" ]; then return 1; fi | |
cz -f 0 -e "xmlstarlet sel -B -t -c {0} $1" < <(xmlstarlet el -v "$1") | |
} | |
cz_xml_value() { | |
req xmlstarlet || return 255 | |
if ! [ -f "$1" ]; then return 1; fi | |
cz -f 0 -e "xmlstarlet sel -B -t -v {0} $1" < <(xmlstarlet el -v "$1") | |
} | |
cz_avconv_format() { | |
req avconv sed || return 255 | |
cz -f 1 -i <(avconv -loglevel 0 -formats | sed 1,4d) | |
} | |
cz_avconv_encoder() { | |
req avconv sed || return 255 | |
cz -f 1 -i <(avconv -loglevel 0 -encoders | sed 1,10d) | |
} | |
cz_avconv_decoder() { | |
req avconv sed || return 255 | |
cz -f 1 -i <(avconv -loglevel 0 -decoders | sed 1,10d) | |
} | |
cz_sysctl() { | |
req sysctl || return 255 | |
cz -f 0 -i <(sysctl -a 2>/dev/null) | |
} | |
cz_dict_strategy() { | |
req dict sed || return 255 | |
cz -f 0 -i <(dict -S | sed 1d) | |
} | |
cz_dict_word() { | |
req dict || return 255 | |
local qry="${1:-.*}" | |
cz -f 3: -e 'dict {3:}' -i <(dict -m -f -s re "$qry" ) | |
} | |
cz_pci() { | |
req lspci || return 255 | |
cz -f 0 -i <(lspci) | |
} | |
cz_usb() { | |
req lsusb || return 255 | |
cz -e 'lsusb -v -d {}' -f 5 -i <(lsusb) | |
} | |
cz_figlet_font() { | |
req figlet || return 255 | |
local fdir="${FIGLET_FONTDIR:-/usr/share/figlet}" | |
cz -f 0 -e "figlet -t -d $fdir -f {0}" \ | |
-i <(for f in "$fdir"/*f; do | |
printf "%s %s\n" "${f##*/}" "${f}" | |
done) | |
} | |
cz_x11_rgb() { | |
req /etc/X11/rgb.txt grep || return 255 | |
cz -i <(grep -v '^!' /etc/X11/rgb.txt) | |
} | |
cz_x11_window() { | |
req sort xwininfo || return 255 | |
local menu=() | |
while IFS= read -r l; do | |
if [[ "$l" =~ ^[[:space:]]+?(0x[[:alnum:]]+)[[:space:]](.*) ]]; then | |
menu+=("${BASH_REMATCH[1]} ${BASH_REMATCH[2]}") | |
fi | |
done < <(xwininfo -children -root) | |
cz -f 0 -i <(printf "%s\n" "${menu[@]}" | sort) | |
} | |
cz_ssh_key() { | |
req ssh-add || return 255 | |
cz -f 2 -i < <(ssh-add -l) | |
} | |
cz_ssh_host() { | |
req ssh sed || return 255 | |
cz -f 0 -e 'ssh {0}' -i <(sed -E -n 's/^\s*Host\s+(.*)\s*/\1/ip' < "$HOME/.ssh/config") | |
} | |
cz_timezone() { | |
local p="/usr/share/zoneinfo/posix" | |
if req date timedatectl 2>/dev/null; then | |
cz -f 0 -e 'TZ={0} date' \ | |
-i <(timedatectl list-timezones) | |
elif req date "$p" sort 2>/dev/null; then | |
cz -f 0 -e 'TZ={0} date' \ | |
-i <(for x in "$p"/*/*; do printf "%s\n" "${x##$p/}"; done | sort) | |
else | |
req systemd "$p" | |
fi | |
} | |
cz_fc_font() { | |
req fc-list || return 255 | |
cz -f 0 -d : -i <(fc-list) | |
} | |
cz_unicode() { | |
cz unicode character | |
} | |
cz_unicode_block() { | |
req perl || return 255 | |
cz -f 0: -i <(perl -MUnicode::UCD=charblocks -E 'say for sort keys %{charblocks()}') | |
} | |
cz_unicode_character() { | |
req perl || return 255 | |
local block="${1:-$(cz unicode block)}" | |
if [ -z "$block" ]; then return 2; fi | |
cz -f 0 -d : \ | |
-i <(perl -MUnicode::UCD=charinfo,charblock -E 'use open ":std", ":encoding(UTF-8)";' \ | |
-E 'my ($s, $e) = @{charblock(shift)->[0]}; say join ":", chr hex $_->{code}, $k, $_->{name} for map { charinfo($_) } ($s .. $e)' "$block") | |
} | |
# run it unless sourced | |
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then cz "$@"; fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment