diff options
Diffstat (limited to 'bin')
-rwxr-xr-x | bin/80 | 85 | ||||
-rwxr-xr-x | bin/archiveit | 98 | ||||
-rwxr-xr-x | bin/assert-arg | 78 | ||||
-rwxr-xr-x | bin/backup | 120 | ||||
-rwxr-xr-x | bin/bins | 84 | ||||
-rwxr-xr-x | bin/boop | 83 | ||||
-rwxr-xr-x | bin/brightness | 12 | ||||
-rwxr-xr-x | bin/check | 58 | ||||
-rwxr-xr-x | bin/cl | 315 | ||||
-rwxr-xr-x | bin/clamp | 86 | ||||
-rwxr-xr-x | bin/color | 215 | ||||
-rwxr-xr-x | bin/copy | 67 | ||||
-rwxr-xr-x | bin/dice | 73 | ||||
-rwxr-xr-x | bin/e | 90 | ||||
-rwxr-xr-x | bin/email | 73 | ||||
-rwxr-xr-x | bin/forever | 68 | ||||
-rwxr-xr-x | bin/gc | 150 | ||||
-rwxr-xr-x | bin/gen-password | 73 | ||||
-rwxr-xr-x | bin/git-cleanup | 74 | ||||
-rwxr-xr-x | bin/grun | 96 | ||||
-rwxr-xr-x | bin/htmlesc | 96 | ||||
-rwxr-xr-x | bin/httpno | 156 | ||||
-rwxr-xr-x | bin/lc | 70 | ||||
-rwxr-xr-x | bin/li | 104 | ||||
-rwxr-xr-x | bin/lines | 81 | ||||
-rwxr-xr-x | bin/m | 66 | ||||
-rwxr-xr-x | bin/mailcfg | 297 | ||||
-rwxr-xr-x | bin/max | 84 | ||||
-rwxr-xr-x | bin/menu | 1587 | ||||
-rwxr-xr-x | bin/min | 85 | ||||
-rwxr-xr-x | bin/mkdtemp | 64 | ||||
-rwxr-xr-x | bin/mkstemp | 64 | ||||
-rwxr-xr-x | bin/msg | 153 | ||||
-rwxr-xr-x | bin/n-times | 73 | ||||
-rwxr-xr-x | bin/nato | 102 | ||||
-rwxr-xr-x | bin/ootb | 103 | ||||
-rwxr-xr-x | bin/open | 101 | ||||
-rwxr-xr-x | bin/player | 136 | ||||
-rwxr-xr-x | bin/playlist | 103 | ||||
-rwxr-xr-x | bin/pre | 78 | ||||
-rwxr-xr-x | bin/print | 151 | ||||
-rwxr-xr-x | bin/prompt | 76 | ||||
-rwxr-xr-x | bin/qr | 71 | ||||
-rwxr-xr-x | bin/repos | 175 | ||||
-rwxr-xr-x | bin/rfc | 150 | ||||
-rwxr-xr-x | bin/serve | 78 | ||||
-rwxr-xr-x | bin/slugify | 69 | ||||
-rwxr-xr-x | bin/status-bar | 104 | ||||
-rwxr-xr-x | bin/stopwatch | 65 | ||||
-rwxr-xr-x | bin/tmp | 91 | ||||
-rwxr-xr-x | bin/tmpname | 66 | ||||
-rwxr-xr-x | bin/tuivid | 65 | ||||
-rwxr-xr-x | bin/uc | 70 | ||||
-rwxr-xr-x | bin/untill | 83 | ||||
-rwxr-xr-x | bin/update | 73 | ||||
-rwxr-xr-x | bin/upgrade | 66 | ||||
-rwxr-xr-x | bin/uri | 75 | ||||
-rwxr-xr-x | bin/uuid | 62 | ||||
-rwxr-xr-x | bin/vcs | 255 | ||||
-rwxr-xr-x | bin/vm | 176 | ||||
-rwxr-xr-x | bin/volume | 112 | ||||
-rwxr-xr-x | bin/with-email | 91 | ||||
-rwxr-xr-x | bin/without-env | 70 | ||||
-rwxr-xr-x | bin/wms | 95 | ||||
-rwxr-xr-x | bin/x | 124 | ||||
l--------- | bin/xdg-open | 1 | ||||
-rwxr-xr-x | bin/xmpp | 99 | ||||
-rwxr-xr-x | bin/yt | 113 | ||||
-rwxr-xr-x | bin/z | 153 |
69 files changed, 8480 insertions, 0 deletions
@@ -0,0 +1,85 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + 80 [FILENAME...] + 80 -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + FILENAME the name of the file to work on (default: + the list of file in the VCS repository) + + + List the lines in the files that contain more than 80 columns. + + + Examples: + + Check for all the files in the current repository: + + $ 80 + + + Detect on the given two files: + + $ 80 f1 f2 + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +len() { + awk ' + length > 80 { + printf "%s:%s:%s\n", FILENAME, FNR, $0 + } + ' "$1" +} + +if [ $# = 0 ]; then + vcs ls-files | while read -r f; do + len "$f" + done +else + for f in "$@"; do + len "$f" + done +fi diff --git a/bin/archiveit b/bin/archiveit new file mode 100755 index 0000000..da132d7 --- /dev/null +++ b/bin/archiveit @@ -0,0 +1,98 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + archiveit + archiveit -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -h, --help show this message + + + Grab all bookmarks from Firefox that contains tags and archive + them with ArchiveBox. + + + Examples: + + Just run it: + + $ archiveit + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +# Derived from ArchiveBox: +# https://github.com/pirate/ArchiveBox/blob/db1f6efc934bbcdf53377bf51a064c6fd0fc5b1d/bin/archivebox-export-browser-history#L23-L37 +QUERY="$( + cat <<-'EOF' + SELECT json_object('timestamp', dateAdded, 'description', title, 'href', url) + FROM ( + SELECT b.dateAdded, b.title, p.url + FROM moz_bookmarks AS b + JOIN moz_places AS p + ON b.fk = p.id + WHERE b.fk IN ( + SELECT DISTINCT(fk) FROM moz_bookmarks + WHERE parent IN ( + -- get all tags + SELECT id FROM moz_bookmarks + WHERE parent = 4 + ) + ) + AND b.title IS NOT NULL + ORDER BY + b.dateAdded ASC, + b.title ASC, + p.url + ) + EOF +)" + +# Copy the database because it is locked. +DB="$(mkstemp)" +cp ~/.mozilla/firefox/*.default/places.sqlite "$DB" + +cd ~/Documents/Archive/ + +sqlite3 "$DB" "$QUERY" | archivebox add +archivebox update diff --git a/bin/assert-arg b/bin/assert-arg new file mode 100755 index 0000000..d7bc8f4 --- /dev/null +++ b/bin/assert-arg @@ -0,0 +1,78 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + assert-arg STRING MESSAGE + assert-arg -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + STRING the string to check if is empty + MESSAGE the message to print when STRING is empty + + + Examples: + + Assert that $1 contains an argument, named FILENAME: + + $ eval "$(assert-arg "${1:-}" 'FILENAME')" + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +STRING="${1:-}" +MESSAGE="${2:-}" + +if [ -z "$MESSAGE" ]; then + printf 'Missing MESSAGE, an argument to assert-arg.\n\n' >&2 + usage >&2 + exit 2 +fi + + +if [ -z "$STRING" ]; then + printf 'Missing %s.\n\n' "$MESSAGE" >&2 + cat <<-'EOF' + usage >&2 + exit 2 + EOF +fi diff --git a/bin/backup b/bin/backup new file mode 100755 index 0000000..be7d996 --- /dev/null +++ b/bin/backup @@ -0,0 +1,120 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + backup [-q] [-C COMMENT] [ARCHIVE_TAG] + backup -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -q disable verbose mode, useful for + non-interactive sessions + -C COMMENT the comment text to be attached to the archive + -h, --help show this message + + ARCHIVE_TAG the tag used to create the new + backup (default: "manual") + + + The repository is expected to have been created with: + + $ borg init -e repokey-blake2 + + The following environment variables are expected to be exported: + + $BORG_PASSCOMMAND + $BORG_REPO + $BORG_REMOTE_PATH + + Password-less SSH access is required, usually done via adding + ~/.ssh/id_rsa.pub to suyin:.ssh/authorized_keys. + + + Examples: + + Run backup manually: + + $ backup + + Create backup with comment, and verbose mode active: + + $ backup -qC 'The backup has a comment' my-backup + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +VERBOSE_FLAGS='--verbose --progress' +COMMENT='' +while getopts 'qC:h' flag; do + case "$flag" in + q) + VERBOSE_FLAGS='' + ;; + C) + COMMENT="$OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +ARCHIVE_TAG="${1:-manual}" + + +run() { + set -x + # The contents of $VERBOSE_FLAGS doesn't involve user input: + # shellcheck disable=2086 + borg create \ + $VERBOSE_FLAGS \ + --comment "$COMMENT" \ + --exclude "$XDG_CACHE_HOME" \ + --exclude ~/Downloads/ \ + --stats \ + --compression lzma,9 \ + "::{hostname}-{now}-$ARCHIVE_TAG" \ + ~/ + STATUS=$? + set +x + + if [ "$STATUS" = 0 ]; then + return 0 + elif [ "$STATUS" = 1 ]; then + printf 'WARNING, but no ERROR.\n' >&2 + return 0 + else + return "$STATUS" + fi +} + +run || exit $? diff --git a/bin/bins b/bin/bins new file mode 100755 index 0000000..9f869de --- /dev/null +++ b/bin/bins @@ -0,0 +1,84 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + bins [-F] + bins -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -F force remove the cache file + -h, --help show this message + + + List the available binaries in $PATH. The result is cached on + the $XDG_CACHE_HOME/euandreh/bins file if $PATH values didn't + change. + + + Examples: + + Pick an executable using `dmenu`: + + $ bins | dmenu + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +FORCE=false +while getopts 'Fh' flag; do + case "$flag" in + F) + FORCE=true + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND- 1)) + + +F="$XDG_CACHE_HOME/euandreh/bins" +if [ "$FORCE" = true ]; then + rm -f "$F" +fi + +IFS=: +# Word-splitting is the goal: +# shellcheck disable=2086 +if stest -rdq -n "$F" $PATH; then + T="$(mkstemp)" + trap 'rm -f $T' EXIT + stest -lxf $PATH | sort -u > "$T" + mv "$T" "$F" +fi + +cat "$F" diff --git a/bin/boop b/bin/boop new file mode 100755 index 0000000..a7792b3 --- /dev/null +++ b/bin/boop @@ -0,0 +1,83 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + boop [-m MESSAGE] -- COMMAND... + boop -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -m MESSAGE text message of the desktop + notification (default: COMMAND) + -h, --help show this message + + COMMAND the commands to be executed + + + Examples: + + Play the positive sound, using the command as message: + + $ boop echo 123 + + Fail with the underlying 127 return code with the + message "ERROR": + + $ boop -m ERROR ech 123 + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'm:h' flag; do + case "$flag" in + m) + MESSAGE="$OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +set +e +"$@" +STATUS=$? +set -e + +if [ "$STATUS" = 0 ]; then + N=0 +else + N=1 +fi + +CMD="$*" +msg -"$N" -bs -D "${MESSAGE:-$CMD}" + +exit "$STATUS" diff --git a/bin/brightness b/bin/brightness new file mode 100755 index 0000000..10bd198 --- /dev/null +++ b/bin/brightness @@ -0,0 +1,12 @@ +#!/bin/sh +set -eu + +BRIGHTNESS_DIFF="$1" + +HANDLER=/sys/class/backlight/"$BACKLIGHT_DEVICE" + +OLD_BRIGHTNESS="$(cat "$HANDLER"/brightness)" +MAX_BRIGHTNESS="$(cat "$HANDLER"/max_brightness)" +SUM=$((OLD_BRIGHTNESS + BRIGHTNESS_DIFF)) +NEW_BRIGHTNESS="$(clamp -- "$SUM" 0 "$MAX_BRIGHTNESS")" +echo "$NEW_BRIGHTNESS" > "$HANDLER"/brightness || sudo chmod 666 "$HANDLER"/brightness diff --git a/bin/check b/bin/check new file mode 100755 index 0000000..8348041 --- /dev/null +++ b/bin/check @@ -0,0 +1,58 @@ +#!/bin/sh +set -eu + + +usage() { + cat <<-'EOF' + Usage: + check + check -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -h, --help show this message + + + Run Makefile tests. This binary is available to simplify the + cronjob. + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +cd "$XDG_PREFIX" +make check @@ -0,0 +1,315 @@ +#!/bin/sh +set -eu + + +uuid() { + od -xN20 /dev/urandom | + head -n1 | + awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}' +} + +tmpname() { + echo "${TMPDIR:-/tmp}/cl.tmpfile.$(uuid)" +} + +mkstemp() { + name="$(tmpname)" + touch "$name" + echo "$name" +} + +escape_name() { + printf '%s' "$1" | + sed 's|"|\\"|g' | + printf '(load "%s")\n' "$(cat -)" +} + + +IMPLEMENTATIONS=' +abcl +allegro +clasp +clisp +clozure +cmucl +ecl +jscl +mkcl +sbcl +' + +usage() { + cat <<-'EOF' + Usage: + cl [-e EXP] [-f FILE] [-p] [-M IMAGE] [-I IMPL] [-n] [-v] [FILE...] [-- LISP_OPTIONS] + cl -l + cl -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -e EXP an sexp to be evaluated (can be given more than once) + -E EXP an sexp to be executed as a script + -f FILE a file to be evaluated (can be given more than once) + -p print the value of the last given expression + -M IMAGE load the given Lisp image + -I IMPL use the given implementation (default: $LISP_CLI_IMPL) + -n skip loading the implementation's init file + -v verbose mode + -l list the known types of implementations + -h, --help show this message + + FILE the file to be executed as a script + LISP_OPTIONS options to be forwarrded to the underlying Lisp command + + + Lauch the desired Lisp implementation, properly adapting the given + CLI options. + + When the implementation is not explicited on the command line, and + the $LISP_CLI_IMPL environment variable in unset, implementations are + searched for alphabetically in $PATH, untill one is found, otherwise + an error is emitted. + + The supported implementations are: + EOF + + + for i in $IMPLEMENTATIONS; do + printf -- '- %s\n' "$i" + done + + cat <<-'EOF' + + + Examples: + + Launch CLISP REPL, when $LISP_CLI_IMPL is set to 'clisp': + + $ cl + + + Lauch SBCL REPL, with the given Lisp image loaded, skipping the + loading of the '.sbclrc' file: + + $ cl -n -Isbcl sbcl.image + + + Run file1.lisp with ABCL: + + $ cl -Iabcl file1.lisp + + + Process STDIN: + + $ cat <<-EOS > process.lisp + + EOS + $ cat f1.txt f2.txt | cl -p process.lisp + + + Print a value on all types of implementations: + + $ for i in `cl -l`; do cl -I$i -pe 'call-arguments-list'; done + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +NO_RC=false +SCRIPT="$(mkstemp)" +LISP_CLI_RC="${XDG_CONFIG_HOME:-$HOME/.config}/lisp-cli/init.lisp" +VERBOSE=false +IMAGE='' +IMPL="${LISP_CLI_IMPL:-}" +INTERACTIVE=true +while getopts 'e:E:f:pM:I:nvlh' flag; do + case "$flag" in + e) + printf '%s\n' "$OPTARG" >> "$SCRIPT" + ;; + E) + printf '%s\n' "$OPTARG" >> "$SCRIPT" + INTERACTIVE=false + ;; + f) + escape_name "$OPTARG" >> "$SCRIPT" + ;; + M) + IMAGE="$OPTARG" + ;; + I) + IMPL="$OPTARG" + ;; + n) + NO_RC=true + ;; + v) + VERBOSE=true + ;; + l) + for i in $IMPLEMENTATIONS; do + printf '%s\n' "$i" + done + exit + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done + +shift $((OPTIND - 2)) +if [ "$1" != '--' ]; then + shift +fi + +PRESERVE_ARGS=false +for f in "$@"; do + if [ "$f" = '--' ]; then + PRESERVE_ARGS=true + shift + break + fi + INTERACTIVE=false + escape_name "$f" >> "$SCRIPT" +done + +if [ "$PRESERVE_ARGS" = false ]; then + set -- +fi + +MAIN="$(mkstemp)" +if [ "$NO_RC" = false ] && [ -e "$LISP_CLI_RC" ]; then + escape_name "$LISP_CLI_RC" > "$MAIN" +fi + +if [ "$INTERACTIVE" = true ]; then + escape_name "$SCRIPT" >> "$MAIN" +else + cat <<-EOF >> "$MAIN" + (handler-case + (progn + (load "$SCRIPT" + :verbose nil + :print nil) + (uiop:quit 0)) + (error (e) + (format *error-output* "~&~%error: ~a~%" e) + (uiop:quit 1))) + EOF +fi + +if [ -z "$IMPL" ]; then + for i in $IMPLEMENTATIONS; do + if command -v "$i" > /dev/null; then + IMPL="$i" + break + fi + done + if [ -z "$IMPL" ]; then + printf "Could not find any implementation in \$PATH.\n" >&2 + exit 2 + fi +fi + + +case "$IMPL" in + abcl) + exit 4 + ;; + allegro) + exit 4 + ;; + clasp) + exit 4 + ;; + clisp) + set -- -ansi -i "$MAIN" "$@" + if [ -n "$IMAGE" ]; then + set -- -M "$IMAGE" "$@" + fi + if [ "$NO_RC" = true ]; then + set -- -norc "$@" + fi + if [ "$VERBOSE" = false ]; then + set -- -q -q "$@" + else + set -x + fi + exec clisp "$@" + ;; + clozure) + set -- -l "$MAIN" "$@" + if [ -n "$IMAGE" ]; then + set -- -I "$IMAGE" "$@" + fi + if [ "$NO_RC" = true ]; then + set -- -n "$@" + fi + if [ "$VERBOSE" = false ]; then + set -- -Q "$@" + else + set -x + fi + exec ccl "$@" + ;; + cmucl) + exit 4 + ;; + ecl) + exit 4 + ;; + jscl) + exit 4 + ;; + mkcl) + exit 4 + ;; + sbcl) + set -- --load "$MAIN" "$@" + if [ -n "$IMAGE" ]; then + # The '--core' "C runtime option" must appear before the + # other "Lisp options", such as '--load'. + set -- --core "$IMAGE" "$@" + fi + if [ "$NO_RC" = true ]; then + set -- --no-sysinit --no-userinit "$@" + fi + if [ "$VERBOSE" = false ]; then + set -- --noinform "$@" + else + set -x + fi + exec sbcl "$@" + ;; + *) + printf 'Unsupported implementation: "%s".\n\n' "$IMPL" >&2 + usage >&2 + exit 2 + ;; +esac diff --git a/bin/clamp b/bin/clamp new file mode 100755 index 0000000..a673f72 --- /dev/null +++ b/bin/clamp @@ -0,0 +1,86 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + clamp NUMBER MIN MAX + clamp -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Clamp the NUMBER between MIN and MAX. + + + Examples: + + Assert the number is within the interval: + + $ clamp 5 3 9 + 5 + + When a number is below MIN it gets clamped: + + $ clamp 1 3 9 + 3 + + When a number is above MAX it gets clamped: + + $ clamp 15 3 9 + 9 + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +NUMBER="${1:-}" +MIN="${2:-}" +MAX="${3:-}" + +eval "$(assert-arg "$NUMBER" 'NUMBER')" +eval "$(assert-arg "$MIN" 'MIN')" +eval "$(assert-arg "$MAX" 'MAX')" + + +if [ "$MIN" -gt "$MAX" ]; then + printf 'MIN (%s) is greater then MAX (%s).\n' "$MIN" "$MAX" >&2 + exit 2 +fi + +min -- "$(max -- "$NUMBER" "$MIN")" "$MAX" diff --git a/bin/color b/bin/color new file mode 100755 index 0000000..dc610e9 --- /dev/null +++ b/bin/color @@ -0,0 +1,215 @@ +#!/bin/sh +# shellcheck disable=2059 +set -eu + +usage() { + cat <<-'EOF' + Usage: + color -c COLOR TEXT + color -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -c COLOR + -h, --help show this message + + + Print the given text with a color. + + The available colors are: + EOF + list_colors | sed 's/^/ /' + + cat <<-'EOF' + + Examples: + + Print "banana" in yellow: + + $ color -c yellow 'banana' + banana + + Print "grass" in green, with a newline: + + $ color -c green 'grass\n' + grass + EOF +} + + +END="\033[0m" + +black() { + BLACK="\033[0;30m" + printf "${BLACK}${1}${END}" +} + +blackb() { + BLACK_B="\033[1;30m" + printf "${BLACK_B}${1}${END}" +} + +blacki() { + BLACK_I="\033[0;90m" + printf "${BLACK_I}${1}${END}" +} + +white() { + WHITE="\033[0;37m" + printf "${WHITE}${1}${END}" +} + +whiteb() { + WHITE_B="\033[1;37m" + printf "${WHITE_B}${1}${END}" +} + +red() { + RED="\033[0;31m" + printf "${RED}${1}${END}" +} + +redb() { + RED_B="\033[1;31m" + printf "${RED_B}${1}${END}" +} + +green() { + GREEN="\033[0;32m" + printf "${GREEN}${1}${END}" +} + +greenb() { + GREEN_B="\033[1;32m" + printf "${GREEN_B}${1}${END}" +} + +yellow() { + YELLOW="\033[0;33m" + printf "${YELLOW}${1}${END}" +} + +yellowb() { + YELLOW_B="\033[1;33m" + printf "${YELLOW_B}${1}${END}" +} + +blue() { + BLUE="\033[0;34m" + printf "${BLUE}${1}${END}" +} + +blueb() { + BLUE_B="\033[1;34m" + printf "${BLUE_B}${1}${END}" +} + +bluei() { + BLUE_I="\033[0;94m" + printf "${BLUE_I}${1}${END}" +} + +purple() { + PURPLE="\033[0;35m" + printf "${PURPLE}${1}${END}" +} + + +purpleb() { + PURPLE_B="\033[1;35m" + printf "${PURPLE_B}${1}${END}" +} + +lightblue() { + LIGHT_BLUE="\033[0;36m" + printf "${LIGHT_BLUE}${1}${END}" +} + +lightblueb() { + LIGHT_BLUE_B="\033[1;36m" + printf "${LIGHT_BLUE_B}${1}${END}" +} + +COLOR_LIST=' +black +blackb +white +whiteb +red +redb +green +greenb +yellow +yellowb +blue +blueb +purple +purpleb +lightblue +lightblueb +blacki +bluei +' +list_colors() { + for c in $COLOR_LIST; do + printf '%s\n' "$("$c" "$c")" + done +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +COLOR_FN='' +while getopts 'c:h' flag; do + case "$flag" in + c) + EXISTS=false + for c in $COLOR_LIST; do + if [ "$OPTARG" = "$c" ]; then + EXISTS=true + break + fi + done + if [ "$EXISTS" = false ]; then + printf 'Invalid color: %s\n' "$OPTARG" >&2 + exit 2 + fi + COLOR_FN="$OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +TEXT="${1:-}" + +eval "$(assert-arg "$COLOR_FN" '-c COLOR')" +eval "$(assert-arg "$TEXT" 'TEXT')" + + +"$COLOR_FN" "$TEXT" diff --git a/bin/copy b/bin/copy new file mode 100755 index 0000000..64e1e32 --- /dev/null +++ b/bin/copy @@ -0,0 +1,67 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + copy [-n] < STDIN + copy -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -n remove newlines + -h, --help show this message + + Examples: + + Copy numbers to clipboard: + seq 10 | copy + + Copy string without newline: + echo 'with automatic newline' | copy -n + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +TRIM=false +while getopts 'nh' flag; do + case "$flag" in + n) + TRIM=true + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +if [ "$TRIM" = true ]; then + cat - | tr -d '\n' | xclip -sel clip +else + cat - | xclip -sel clip +fi diff --git a/bin/dice b/bin/dice new file mode 100755 index 0000000..4a145d0 --- /dev/null +++ b/bin/dice @@ -0,0 +1,73 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + dice [SIZE] + dice -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + SIZE the size of the dice (default: 6) + + + Roll a dice of SIZE. Caveat: rolling a dice more than once in + the same second will give you the same number. + + + Examples: + + Roll a dice of size 6: + + $ dice + 3 + + Roll a D20: + + $ dice 20 + 15 + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +SIZE="${1:-6}" +RAND="$(awk 'BEGIN { srand(); print int(rand()*32768) }' /dev/null)" +echo $(((RAND % SIZE) + 1)) @@ -0,0 +1,90 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + e [FILE] + e -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help + + + Flexibly run a text editor, either directly on in a pipe. + + Examples: + + Edit "file.txt": + + $ e file.txt + + Manipulate the content of a pipe midway: + + $ seq 10 | e | grep 5 + + The editor used is either $VISUAL or $EDITOR, with a fallback to + vi in case any of those variables aren't defined. + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +CMD="${VISUAL:-${EDITOR:-vi}}" +if [ "$CMD" = 'e' ]; then + CMD='vi' +fi + +if [ ! -t 0 ]; then + F="$(mkstemp)" + cat > "$F" + exec 0</dev/tty + exec 3>&1 + exec 1>/dev/tty + $CMD "$F" + cat "$F" >&3 +else + if [ $# -eq 0 ]; then + f="$(fzf --select-1 --exit-0 < "$XDG_DATA_HOME"/euandreh/e.list.txt)" + if [ -n "$f" ]; then + history -s e "$f" + sh -c "$CMD $f" + fi + else + $CMD "$@" + fi +fi diff --git a/bin/email b/bin/email new file mode 100755 index 0000000..87d1cec --- /dev/null +++ b/bin/email @@ -0,0 +1,73 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + email -s SUBJECT ADDRESS... < BODY + email -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -s SUBJECT the email subject + -h, --help show this message + + ADDRESS the email addresses to send the email to + BODY the text to be sent as the body + + + Send an email to ADDRESS using BODY. + + + Examples: + + Send 10 numbers to mail@example.com: + + $ seq 10 | email -s number mail@email.com + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 's:h' flag; do + case "$flag" in + s) + SUBJECT="$OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +eval "$(assert-arg "${SUBJECT:-}" '-s SUBJECT')" +eval "$(assert-arg "${1:-}" 'ADDRESS')" + +printf 'Content-Type: text/plain; charset=UTF-8\nSubject: %s\n\n%s' \ + "$(echo "$SUBJECT" | tr -d '\n')" \ + "$(cat)" | + msmtpq -a EuAndreh "$@" diff --git a/bin/forever b/bin/forever new file mode 100755 index 0000000..d4410e5 --- /dev/null +++ b/bin/forever @@ -0,0 +1,68 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + forever -- COMMAND... + forever -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Run COMMAND forever. + + + Examples: + + Print 123 forever: + + $ forever echo 123 + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +eval "$(assert-arg "${1:-}" 'COMMAND')" + + +while true; do + STATUS=0 + "$@" || STATUS=$? + printf 'Exitted with code %s.\n' "$STATUS" >&2 +done @@ -0,0 +1,150 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + gc [TYPE] + gc -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + TYPE what to do GC on (default: all): + - guix + - nohup + - trash + - tmpdir + - docker + - email + - vcs + + + Free disk space system-wide. + + + Examples: + + Just run it, for all: + + $ gc + + + Cleanup tmpdir: + + $ gc tmpdir + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +disk() { + df -h . | + awk 'NR == 2 { printf "%s - %s/%s\n", $4, $3, $2 }' +} + + +gc_guix() { + pass show velhinho/0-andreh-password | head -n1 | sudo -iS guix system delete-generations + pass show velhinho/0-andreh-password | head -n1 | sudo -iS guix gc --optimize -d + guix home delete-generations + guix gc --optimize -d +} + +gc_nohup() { + find ~/ -type f -name 'nohup.out' -exec rm -vf "{}" \; +} + +gc_trash() { + yes | trash-empty +} + +gc_tmpdir() { + find "${TMPDIR:-/tmp}" -type f -atime +10 -exec rm -vf "{}" \; ||: +} + +gc_docker() { + if command -v docker; then + yes | docker system prune -a + docker rmi "$(docker images -a -q)" ||: + docker rm "$(docker ps -a -f status=exited -q)" ||: + docker stop "$(docker ps -a -q)" ||: + docker rm "$(docker ps -a -q)" ||: + yes | docker volume prune + yes | docker container prune + fi +} + +gc_email() { + notmuch search --output=files --exclude=false tag:killed | + xargs -I{} rm -vf "{}" +} + +gc_vcs() { + repos -e ~/dev/go/ -e ~/dev/quicklisp/ -e ~/dev/archive/ ~/dev/ | + xargs -I% -P4 x vcs -C% gc OR true +} + + +gc_all() { + set -x + gc_guix + gc_nohup + gc_trash + gc_tmpdir + gc_docker + gc_email + gc_vcs + set +x +} + +TYPE="${1:-all}" +CMD=gc_"$TYPE" +if ! command -v "$CMD" >/dev/null; then + printf 'Invalid TYPE: "%s".\n\n' "$TYPE" >&2 + usage >&2 + exit 2 +fi + + +BEFORE="$(disk)" +"$CMD" +AFTER="$(disk)" + +printf 'Disk space:\n' +printf ' before: %s\n' "$BEFORE" +printf ' after: %s\n' "$AFTER" diff --git a/bin/gen-password b/bin/gen-password new file mode 100755 index 0000000..327858f --- /dev/null +++ b/bin/gen-password @@ -0,0 +1,73 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + gen-password [LENGTH] + gen-password -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + LENGTH the length of the generated password + (default: 999) + + + Generate a random password, and emit it to STDOUT. + + + Examples: + + Create a file with a secret: + + $ gen-password > secret.txt + + + Generate an absurdly long secret: + + $ gen-password 9999 > giant-secret.txt + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +LENGTH="${1:-999}" + +tr -cd '[:alnum:]' < /dev/random | + fold -w "$LENGTH" | + head -n1 diff --git a/bin/git-cleanup b/bin/git-cleanup new file mode 100755 index 0000000..4196cff --- /dev/null +++ b/bin/git-cleanup @@ -0,0 +1,74 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + git cleanup [REMOTE] + git cleanup -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + REMOTE the remote to prune the remote tracking + branches from (default: origin) + + + Delete merged branches, both local and remote-tracking. + + + Examples: + + Cleanup branches from "origin": + + $ git cleanup + + + Delete branches from "upstream": + + $ git cleanup upstream + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +REMOTE="${1:-origin}" + +git branch --merged | + grep -v -e '^\*' -e '^. main$' | + xargs git branch -d + +git remote prune "$REMOTE" diff --git a/bin/grun b/bin/grun new file mode 100755 index 0000000..74d8819 --- /dev/null +++ b/bin/grun @@ -0,0 +1,96 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + grun [-r RECIPIENT] FILENAME -- COMMAND... + grun -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -r RECIPIENT the recipient to encrypt to. Can be provided + multiple times for multiple recipients. + -h, --help show this message + + COMMAND A command to be executed, that accepts input + in STDIN and emits result in STDOUT, and emits + errors as non-zero return codes. + FILENAME The GPG-encrypted file to be processed. If it + doesn't exist yet, it will be created. + + Examples: + + Edit "secrets.txt.gpg" using `vipe` and the default recipient: + + $ grun secrets.txt.gpg -- vipe + + Delete lines containing "TODO" in todos.gpg for specific keys: + + $ grun -r ABC123DEF321 todos.gpg -- sed '/TODO/d' + + If COMMAND emits a non-zero return code, the file is left + unmodified. + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'rh' flag; do + case "$flag" in + r) + RECIPIENTS_FLAG="${RECIPIENTS_FLAG:-} -r $OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +FILENAME="${1:-}" +eval "$(assert-arg "$FILENAME" 'FILENAME')" +shift + +if [ "${1:-}" != '--' ]; then + printf 'Missing "--" separator\n\n' >&2 + usage >&2 + exit 2 +fi +shift + +eval "$(assert-arg "${1:-}" 'COMMAND')" + + +if [ ! -e "$FILENAME" ]; then + OUT="$(printf '' | "$@")" +else + OUT="$(gpg -dq "$FILENAME" | "$@")" +fi + +# GPG recipients can't contain spaces: +# shellcheck disable=2086 +echo "$OUT" | gpg -e ${RECIPIENTS_FLAG:--r eu@euandre.org} | sponge "$FILENAME" diff --git a/bin/htmlesc b/bin/htmlesc new file mode 100755 index 0000000..d9c59bd --- /dev/null +++ b/bin/htmlesc @@ -0,0 +1,96 @@ +#!/bin/sh +set -eu + + +usage() { + cat <<-'EOF' + Usage: + htmlesc [-e|d] + htmlesc -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -e escape the string (the default action) + -d unescape (de-escape?) the string + -h, --help show this message + + + Get a string from STDIN and convert it to/from HTML escaping. + + + Examples: + + oij + + $ printf 'a > 5 && !b' | htmlesc + a > 5 && !b + + + Unescape the content from a file: + + $ htmlesc -d < file.html + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +ENCODE=false +DECODE=false +while getopts 'edh' flag; do + case "$flag" in + e) + ENCODE=true + ;; + d) + DECODE=true + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +if [ "$ENCODE" = true ] && [ "$DECODE" = true ]; then + printf 'Both -e and -d given. Pick one.\n' >&2 + usage >&2 + exit 2 +elif [ "$DECODE" = true ]; then + sed \ + -e 's|&|\&|g' \ + -e 's|<|<|g' \ + -e 's|>|>|g' \ + -e 's|"|"|g' \ + -e "s|'|'|g" +else + sed \ + -e 's|&|\&|g' \ + -e 's|<|\<|g' \ + -e 's|>|\>|g' \ + -e 's|"|\"|g' \ + -e "s|'|\'|g" +fi diff --git a/bin/httpno b/bin/httpno new file mode 100755 index 0000000..e64b872 --- /dev/null +++ b/bin/httpno @@ -0,0 +1,156 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + httpno [NUMBER|TEXT] + httpno -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + NUMBER the number of the HTTP status code + TEXT the text of the description of the status code + + + Print the given status code, or list them all when no arguments + are given. + + + Examples: + + Get the status code 404: + + $ httpno 404 + 404 Not Found + + + Get the status code for "created": + + $ httpno created + 201 Created + + + List all statuses: + + $ httpno + ... + EOF +} + +DATA() { + awk 'd == 1 { print; next } /^__DATA__$/ { d = 1 }' "$0" | + head -n -1 # trim ShellCheck quote +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +if [ $# -eq 0 ]; then + DATA +else + DATA | grep -i "$@" +fi + + +exit + +# Make ShellCheck happy. See https://github.com/koalaman/shellcheck/issues/1201 +# shellcheck disable=1112 +echo " +__DATA__ +100 Continue +101 Switching Protocols +102 Processing +200 OK +201 Created +202 Accepted +203 Non-Authoritative Information +204 No Content +205 Reset Content +206 Partial Content +207 Multi-Status +208 Already Reported +300 Multiple Choices +301 Moved Permanently +302 Found +303 See Other +304 Not Modified +305 Use Proxy +307 Temporary Redirect +400 Bad Request +401 Unauthorized +402 Payment Required +403 Forbidden +404 Not Found +405 Method Not Allowed +406 Not Acceptable +407 Proxy Authentication Required +408 Request Timeout +409 Conflict +410 Gone +411 Length Required +412 Precondition Failed +413 Request Entity Too Large +414 Request-URI Too Large +415 Unsupported Media Type +416 Request Range Not Satisfiable +417 Expectation Failed +418 I'm a teapot +420 Blaze it +422 Unprocessable Entity +423 Locked +424 Failed Dependency +425 No code +426 Upgrade Required +428 Precondition Required +429 Too Many Requests +431 Request Header Fields Too Large +449 Retry with +500 Internal Server Error +501 Not Implemented +502 Bad Gateway +503 Service Unavailable +504 Gateway Timeout +505 HTTP Version Not Supported +506 Variant Also Negotiates +507 Insufficient Storage +509 Bandwidth Limit Exceeded +510 Not Extended +511 Network Authentication Required +" @@ -0,0 +1,70 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + lc + lc -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Transforms text from STDIN from upper-case to lower-case. It is + the equivalent of running 'tr [:upper:] [:lower:]'. + + + Examples: + + Normalize to lower-case: + + $ echo EuAndreh | lc + euandreh + + + Keep the text as-is: + + $ echo andreh | lc + andreh + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +tr '[:upper:]' '[:lower:]' @@ -0,0 +1,104 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + li [-I IMPL ] [-v] [OPTIONS...] + li -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -I IMPL use the given implementation (default: $LISP_CLI_IMPL) + -v verbose mode + -h, --help show this message + + OPTIONS options to be forwarded to cl(1) (lisp-cli) + + + Run the cl(1) executable with OPTIONS, but make sure an up-to-date + image exists and load it. + + + Examples: + + Launch a REPL from an image: + + $ li + + + Give options to cl(1): + + $ li -I sbcl -e '(print 123)' + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +VERBOSE=false +IMPL="${LISP_CLI_IMPL:-clisp}" +while getopts ':I:vh' flag; do + case "$flag" in + I) + IMPL="$OPTARG" + ;; + v) + VERBOSE=true + ;; + h) + usage + help + exit + ;; + *) + ;; + esac +done +if [ "${1:-}" = '--' ]; then + shift +fi + + +IMAGE="${XDG_DATA_HOME:-$HOME/.local/share}/lisp-cli/$IMPL.image" +BIN="$(command -v cl)" +INIT="${XDG_CONFIG_HOME:-$HOME/.config}/lisp-cli/init.lisp" +if [ ! -e "$IMAGE" ]; then + printf 'Bootstrapping a new "%s" image...\n' "$IMPL" >&2 + cl \ + -I "$IMPL" \ + -v \ + -e '(ql:quickload :trivial-dump-core)' \ + -E "(trivial-dump-core:dump-image \"$IMAGE\")" +elif [ -n "$(find "$0" "$BIN" "$INIT" -newer "$IMAGE")" ]; then + printf 'Refresh existing "%s" image...\n' "$IMPL" >&2 + cl \ + -M "$IMAGE" \ + -I "$IMPL" \ + -v \ + -e '(ql:quickload :trivial-dump-core)' \ + -E "(trivial-dump-core:dump-image \"$IMAGE\")" +fi + +if [ "$VERBOSE" = true ]; then + set -x +fi +exec cl -M "$IMAGE" "$@" diff --git a/bin/lines b/bin/lines new file mode 100755 index 0000000..2f0bf46 --- /dev/null +++ b/bin/lines @@ -0,0 +1,81 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + lines START [END] + lines -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + START the nth line number to start showing + END the nth line number to stop + showing (default: START + 1) + + + Print the range START-END of lines of the content of STDIN. + + + Examples: + + Print 3rd line: + + $ seq 10 | lines 3 + 3 + + + Print lines 5~8: + + $ lines 5 8 < file.txt + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +START="${1:-}" + +if [ -z "${2:-}" ]; then + END=1 +else + END=$(($2 - START + 1)) +fi + +eval "$(assert-arg "$START" 'START')" + +tail -n +"$START" | head -n "$END" @@ -0,0 +1,66 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + m + m -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Fetch email via IMAP and update the notmuch index. + + + Examples: + + Just fetch email + + $ m + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +F="${XDG_DATA_HOME:-$HOME/.local/share}"/euandreh/mailcfg-accounts.txt + +notmuch new +xargs -I% -P "$(wc -l < "$F")" mbsync '%' < "$F" +notmuch new diff --git a/bin/mailcfg b/bin/mailcfg new file mode 100755 index 0000000..f77355e --- /dev/null +++ b/bin/mailcfg @@ -0,0 +1,297 @@ +#!/bin/sh +# shellcheck disable=1090,2153 +set -eu + +usage() { + cat <<-'EOF' + Usage: + mailcfg ACTION + mailcfg -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + ACTION one of: + - mbsync + - msmtp + - notmuchcfg + - notmuchhook + - alot + - list + + + Emit the generated configuration file for the chosen email + program. Get the configuration files from + $XDG_CONFIG_HOME/mailcfg/*.env, where every *.env file is a + shell script that defines the variables used in this program: + - $NAME + - $LABEL + - $IMAP + - $SMTP + - $ADDR + + One of the files also needs to define: + - $DEFAULT_NAME + - $DEFAULT_ADDR + - $DEFAULT_LABEL + + An example of such file could be "30-andre@work.com.env": + + #!/bin/sh + set -eu + + + NAME='André!' + LABEL='Work' + IMAP='imap.work.com' + SMTP='smtp.work.com' + ADDR='andre@work.com' + + + Examples: + + Get the alot configuration file: + + $ mailcfg alot + + + List the existing account labels: + + $ mailcfg list + EOF +} + + +for flag; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +ACTION="${1:-}" +eval "$(assert-arg "$ACTION" 'ACTION')" + +CFGDIR="${XDG_CONFIG_HOME:-$HOME/.config}/mailcfg" + + +mbsync() { + cat <<-'EOF' + SyncState * + Create Both + Expunge Both + Remove Both + Sync All + EOF + + for env in "$CFGDIR"/*.env; do + . "$env" + cat <<-EOF + + + ## $LABEL + + IMAPAccount $LABEL + Host $IMAP + User $ADDR + PassCmd "pass show $ADDR" + SSLType IMAPS + + IMAPStore ${LABEL}Remote + Account $LABEL + + MaildirStore ${LABEL}Local + Path ~/Maildir/$LABEL/ + Inbox ~/Maildir/$LABEL/INBOX + SubFolders Verbatim + + Channel ${LABEL}Folders + Far :${LABEL}Remote: + Near :${LABEL}Local: + Patterns * + + Group $LABEL + Channel ${LABEL}Folders + EOF + done +} + +msmtp() { + cat <<-EOF + defaults + auth on + tls on + port 587 + syslog on + logfile $XDG_LOG_HOME/msmtp.log + EOF + + for env in "$CFGDIR"/*.env; do + . "$env" + cat <<-EOF + + account $LABEL + host $SMTP + from $ADDR + user $ADDR + passwordeval pass show $ADDR + EOF + done + + cat <<-EOF + + account default : $DEFAULT_LABEL + EOF +} + +notmuchcfg() { + for env in "$CFGDIR"/*.env; do + . "$env" + done + + cat <<-EOF + [user] + name = $DEFAULT_NAME + primary_email = $DEFAULT_ADDR + EOF + + printf 'other_email = ' + for env in "$CFGDIR"/*.env; do + . "$env" + if [ "$ADDR" = "$DEFAULT_ADDR" ]; then + continue + fi + echo "$ADDR" + done | paste -sd';' + + cat <<-'EOF' + + [new] + tags = new; + ignore = .mbsyncstate;.uidvalidity + + [search] + exclude_tags = deleted;spam + + [maildir] + synchronize_flags = true + EOF +} + +notmuchhook() { + LABELS='' + for env in "$CFGDIR"/*.env; do + . "$env" + if [ -z "$LABELS" ]; then + LABELS="$LABEL" + else + LABELS="$LABELS $LABEL" + fi + done + sed \ + -e "s|@DIRS@|$LABELS|g" \ + -e "s|@DEFAULT_LABEL@|$DEFAULT_LABEL|g" \ + "$CFGDIR"/post-new +} + +alot() { + cat <<-'EOF' + attachment_prefix = "~/Downloads/" + + [bindings] + i = toggletags inbox + I = search folder:/INBOX/ AND NOT tag:killed AND NOT tag:archive + EOF + echo " + z archive + s spam + u unread + r keep + t track + " | while read -r l; do + if [ -z "$l" ]; then + continue + fi + LC="$( echo "$l" | cut -d' ' -f1)" + TAG="$(echo "$l" | cut -d' ' -f2)" + UC="$(echo "$LC" | tr '[:lower:]' '[:upper:]')" + cat <<-EOF + $LC = toggletags $TAG + $UC = search tag:$TAG AND NOT tag:killed + EOF + done + + cat <<-'EOF' + M = search folder:/lists/ AND NOT tag:killed + m = compose --tags inbox + [[thread]] + v = pipeto urlscan 2>/dev/null + V = pipeto 'gpg -d | less' + r = reply --all + R = reply + ' ' = fold; untag unread; move next unfolded + P = pipeto 'git am' + + [accounts] + EOF + + for env in "$CFGDIR"/*.env; do + . "$env" + cat <<-EOF + [[$LABEL]] + realname = $NAME + address = $ADDR + sendmail_command = msmtpq --account=$LABEL -t + sent_box = maildir://~/Maildir/$LABEL/Sent + draft_box = maildir://~/Maildir/$LABEL/Drafts + gpg_key = 5BDAE9B8B2F6C6BCBB0D6CE581F90EC3CD356060 + EOF + done +} + +list() { + for env in "$CFGDIR"/*.env; do + . "$env" + printf '%s\n' "$LABEL" + done +} + + +case "$ACTION" in + mbsync|msmtp|notmuchcfg|notmuchhook|alot|list) + "$1" + ;; + *) + printf 'Unsupported ACTION: "%s".\n\n' "$ACTION" >&2 + usage >&2 + exit 2 + ;; +esac @@ -0,0 +1,84 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + max NUMBER... + max -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + Get the maximum number from the given values. + + + Examples: + + Get the maximum number from a list: + + $ min 5 3 9 9 4 + 9 + + Get the maximum number when negative numbers are given + + $ max -- -3 -5 + -3 + + Get the maximum number given a single number + + $ max 8 + 8 + + The maximum default number: + + $ max + 0 + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +if [ -z "${1:-}" ]; then + echo 0 + exit +fi + +N="$1" +for n in "$@"; do + N=$((N > n ? N : n)) +done +echo "$N" diff --git a/bin/menu b/bin/menu new file mode 100755 index 0000000..90af758 --- /dev/null +++ b/bin/menu @@ -0,0 +1,1587 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + menu ACTION + menu -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + ACTION one of: + - emoji + - username + - password + - bin + - clipboard + - yubikey + + + Lauch an interactive GUI menu for various desktop activities, to + be launched manually or via desktop keybindings. + + + Examples: + + Choose and copy the password to the clipboard: + + $ menu password + + + Execute a binary available in $PATH: + + $ menu bin + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) +ACTION="${1:-}" + +eval "$(assert-arg "$ACTION" 'ACTION')" + + +DATA() { + awk 'd == 1 { print; next } /^__DATA__$/ { d = 1 }' "$0" | + head -n -1 # trim ShellCheck quote +} + +show() { + dmenu -i -l 20 -fn Monospace-18 -p "$1:" +} + +pass_list() { + walk "$PASSWORD_STORE_DIR" | + grep '\.gpg$' | + sor 'test -f' | + sed -e "s|^$PASSWORD_STORE_DIR/||" \ + -e 's|\.gpg$||' | + LANG=POSIX.UTF-8 sort +} + +case "$ACTION" in + emoji) + DATA | show 'emoji' | awk '{print $(NF)}' | copy -n + ;; + username) + CHOICE="$(pass_list | show 'username')" + if [ -n "$CHOICE" ]; then + if pass show -c2 "$CHOICE"; then + notify-send -t 5000 -u normal -- \ + "$CHOICE" 'username copied to clipboard' + fi + fi + ;; + password) + CHOICE="$(pass_list | show 'password')" + if [ -n "$CHOICE" ]; then + if pass show -c1 "$CHOICE"; then + notify-send -t 5000 -u critical -- \ + "$CHOICE" 'password copied to clipboard' + fi + fi + ;; + bin) + CHOICE="$(bins | show 'bins')" + if [ -n "$CHOICE" ]; then + exec "$CHOICE" + fi + ;; + clipboard) + # For a potential improved version, see: + # https://github.com/cdown/clipmenu/pull/162 + clipmenu -i -l 20 -fn Monospace-18 -p "$1:" + notify-send -t 5000 -u low -- 'copied to clipboard' + ;; + yubikey) + CHOICE="$(ykman oath accounts list | show 'OTP')" + if [ -n "$CHOICE" ]; then + ykman oath accounts code "$CHOICE" | + awk '{ print $(NF) }' | + copy -n + notify-send -t 5000 -u normal -- \ + "$CHOICE" 'code copied to clipboard' + fi + ;; + *) + printf 'Bad ACTION: %s.\n\n' "$ACTION" >&2 + usage >&2 + exit 2 + ;; +esac + + + +exit + +# Make ShellCheck happy. See https://github.com/koalaman/shellcheck/issues/1201 +# shellcheck disable=1112 +echo ' +__DATA__ +grinning face 😀 +smiling face with open mouth 😃 +smiling face with open mouth & smiling eyes 😄 +grinning face with smiling eyes 😁 +smiling face with open mouth & closed eyes 😆 +smiling face with open mouth & cold sweat 😅 +face with tears of joy 😂 +rolling on the floor laughing 🤣 +smiling face ☺️ +smiling face with smiling eyes 😊 +smiling face with halo 😇 +slightly smiling face 🙂 +upside-down face 🙃 +winking face 😉 +relieved face 😌 +smiling face with heart-eyes 😍 +face blowing a kiss 😘 +kissing face 😗 +kissing face with smiling eyes 😙 +kissing face with closed eyes 😚 +face savouring delicious food 😋 +face with stuck-out tongue & winking eye 😜 +face with stuck-out tongue & closed eyes 😝 +face with stuck-out tongue 😛 +money-mouth face 🤑 +hugging face 🤗 +nerd face 🤓 +smiling face with sunglasses 😎 +clown face 🤡 +cowboy hat face 🤠 +smirking face 😏 +unamused face 😒 +disappointed face 😞 +pensive face 😔 +worried face 😟 +confused face 😕 +slightly frowning face 🙁 +frowning face ☹️ +persevering face 😣 +confounded face 😖 +tired face 😫 +weary face 😩 +face with steam from nose 😤 +angry face 😠 +pouting face 😡 +face without mouth 😶 +neutral face 😐 +expressionless face 😑 +hushed face 😯 +frowning face with open mouth 😦 +anguished face 😧 +face with open mouth 😮 +astonished face 😲 +dizzy face 😵 +flushed face 😳 +face screaming in fear 😱 +fearful face 😨 +face with open mouth & cold sweat 😰 +crying face 😢 +disappointed but relieved face 😥 +drooling face 🤤 +loudly crying face 😭 +face with cold sweat 😓 +sleepy face 😪 +sleeping face 😴 +face with rolling eyes 🙄 +thinking face 🤔 +lying face 🤥 +grimacing face 😬 +zipper-mouth face 🤐 +nauseated face 🤢 +sneezing face 🤧 +face with medical mask 😷 +face with thermometer 🤒 +face with head-bandage 🤕 +smiling face with horns 😈 +angry face with horns 👿 +ogre 👹 +goblin 👺 +pile of poo 💩 +ghost 👻 +skull 💀 +skull and crossbones ☠️ +alien 👽 +alien monster 👾 +robot face 🤖 +jack-o-lantern 🎃 +smiling cat face with open mouth 😺 +grinning cat face with smiling eyes 😸 +cat face with tears of joy 😹 +smiling cat face with heart-eyes 😻 +cat face with wry smile 😼 +kissing cat face with closed eyes 😽 +weary cat face 🙀 +crying cat face 😿 +pouting cat face 😾 +open hands 👐 +raising hands 🙌 +clapping hands 👏 +folded hands 🙏 +handshake 🤝 +thumbs up 👍 +thumbs down 👎 +oncoming fist 👊 +raised fist ✊ +left-facing fist 🤛 +right-facing fist 🤜 +crossed fingers 🤞 +victory hand ✌️ +sign of the horns 🤘 +OK hand 👌 +backhand index pointing left 👈 +backhand index pointing right 👉 +backhand index pointing up 👆 +backhand index pointing down 👇 +index pointing up ☝️ +raised hand ✋ +raised back of hand 🤚 +raised hand with fingers splayed 🖐 +vulcan salute 🖖 +waving hand 👋 +call me hand 🤙 +flexed biceps 💪 +middle finger 🖕 +writing hand ✍️ +selfie 🤳 +nail polish 💅 +ring 💍 +lipstick 💄 +kiss mark 💋 +mouth 👄 +tongue 👅 +ear 👂 +nose 👃 +footprints 👣 +eye 👁 +eyes 👀 +speaking head 🗣 +bust in silhouette 👤 +busts in silhouette 👥 +baby 👶 +boy 👦 +girl 👧 +man 👨 +woman 👩 +blond-haired woman 👱♀ +blond-haired person 👱 +old man 👴 +old woman 👵 +man with Chinese cap 👲 +woman wearing turban 👳♀ +person wearing turban 👳 +woman police officer 👮♀ +police officer 👮 +woman construction worker 👷♀ +construction worker 👷 +woman guard 💂♀ +guard 💂 +woman detective 🕵️♀️ +detective 🕵 +woman health worker 👩⚕ +man health worker 👨⚕ +woman farmer 👩🌾 +man farmer 👨🌾 +woman cook 👩🍳 +man cook 👨🍳 +woman student 👩🎓 +man student 👨🎓 +woman singer 👩🎤 +man singer 👨🎤 +woman teacher 👩🏫 +man teacher 👨🏫 +woman factory worker 👩🏭 +man factory worker 👨🏭 +woman technologist 👩💻 +man technologist 👨💻 +woman office worker 👩💼 +man office worker 👨💼 +woman mechanic 👩🔧 +man mechanic 👨🔧 +woman scientist 👩🔬 +man scientist 👨🔬 +woman artist 👩🎨 +man artist 👨🎨 +woman firefighter 👩🚒 +man firefighter 👨🚒 +woman pilot 👩✈ +man pilot 👨✈ +woman astronaut 👩🚀 +man astronaut 👨🚀 +woman judge 👩⚖ +man judge 👨⚖ +Mrs. Claus 🤶 +Santa Claus 🎅 +princess 👸 +prince 🤴 +bride with veil 👰 +man in tuxedo 🤵 +baby angel 👼 +pregnant woman 🤰 +woman bowing 🙇♀ +person bowing 🙇 +person tipping hand 💁 +man tipping hand 💁♂ +person gesturing NO 🙅 +man gesturing NO 🙅♂ +person gesturing OK 🙆 +man gesturing OK 🙆♂ +person raising hand 🙋 +man raising hand 🙋♂ +woman facepalming 🤦♀ +man facepalming 🤦♂ +woman shrugging 🤷♀ +man shrugging 🤷♂ +person pouting 🙎 +man pouting 🙎♂ +person frowning 🙍 +man frowning 🙍♂ +person getting haircut 💇 +man getting haircut 💇♂ +person getting massage 💆 +man getting massage 💆♂ +man in business suit levitating 🕴 +woman dancing 💃 +man dancing 🕺 +people with bunny ears partying 👯 +men with bunny ears partying 👯♂ +woman walking 🚶♀ +person walking 🚶 +woman running 🏃♀ +person running 🏃 +man and woman holding hands 👫 +two women holding hands 👭 +two men holding hands 👬 +couple with heart 💑 +couple with heart: woman woman 👩❤️👩 +couple with heart: man man 👨❤️👨 +kiss 💏 +kiss: woman woman 👩❤️💋👩 +kiss: man man 👨❤️💋👨 +family 👪 +family: man woman girl 👨👩👧 +family: man woman girl boy 👨👩👧👦 +family: man woman boy boy 👨👩👦👦 +family: man woman girl girl 👨👩👧👧 +family: woman woman boy 👩👩👦 +family: woman woman girl 👩👩👧 +family: woman woman girl boy 👩👩👧👦 +family: woman woman boy boy 👩👩👦👦 +family: woman woman girl girl 👩👩👧👧 +family: man man boy 👨👨👦 +family: man man girl 👨👨👧 +family: man man girl boy 👨👨👧👦 +family: man man boy boy 👨👨👦👦 +family: man man girl girl 👨👨👧👧 +family: woman boy 👩👦 +family: woman girl 👩👧 +family: woman girl boy 👩👧👦 +family: woman boy boy 👩👦👦 +family: woman girl girl 👩👧👧 +family: man boy 👨👦 +family: man girl 👨👧 +family: man girl boy 👨👧👦 +family: man boy boy 👨👦👦 +family: man girl girl 👨👧👧 +woman’s clothes 👚 +t-shirt 👕 +jeans 👖 +necktie 👔 +dress 👗 +bikini 👙 +kimono 👘 +high-heeled shoe 👠 +woman’s sandal 👡 +woman’s boot 👢 +man’s shoe 👞 +running shoe 👟 +woman’s hat 👒 +top hat 🎩 +graduation cap 🎓 +crown 👑 +rescue worker’s helmet ⛑ +school backpack 🎒 +clutch bag 👝 +purse 👛 +handbag 👜 +briefcase 💼 +glasses 👓 +sunglasses 🕶 +closed umbrella 🌂 +umbrella ☂️ +dog face 🐶 +cat face 🐱 +mouse face 🐭 +hamster face 🐹 +rabbit face 🐰 +fox face 🦊 +bear face 🐻 +panda face 🐼 +koala 🐨 +tiger face 🐯 +lion face 🦁 +cow face 🐮 +pig face 🐷 +pig nose 🐽 +frog face 🐸 +monkey face 🐵 +see-no-evil monkey 🙈 +hear-no-evil monkey 🙉 +speak-no-evil monkey 🙊 +monkey 🐒 +chicken 🐔 +penguin 🐧 +bird 🐦 +baby chick 🐤 +hatching chick 🐣 +front-facing baby chick 🐥 +duck 🦆 +eagle 🦅 +owl 🦉 +bat 🦇 +wolf face 🐺 +boar 🐗 +horse face 🐴 +unicorn face 🦄 +honeybee 🐝 +bug 🐛 +butterfly 🦋 +snail 🐌 +spiral shell 🐚 +lady beetle 🐞 +ant 🐜 +spider 🕷 +spider web 🕸 +turtle 🐢 +snake 🐍 +lizard 🦎 +scorpion 🦂 +crab 🦀 +squid 🦑 +octopus 🐙 +shrimp 🦐 +tropical fish 🐠 +fish 🐟 +blowfish 🐡 +dolphin 🐬 +shark 🦈 +spouting whale 🐳 +whale 🐋 +crocodile 🐊 +leopard 🐆 +tiger 🐅 +water buffalo 🐃 +ox 🐂 +cow 🐄 +deer 🦌 +camel 🐪 +two-hump camel 🐫 +elephant 🐘 +rhinoceros 🦏 +gorilla 🦍 +horse 🐎 +pig 🐖 +goat 🐐 +ram 🐏 +sheep 🐑 +dog 🐕 +poodle 🐩 +cat 🐈 +rooster 🐓 +turkey 🦃 +dove 🕊 +rabbit 🐇 +mouse 🐁 +rat 🐀 +chipmunk 🐿 +paw prints 🐾 +dragon 🐉 +dragon face 🐲 +cactus 🌵 +Christmas tree 🎄 +evergreen tree 🌲 +deciduous tree 🌳 +palm tree 🌴 +seedling 🌱 +herb 🌿 +shamrock ☘️ +four leaf clover 🍀 +pine decoration 🎍 +tanabata tree 🎋 +leaf fluttering in wind 🍃 +fallen leaf 🍂 +maple leaf 🍁 +mushroom 🍄 +sheaf of rice 🌾 +bouquet 💐 +tulip 🌷 +rose 🌹 +wilted flower 🥀 +sunflower 🌻 +blossom 🌼 +cherry blossom 🌸 +hibiscus 🌺 +globe showing Americas 🌎 +globe showing Europe-Africa 🌍 +globe showing Asia-Australia 🌏 +full moon 🌕 +waning gibbous moon 🌖 +last quarter moon 🌗 +waning crescent moon 🌘 +new moon 🌑 +waxing crescent moon 🌒 +first quarter moon 🌓 +waxing gibbous moon 🌔 +new moon face 🌚 +full moon with face 🌝 +sun with face 🌞 +first quarter moon with face 🌛 +last quarter moon with face 🌜 +crescent moon 🌙 +dizzy 💫 +white medium star ⭐️ +glowing star 🌟 +sparkles ✨ +high voltage ⚡️ +fire 🔥 +collision 💥 +comet ☄ +sun ☀️ +sun behind small cloud 🌤 +sun behind cloud ⛅️ +sun behind large cloud 🌥 +sun behind rain cloud 🌦 +rainbow 🌈 +cloud ☁️ +cloud with rain 🌧 +cloud with lightning and rain ⛈ +cloud with lightning 🌩 +cloud with snow 🌨 +snowman ☃️ +snowman without snow ⛄️ +snowflake ❄️ +wind face 🌬 +dashing away 💨 +tornado 🌪 +fog 🌫 +water wave 🌊 +droplet 💧 +sweat droplets 💦 +umbrella with rain drops ☔️ +green apple 🍏 +red apple 🍎 +pear 🍐 +tangerine 🍊 +lemon 🍋 +banana 🍌 +watermelon 🍉 +grapes 🍇 +strawberry 🍓 +melon 🍈 +cherries 🍒 +peach 🍑 +pineapple 🍍 +kiwi fruit 🥝 +avocado 🥑 +tomato 🍅 +eggplant 🍆 +cucumber 🥒 +carrot 🥕 +ear of corn 🌽 +hot pepper 🌶 +potato 🥔 +roasted sweet potato 🍠 +chestnut 🌰 +peanuts 🥜 +honey pot 🍯 +croissant 🥐 +bread 🍞 +baguette bread 🥖 +cheese wedge 🧀 +egg 🥚 +cooking 🍳 +bacon 🥓 +pancakes 🥞 +fried shrimp 🍤 +poultry leg 🍗 +meat on bone 🍖 +pizza 🍕 +hot dog 🌭 +hamburger 🍔 +french fries 🍟 +stuffed flatbread 🥙 +taco 🌮 +burrito 🌯 +green salad 🥗 +shallow pan of food 🥘 +spaghetti 🍝 +steaming bowl 🍜 +pot of food 🍲 +fish cake with swirl 🍥 +sushi 🍣 +bento box 🍱 +curry rice 🍛 +cooked rice 🍚 +rice ball 🍙 +rice cracker 🍘 +oden 🍢 +dango 🍡 +shaved ice 🍧 +ice cream 🍨 +soft ice cream 🍦 +shortcake 🍰 +birthday cake 🎂 +custard 🍮 +lollipop 🍭 +candy 🍬 +chocolate bar 🍫 +popcorn 🍿 +doughnut 🍩 +cookie 🍪 +glass of milk 🥛 +baby bottle 🍼 +hot beverage ☕️ +teacup without handle 🍵 +sake 🍶 +beer mug 🍺 +clinking beer mugs 🍻 +clinking glasses 🥂 +wine glass 🍷 +tumbler glass 🥃 +cocktail glass 🍸 +tropical drink 🍹 +bottle with popping cork 🍾 +spoon 🥄 +fork and knife 🍴 +fork and knife with plate 🍽 +soccer ball ⚽️ +basketball 🏀 +american football 🏈 +baseball ⚾️ +tennis 🎾 +volleyball 🏐 +rugby football 🏉 +pool 8 ball 🎱 +ping pong 🏓 +badminton 🏸 +goal net 🥅 +ice hockey 🏒 +field hockey 🏑 +cricket 🏏 +flag in hole ⛳️ +bow and arrow 🏹 +fishing pole 🎣 +boxing glove 🥊 +martial arts uniform 🥋 +ice skate ⛸ +skis 🎿 +skier ⛷ +snowboarder 🏂 +woman lifting weights 🏋️♀️ +person lifting weights 🏋 +person fencing 🤺 +women wrestling 🤼♀ +men wrestling 🤼♂ +woman cartwheeling 🤸♀ +man cartwheeling 🤸♂ +woman bouncing ball ⛹️♀️ +person bouncing ball ⛹ +woman playing handball 🤾♀ +man playing handball 🤾♂ +woman golfing 🏌️♀️ +person golfing 🏌 +woman surfing 🏄♀ +person surfing 🏄 +woman swimming 🏊♀ +person swimming 🏊 +woman playing water polo 🤽♀ +man playing water polo 🤽♂ +woman rowing boat 🚣♀ +person rowing boat 🚣 +horse racing 🏇 +woman biking 🚴♀ +person biking 🚴 +woman mountain biking 🚵♀ +person mountain biking 🚵 +running shirt 🎽 +sports medal 🏅 +military medal 🎖 +1st place medal 🥇 +2nd place medal 🥈 +3rd place medal 🥉 +trophy 🏆 +rosette 🏵 +reminder ribbon 🎗 +ticket 🎫 +admission tickets 🎟 +circus tent 🎪 +woman juggling 🤹♀ +man juggling 🤹♂ +performing arts 🎭 +artist palette 🎨 +clapper board 🎬 +microphone 🎤 +headphone 🎧 +musical score 🎼 +musical keyboard 🎹 +drum 🥁 +saxophone 🎷 +trumpet 🎺 +guitar 🎸 +violin 🎻 +game die 🎲 +direct hit 🎯 +bowling 🎳 +video game 🎮 +slot machine 🎰 +automobile 🚗 +taxi 🚕 +sport utility vehicle 🚙 +bus 🚌 +trolleybus 🚎 +racing car 🏎 +police car 🚓 +ambulance 🚑 +fire engine 🚒 +minibus 🚐 +delivery truck 🚚 +articulated lorry 🚛 +tractor 🚜 +kick scooter 🛴 +bicycle 🚲 +motor scooter 🛵 +motorcycle 🏍 +police car light 🚨 +oncoming police car 🚔 +oncoming bus 🚍 +oncoming automobile 🚘 +oncoming taxi 🚖 +aerial tramway 🚡 +mountain cableway 🚠 +suspension railway 🚟 +railway car 🚃 +tram car 🚋 +mountain railway 🚞 +monorail 🚝 +high-speed train 🚄 +high-speed train with bullet nose 🚅 +light rail 🚈 +locomotive 🚂 +train 🚆 +metro 🚇 +tram 🚊 +station 🚉 +helicopter 🚁 +small airplane 🛩 +airplane ✈️ +airplane departure 🛫 +airplane arrival 🛬 +rocket 🚀 +satellite 🛰 +seat 💺 +canoe 🛶 +sailboat ⛵️ +motor boat 🛥 +speedboat 🚤 +passenger ship 🛳 +ferry ⛴ +ship 🚢 +anchor ⚓️ +construction 🚧 +fuel pump ⛽️ +bus stop 🚏 +vertical traffic light 🚦 +horizontal traffic light 🚥 +world map 🗺 +moai 🗿 +Statue of Liberty 🗽 +fountain ⛲️ +Tokyo tower 🗼 +castle 🏰 +Japanese castle 🏯 +stadium 🏟 +ferris wheel 🎡 +roller coaster 🎢 +carousel horse 🎠 +umbrella on ground ⛱ +beach with umbrella 🏖 +desert island 🏝 +mountain ⛰ +snow-capped mountain 🏔 +mount fuji 🗻 +volcano 🌋 +desert 🏜 +camping 🏕 +tent ⛺️ +railway track 🛤 +motorway 🛣 +building construction 🏗 +factory 🏭 +house 🏠 +house with garden 🏡 +house 🏘 +derelict house 🏚 +office building 🏢 +department store 🏬 +Japanese post office 🏣 +post office 🏤 +hospital 🏥 +bank 🏦 +hotel 🏨 +convenience store 🏪 +school 🏫 +love hotel 🏩 +wedding 💒 +classical building 🏛 +church ⛪️ +mosque 🕌 +synagogue 🕍 +kaaba 🕋 +shinto shrine ⛩ +map of Japan 🗾 +moon viewing ceremony 🎑 +national park 🏞 +sunrise 🌅 +sunrise over mountains 🌄 +shooting star 🌠 +sparkler 🎇 +fireworks 🎆 +sunset 🌇 +cityscape at dusk 🌆 +cityscape 🏙 +night with stars 🌃 +milky way 🌌 +bridge at night 🌉 +foggy 🌁 +watch ⌚️ +mobile phone 📱 +mobile phone with arrow 📲 +laptop computer 💻 +keyboard ⌨️ +desktop computer 🖥 +printer 🖨 +computer mouse 🖱 +trackball 🖲 +joystick 🕹 +clamp 🗜 +computer disk 💽 +floppy disk 💾 +optical disk 💿 +dvd 📀 +videocassette 📼 +camera 📷 +camera with flash 📸 +video camera 📹 +movie camera 🎥 +film projector 📽 +film frames 🎞 +telephone receiver 📞 +telephone ☎️ +pager 📟 +fax machine 📠 +television 📺 +radio 📻 +studio microphone 🎙 +level slider 🎚 +control knobs 🎛 +stopwatch ⏱ +timer clock ⏲ +alarm clock ⏰ +mantelpiece clock 🕰 +hourglass ⌛️ +hourglass with flowing sand ⏳ +satellite antenna 📡 +battery 🔋 +electric plug 🔌 +light bulb 💡 +flashlight 🔦 +candle 🕯 +wastebasket 🗑 +oil drum 🛢 +money with wings 💸 +dollar banknote 💵 +yen banknote 💴 +euro banknote 💶 +pound banknote 💷 +money bag 💰 +credit card 💳 +gem stone 💎 +balance scale ⚖️ +wrench 🔧 +hammer 🔨 +hammer and pick ⚒ +hammer and wrench 🛠 +pick ⛏ +nut and bolt 🔩 +gear ⚙️ +chains ⛓ +pistol 🔫 +bomb 💣 +kitchen knife 🔪 +dagger 🗡 +crossed swords ⚔️ +shield 🛡 +cigarette 🚬 +coffin ⚰️ +funeral urn ⚱️ +amphora 🏺 +crystal ball 🔮 +prayer beads 📿 +barber pole 💈 +alembic ⚗️ +telescope 🔭 +microscope 🔬 +hole 🕳 +pill 💊 +syringe 💉 +thermometer 🌡 +toilet 🚽 +potable water 🚰 +shower 🚿 +bathtub 🛁 +person taking bath 🛀 +bellhop bell 🛎 +key 🔑 +old key 🗝 +door 🚪 +couch and lamp 🛋 +bed 🛏 +person in bed 🛌 +framed picture 🖼 +shopping bags 🛍 +shopping cart 🛒 +wrapped gift 🎁 +balloon 🎈 +carp streamer 🎏 +ribbon 🎀 +confetti ball 🎊 +party popper 🎉 +Japanese dolls 🎎 +red paper lantern 🏮 +wind chime 🎐 +envelope ✉️ +envelope with arrow 📩 +incoming envelope 📨 +e-mail 📧 +love letter 💌 +inbox tray 📥 +outbox tray 📤 +package 📦 +label 🏷 +closed mailbox with lowered flag 📪 +closed mailbox with raised flag 📫 +open mailbox with raised flag 📬 +open mailbox with lowered flag 📭 +postbox 📮 +postal horn 📯 +scroll 📜 +page with curl 📃 +page facing up 📄 +bookmark tabs 📑 +bar chart 📊 +chart increasing 📈 +chart decreasing 📉 +spiral notepad 🗒 +spiral calendar 🗓 +tear-off calendar 📆 +calendar 📅 +card index 📇 +card file box 🗃 +ballot box with ballot 🗳 +file cabinet 🗄 +clipboard 📋 +file folder 📁 +open file folder 📂 +card index dividers 🗂 +rolled-up newspaper 🗞 +newspaper 📰 +notebook 📓 +notebook with decorative cover 📔 +ledger 📒 +closed book 📕 +green book 📗 +blue book 📘 +orange book 📙 +books 📚 +open book 📖 +bookmark 🔖 +link 🔗 +paperclip 📎 +linked paperclips 🖇 +triangular ruler 📐 +straight ruler 📏 +pushpin 📌 +round pushpin 📍 +scissors ✂️ +pen 🖊 +fountain pen 🖋 +black nib ✒️ +paintbrush 🖌 +crayon 🖍 +memo 📝 +pencil ✏️ +left-pointing magnifying glass 🔍 +right-pointing magnifying glass 🔎 +locked with pen 🔏 +locked with key 🔐 +locked 🔒 +unlocked 🔓 +red heart ❤️ +yellow heart 💛 +green heart 💚 +blue heart 💙 +purple heart 💜 +black heart 🖤 +broken heart 💔 +heavy heart exclamation ❣️ +two hearts 💕 +revolving hearts 💞 +beating heart 💓 +growing heart 💗 +sparkling heart 💖 +heart with arrow 💘 +heart with ribbon 💝 +heart decoration 💟 +peace symbol ☮️ +latin cross ✝️ +star and crescent ☪️ +om 🕉 +wheel of dharma ☸️ +star of David ✡️ +dotted six-pointed star 🔯 +menorah 🕎 +yin yang ☯️ +orthodox cross ☦️ +place of worship 🛐 +Ophiuchus ⛎ +Aries ♈️ +Taurus ♉️ +Gemini ♊️ +Cancer ♋️ +Leo ♌️ +Virgo ♍️ +Libra ♎️ +Scorpius ♏️ +Sagittarius ♐️ +Capricorn ♑️ +Aquarius ♒️ +Pisces ♓️ +ID button 🆔 +atom symbol ⚛️ +Japanese “acceptable” button 🉑 +radioactive ☢️ +biohazard ☣️ +mobile phone off 📴 +vibration mode 📳 +Japanese “not free of charge” button 🈶 +Japanese “free of charge” button 🈚️ +Japanese “application” button 🈸 +Japanese “open for business” button 🈺 +Japanese “monthly amount” button 🈷️ +eight-pointed star ✴️ +VS button 🆚 +white flower 💮 +Japanese “bargain” button 🉐 +Japanese “secret” button ㊙️ +Japanese “congratulations” button ㊗️ +Japanese “passing grade” button 🈴 +Japanese “no vacancy” button 🈵 +Japanese “discount” button 🈹 +Japanese “prohibited” button 🈲 +A button (blood type) 🅰️ +B button (blood type) 🅱️ +AB button (blood type) 🆎 +CL button 🆑 +O button (blood type) 🅾️ +SOS button 🆘 +cross mark ❌ +heavy large circle ⭕️ +stop sign 🛑 +no entry ⛔️ +name badge 📛 +prohibited 🚫 +hundred points 💯 +anger symbol 💢 +hot springs ♨️ +no pedestrians 🚷 +no littering 🚯 +no bicycles 🚳 +non-potable water 🚱 +no one under eighteen 🔞 +no mobile phones 📵 +no smoking 🚭 +exclamation mark ❗️ +white exclamation mark ❕ +question mark ❓ +white question mark ❔ +double exclamation mark ‼️ +exclamation question mark ⁉️ +dim button 🔅 +bright button 🔆 +part alternation mark 〽️ +warning ⚠️ +children crossing 🚸 +trident emblem 🔱 +fleur-de-lis ⚜️ +Japanese symbol for beginner 🔰 +recycling symbol ♻️ +white heavy check mark ✅ +Japanese “reserved” button 🈯️ +chart increasing with yen 💹 +sparkle ❇️ +eight-spoked asterisk ✳️ +cross mark button ❎ +globe with meridians 🌐 +diamond with a dot 💠 +circled M Ⓜ️ +cyclone 🌀 +zzz 💤 +ATM sign 🏧 +water closet 🚾 +wheelchair symbol ♿️ +P button 🅿️ +Japanese “vacancy” button 🈳 +Japanese “service charge” button 🈂️ +passport control 🛂 +customs 🛃 +baggage claim 🛄 +left luggage 🛅 +men’s room 🚹 +women’s room 🚺 +baby symbol 🚼 +restroom 🚻 +litter in bin sign 🚮 +cinema 🎦 +antenna bars 📶 +Japanese “here” button 🈁 +input symbols 🔣 +information ℹ️ +input latin letters 🔤 +input latin lowercase 🔡 +input latin uppercase 🔠 +NG button 🆖 +OK button 🆗 +UP! button 🆙 +COOL button 🆒 +NEW button 🆕 +FREE button 🆓 +keycap: 0 0️⃣ +keycap: 1 1️⃣ +keycap: 2 2️⃣ +keycap: 3 3️⃣ +keycap: 4 4️⃣ +keycap: 5 5️⃣ +keycap: 6 6️⃣ +keycap: 7 7️⃣ +keycap: 8 8️⃣ +keycap: 9 9️⃣ +keycap 10 🔟 +input numbers 🔢 +keycap: # #️⃣ +keycap: * *️⃣ +play button ▶️ +pause button ⏸ +play or pause button ⏯ +stop button ⏹ +record button ⏺ +next track button ⏭ +last track button ⏮ +fast-forward button ⏩ +fast reverse button ⏪ +fast up button ⏫ +fast down button ⏬ +reverse button ◀️ +up button 🔼 +down button 🔽 +right arrow ➡️ +left arrow ⬅️ +up arrow ⬆️ +down arrow ⬇️ +up-right arrow ↗️ +down-right arrow ↘️ +down-left arrow ↙️ +up-left arrow ↖️ +up-down arrow ↕️ +left-right arrow ↔️ +left arrow curving right ↪️ +right arrow curving left ↩️ +right arrow curving up ⤴️ +right arrow curving down ⤵️ +shuffle tracks button 🔀 +repeat button 🔁 +repeat single button 🔂 +anticlockwise arrows button 🔄 +clockwise vertical arrows 🔃 +musical note 🎵 +musical notes 🎶 +heavy plus sign ➕ +heavy minus sign ➖ +heavy division sign ➗ +heavy multiplication x ✖️ +heavy dollar sign 💲 +currency exchange 💱 +trade mark ™️ +copyright ©️ +registered ®️ +wavy dash 〰️ +curly loop ➰ +double curly loop ➿ +END arrow 🔚 +BACK arrow 🔙 +ON! arrow 🔛 +TOP arrow 🔝 +SOON arrow 🔜 +heavy check mark ✔️ +ballot box with check ☑️ +radio button 🔘 +white circle ⚪️ +black circle ⚫️ +red circle 🔴 +blue circle 🔵 +red triangle pointed up 🔺 +red triangle pointed down 🔻 +small orange diamond 🔸 +small blue diamond 🔹 +large orange diamond 🔶 +large blue diamond 🔷 +white square button 🔳 +black square button 🔲 +black small square ▪️ +white small square ▫️ +black medium-small square ◾️ +white medium-small square ◽️ +black medium square ◼️ +white medium square ◻️ +black large square ⬛️ +white large square ⬜️ +speaker low volume 🔈 +muted speaker 🔇 +speaker medium volume 🔉 +speaker high volume 🔊 +bell 🔔 +bell with slash 🔕 +megaphone 📣 +loudspeaker 📢 +eye in speech bubble 👁🗨 +speech balloon 💬 +thought balloon 💭 +right anger bubble 🗯 +spade suit ♠️ +club suit ♣️ +heart suit ♥️ +diamond suit ♦️ +joker 🃏 +flower playing cards 🎴 +mahjong red dragon 🀄️ +one o’clock 🕐 +two o’clock 🕑 +three o’clock 🕒 +four o’clock 🕓 +five o’clock 🕔 +six o’clock 🕕 +seven o’clock 🕖 +eight o’clock 🕗 +nine o’clock 🕘 +ten o’clock 🕙 +eleven o’clock 🕚 +twelve o’clock 🕛 +one-thirty 🕜 +two-thirty 🕝 +three-thirty 🕞 +four-thirty 🕟 +five-thirty 🕠 +six-thirty 🕡 +seven-thirty 🕢 +eight-thirty 🕣 +nine-thirty 🕤 +ten-thirty 🕥 +eleven-thirty 🕦 +twelve-thirty 🕧 +white flag 🏳️ +black flag 🏴 +chequered flag 🏁 +triangular flag 🚩 +rainbow flag 🏳️🌈 +Afghanistan 🇦🇫 +Åland Islands 🇦🇽 +Albania 🇦🇱 +Algeria 🇩🇿 +American Samoa 🇦🇸 +Andorra 🇦🇩 +Angola 🇦🇴 +Anguilla 🇦🇮 +Antarctica 🇦🇶 +Antigua & Barbuda 🇦🇬 +Argentina 🇦🇷 +Armenia 🇦🇲 +Aruba 🇦🇼 +Australia 🇦🇺 +Austria 🇦🇹 +Azerbaijan 🇦🇿 +Bahamas 🇧🇸 +Bahrain 🇧🇭 +Bangladesh 🇧🇩 +Barbados 🇧🇧 +Belarus 🇧🇾 +Belgium 🇧🇪 +Belize 🇧🇿 +Benin 🇧🇯 +Bermuda 🇧🇲 +Bhutan 🇧🇹 +Bolivia 🇧🇴 +Caribbean Netherlands 🇧🇶 +Bosnia & Herzegovina 🇧🇦 +Botswana 🇧🇼 +Brazil 🇧🇷 +British Indian Ocean Territory 🇮🇴 +British Virgin Islands 🇻🇬 +Brunei 🇧🇳 +Bulgaria 🇧🇬 +Burkina Faso 🇧🇫 +Burundi 🇧🇮 +Cape Verde 🇨🇻 +Cambodia 🇰🇭 +Cameroon 🇨🇲 +Canada 🇨🇦 +Canary Islands 🇮🇨 +Cayman Islands 🇰🇾 +Central African Republic 🇨🇫 +Chad 🇹🇩 +Chile 🇨🇱 +China 🇨🇳 +Christmas Island 🇨🇽 +Cocos (Keeling) Islands 🇨🇨 +Colombia 🇨🇴 +Comoros 🇰🇲 +Congo - Brazzaville 🇨🇬 +Congo - Kinshasa 🇨🇩 +Cook Islands 🇨🇰 +Costa Rica 🇨🇷 +Côte d’Ivoire 🇨🇮 +Croatia 🇭🇷 +Cuba 🇨🇺 +Curaçao 🇨🇼 +Cyprus 🇨🇾 +Czech Republic 🇨🇿 +Denmark 🇩🇰 +Djibouti 🇩🇯 +Dominica 🇩🇲 +Dominican Republic 🇩🇴 +Ecuador 🇪🇨 +Egypt 🇪🇬 +El Salvador 🇸🇻 +Equatorial Guinea 🇬🇶 +Eritrea 🇪🇷 +Estonia 🇪🇪 +Ethiopia 🇪🇹 +European Union 🇪🇺 +Falkland Islands 🇫🇰 +Faroe Islands 🇫🇴 +Fiji 🇫🇯 +Finland 🇫🇮 +France 🇫🇷 +French Guiana 🇬🇫 +French Polynesia 🇵🇫 +French Southern Territories 🇹🇫 +Gabon 🇬🇦 +Gambia 🇬🇲 +Georgia 🇬🇪 +Germany 🇩🇪 +Ghana 🇬🇭 +Gibraltar 🇬🇮 +Greece 🇬🇷 +Greenland 🇬🇱 +Grenada 🇬🇩 +Guadeloupe 🇬🇵 +Guam 🇬🇺 +Guatemala 🇬🇹 +Guernsey 🇬🇬 +Guinea 🇬🇳 +Guinea-Bissau 🇬🇼 +Guyana 🇬🇾 +Haiti 🇭🇹 +Honduras 🇭🇳 +Hong Kong SAR China 🇭🇰 +Hungary 🇭🇺 +Iceland 🇮🇸 +India 🇮🇳 +Indonesia 🇮🇩 +Iran 🇮🇷 +Iraq 🇮🇶 +Ireland 🇮🇪 +Isle of Man 🇮🇲 +Israel 🇮🇱 +Italy 🇮🇹 +Jamaica 🇯🇲 +Japan 🇯🇵 +crossed flags 🎌 +Jersey 🇯🇪 +Jordan 🇯🇴 +Kazakhstan 🇰🇿 +Kenya 🇰🇪 +Kiribati 🇰🇮 +Kosovo 🇽🇰 +Kuwait 🇰🇼 +Kyrgyzstan 🇰🇬 +Laos 🇱🇦 +Latvia 🇱🇻 +Lebanon 🇱🇧 +Lesotho 🇱🇸 +Liberia 🇱🇷 +Libya 🇱🇾 +Liechtenstein 🇱🇮 +Lithuania 🇱🇹 +Luxembourg 🇱🇺 +Macau SAR China 🇲🇴 +Macedonia 🇲🇰 +Madagascar 🇲🇬 +Malawi 🇲🇼 +Malaysia 🇲🇾 +Maldives 🇲🇻 +Mali 🇲🇱 +Malta 🇲🇹 +Marshall Islands 🇲🇭 +Martinique 🇲🇶 +Mauritania 🇲🇷 +Mauritius 🇲🇺 +Mayotte 🇾🇹 +Mexico 🇲🇽 +Micronesia 🇫🇲 +Moldova 🇲🇩 +Monaco 🇲🇨 +Mongolia 🇲🇳 +Montenegro 🇲🇪 +Montserrat 🇲🇸 +Morocco 🇲🇦 +Mozambique 🇲🇿 +Myanmar (Burma) 🇲🇲 +Namibia 🇳🇦 +Nauru 🇳🇷 +Nepal 🇳🇵 +Netherlands 🇳🇱 +New Caledonia 🇳🇨 +New Zealand 🇳🇿 +Nicaragua 🇳🇮 +Niger 🇳🇪 +Nigeria 🇳🇬 +Niue 🇳🇺 +Norfolk Island 🇳🇫 +Northern Mariana Islands 🇲🇵 +North Korea 🇰🇵 +Norway 🇳🇴 +Oman 🇴🇲 +Pakistan 🇵🇰 +Palau 🇵🇼 +Palestinian Territories 🇵🇸 +Panama 🇵🇦 +Papua New Guinea 🇵🇬 +Paraguay 🇵🇾 +Peru 🇵🇪 +Philippines 🇵🇭 +Pitcairn Islands 🇵🇳 +Poland 🇵🇱 +Portugal 🇵🇹 +Puerto Rico 🇵🇷 +Qatar 🇶🇦 +Réunion 🇷🇪 +Romania 🇷🇴 +Russia 🇷🇺 +Rwanda 🇷🇼 +St. Barthélemy 🇧🇱 +St. Helena 🇸🇭 +St. Kitts & Nevis 🇰🇳 +St. Lucia 🇱🇨 +St. Pierre & Miquelon 🇵🇲 +St. Vincent & Grenadines 🇻🇨 +Samoa 🇼🇸 +San Marino 🇸🇲 +São Tomé & Príncipe 🇸🇹 +Saudi Arabia 🇸🇦 +Senegal 🇸🇳 +Serbia 🇷🇸 +Seychelles 🇸🇨 +Sierra Leone 🇸🇱 +Singapore 🇸🇬 +Sint Maarten 🇸🇽 +Slovakia 🇸🇰 +Slovenia 🇸🇮 +Solomon Islands 🇸🇧 +Somalia 🇸🇴 +South Africa 🇿🇦 +South Georgia & South Sandwich Islands 🇬🇸 +South Korea 🇰🇷 +South Sudan 🇸🇸 +Spain 🇪🇸 +Sri Lanka 🇱🇰 +Sudan 🇸🇩 +Suriname 🇸🇷 +Swaziland 🇸🇿 +Sweden 🇸🇪 +Switzerland 🇨🇭 +Syria 🇸🇾 +Taiwan 🇹🇼 +Tajikistan 🇹🇯 +Tanzania 🇹🇿 +Thailand 🇹🇭 +Timor-Leste 🇹🇱 +Togo 🇹🇬 +Tokelau 🇹🇰 +Tonga 🇹🇴 +Trinidad & Tobago 🇹🇹 +Tunisia 🇹🇳 +Turkey 🇹🇷 +Turkmenistan 🇹🇲 +Turks & Caicos Islands 🇹🇨 +Tuvalu 🇹🇻 +Uganda 🇺🇬 +Ukraine 🇺🇦 +United Arab Emirates 🇦🇪 +United Kingdom 🇬🇧 +United States 🇺🇸 +U.S. Virgin Islands 🇻🇮 +Uruguay 🇺🇾 +Uzbekistan 🇺🇿 +Vanuatu 🇻🇺 +Vatican City 🇻🇦 +Venezuela 🇻🇪 +Vietnam 🇻🇳 +Wallis & Futuna 🇼🇫 +Western Sahara 🇪🇭 +Yemen 🇾🇪 +Zambia 🇿🇲 +Zimbabwe 🇿🇼 +' @@ -0,0 +1,85 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + min NUMBER... + min -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Get the minimun number from the given values. + + + Examples: + + Get the minimum number from a list: + + $ min 5 3 9 9 4 + 3 + + Get the minimum number when negative numbers are given + + $ min -- -3 -5 + -5 + + Get the minimum number given a single number + + $ min 8 + 8 + + The minimum default number: + + $ min + 0 + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +if [ -z "${1:-}" ]; then + echo 0 + exit +fi + +N="$1" +for n in "$@"; do + N=$((N < n ? N : n)) +done +echo "$N" diff --git a/bin/mkdtemp b/bin/mkdtemp new file mode 100755 index 0000000..3729dd4 --- /dev/null +++ b/bin/mkdtemp @@ -0,0 +1,64 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + mkdtemp + mkdtemp -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Create a new temporary file and echo its name back. + + + Examples: + + `cd` into temporary directory: + + $ cd "$(mkdtemp)" + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +name="$(tmpname)" +mkdir "$name" +echo "$name" diff --git a/bin/mkstemp b/bin/mkstemp new file mode 100755 index 0000000..4097e59 --- /dev/null +++ b/bin/mkstemp @@ -0,0 +1,64 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + mkstemp + mkstemp -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Create a new temporary file and echo its name back. + + + Examples: + + Capture output into temporary file: + + $ OUT="$(mkstemp)"; cmd > "$OUT" + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +name="$(tmpname)" +touch "$name" +echo "$name" @@ -0,0 +1,153 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + msg [-0|-1] [-X|-s|-S|-m|-D|-b] [MESSAGE] + msg -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -X send MESSAGE using the `xmpp` command + -s play $XDG_DATA_HOME/msg/{good,bad}.ogg sound + -S say MESSAGE using `speak` + -m send email with MESSAGE as subject and empty body + -D send desktop MESSAGE via `notify-send` + -b print terminal bell + -0 an OK message + -1 an error message + -h, --help show this message + + MESSAGE the text to be sent by the relevant channel + + Examples: + + Ring a terminal bell and play a sound, representing an error: + + $ msg -1sb + + Send an email and an XMPP message: + + $ msg -mX 'The message goes here' + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +sound() { + if [ "$OK" = true ]; then + play "$XDG_DATA_HOME"/msg/good.ogg 2>/dev/null + else + play "$XDG_DATA_HOME"/msg/bad.ogg 2>/dev/null + fi +} + +OK=true +XMPP=false +SOUND=false +SPEAK=false +MAIL=false +DESKTOP=false +BELL=false +ACTION_DONE=false +while getopts '01XsSmDbh' flag; do + case "$flag" in + 0) + OK=true + ;; + 1) + OK=false + ;; + X) + XMPP=true + ACTION_DONE=true + ;; + s) + SOUND=true + ACTION_DONE=true + ;; + S) + SPEAK=true + ACTION_DONE=true + ;; + m) + MAIL=true + ACTION_DONE=true + ;; + D) + DESKTOP=true + ACTION_DONE=true + ;; + b) + BELL=true + ACTION_DONE=true + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +if [ "$ACTION_DONE" = false ]; then + sound + usage + help + exit +fi + + +MESSAGE="${1:-}" + +if [ "$XMPP" = true ]; then + eval "$(assert-arg "$MESSAGE" '-X MESSAGE')" + xmpp -m "$MESSAGE" eu@euandreh.xyz & +fi +if [ "$SOUND" = true ]; then + sound & +fi +if [ "$SPEAK" = true ]; then + eval "$(assert-arg "$MESSAGE" '-S MESSAGE')" + echo "$MESSAGE" | speak -v pt-BR & +fi +if [ "$MAIL" = true ]; then + eval "$(assert-arg "$MESSAGE" '-m MESSAGE')" + echo " " | email -s "$MESSAGE" eu@euandre.org & +fi +if [ "$DESKTOP" = true ]; then + eval "$(assert-arg "$MESSAGE" '-D MESSAGE')" + if [ "$OK" = true ]; then + notify-send -t 5000 "$MESSAGE" & + else + notify-send -t 5000 -u critical "$MESSAGE" & + fi +fi +if [ "$BELL" = true ]; then + printf '\a' & +fi + +wait diff --git a/bin/n-times b/bin/n-times new file mode 100755 index 0000000..4fa8b96 --- /dev/null +++ b/bin/n-times @@ -0,0 +1,73 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + n-times COUNT -- COMMAND... + n-times -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + COUNT the number of times for COMMAND to be executed + + + Examples: + + Print 123 5 times: + + $ n-times 5 echo 123 + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +COUNT="${1:-}" +shift + +eval "$(assert-arg "$COUNT" 'COUNT')" + + +while true; do + if [ "$COUNT" = 0 ]; then + break + fi + COUNT=$((COUNT - 1)) + "$@" +done diff --git a/bin/nato b/bin/nato new file mode 100755 index 0000000..a9c21bf --- /dev/null +++ b/bin/nato @@ -0,0 +1,102 @@ +#!/usr/bin/env perl + +use v5.34; +use warnings; +use feature 'signatures'; +no warnings ('experimental::signatures'); +use Getopt::Std (); + +sub usage($fh) { + print $fh <<~'EOF' + Usage: + nato + nato -h + EOF +} + +sub help($fh) { + print $fh <<~'EOF' + + Options: + -h, --help show this message + + + Translate the given input to the NATO phonetic alphabet. + + + Examples: + + Spell 'EuAndreh': + + $ echo 'EuAndreh' | nato + Echo Uniform Alfa November Delta Romeo Echo Hotel + EOF +} + +for (@ARGV) { + last if $_ eq '--'; + if ($_ eq '--help') { + usage *STDOUT; + help *STDOUT; + exit + } +} + +my %opts; +if (!Getopt::Std::getopts('h', \%opts)) { + usage *STDERR; + exit 2; +} + +if ($opts{h}) { + usage *STDOUT; + help *STDOUT; + exit; +} + +my %DICT = ( + 'a' => 'Alfa', + 'b' => 'Bravo', + 'c' => 'Charlie', + 'd' => 'Delta', + 'e' => 'Echo', + 'f' => 'Foxtrot', + 'g' => 'Golf', + 'h' => 'Hotel', + 'i' => 'India', + 'j' => 'Juliett', + 'k' => 'Kilo', + 'l' => 'Lima', + 'm' => 'Mike', + 'n' => 'November', + 'o' => 'Oscar', + 'p' => 'Papa', + 'q' => 'Quebec', + 'r' => 'Romeo', + 's' => 'Sierra', + 't' => 'Tango', + 'u' => 'Uniform', + 'v' => 'Victor', + 'w' => 'Whiskey', + 'x' => 'X-ray', + 'y' => 'Yankee', + 'z' => 'Zulu', + '1' => 'One', + '2' => 'Two', + '3' => 'Three', + '4' => 'Four', + '5' => 'Five', + '6' => 'Six', + '7' => 'Seven', + '8' => 'Eight', + '9' => 'Nine', + '0' => 'Zero', +); + +while (<STDIN>) { + for my $c (split //, $_) { + my $char = $DICT{lc $c}; + print "$char " if defined $char; + } + print "\n"; +} diff --git a/bin/ootb b/bin/ootb new file mode 100755 index 0000000..3aa58db --- /dev/null +++ b/bin/ootb @@ -0,0 +1,103 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + ootb BUILD_DIRECTORY < FILE... + ootb -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -h, --help show this message + + BUILD_DIRECTORY the path of the build directory + FILE the files to be linked + + + Create a directory out of symlinks of the given files. + + The goal is to enable parallel build directories to coexist, + so that one do *O*ut *O*f *T*ree *B*uilds without requiring the + build system or the project to explicitly support it. + + If a repository contains the files: + + .git/ + Makefile + README.md + src/ + file1.ext + file2.ext + + Running `git ls-files | ootb build-1/` would create the + 'build-1/' directory with: + + build-1/ + Makefile -> /absolute/path/to/Makefile + README.md -> /absolute/path/to/README.md + src/ + file1.ext -> /absolute/path/to/file1.ext + file2.ext -> /absolute/path/to/file2.ext + + With that one can `cd build-1/` and run builds there, without + the build artifacts littering the source tree. Also, one could + create a build-2/ directory, where different compiler flags or + build options are given, such as debug/release, while sharing the + underlying source code. + + + Examples: + + Create a 'build/' directory with the files from the Git repository: + + $ git ls-files | ootb build/ + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +BUILD_DIRECTORY="${1:-}" +eval "$(assert-arg "$BUILD_DIRECTORY" 'BUILD_DIRECTORY')" +mkdir -p "$BUILD_DIRECTORY" + + +while read -r f; do + mkdir -p "$BUILD_DIRECTORY"/"$(dirname "$f")" + ln -fs "$PWD"/"$f" "$BUILD_DIRECTORY"/"$f" +done diff --git a/bin/open b/bin/open new file mode 100755 index 0000000..d2eedd3 --- /dev/null +++ b/bin/open @@ -0,0 +1,101 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + open FILE... + open -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + Examples: + + Open an HTML file on the current $BROWSER: + $ open index.html + + Open multiple PDF files (with zathura): + $ open *.pdf + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +if [ -z "${1:-}" ]; then + usage >&2 + exit 2 +fi + +for f in "$@"; do + case "$f" in + *.ico|*.jpg|*.jpeg|*.png) + feh "$f" + ;; + https://www.youtube.com/watch*) + nohup mpv "$f" 1>&2 2>/dev/null & + ;; + *.flac|*.ogg|*.mkv|*.avi|*.mp4) + nohup mpv "$f" 1>&2 2>/dev/null & + ;; + http*|*.svg|*.html) + "$BROWSER" "$f" + ;; + gopher://*) + amfora "$f" + ;; + gemini://*) + telescope "$f" + ;; + *.pdf|*.djvu|*.ps|*.epub) + nohup zathura "$f" 1>&2 2>/dev/null & + ;; + *.txt) + less "$f" + ;; + *.midi) + timidity "$f" + ;; + mailto:*) + alot compose "$f" + ;; + *) + DIR="$(cd -- "$(dirname -- "$0")"; pwd)" + CMD="$(without-env PATH "$DIR" -- command -v xdg-open)" + "$CMD" "$f" + ;; + esac +done diff --git a/bin/player b/bin/player new file mode 100755 index 0000000..b6e66d7 --- /dev/null +++ b/bin/player @@ -0,0 +1,136 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + player ACTION + player -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + ACTION one of: + - backward: go back 5 seconds + - forward: go forward 5 seconds + - previous: go to the previous track + - next: go to the next track + - play-pause: play/pause + - rotate: rotate across available MPRIS players + - current: show the current MPRIS player + + + Manipulate the MPRIS audio player. + + + Examples: + + Change the current MPRIS player: + + $ player current + + + Play/pause: + + $ player play-pause + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'P:h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) +ACTION="${1:-}" + +eval "$(assert-arg "$ACTION" 'ACTION')" + + + +CURRENT_PLAYER_PATH="$XDG_CACHE_HOME"/euandreh-mpris-player.txt +CURRENT_PLAYER="$(cat "$CURRENT_PLAYER_PATH" ||:)" +AVAILABLE_PLAYERS="$(playerctl --list-all | LANG=POSIX.UTF-8 sort)" + +pick_first() { + echo "$AVAILABLE_PLAYERS" | head -n1 +} + +next_player() { + if [ -z "$CURRENT_PLAYER" ]; then + pick_first + elif ! echo "$AVAILABLE_PLAYERS" | grep -q "$CURRENT_PLAYER"; then + # Unknown $CURRENT_PLAYER, pick anyone + pick_first + else + INDEX="$(echo "$AVAILABLE_PLAYERS" | grep -n "$CURRENT_PLAYER" | cut -d: -f1)" + LENGTH="$(echo "$AVAILABLE_PLAYERS" | wc -l)" + if [ "$INDEX" = "$LENGTH" ]; then + # Reached the end of the $AVAILABLE_PLAYERS list, wrapping + pick_first + else + # Get the next player instead + echo "$AVAILABLE_PLAYERS" | awk -v idx="$INDEX" 'NR == idx+1 {print}' + fi + fi +} + +case "$ACTION" in + backward) + playerctl --player="$CURRENT_PLAYER" position 5- + ;; + forward) + playerctl --player="$CURRENT_PLAYER" position 5+ + ;; + previous) + playerctl --player="$CURRENT_PLAYER" previous + ;; + next) + playerctl --player="$CURRENT_PLAYER" next + ;; + play-pause) + playerctl --player="$CURRENT_PLAYER" play-pause + ;; + rotate) + PLAYER="$(next_player)" + echo "$PLAYER" > "$CURRENT_PLAYER_PATH" + notify-send -t 1000 "$PLAYER" 'current MPRIS target' + ;; + current) + printf '%s\n' "$CURRENT_PLAYER" + ;; + *) + printf 'Bad ACTION: "%s".\n\n' "$ACTION" >&2 + usage >&2 + exit 2 + ;; +esac diff --git a/bin/playlist b/bin/playlist new file mode 100755 index 0000000..bd01d23 --- /dev/null +++ b/bin/playlist @@ -0,0 +1,103 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + playlist ACTION + playlist -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + ACTION one of: + - prompt + - run + + + Manage the playlist. + + + Examples: + + Enqueue a video: + + $ playlist prompt + + + Play the next video in the queue: + + $ playlist run + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) +ACTION="${1:-}" + +eval "$(assert-arg "$ACTION" 'ACTION')" + +F="$XDG_DATA_HOME"/euandreh/playlist.txt + +prompt() { + ENTRY="$(zenity --text 'URL of the video to enqueue:' --entry ||:)" + if [ -n "$ENTRY" ]; then + echo "$ENTRY" >> "$F" + fi +} + +run() { + next="$(head -n1 "$F")" + if [ -z "$next" ]; then + return + fi + mpv "$next" + echo "$next" >> "queue.$F" + tail -n+2 "$F" | sponge "$F" +} + +case "$ACTION" in + prompt) + prompt + ;; + run) + run + ;; + *) + printf 'Bad ACTION: %s.\n\n' "$ACTION" >&2 + usage >&2 + exit 2 + ;; +esac @@ -0,0 +1,78 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + pre [-c COLOR] PREFIX + pre -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -c COLOR ANSI color to be used on the prefix text + -h, --help show this message + + Prefix STDIN with PREFIX. + + + Examples: + + Prefix with 'database': + + $ ./run-db.sh | pre 'database' + + + Prefix with yellow 'numbers': + + $ seq 10 | pre -c yellow numbers + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +COLOR='' +while getopts 'c:h' flag; do + case "$flag" in + c) + COLOR="$OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +PREFIX="${1:-}" +eval "$(assert-arg "$PREFIX" 'PREFIX')" + +while read -r line; do + if [ -z "$COLOR" ]; then + printf '%s: %s\n' "$PREFIX" "$line" + else + printf '%s: %s\n' "$(color -c "$COLOR" "$PREFIX")" "$line" + fi +done diff --git a/bin/print b/bin/print new file mode 100755 index 0000000..e0d3d6e --- /dev/null +++ b/bin/print @@ -0,0 +1,151 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + print [-d] [-q QUALITY] [FILE...] + print -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -d print duplex/double-sided + -q QUALITY choose the print quality, either: + low, medium (default) or high. + -h, --help show this message + + Examples: + + Print the given PostScript file with default quality: + $ print f1.ps + + Print multiple PDF files with high quality: + $ print -dq high *.pdf + + Print the file from STDIN, double-sided: + $ print -d < f2.ps + + Print multiple source code files: + $ print src/*.{c,h} + EOF +} + +mkdtemp() { + name="$(echo 'mkstemp(template)' | + m4 -D template="${TMPDIR:-/tmp}/m4-tmpname.")" + rm -f "$name" + mkdir "$name" + echo "$name" +} + +n_pages() { + pdftk "$1" dump_data | awk '/NumberOfPages/ { print $2 }' +} + +main() { + if file -b "$FILE" | grep -q PostScript; then + ps2pdf "$FILE" "$NEWDIR"/in.pdf + elif file -b "$FILE" | grep -q PDF; then + cp "$FILE" "$NEWDIR"/in.pdf + else + enscript -o- "$FILE" | ps2pdf - "$NEWDIR"/in.pdf + fi + cd "$NEWDIR" + + if [ -z "$DUPLEX" ]; then + lp in.pdf + cd - > /dev/null + return + fi + + if [ "$(n_pages in.pdf)" = '1' ]; then + lp in.pdf + return + fi + + pdftk A=in.pdf cat Aodd output odd.pdf + pdftk A=in.pdf cat Aeven output even.pdf + + NODD="$(n_pages odd.pdf)" + NEVEN="$(n_pages even.pdf)" + + printf 'Printing odd pages...\n' >&2 + lp odd.pdf + printf 'Has printing finished yet? Once it does, reload the pages and hit it enter to continue. ' + read -r < /dev/tty + lp even.pdf + + if [ "$NODD" != "$NEVEN" ]; then + printf '\n' | lp + fi + + cd - > /dev/null +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +lpoptions -o PrintQuality=standard +DUPLEX= +while getopts 'dq:h' flag; do + case "$flag" in + d) + DUPLEX=1 + ;; + q) + case "$OPTARG" in + low) + lpoptions -o PrintQuality=draft + ;; + medium) + lpoptions -o PrintQuality=standard + ;; + high) + lpoptions -o PrintQuality=high + ;; + *) + echo "Bad QUALITY option: \"$OPTARG\"" >&2 + exit 2 + ;; + esac + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +NEWDIR="$(mkdtemp)" +if [ -z "${1:-}" ]; then + FILE="$NEWDIR"/STDIN + cat - > "$FILE" + main +else + for f in "$@"; do + FILE="$f" + main + done +fi diff --git a/bin/prompt b/bin/prompt new file mode 100755 index 0000000..247c81a --- /dev/null +++ b/bin/prompt @@ -0,0 +1,76 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + prompt STRING|- + prompt -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + STRING the text to be displayed in the prompt + + + Display a prompt and return a value corresponding to the + response. + + + Examples: + + Conditionally run download command + + if prompt 'Download files?'; then + run_download; + fi + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +STRING="${1:-}" +eval "$(assert-arg "$STRING" 'STRING')" + +printf '%s' "$STRING" +printf ' [Y/n]: ' +read -r yesno +if [ "$yesno" != 'n' ] && [ "$yesno" != 'N' ]; then + exit 0 +else + exit 1 +fi @@ -0,0 +1,71 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + qr [-s PIXEL_SIZE] + qr -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -s PIXEL_SIZE size of the pixel (default 10) + -h, --help show this help message + + + Read data from STDIN and present a QR image with said data. + + + Examples: + + Link to my homepage: + + $ printf 'https://euandre.org' | qr + + + Numbers with a smaller pixel size: + + $ seq 99 | qr -s 5 + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +PIXEL_SIZE=10 +while getopts 's:h' flag; do + case "$flag" in + s) + PIXEL_SIZE="$OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +cat | qrencode -s "$PIXEL_SIZE" -o- | feh - diff --git a/bin/repos b/bin/repos new file mode 100755 index 0000000..e45f4c8 --- /dev/null +++ b/bin/repos @@ -0,0 +1,175 @@ +#!/bin/sh +set -eu + + +usage() { + cat <<-'EOF' + Usage: + repos [-e DIR...] [-v] [DIRECTORY] + repos -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -e DIR exclude the given directory from traversing + -v enable verbose mode + -h, --help show this message + + DIRECTORY the folder to traverse + + + Traverse DIRECTORY looking for VCS repositores. As soon as + one is found, stop recursing, and emit its name alongside its + type. + + On verbose mode, print the directories being visited. + + + Examples: + + Show all repositories under ~/dev/, excluding a few directories: + + $ repos -e ~/dev/go/ -e ~/dev/quicklisp/ ~/dev/ + # ... + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + + +# Similar to urlencode but only for % and \n (and not \0). +array_encode() { + sed 's/%/%25/g' | sed -e :a -e '$!N; s/\n/%0A/; ta' +} + +array_decode() { + sed -e 's/%0A/\ +/g' -e 's/%25/%/g' +} + + +# +# CAVEAT: +# To avoid needing to keep decoding the array elements on every call to +# `arr_includes`, assume directory names don't contain newlines. This makes the +# current code scanning ~/dev/ from 8 seconds go to 1 second. +# + +arr_push() { + ARR="$1" + ELT="$2" + if [ -n "$ARR" ]; then + echo "$ARR" + fi + echo "$ELT" # | array_encode (see CAVEAT) +} + +arr_includes() { + ARR="$1" + ELT="$2" + echo "$ARR" | while read -r el; do + # if [ "$(printf '%s\n' "$el" | array_decode)" = "$ELT" ]; then (see CAVEAT) + if [ "$el" = "$ELT" ]; then + return 2 + fi + done + if [ $? = 2 ]; then + return 0 + else + return 1 + fi +} + +EXCLUDE= +VERBOSE=false +while getopts 'e:vh' flag; do + case "$flag" in + e) + case "$OPTARG" in + */) + ARG="$OPTARG" + ;; + *) + ARG="$OPTARG/" + ;; + esac + EXCLUDE="$(arr_push "$EXCLUDE" "$ARG")" + ;; + v) + VERBOSE=true + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +is_repository() { + TYPE="$(vcs -C "$1" -t 2>/dev/null)" + if [ -n "$TYPE" ]; then + echo "$1" + else + return 1 + fi +} + + +traverse_directory() { + if [ "$VERBOSE" = true ]; then + printf 'cur: %s\n' "$1" >&2 + fi + if arr_includes "$EXCLUDE" "$1"; then + return + fi + if is_repository "$1"; then + return + fi + for d in "$1"/*; do + if [ "$VERBOSE" = true ]; then + printf 'cur: %s\n' "$d" >&2 + fi + if [ ! -d "$d" ]; then + continue + fi + if arr_includes "$EXCLUDE" "$d/"; then + continue + fi + if ! is_repository "$d"; then + traverse_directory "$d" + fi + done +} + +if [ -z "${1:-}" ]; then + set -- "$PWD" +fi + +for dir in "$@"; do + traverse_directory "${dir%%/}" +done @@ -0,0 +1,150 @@ +#!/bin/sh +set -eu + +D="${XDG_DATA_HOME:-$HOME/.local/share}/doc/rfc" +PROMPT_DB="$( + cat <<-EOF + RFC directory does not exist: + $D/ + + Do you want to download the files to create it? + EOF +)" + +usage() { + cat <<-'EOF' + Usage: + rfc [-w] RFC_NUMBER + rfc -u + rfc -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -w show the path to the RFC file instead of displaying + its contents + -u update the local RFC database + -h, --help show this message + + Lookup the given RFC + in $XDG_DATA_HOME/doc/rfc/ (defaults to ~/.local/share), + and feed it into the $PAGER, akin to doing: + + $ $PAGER $XDG_DATA_HOME/doc/rfc/rfc$RFC_NUMBER.txt + + If the $XDG_DATA_HOME/doc/rfc/ directory doesn't exist, it gets + created it by downloading the latest RFC files and placing all .txt + files there. + + + Examples: + + + Show RFC 1234 in $PAGER: + + $ rfc 1234 + + + Print path to RFC 2222: + + $ rfc 2222 + + + Download the latest RFCs: + + $ rfc -u + EOF +} + +view() { + if [ -t 1 ]; then + ${PAGER:-cat} + else + cat + fi +} + +update() { + rsync -avzP --delete ftp.rfc-editor.org::rfcs-text-only "$D" + STATUS=$? + if [ "$STATUS" != 0 ]; then + exit "$STATUS" + fi +} + +check_local_db() { + if [ ! -e "$D" ]; then + if prompt "$PROMPT_DB"; then + update + else + echo 'No local RFC database to operate on.' >&2 + exit 1 + fi + fi +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'wuh' flag; do + case "$flag" in + w) + WHERE=true + ;; + u) + UPDATE=true + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +RFC_NUMBER="${1:-}" +F="$D/rfc${RFC_NUMBER}.txt" + +check_local_db + +if [ "${UPDATE:-}" = true ]; then + update + exit +fi + +eval "$(assert-arg "$RFC_NUMBER" 'RFC_NUMBER')" + +if [ ! -e "$F" ]; then + printf 'Given RFC_NUMBER "%s" does not exist at:\n%s\n' \ + "$RFC_NUMBER" "$F" >&2 + exit 2 +fi + +if [ "${WHERE:-}" = true ]; then + printf '%s\n' "$F" + exit +else + view < "$F" + exit +fi diff --git a/bin/serve b/bin/serve new file mode 100755 index 0000000..4a06c50 --- /dev/null +++ b/bin/serve @@ -0,0 +1,78 @@ +#!/bin/sh +set -eu + + +usage() { + cat <<-'EOF' + Usage: + serve [-d DIRECTORY] [-p PORT] + serve -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -d DIRECTORY the directory to serve (default: ".") + -p PORT the port to listen on (default: 8000) + -h, --help show this message + + + Serve DIRECTORY via HTTP as a static file server, and open the + URL on the $BROWSER. + + + Examples: + + Serve "." on the default PORT: + + $ serve + + + Serve "public/" on port 1234: + + $ serve -d public/ -p 1234 + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +DIRECTORY='.' +PORT=8000 +while getopts 'd:p:h' flag; do + case "$flag" in + d) + DIRECTORY="$OPTARG" + ;; + p) + PORT="$OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done + +open "http://localhost:$PORT" +python3 -m http.server -d "$DIRECTORY" "$PORT" diff --git a/bin/slugify b/bin/slugify new file mode 100755 index 0000000..aa3b50a --- /dev/null +++ b/bin/slugify @@ -0,0 +1,69 @@ +#!/bin/sh +set -eu + + +usage() { + cat <<-'EOF' + Usage: + slugify < STDIN + slugify -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + "slugify" the input string, removing diacritics and punctuation + from the string, on a best-effort basis. + + + Examples: + + Slugify the input string: + + $ echo 'Saçi-pererê, tomando açaí!!' | slugify + saci-perere-tomando-acai + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + esac +done +shift $((OPTIND - 1)) + +iconv -ct ASCII//TRANSLIT | + tr '[:upper:]' '[:lower:]' | + sed \ + -e 's/[^a-z0-9]/-/g' \ + -e 's/--*/-/g' \ + -e 's/^-//' \ + -e 's/-$//' diff --git a/bin/status-bar b/bin/status-bar new file mode 100755 index 0000000..bd55b05 --- /dev/null +++ b/bin/status-bar @@ -0,0 +1,104 @@ +#!/usr/bin/env perl + +# +# Derived from: +# https://github.com/i3/i3status/blob/28399bf84693a03eb772be647d5927011c1d2619/contrib/wrapper.pl +# + +use v5.34; +use warnings; +use feature 'signatures'; +no warnings ('experimental::signatures'); +use Getopt::Std (); +use JSON (); + +sub usage($fh) { + print $fh <<~'EOF' + Usage: + status-bar + status-bar -h + EOF +} + +sub help($fh) { + print $fh <<~'EOF' + + Options: + -h, --help show this message + + + Process the output of i3status and add custom information for + showing in the i3 bar. + + + Examples: + + Process i3status: + + $ i3status | status-bar + + + Configure i3 to use status-bar: + + # In $XDG_CONFIG_HOME/i3/config + bar { + status_command i3status | status-bar + } + EOF +} + +for (@ARGV) { + last if $_ eq '--'; + if ($_ eq '--help') { + usage *STDOUT; + help *STDOUT; + exit; + } +} + +my %opts; +if (!Getopt::Std::getopts('h', \%opts)) { + usage *STDERR; + exit 2; +} + +if ($opts{h}) { + usage *STDOUT; + help *STDOUT; + exit; +} + +# Don't buffer any output +$| = 1; + +# Skip the first line which contains the version header. +print scalar <STDIN>; + +# The second line contains the start of the infinite array. +print scalar <STDIN>; + +# Read lines forever, ignore a comma at the beginning if it exists. +while (my ($statusline) = (<STDIN> =~ /^,?(.*)/)) { + # Decode the JSON-encoded line. + my @blocks = @{JSON::decode_json($statusline)}; + + # Prefix our own information (you coud also suffix or insert in the + # middle). + + my $mpris = `player current`; + chomp $mpris; + + my $vms = `vm status | grep :up\$ | wc -l`; + chomp $vms; + + @blocks = ({ + full_text => $vms, + name => 'vms', + }, { + full_text => $mpris, + name => 'mpris', + }, @blocks); + + # Output the line as JSON. + print JSON::encode_json(\@blocks) . ",\n"; +} diff --git a/bin/stopwatch b/bin/stopwatch new file mode 100755 index 0000000..3d5cd07 --- /dev/null +++ b/bin/stopwatch @@ -0,0 +1,65 @@ +#!/bin/sh +set -eu + + +usage() { + cat <<-'EOF' + Usage: + stopwatch + stopwatch -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Run a TUI stopwatch. + + + Examples: + + Just run it: + + $ stopwatch + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +date "+%l:%M:%S%p: stopwatch started (^D to stop)" +time cat +date "+%l:%M:%S%p: stopwatch stopped" @@ -0,0 +1,91 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + tmp FILE... + tmp -d + tmp -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -d delete the remote "tmp/" folder + -h, --help show this message + + + Copies a file to the public server. + + + Examples: + + Copy f.txt: + + $ tmp f.txt + + + Cleanup the $REMOTE: + + $ tmp -d + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +REMOTE='euandreh.xyz' +DIR='/opt/www/euandreh.xyz/static/tmp' +while getopts 'dh' flag; do + case "$flag" in + d) + printf 'Deleting %s:%s...\n' "$REMOTE" "$DIR/" >&2 + ssh "$REMOTE" rm -rf "$DIR" + exit + ;; + h) + usage + help + exit + ;; + *) + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) +FILE="${1:-}" + +if [ -z "$FILE" ]; then + printf 'Missing FILE.\n\n' >&2 + usage >&2 + exit 2 +fi + +for f in "$@"; do + FILENAME="$(basename "$f")" + # shellcheck disable=2029 + ssh "$REMOTE" "mkdir -p '$DIR' && cat > '$DIR/$FILENAME'" < "$f" + + LINK="$(printf 'https://%s/tmp/%s' "$REMOTE" "$FILENAME")" + open "$LINK" + if [ $# = 1 ]; then + printf '%s' "$LINK" | copy + printf 'Copied %s to the clipboard!\n' "$LINK" >&2 + fi +done diff --git a/bin/tmpname b/bin/tmpname new file mode 100755 index 0000000..89d7e4d --- /dev/null +++ b/bin/tmpname @@ -0,0 +1,66 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + tmpname + tmpname -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Generate a temporary name. + + + Examples: + + Create a temporary file: + + $ OUT="$(tmpname)"; touch "$OUT"; cmd > "$OUT" + + + `cd` into a temporary directory: + + $ DIR="$(tmpname)"; mkdir -p "$DIR"; cd "$DIR" + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +echo "${TMPDIR:-/tmp}/uuid-tmpname with spaces.$(uuid)" diff --git a/bin/tuivid b/bin/tuivid new file mode 100755 index 0000000..54bddcf --- /dev/null +++ b/bin/tuivid @@ -0,0 +1,65 @@ +#!/bin/sh +set -eu + + +usage() { + cat <<-'EOF' + Usage: + tuivid FILE + tuivid -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + FILE the name of the video file + + + Play a video in the terminal, withut X or a GUI. + + + Examples: + + Play "movie.mp4": + + $ tuivid movie.mp4 + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +exec mpv --quiet --vo=tct --vo-tct-256=yes --vo-tct-algo=plain --framedrop=vo "$@" @@ -0,0 +1,70 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + uc + uc -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Transforms text from STDIN from upper-case to lower-case. It is + the equivalent of running 'tr [:lower:] [:upper:]'. + + + Examples: + + Normalize to lower-case: + + $ echo EuAndreh | uc + EUANDREH + + + Keep the text as-is: + + $ echo ANDREH | uc + ANDREH + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +tr '[:lower:]' '[:upper:]' diff --git a/bin/untill b/bin/untill new file mode 100755 index 0000000..030f921 --- /dev/null +++ b/bin/untill @@ -0,0 +1,83 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + until [-n SECONDS] -- COMMAND... + until -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -n SECONDS the amount of seconds to sleep between + attempts (default: 5) + -h, --help show this message + + + Runs COMMAND until it eventually succeeds. Sleep SECONDS + between attempts. + + + Examples: + + Try flaky build until it succeeds: + + $ until guix home build home.scm + + + Try to build until a new version is successfull, + waiting 30 seconds between attempts: + + $ until -n 30 -- x git pull AND make + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +_SECONDS=5 +while getopts 'n:h' flag; do + case "$flag" in + n) + _SECONDS="$OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +ATTEMPT=1 +while true; do + printf 'Attempt %s.\n' "$ATTEMPT" >&2 + ATTEMPT=$((ATTEMPT + 1)) + if "$@"; then + break + fi + sleep "$_SECONDS" +done diff --git a/bin/update b/bin/update new file mode 100755 index 0000000..cc2412e --- /dev/null +++ b/bin/update @@ -0,0 +1,73 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + update + update -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Updates miscellaneous things on the workstation: + - "guix pull" on the "andreh" and "root" accounts; + - get latest RFCs; + - updates RSS feeds; + - updates source code repositories. + + + Examples: + + Just use it: + + $ update + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +guix pull +rfc -u + +repos -e ~/dev/go/ -e ~/dev/quicklisp/ -e ~/dev/archive/ ~/dev/ | + xargs -I% -P4 x \ + echo 'Fetching on %.' AND \ + vcs -C% fetch OR \ + echo 'WARNING: Failed to fetch repository: %.' >&2 diff --git a/bin/upgrade b/bin/upgrade new file mode 100755 index 0000000..4447a3d --- /dev/null +++ b/bin/upgrade @@ -0,0 +1,66 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + upgrade + upgrade -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Upgrades the system: + - reconfigure the Guix "home" environment and "system". + + + Examples: + + Just use it: + + $ upgrade + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +pass show velhinho/0-andreh-password | + head -n1 | + sudo -ES guix system -v3 reconfigure /etc/guix/configuration.scm +guix home -v3 reconfigure "$XDG_CONFIG_HOME"/guix/home.scm @@ -0,0 +1,75 @@ +#!/usr/bin/env perl + +use v5.34; +use warnings; +use feature 'signatures'; +no warnings ('experimental::signatures'); +use Getopt::Std (); +use URI::Escape (); + +sub usage($fh) { + print $fh <<~'EOF' + Usage: + uri [-e|-d] + uri -h + EOF +} + +sub help($fh) { + print $fh <<~'EOF' + + Options: + -e encode the string (the default action) + -d decode the string + -h, --help show this message + + + Get a string from STDIN and convert it to/from URL encoding. + + + Examples: + + Encode the URL: + + $ echo "https://euandre.org/?q=$(printf '%s' 'a param' | uri)" + https://euandre.org/?q=a%20param + + + Decode the content from the file: + + $ uri -d < file + EOF +} + +for (@ARGV) { + last if $_ eq '--'; + if ($_ eq '--help') { + usage *STDOUT; + help *STDOUT; + exit; + } +} + +my %opts; +if (!Getopt::Std::getopts('edh', \%opts)) { + usage *STDERR; + exit 2; +} + +if ($opts{h}) { + usage *STDOUT; + help *STDOUT; + exit; +} elsif ($opts{e} and $opts{d}) { + say STDERR 'Both -e and -d given. Pick one.'; + say STDERR ''; + usage *STDERR; + exit 2; +} + + +if ($opts{d}) { + print URI::Escape::uri_unescape(<STDIN>); +} else { + print URI::Escape::uri_escape(<STDIN>); +} diff --git a/bin/uuid b/bin/uuid new file mode 100755 index 0000000..34b685f --- /dev/null +++ b/bin/uuid @@ -0,0 +1,62 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + uuid + uuid -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + + Generate UUID from /dev/random. + + + Examples: + + Generate a UUID: + + $ uuid + 755244c8-f955-16df-75cc-f25600c90422 + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +od -xN20 /dev/random | + awk 'NR == 1 { OFS="-"; print $2$3,$4,$5,$6,$7$8$9; exit }' @@ -0,0 +1,255 @@ +#!/bin/sh +set -eu + + +TYPES=' +git +mercurial +bitkeeper +darcs +cvs +fossil +' + + +guess_type() { + if [ -e "$1"/.git ]; then + echo git + elif [ -e "$1"/.hg ]; then + echo mercurial + elif [ -e "$1"/.bk ]; then + echo bitkeeper + elif [ -e "$1"/_darcs ]; then + echo darcs + elif [ -e "$1"/CVS/ ]; then + echo cvs + elif [ -e "$1"/.fslckout ]; then + echo fossil + else + return 1 + fi +} + + + + +git_fetch() { + git fetch "$@" +} + +darcs_fetch() { + darcs fetch "$@" +} + +mercurial_fetch() { + hg pull "$@" +} + +fossil_fetch() { + fossil pull "$@" +} + +cvs_fetch() { + timeout 300 cvs update "$@" +} + +git_status() { + git status "$@" +} + +fossil_status() { + fossil status "$@" +} + +git_diff() { + git diff "$@" +} + +git_ps1() { + BRANCH_NAME="$(git rev-parse --abbrev-ref HEAD)" + OUT="$(git status --short --branch --porcelain)" + BRANCH_LINE="$(echo "$OUT" | head -n 1)" + DIFF_LINES="$(echo "$OUT" | tail -n +2)" + + IS_AHEAD=false + IS_BEHIND=false + if echo "$BRANCH_LINE" | grep -q 'ahead'; then + IS_AHEAD=true + fi + if echo "$BRANCH_LINE" | grep -q 'behind'; then + IS_BEHIND=true + fi + + LINE='' + + if [ "$IS_AHEAD" = true ] && [ "$IS_BEHIND" = true ]; then + LINE="^^^ $BRANCH_NAME vvv" + elif [ "$IS_AHEAD" = true ]; then + LINE="^ $BRANCH_NAME ^" + elif [ "$IS_BEHIND" = true ]; then + LINE="v $BRANCH_NAME v" + else + LINE="$BRANCH_NAME" + fi + + HAS_DIFF=false + HAS_UNTRACKED=false + if echo "$DIFF_LINES" | grep -q '^[A|D|M| ][M|D| ]'; then + HAS_DIFF=true + fi + if echo "$DIFF_LINES" | grep -q '^[?][?]'; then + HAS_UNTRACKED=true + fi + + if [ "$HAS_DIFF" = true ]; then + COLOR_FN=redb + LINE="{$LINE}" + elif [ "$IS_AHEAD" = true ] || [ "$IS_BEHIND" = true ]; then + COLOR_FN=bluei + LINE="[$LINE]" + elif [ "$HAS_UNTRACKED" = true ]; then + COLOR_FN=lightblue + LINE="{$LINE}" + else + COLOR_FN=green + LINE="($LINE)" + fi + + color -c "$COLOR_FN" "$LINE" + + BRANCH_COUNT="$(git branch --list | wc -l)" + if [ "$BRANCH_COUNT" -gt 1 ]; then + color -c lightblue "<$BRANCH_COUNT>" + fi + + STASH_COUNT="$(git stash list | wc -l)" + if [ "$STASH_COUNT" != 0 ]; then + color -c red "*$STASH_COUNT" + fi + + color -c blacki " - git/$(git rev-parse HEAD)" +} + +fossil_ps1() { + BRANCH_NAME="$(fossil branch current)" + + if [ -n "$(fossil extras)" ]; then + HAS_UNTRACKED=1 + fi + + BRANCH_MARKER="$BRANCH_NAME" + + if [ -n "${HAS_UNTRACKED:-}" ]; then + COLOR_FN=lightblue + LINE="($BRANCH_MARKER)" + else + COLOR_FN=green + LINE="($BRANCH_MARKER)" + fi + + color -c "$COLOR_FN" "$LINE" + + color -c blacki " - fossil/$(fossil info | awk '/^checkout:/ { print $2 }')" +} + +mercurial_ps1() { + BRANCH_NAME="$(hg branch)" +} + +git_gc() { + git remote prune origin "$@" +} + + +usage() { + cat <<-'EOF' + Usage: + vcs [-C DIR] ACTION + vcs [-C DIR] -l|-t + vcs -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -C DIR change to DIR instead of PWD + -l list the supported VCS types + -t show the guesses VCS type + -h, --help show this message + + ACTION the action to be performed on the repository: + - fetch + - ps1 + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'C:lth' flag; do + case "$flag" in + C) + cd "$OPTARG" + ;; + l) + # shellcheck disable=2086 + echo $TYPES | tr ' ' '\n' + exit + ;; + t) + guess_type "$PWD" || { + printf 'Could not guess the type of the repository.\n' >&2 + exit 2 + } + exit + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + esac +done +shift $((OPTIND - 1)) + +ACTION="${1:-}" +eval "$(assert-arg "$ACTION" 'ACTION')" +shift + +if [ "${1:-}" = '--' ]; then + shift +fi + +TYPE="$(guess_type "$PWD")" +if [ -z "$TYPE" ]; then + printf 'Could not guess the type of the repository.\n' >&2 + exit 2 +fi + +CMD="$TYPE"_"$ACTION" +if ! command -v "$CMD" >/dev/null; then + printf 'Invalid ACTION: "%s".\n\n' "$ACTION" >&2 + usage >&2 + exit 2 +fi + +"$CMD" "$@" @@ -0,0 +1,176 @@ +#!/bin/sh +set -eu + + +usage() { + cat <<-'EOF' + Usage: + vm ACTION [OS] + vm -h + EOF +} + +help() { + cat <<-'EOF' + + + Options: + -h, --help show this message + + ACTION one of: + - up + - down + - status + OS the name of the OS to be acted upon + + + Manage the state of known virtual machines. + + The VM QCOW2 images are stored under + $XDG_STATE_HOME/euandreh/qemu/, as "$OS.qcow2" files. The PIDs + of the running images are stored in individual files under + $XDG_RUNTIME_DIR/vm-pids/, as "$OS.pid" files. + + It also generates an SSH configuration file to bu `Included` + by the main $XDG_CONFIG_HOME/ssh/config file, which contains + one alias entry for each VM, so that one can do a combination + of `vm up alpine && ssh alpine`. + + + Examples: + + Start the VM for Alpine: + + $ vm up alpine + + + Stop the VM for Slackware, which was already down + + $ vm down slackware + The VM for "slackware" is not running, already. + + + List the available VMs, and their current state: + + $ vm status + alpine:up + slackware:down + freebsd:up + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + + +ACTION="${1:-}" +OS="${2:-}" + +eval "$(assert-arg "$ACTION" 'ACTION')" + + +VMS=' +alpine:60022 +' + +for vm in $VMS; do + NAME="$(echo "$vm" | cut -d: -f1)" + PORT="$(echo "$vm" | cut -d: -f2)" + cat <<-EOF + Host $NAME + HostName localhost + Port $PORT + + EOF +done > "$XDG_DATA_HOME"/euandreh/vm-ssh.conf + +PIDS_DIR="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/vm-pids" +PID_F="$PIDS_DIR/$OS.pid" +mkdir -p "$PIDS_DIR" + + +port_for_os() { + _OS="$1" + _PORT="$(echo "$VMS" | awk -F: -vOS="$_OS" '$1 == OS { print $2 }')" + if [ -z "$_PORT" ]; then + printf 'Unknown OS: "%s".\n\n' "$_OS" >&2 + usage >&2 + exit 2 + fi + printf '%s' "$_PORT" +} + +case "$ACTION" in + status) + echo "$VMS" | awk -F: '$0=$1' | while read -r vm; do + if [ -e "$PIDS_DIR/$vm.pid" ]; then + STATUS=up + else + STATUS=down + fi + printf '%s:%s\n' "$vm" "$STATUS" + done + ;; + up) + eval "$(assert-arg "$OS" 'OS')" + PORT="$(port_for_os "$OS")" + + if [ -e "$PID_F" ]; then + printf 'The VM for "%s" is already running with PID %s.\n' \ + "$OS" "$(cat "$PID_F")" >&2 + else + QCOW="$XDG_STATE_HOME"/euandreh/qemu/"$OS".qcow2 + qemu-system-x86_64 \ + -m 1G \ + -nic user,model=virtio,hostfwd=tcp::"$PORT"-:22 \ + -drive file="$QCOW",media=disk,if=virtio \ + -enable-kvm \ + -nographic 1>/dev/null 2>&1 & + printf '%s' $! > "$PID_F" + fi + ;; + down) + eval "$(assert-arg "$OS" 'OS')" + PORT="$(port_for_os "$OS")" + + if [ ! -e "$PID_F" ]; then + printf 'The VM for "%s" is not running, already.\n' "$OS" >&2 + else + # shellcheck disable=2064 + trap "rm -f $PID_F" EXIT + kill "$(cat "$PID_F")" + fi + ;; + *) + printf 'Unrecognized action: "%s".\n\n' "$ACTION" >&2 + usage >&2 + exit 2 +esac diff --git a/bin/volume b/bin/volume new file mode 100755 index 0000000..991fbc7 --- /dev/null +++ b/bin/volume @@ -0,0 +1,112 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + volume ACTION + volume -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + ACTION one of: + - up + - down + - toggle + - rotate + + + Manage the audio output. + + + Examples: + + Increase the volume: + + $ volume up + + + Toggle mute/unmute in the current audio output: + + $ volume toggle + + + Change the audio output: + + $ volume rotate + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) +ACTION="${1:-}" + +eval "$(assert-arg "$ACTION" 'ACTION')" + + +rotate() { + # This script assumes that at most 2 sinks exist at any time. + # When this premise is no longer true, it needs to be upgraded. + + CURRENT="$(pacmd list-sinks | grep '\* index' | cut -d: -f2 | tr -d ' ')" + OTHER="$(pacmd list-sinks | grep index | grep -v '\* index' | tail -n1 | cut -d: -f2 | tr -d ' ')" + + if [ "$CURRENT" = 0 ]; then + pacmd set-default-sink "$OTHER" + else + pacmd set-default-sink 0 + fi +} + +case "$ACTION" in + up) + pactl set-sink-volume @DEFAULT_SINK@ +10% + ;; + down) + pactl set-sink-volume @DEFAULT_SINK@ -10% + ;; + toggle) + pactl set-sink-mute @DEFAULT_SINK@ toggle + ;; + rotate) + rotate + ;; + *) + printf 'Bad ACTION: %s.\n\n' "$ACTION" >&2 + usage >&2 + exit 2 + ;; +esac diff --git a/bin/with-email b/bin/with-email new file mode 100755 index 0000000..7df101a --- /dev/null +++ b/bin/with-email @@ -0,0 +1,91 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + with-email [-s SUBJECT] COMMAND... + with-email -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -s SUBJECT set the subject of the email + -h, --help show this message + + COMMAND the command to be wrapped + + + Executes COMMAND and send all of its output via email + to eu@euandre.org. + + + Examples: + + Run a script and use the default subject: + + $ with-email -- ./script + + Run a command and use a custom subject: + + $ with-email -s 'CRONJOB' echo 123 + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +SUBJECT='NO SUBJECT' +while getopts 's:h' flag; do + case "$flag" in + s) + SUBJECT="$OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +eval "$(assert-arg "${1:-}" 'COMMAND...')" + +now() { + date '+%Y-%m-%dT%H:%M:%S%Z' +} + +OUT="$(mkstemp)" +{ + printf 'Running command: %s\n' "$*" + printf 'Starting at: %s\n' "$(now)" + printf '\n' + + STATUS=0 + "$@" || STATUS=$? + + printf '\n' + printf '\nFinished at: %s\n' "$(now)" +} 1>"$OUT" 2>&1 + +email -s "(exit status: $STATUS) - $SUBJECT" eu@euandre.org < "$OUT" diff --git a/bin/without-env b/bin/without-env new file mode 100755 index 0000000..fd9d1e8 --- /dev/null +++ b/bin/without-env @@ -0,0 +1,70 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + without-env ENVVAR PATH -- COMMAND... + without-env -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + Examples: + + Execute "command -V" filtering ~/bin, to get where "w3m" is + in $PATH, other than ~/bin: + $ without-env PATH ~/bin -- command -v w3m + + Compile foo.c, excluding ~/.local/include + from $C_INCLUDE_PATH: + $ without-env C_INCLUDE_PATH ~/.local/include -- cc -co foo.o foo.c + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) + +eval "$(assert-arg "${1:-}" 'ENVVAR')" +eval "$(assert-arg "${2:-}" 'PATH')" + +eval "export $1=\"\$(echo \"\$$1\" | sed \"s|\$2:||g\")\"" +shift # drop $1 +shift # drop $2 +if [ "${1:-}" = '--' ]; then + shift # drop -- +fi + +"$@" @@ -0,0 +1,95 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + wms ACTION + wms -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -h, --help show this message + + ACTION one of: + - uuid + - date + - clear-notification + + + Helper script to launch CLI commands, without having complex + quoting, piping, flow control, etc. clutter the wm configuration + file. + + + Examples: + + Generate a new UUID, copy it to the clipboard and send a + desktop notification: + + $ wms uuid + EOF +} + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +while getopts 'h' flag; do + case "$flag" in + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) +ACTION="${1:-}" + +eval "$(assert-arg "$ACTION" 'ACTION')" + + +copy_and_notify() { + STR="$1" + LABEL="$2" + printf '%s' "$STR" | copy -n + notify-send -t 5000 -u normal -- \ + "$STR" "$LABEL copied to clipboard" +} + +case "$ACTION" in + uuid) + copy_and_notify "$(uuid)" 'UUID' + ;; + date) + copy_and_notify "$(date '+%Y-%m-%d')" 'date' + ;; + clear-notification) + dunstctl close + ;; + *) + printf 'Bad ACTION: %s.\n\n' "$ACTION" >&2 + usage >&2 + exit 2 + ;; +esac @@ -0,0 +1,124 @@ +#!/usr/bin/env perl + +use v5.34; +use warnings; +use feature 'signatures'; +no warnings ('experimental::signatures', 'experimental::smartmatch'); +use Getopt::Std (); + +sub usage($fh) { + print $fh <<~'EOF' + Usage: + x COMMANDS... [ '&&' / '||' / '|' ] COMMANDS... + x COMMANDS... [ 'AND' / 'OR' / 'PIPE' ] COMMANDS... + x -h + EOF +} + +sub help($fh) { + print $fh <<~'EOF' + + Options: + -h, --help show this message + + COMMAND the command to be executed + '&&' / AND "AND" logical operator + '||' / OR "OR" logical operator + '|' / PIPE pipe operator + + + Run command chained together with operators. + + NOTE: Remember to quote '&&', 'OR' and '|' operators, otherwise + they'll get captured by the shell and not be passed to the 'x' + program! + + + Examples: + + Measure the time of two commands: + + $ time x sleep 1 '&&' sleep 2 + # equivalent to: + $ time sh -c 'sleep 1 && sleep 2' + + + Notify when either of the commands finish: + + $ boop x cmd-1 '||' cmd-2 + EOF +} + + +for (@ARGV) { + last if $_ eq '--'; + if ($_ eq '--help') { + usage *STDOUT; + help *STDOUT; + exit; + } +} + +my %opts; +if (!Getopt::Std::getopts('h', \%opts)) { + usage *STDERR; + exit 2; +} + +if ($opts{h}) { + usage *STDOUT; + help *STDOUT; + exit; +} + + +sub status_for($n) { + if ($n == -1) { + return 127; + } elsif ($n & 127) { + return $n & 127; + } else { + return $n >> 8; + } +} + +my @AND = ('&&', 'AND'); +my @OR = ('||', 'OR'); +my @PIPE = ('|', 'PIPE'); +my @OPS = (@AND, @OR, @PIPE); + +my @CMD; +for (@ARGV) { + if ($_ ~~ @OPS) { + system @CMD; + @CMD = (); + if ($_ ~~ @AND && $?) { + exit status_for($?); + } elsif ($_ ~~ @OR && !$?) { + exit 0; + } elsif ($_ ~~ @PIPE) { + ... + } + } else { + push @CMD, $_; + } +} + +exit status_for(system @CMD); + + +__END__ + +=head1 NAME + +x - chain shell commands without creating a subshell + +=head1 SYNOPSYS + +x COMMANDS... [ '&&' / '||' / '|' ] COMMANDS... + +x COMMANDS... [ 'AND' / 'OR' / 'PIPE' ] COMMANDS... + +x -h + +=cut diff --git a/bin/xdg-open b/bin/xdg-open new file mode 120000 index 0000000..ce4a72b --- /dev/null +++ b/bin/xdg-open @@ -0,0 +1 @@ +open
\ No newline at end of file diff --git a/bin/xmpp b/bin/xmpp new file mode 100755 index 0000000..b08a783 --- /dev/null +++ b/bin/xmpp @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import os +import sys +import getopt +import logging +import slixmpp + +USAGE = """\ +Usage: + xmpp [-d] [-F FROM_JID] -m MESSAGE TO_JID... + xmpp -h""" + +HELP = """ +Options: + -d run in DEBUG mode + -m MESSAGE the text of the message to be sent + -h, --help show this message + + FROM_JID the address used to send the message from + TO_JID the addresses where to send the message to + + +Send a one-off XMPP message. + + +Examples: + + Send a message to eu@euandreh.xyz: + + $ xmpp -m 'Hello, XMPP!' eu@euandreh.xyz""" + +class SendMsgBot(slixmpp.ClientXMPP): + def __init__(self, jid, password, on_start): + slixmpp.ClientXMPP.__init__(self, jid, password) + + self.on_start = on_start + self.add_event_handler("session_start", self.start) + + def start(self, event): + self.on_start(self) + self.disconnect(wait=True) + +def main(): + logging.basicConfig(level=logging.INFO) + from_ = "bot@euandreh.xyz" + message = "" + + for s in sys.argv: + if s == "--": + break + elif s == "--help": + print(USAGE) + print(HELP) + sys.exit() + + try: + opts, args = getopt.getopt(sys.argv[1:], 'm:F:dh') + except getopt.GetoptError as err: + print(err, file=sys.stderr) + print(USAGE, file=sys.stderr) + sys.exit(2) + for o, a in opts: + if o == "-m": + message = a + elif o == "-F": + from_ = a + elif o == "-d": + logging.basicConfig(level=logging.DEBUG) + elif o == "-h": + print(USAGE) + print(HELP) + sys.exit() + else: + assert False, "unhandled option" + + if message == "": + print("Missing -m MESSAGE", file=sys.stderr) + print(USAGE, file=sys.stderr) + sys.exit(2) + + if args == []: + print("Missing TO_JID", file=sys.stderr) + print(USAGE, file=sys.stderr) + sys.exit(2) + + passcmd = "pass show VPS/kuvira/XMPP/" + from_ + " | head -n1 | tr -d '\\n'" + password = os.popen(passcmd).read() + + def on_start(self): + for to in args: + self.send_message(mto=to, mbody=message, mtype='chat') + + xmpp = SendMsgBot(from_, password, on_start) + xmpp.connect() + xmpp.process(forever=False) + +if __name__ == "__main__": + main() @@ -0,0 +1,113 @@ +#!/bin/sh +set -eu + +usage() { + cat <<-'EOF' + Usage: + yt [-f] [-n PLAYLIST_COUNT] [URL...|FILE...|-] + yt -h + EOF +} + +help() { + cat <<-'EOF' + + Options: + -n PLAYLIST_COUNT the number of videos to grab from a + playlist (default: 15) + -f force to download a video already in the + archive + -h, --help show this message + + URL an 'https://...' address + FILE a file with 'https://...' addresses, one + per line + + Download videos and store them locally. + + + Examples: + + Download the video from the given URL: + + $ yt https://www.youtube.com/watch?v=EihZv2XgJdU + + + Force re-download the latest 5 videos already downloaded: + + $ yt -nf5 https://www.youtube.com/watch?list=TLPQMTIwODIwMjLnL2XjyRRgSw + EOF +} + + +for flag in "$@"; do + case "$flag" in + --) + break + ;; + --help) + usage + help + exit + ;; + *) + ;; + esac +done + +PLAYLIST_COUNT=15 +while getopts 'fn:h' flag; do + case "$flag" in + f) + FORCE=1 + ;; + n) + PLAYLIST_COUNT="$OPTARG" + ;; + h) + usage + help + exit + ;; + *) + usage >&2 + exit 2 + ;; + esac +done +shift $((OPTIND - 1)) +set -x + +if [ -z "${1:-}" ]; then + echo 'Missing URL|FILE argument' >&2 + usage >&2 + exit 2 +fi + + +if [ ! -e "$1" ]; then + F="$(mkstemp)" + printf '%s\n' "$1" > "$F" +else + F="$1" +fi + + +YT_TEMPLATE="$HOME/Downloads/yt-dl/%(uploader)s/%(upload_date)s %(title)s.%(ext)s" + +EXTRA_OPTIONS='' +if [ -z "${FORCE:-}" ]; then + EXTRA_OPTIONS="--download-archive $HOME/Documents/yt-dl-seen.txt" +fi + +# The value of $EXTRA_OPTIONS doesn't depend on user input, and can't contain +# spaces, unless $HOME contains spaces: +# shellcheck disable=2086 +youtube-dl \ + --batch-file "$F" \ + --format best \ + --prefer-free-formats \ + --playlist-end "$PLAYLIST_COUNT" \ + --write-description \ + --output "$YT_TEMPLATE" \ + $EXTRA_OPTIONS @@ -0,0 +1,153 @@ +#!/usr/bin/env perl + +use v5.34; +use warnings; +use feature 'signatures'; +no warnings qw(experimental::signatures); +use Getopt::Std (); +use File::Temp (); +use File::Basename (); +use List::Util qw(any); + + +sub usage($fh) { + print $fh <<~'EOF' + Usage: + z COMMANDS... + z -h + EOF +} + +sub help($fh) { + print $fh <<~'EOF' + + + Options: + -h, --help show this message + + + Wrapper that uncompresses file arguments to commands. + This enables having commands that operate on plain files to not + need to know if they're compressed or not. + + It doesn't depend on the file extension, but on what file(1) says + of it. + + + Examples: + + Replacement for zcat(1p): + + $ z cat a-file.gz + + + Transparent grep (where my-file.dat is xz compressed): + + $ z grep -E '[a-z]+' my-file.dat + EOF +} + +for (@ARGV) { + last if $_ eq '--'; + if ($_ eq '--help') { + usage *STDOUT; + help *STDOUT; + exit; + } +} + +my %opts; +if (!Getopt::Std::getopts('h', \%opts)) { + usage *STDERR; + exit 2; +} + +if ($opts{h}) { + usage *STDOUT; + help *STDOUT; + exit; +} + + +# Transform +# FILENAME: content/type; charset=some\n +# into +# content/type +sub trim($x) { + chomp $x; + $x =~ s/^.*?: //; + $x =~ s/;.*$//; + return $x; +} + +my %TYPES = ( + 'application/gzip' => [qw(gzip -dc)], + 'application/x-bzip2' => [qw(bzip2 -dc)], + 'application/x-xz' => [qw(xz -dc)], + 'application/x-lzma' => [qw(lzma -dc)], +); + +my @tmpfiles; +sub arg_for($arg) { + if (! -e $arg) { + return $arg; + } + + my $type = trim `file -i $_`; + if (any { $type eq $_ } keys %TYPES) { + my $template = File::Basename::basename $arg . '.XXXXXX'; + my ($fh, $tmpname) = File::Temp::tempfile(TEMPLATE => $template); + push @tmpfiles, $tmpname; + my @command = @{$TYPES{$type}}; + print $fh `@command $arg`; + die $! if $?; + close $fh; + return $tmpname; + } + + return $arg; +} + +sub status_for($n) { + if ($n == -1) { + return 127; + } elsif ($n & 127) { + return $n & 127; + } else { + return $n >> 8; + } +} + +my @CMD = map { arg_for $_ } @ARGV; +exit status_for(system @CMD); + +END { + unlink @tmpfiles; +} + + +__END__ + +=head1 NAME + +z - Wrapper that uncompresses file arguments to commands. + +=head1 SYNOPSIS + +z COMMAND FILE... + +=head1 DESCRIPTION + +Prefixing a shell command with "zrun" causes any compressed files that are +arguments of the command to be transparently uncompressed to temp files +(not pipes) and the uncompressed files fed to the command. + +This is a quick way to run a command that does not itself support +compressed files, without manually uncompressing the files. + +The following compression types are supported: gz bz2 Z xz lzma lzo + +If zrun is linked to some name beginning with z, like zprog, and the link is +executed, this is equivalent to executing "zrun prog". + +=cut |