X-Git-Url: https://git.xandkar.net/?a=blobdiff_plain;f=home%2Flib%2Flogin_functions.sh;h=56e0c2b7a34457404fc23e26ea4e934bcaf783ec;hb=HEAD;hp=9f24d3e93968f0c81938a8c9484d43dc1eeb5186;hpb=ea9d43c930a633f22bf40c8f9b08abb1dce6bb8c;p=khome.git diff --git a/home/lib/login_functions.sh b/home/lib/login_functions.sh index 9f24d3e..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,36 +295,43 @@ 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 -r base_dir="$1" - local -r opts="$2" - local -r uri="$3" + local -r uri="$2" + local -r opts="$3" - local -r id=$(youtube-dlc --get-id "$uri") - local -r title=$(youtube-dlc --get-title "$uri" | sed 's/[^A-Za-z0-9._-]/_/g') + 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 "$dir" cd "$dir" || kill -INT $$ echo "$uri" > 'uri' - youtube-dlc $opts -c --write-description --write-info-json "$uri" + "$yt" $opts -c --write-all-thumbnails --write-description --write-info-json "$uri" } yt_audio() { local -r uri="$1" - _yt "${DIR_YOUTUBE_AUDIO}/individual" '-f 140' "$uri" + _yt "${DIR_YOUTUBE_AUDIO}/individual" "$uri" '-f 140' } yt_video() { @@ -263,7 +340,10 @@ yt_video() { } 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() { @@ -274,7 +354,7 @@ gh_clone() { 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 {} } @@ -295,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') @@ -318,7 +451,7 @@ EOF work_log() { mkdir -p "$DIR_WORK_LOG" - local -r 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" @@ -332,15 +465,6 @@ 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 @@ -375,10 +499,14 @@ run() { 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 } { @@ -389,9 +517,10 @@ bar_gauge() { cur_scaled = num_scale(cur, max, 1, width) printf \ - "%s%s%s", \ + "%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 @@ -406,33 +535,57 @@ bar_gauge() { ' } -motd_batt() { - case "$(uname)" in - 'Linux') - upower --dump \ - | awk ' - /^Device:[ \t]+/ { - device["path"] = $2 - next - } +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 = ", " + } - / battery/ && device["path"] { - device["is_battery"] = 1 - next - } + END {printf "\n"} + ' +} - / percentage:/ && device["is_battery"] { - device["battery_percentage"] = $2 - sub("%$", "", device["battery_percentage"]) - next - } +internet_addr() { + curl --silent --show-error --max-time "${1:=1}" 'https://api.ipify.org' 2>&1 +} - /^$/ { - if (device["is_battery"] && device["path"] == "/org/freedesktop/UPower/devices/DisplayDevice") - print device["battery_percentage"], 100, "batt" - delete device - } - ' +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 } @@ -441,8 +594,7 @@ indent() { awk -v unit="$1" '{printf "%s%s\n", unit, $0}' } -motd() { - local -r bar_width='60' +status() { local -r indent_unit=' ' uname -srvmo @@ -451,25 +603,139 @@ motd() { echo - printf 'tmux: sessions %d, clients %d\n' \ - "$(tmux list-sessions 2> /dev/null | wc -l)" \ - "$(tmux list-clients 2> /dev/null | wc -l)" + 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' + echo 'resources' ( free | awk '$1 == "Mem:" {print $3, $2, "mem"}' df ~ | awk 'NR == 2 {print $3, $3 + $4, "disk"}' - motd_batt + status_batt ) \ - | bar_gauge -v width="$bar_width" \ + | bar_gauge -v width=60 -v pct=1 \ | column -t \ | indent "$indent_unit" echo - echo 'Network' + 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 ' @@ -517,72 +783,122 @@ motd() { echo "${indent_unit}-->" - printf '%sUDP: ' "${indent_unit}${indent_unit}" - sudo -n netstat -ulnp \ - | awk 'NR > 2 {print $6}' \ - | awk -F/ '{print $2}' \ - | sort -u \ - | xargs \ - | column -t + # 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 + } - printf '%sTCP: ' "${indent_unit}${indent_unit}" - sudo -n netstat -tlnp \ - | awk 'NR > 2 {print $7}' \ - | awk -F/ '{print $2}' \ - | sort -u \ - | xargs \ - | column -t + 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: ' "${indent_unit}${indent_unit}" + printf '%sTCP\t' "${indent_unit}${indent_unit}" sudo -n netstat -tnp \ | awk 'NR > 2 && $6 == "ESTABLISHED" {print $7}' \ - | awk -F/ '{print $2}' \ + | awk '{sub("^[0-9]+/", ""); print}' \ | sort -u \ | xargs \ | column -t - echo + # TODO: iptables summary +} - echo 'Loggers' +ssh_invalid_by_addr() { awk ' - { - split($5, prog, "[") - sub(":$", "", prog[1]) # if there were no [], than : will is left behind - print prog[1] - }' /var/log/syslog \ - | awk ' - { - n = split($1, path, "/") # prog may be in path form - prog = path[n] - total++ - count[prog]++ + /: Invalid user/ && $5 ~ /^sshd/ { + addr=$10 == "port" ? $9 : $10 + max++ + by_addr[addr]++ } END { - for (prog in count) - print count[prog], total, prog - }' \ - | sort -n -k 1 -r \ - | bar_gauge -v width=30 -v num=1 -v ch_left=' ' -v ch_right=' ' -v ch_blank=' ' \ - | column -t \ - | indent "${indent_unit}" + 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_attempts_from() { +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/ { - u=$8 - addr=$10 == "port" ? $9 : $10 + 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++ - curr[addr]++ + by_user[user]++ } END { - for (addr in curr) - if ((c = curr[addr]) > 1) - print c, max, addr + for (user in by_user) + if ((c = by_user[user]) > 1) + printf "%d %d %s\n", c, max, user } ' \ /var/log/auth.log \ @@ -591,3 +907,27 @@ ssh_invalid_attempts_from() { | 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 +}