X-Git-Url: https://git.xandkar.net/?a=blobdiff_plain;f=home%2Flib%2Flogin_functions.sh;h=56e0c2b7a34457404fc23e26ea4e934bcaf783ec;hb=HEAD;hp=c15ecefafd0c3479a45317737e34ef2b1d5feecb;hpb=a744f57500432e500bd602372e98da670cedf244;p=khome.git diff --git a/home/lib/login_functions.sh b/home/lib/login_functions.sh index c15ecef..56e0c2b 100644 --- a/home/lib/login_functions.sh +++ b/home/lib/login_functions.sh @@ -1,10 +1,85 @@ -# +#! /bin/bash + +## open : string -> unit +## +## Fork xdg-open so we don't block current terminal session when opening +## things like pdf files. For example: +## +## open book.pdf +## +open() { + (xdg-open "$1" &) & +} + +## notify_done : unit -> unit +notify_done() { + local -r _status_code="$?" + local -r _program="$1" + local _timestamp + _timestamp="$(timestamp)" + local -r _msg="$_timestamp [$_program] done " + if [[ "$_status_code" -eq 0 ]] + then + notify-send -u normal "$_msg OK: $_status_code" + else + notify-send -u critical "$_msg ERROR: $_status_code" + fi +} + +_dl_script() { +cat << EOF +#! /bin/bash +wget -c \$(< ./url) +EOF +} + +dl() { + local -r name="$1" + local -r url="$2" + + local -r timestamp="$(date --iso-8601=ns)" + local -r dir="$HOME"/dl/adhoc/"$timestamp"--"$name" + local -r url_file_path="${dir}/url" + local -r dl_file_path="${dir}/dl" + mkdir -p "$dir" + touch "$url_file_path" + if [ "$url" != '' ] + then + echo "$url" > "$url_file_path" + fi + _dl_script > "$dl_file_path" + chmod +x "$dl_file_path" + cd "$dir" +} + +## web search +## ws : string -> unit +ws() { + local line search_string0 search_string + + search_string0="$*" + case "$search_string0" in + '') + while read -r line; do + search_string="${search_string} ${line}" + done;; + *) + search_string="$search_string0";; + esac + + firefox --search "$search_string" +} + + +## dictionary +## d : string -> string list d() { local -r word=$(fzf < /usr/share/dict/words) dict "$word" } +## shell_activity_report : (mon | dow) -> string list shell_activity_report() { # TODO: optional concrete number output # TODO: optional combinations of granularities: hour, weekday, month, year @@ -102,6 +177,7 @@ shell_activity_report() { }' } +## top_commands : unit -> (command:string * count:number * bar:string) list top_commands() { history \ | awk ' @@ -136,46 +212,35 @@ top_commands() { | column -t } -# Top Disk-Using directories -# TODO: Consider using numfmt instead of awk +## Top Disk-Using directories +## tdu : path-string -> (size:number * directory:path-string) list tdu() { - du "$1" \ - | sort -n -k 1 \ - | tail -50 \ - | awk ' - { - size = $1 - path = $0 - sub("^" $1 "\t+", "", path) - gb = size / 1024 / 1024 - printf("%f\t%s\n", gb, path) - }' \ - | cut -c 1-115 + local -r root_path="$1" + + du -h "$root_path" \ + | sort -r -h -k 1 \ + | head -50 \ + | tac + # A slight optimization: head can exit before traversing the full input. } -# Top Disk-Using Files +## Top Disk-Using Files +## tduf : path-string list -> (size:number * file:path-string) list tduf() { - find "$1" -type f -printf '%s\t%p\0' \ + find "$@" -type f -printf '%s\t%p\0' \ | sort -z -n -k 1 \ | tail -z -n 50 \ - | gawk -v RS='\0' ' - { - size = $1 - path = $0 - sub("^" $1 "\t+", "", path) - gb = size / 1024 / 1024 / 1024 - printf("%f\t%s\n", gb, path) - }' + | numfmt -z --to=iec \ + | tr '\0' '\n' } # Most-recently modified file system objects +## recent : ?(path-string list) -> path-string list recent() { # NOTES: - # - intentionally not quoting the parameters, so that some can be ignored - # if not passed, rather than be passed to find as an empty string; # - %T+ is a GNU extension; # - gawk is able to split records on \0, while awk cannot. - find $@ -printf '%T@ %T+ %p\0' \ + find "$@" -printf '%T@ %T+ %p\0' \ | tee >(gawk -v RS='\0' 'END { printf("[INFO] Total found: %d\n", NR); }') \ | sort -z -k 1 -n -r \ | head -n "$(stty size | awk 'NR == 1 {print $1 - 5}')" -z \ @@ -188,23 +253,28 @@ recent() { }' } +## recent_dirs : ?(path-string list) -> path-string list recent_dirs() { - recent "$1" -type d + recent "$@" -type d } +## recent_files : ?(path-string list) -> path-string list recent_files() { - recent "$1" -type f + recent "$@" -type f } +## pa_def_sink : unit -> string pa_def_sink() { pactl info | awk '/^Default Sink:/ {print $3}' } +## void_pkgs : ?(string) -> json void_pkgs() { curl "https://xq-api.voidlinux.org/v1/query/x86_64?q=$1" | jq '.data' } -# Colorful man +## Colorful man +## man : string -> string man() { # mb: begin blink # md: begin bold @@ -225,47 +295,66 @@ man() { command man "$@" } -experiment() { - cd "$(~/bin/experiment $@)" || kill -INT $$ +## new experiment +## x : string list -> unit +x() { + cd "$(~/bin/x $@)" || kill -INT $$ } +## ocaml repl +## hump : unit -> unit hump() { ledit -l "$(stty size | awk '{print $2}')" ocaml $@ } +## search howtos +## howto : unit -> string howto() { - cat "$(find ~/Archives/Documents/HOWTOs -mindepth 1 -maxdepth 1 | sort | fzf)" + cat "$(find ~/arc/doc/HOWTOs -mindepth 1 -maxdepth 1 | sort | fzf)" } -yt() { - local _yt_uri - local _yt_id - local _yt_title - local _yt_dir +_yt() { + local -r base_dir="$1" + local -r uri="$2" + local -r opts="$3" - _yt_uri="$1" - _yt_id=$(youtube-dl --get-id "$_yt_uri") - _yt_title=$(youtube-dl --get-title "$_yt_uri") - _yt_dir="${DIR_YOUTUBE}/individual-videos/${_yt_title}--${_yt_id}" + local -r yt=youtube-dl + local -r id=$("$yt" --get-id "$uri") + local -r title=$("$yt" --get-title "$uri" | sed 's/[^А-Яа-яA-Za-z0-9._-]/_/g') + local -r dir="${base_dir}/${title}--${id}" - mkdir -p "$_yt_dir" - cd "$_yt_dir" || kill -INT $$ - echo "$_yt_uri" > 'uri' - youtube-dl -c --write-description --write-info-json "$_yt_uri" + mkdir -p "$dir" + cd "$dir" || kill -INT $$ + echo "$uri" > 'uri' + "$yt" $opts -c --write-all-thumbnails --write-description --write-info-json "$uri" +} + +yt_audio() { + local -r uri="$1" + _yt "${DIR_YOUTUBE_AUDIO}/individual" "$uri" '-f 140' +} + +yt_video() { + local -r uri="$1" + _yt "${DIR_YOUTUBE_VIDEO}/individual" "$uri" } gh_fetch_repos() { - curl "https://api.github.com/$1/$2/repos?page=1&per_page=10000" + local -r user_type="$1" + local -r user_name="$2" + + curl "https://api.github.com/$user_type/$user_name/repos?page=1&per_page=10000" } gh_clone() { - gh_user_type="$1" - gh_user_name="$2" - gh_dir="${DIR_GITHUB}/${gh_user_name}" + local -r gh_user_type="$1" + local -r gh_user_name="$2" + + local -r gh_dir="${DIR_GITHUB}/${gh_user_name}" mkdir -p "$gh_dir" cd "$gh_dir" || kill -INT $$ gh_fetch_repos "$gh_user_type" "$gh_user_name" \ - | jq --raw-output '.[] | select(.fork | not) | .git_url' \ + | jq --raw-output '.[] | select(.fork | not) | .clone_url' \ | parallel -j 25 \ git clone {} } @@ -286,6 +375,59 @@ gh_clone_repo() { git clone "$1" } +bar() { + local -r len="${1:-79}" # 1st arg or 79. + local -r char="${2:--}" # 2nd arg or a dash. + for _ in {1.."$len"}; do + printf '%c' "$char"; + done +} + +daily_todo_file_template() { +cat << EOF +=============================================================================== +$(date '+%F %A') +=============================================================================== + +------------------------------------------------------------------------------- +TODAY +------------------------------------------------------------------------------- + + +------------------------------------------------------------------------------- +CURRENT +------------------------------------------------------------------------------- + + +------------------------------------------------------------------------------- +BLOCKED +------------------------------------------------------------------------------- + + +------------------------------------------------------------------------------- +BACKLOG +------------------------------------------------------------------------------- +EOF +} + +today() { + local date + date="$(date +%F)" + local -r dir="$DIR_TODO/daily" + local -r file="$dir/$date.txt" + + mkdir -p "$dir" + if [ ! -f "$file" ] + then + daily_todo_file_template > "$file" + fi + cd "$DIR_TODO" && "$EDITOR" $EDITOR_ARGS "$file" +} + +todo() { + cd "$DIR_TODO" && "$EDITOR" TODO +} + work_log_template() { cat << EOF $(date '+%F %A') @@ -309,7 +451,7 @@ EOF work_log() { mkdir -p "$DIR_WORK_LOG" - file_work_log_today="${DIR_WORK_LOG}/$(date +%F).md" + local -r file_work_log_today="${DIR_WORK_LOG}/daily-$(date +%F).md" if [ ! -f "$file_work_log_today" ] then work_log_template > "$file_work_log_today" @@ -323,32 +465,29 @@ note() { vim -c 'set spell' "$DIR_NOTES/$(date +'%Y_%m_%d--%H_%M_%S%z')--$1.md" } -weather() { - local _weather_location - case "$1" in - '') _weather_location="$WEATHER_LOCATION";; - *) _weather_location="$1" - esac - curl "http://wttr.in/$_weather_location?format=v2" +_bt_devs_infos() { + # grep's defintion of a line does not include \r, wile awk's does and + # which bluetoothctl outputs + awk '/^Device +/ {print $2}' \ + | xargs -I% sh -c 'echo info % | bluetoothctl' \ + | awk '/^Device |^\t[A-Z][A-Za-z0-9]+: /' } bt_devs_paired() { - bluetoothctl -- paired-devices \ - | awk '{print $2}' \ - | xargs bluetoothctl -- info + echo 'paired-devices' | bluetoothctl | _bt_devs_infos } bt_devs() { - bluetoothctl -- devices \ - | awk '{print $2}' \ - | xargs bluetoothctl -- info + echo 'devices' | bluetoothctl | _bt_devs_infos } run() { - stderr="$(mktemp)" + local -r stderr="$(mktemp)" + + local code urgency + $@ 2> >(tee "$stderr") code="$?" - urgency='' case "$code" in 0) urgency='normal';; *) urgency='critical' @@ -356,3 +495,439 @@ run() { notify-send -u "$urgency" "Job done: $code" "$(cat $stderr)" rm "$stderr" } + +bar_gauge() { + awk "$@" ' + BEGIN { + # CLI options + width = width ? width : 80 + ch_left = ch_left ? ch_left : "[" + ch_right = ch_right ? ch_right : "]" + ch_blank = ch_blank ? ch_blank : "-" + ch_used = ch_used ? ch_used : "|" + num = num ? 1 : 0 + pct = pct ? 1 : 0 + } + + { + cur = $1 + max = $2 + lab = $3 + + cur_scaled = num_scale(cur, max, 1, width) + + printf \ + "%s%s%s%s", \ + lab ? lab " " : "", \ + num ? cur "/" max " " : "", \ + pct ? sprintf("%3.0f%% ", cur / max * 100) : "", \ + ch_left + for (i=1; i<=width; i++) { + c = i <= cur_scaled ? ch_used : ch_blank + printf "%s", c + } + printf "%s\n", ch_right + } + + function num_scale(src_cur, src_max, dst_min, dst_max) { + return dst_min + ((src_cur * (dst_max - dst_min)) / src_max) + } + ' +} + +flat_top_5() { + sort -n -k 1 -r \ + | head -5 \ + | awk ' + { + cur = $1 + max = $2 + name = $3 + pct = cur / max * 100 + printf "%s%s %.2f%%", sep, name, pct + sep = ", " + } + + END {printf "\n"} + ' +} + +internet_addr() { + curl --silent --show-error --max-time "${1:=1}" 'https://api.ipify.org' 2>&1 +} + +status_batt() { + case "$(uname)" in + 'Linux') + if which upower > /dev/null + then + upower --dump \ + | awk ' + /^Device:[ \t]+/ { + device["path"] = $2 + next + } + + / battery/ && device["path"] { + device["is_battery"] = 1 + next + } + + / percentage:/ && device["is_battery"] { + device["battery_percentage"] = $2 + sub("%$", "", device["battery_percentage"]) + next + } + + /^$/ { + if (device["is_battery"] && device["path"] == "/org/freedesktop/UPower/devices/DisplayDevice") + print device["battery_percentage"], 100, "batt" + delete device + } + ' + fi + ;; + esac +} + +indent() { + awk -v unit="$1" '{printf "%s%s\n", unit, $0}' +} + +status() { + local -r indent_unit=' ' + + uname -srvmo + hostname | figlet + uptime + + echo + + echo 'accounting' + + # TODO Bring back seesion and client listing, but per server/socket. + printf '%stmux\n' "$indent_unit" + ps -eo comm,cmd \ + | awk ' + # Expecting lines like: + # "tmux: server tmux -L pistactl new-session -d -s pistactl" + # "tmux: client tmux -L foo" + # "tmux: client tmux -Lbar" + # "tmux: client tmux" + # "tmux: server tmux -L foo -S bar" <-- -S takes precedence + /^tmux:/ { + # XXX This of course assumes pervasive usage of -L + # TODO Handle -S + role=$2 + + split($0, sides_of_S, "-S") + split(sides_of_S[2], words_right_of_S, FS) + + split($0, sides_of_L, "-L") + split(sides_of_L[2], words_right_of_L, FS) + + if (words_right_of_S[1]) { + sock = "path." words_right_of_S[1] + } else if (words_right_of_L[1]) { + sock = "name." words_right_of_L[1] + } else { + sock = "default" + } + + roles[role]++ + socks[sock]++ + count[role, sock]++ + } + + END { + for (sock in socks) { + clients = count["client", sock] + printf "%s ", sock + if (clients) { + printf "<-> %d", clients + } + printf "\n" + } + printf "\n" + }' \ + | sort \ + | column -t \ + | indent "${indent_unit}${indent_unit}" + + echo + + printf '%sprocs by user\n' "${indent_unit}" + ps -eo user \ + | awk ' + NR > 1 { + count_by_user[$1]++ + total++ + } + + END { + for (user in count_by_user) + print count_by_user[user], total, user + } + ' \ + | flat_top_5 \ + | indent "${indent_unit}${indent_unit}" + + echo + + echo 'resources' + ( + free | awk '$1 == "Mem:" {print $3, $2, "mem"}' + df ~ | awk 'NR == 2 {print $3, $3 + $4, "disk"}' + status_batt + ) \ + | bar_gauge -v width=60 -v pct=1 \ + | column -t \ + | indent "$indent_unit" + + echo + + printf '%smem by proc\n' "$indent_unit" + ps -eo rss,comm \ + | awk -v total="$(free | awk '$1 == "Mem:" {print $2; exit}')" ' + NR > 1 { + rss = $1 + proc = $2 + by_proc[proc] += rss + } + + END { + for (proc in by_proc) + print by_proc[proc], total, proc + } + ' \ + | flat_top_5 \ + | indent "${indent_unit}${indent_unit}" + + echo + + local _dir temp_input label_file label + + printf '%sthermal\n' "$indent_unit" + for _dir in /sys/class/hwmon/hwmon*; do + cat "$_dir"/name + find "$_dir"/ -name 'temp*_input' \ + | while read -r temp_input; do + label_file=${temp_input//_input/_label} + if [ -f "$label_file" ]; then + label=$(< "$label_file") + else + label='' + fi + awk -v label="$label" '{ + if (label) + label = sprintf(" (%s)", label) + printf("%.2f°C%s\n", $1 / 1000, label) + }' \ + "$temp_input" + done \ + | sort \ + | indent "$indent_unit" + done \ + | indent "${indent_unit}${indent_unit}" + + echo 'net' + #local -r internet_addr=$(internet_addr 0.5) + #local -r internet_ptr=$(host -W 1 "$internet_addr" | awk 'NR == 1 {print $NF}' ) + + #echo "${indent_unit}internet" + #echo "${indent_unit}${indent_unit}$internet_addr $internet_ptr" + echo "${indent_unit}if" + (ifconfig; iwconfig) 2> /dev/null \ + | awk ' + /^[^ ]/ { + device = $1 + sub(":$", "", device) + if ($4 ~ "ESSID:") { + _essid = $4 + sub("^ESSID:\"", "", _essid) + sub("\"$", "", _essid) + essid[device] = _essid + } + next + } + + /^ / && $1 == "inet" { + address[device] = $2 + next + } + + /^ +Link Quality=[0-9]+\/[0-9]+ +Signal level=/ { + split($2, lq_parts_eq, "=") + split(lq_parts_eq[2], lq_parts_slash, "/") + cur = lq_parts_slash[1] + max = lq_parts_slash[2] + link[device] = cur / max * 100 + next + } + + END { + for (device in address) + if (device != "lo") { + l = link[device] + e = essid[device] + l = l ? sprintf("%.0f%%", l) : "--" + e = e ? e : "--" + print device, address[device], e, l + } + } + ' \ + | column -t \ + | indent "${indent_unit}${indent_unit}" + + # WARN: ensure: $USER ALL=(ALL) NOPASSWD:/bin/netstat + + echo "${indent_unit}-->" + + # TODO Populate pid->cmd dict from `ps -eo pid,comm` and lookup progs there + # since netstat -p output comes out truncated. + + sudo -n netstat -tulnp \ + | awk -v indent="${indent_unit}${indent_unit}" ' + NR > 2 && ((/^tcp/ && proc = $7) || (/^udp/ && proc = $6)) { + protocol = $1 + addr = $4 + port = a[split(addr, a, ":")] + name = p[split(proc, p, "/")] + names[name] = 1 + protocols[protocol] = 1 + if (!seen[protocol, name, port]++) + ports[protocol, name, ++seen[protocol, name]] = port + } + + END { + for (protocol in protocols) { + printf "%s%s\t", indent, toupper(protocol) + for (name in names) { + if (n = seen[protocol, name]) { + sep = "" + printf "%s:", name + for (i = 1; i <= n; i++) { + printf "%s%d", sep, ports[protocol, name, i] + sep = "," + } + printf " " + } + } + printf "\n" + } + }' + + echo "${indent_unit}<->" + + printf '%sTCP\t' "${indent_unit}${indent_unit}" + sudo -n netstat -tnp \ + | awk 'NR > 2 && $6 == "ESTABLISHED" {print $7}' \ + | awk '{sub("^[0-9]+/", ""); print}' \ + | sort -u \ + | xargs \ + | column -t + + # TODO: iptables summary +} + +ssh_invalid_by_addr() { + awk ' + /: Invalid user/ && $5 ~ /^sshd/ { + addr=$10 == "port" ? $9 : $10 + max++ + by_addr[addr]++ + } + + END { + for (addr in by_addr) + if ((c = by_addr[addr]) > 1) + printf "%d %d %s\n", c, max, addr + } + ' \ + /var/log/auth.log \ + /var/log/auth.log.1 \ + | sort -n -k 1 \ + | bar_gauge -v width="$(stty size | awk '{print $2}')" -v num=1 -v ch_right=' ' -v ch_left=' ' -v ch_blank=' ' \ + | column -t +} + +ssh_invalid_by_day() { + awk ' + BEGIN { + m["Jan"] = "01" + m["Feb"] = "02" + m["Mar"] = "03" + m["Apr"] = "04" + m["May"] = "05" + m["Jun"] = "06" + m["Jul"] = "07" + m["Aug"] = "08" + m["Sep"] = "09" + m["Oct"] = "10" + m["Nov"] = "11" + m["Dec"] = "12" + } + + /: Invalid user/ && $5 ~ /^sshd/ { + day = m[$1] "-" $2 + max++ + by_day[day]++ + } + + END { + for (day in by_day) + if ((c = by_day[day]) > 1) + printf "%d %d %s\n", c, max, day + } + ' \ + /var/log/auth.log \ + /var/log/auth.log.1 \ + | sort -k 3 \ + | bar_gauge -v width="$(stty size | awk '{print $2}')" -v num=1 -v ch_right=' ' -v ch_left=' ' -v ch_blank=' ' \ + | column -t +} + +ssh_invalid_by_user() { + awk ' + /: Invalid user/ && $5 ~ /^sshd/ { + user=$8 + max++ + by_user[user]++ + } + + END { + for (user in by_user) + if ((c = by_user[user]) > 1) + printf "%d %d %s\n", c, max, user + } + ' \ + /var/log/auth.log \ + /var/log/auth.log.1 \ + | sort -n -k 1 \ + | bar_gauge -v width="$(stty size | awk '{print $2}')" -v num=1 -v ch_right=' ' -v ch_left=' ' -v ch_blank=' ' \ + | column -t +} + +loggers() { + awk ' + { + split($5, prog, "[") + sub(":$", "", prog[1]) # if there were no [], than : will is left behind + print prog[1] + }' /var/log/syslog /var/log/syslog.1 \ + | awk ' + { + n = split($1, path, "/") # prog may be in path form + prog = path[n] + total++ + count[prog]++ + } + + END { + for (prog in count) + print count[prog], total, prog + }' \ + | sort -n -k 1 \ + | bar_gauge -v num=1 -v ch_right=' ' -v ch_left=' ' -v ch_blank=' ' \ + | column -t +}