~dricottone/my-utils

cfa246b763c17400a5f078a5733043fa52e5d104 — Dominic Ricottone 4 years ago 86e36be
Initial commit of cleaned-up dev branch
96 files changed, 2599 insertions(+), 839 deletions(-)

A Makefile
M README.md
A bash-completion/mybashcompletion.bash
D check-x.sh
D clean-unittest.awk
D color-unittest.sh
D ctdir
D debom
D filecheck.sh
D is-int.sh
D match.bash
D rand
D safe-rm.bash
A src/check-x
A src/ctdir
A src/debom
A src/enumerate
A src/epub
A src/mkbak
A src/mktar
A src/mylib.bash
A src/myminiparse.sh
A src/myparse.bash
A src/rand
A src/rebom
A src/rmtar
A src/rmzip
A src/stop-at
A src/tarcat
A src/unittest
A src/unittest-color.awk
A src/untar
A src/whichcat
A src/whiched
A src/whichhead
A src/whichvi
A src/whisper
A src/wttr
A tests/bom_test.sh
A tests/compression_test.sh
A tests/ctdir_test.sh
A tests/static/compression_result.txt
A tests/static/compression_target.txt
A tests/static/debom_result.txt
A tests/static/debom_target.txt
A tests/static/decompression_result.txt
A tests/static/decompression_target.tar
A tests/static/decompression_target.tar.bz2
A tests/static/decompression_target.tar.gz
A tests/static/decompression_target.tar.xz
A tests/static/decompression_target.tar.zst
A tests/temp_bom/debom.txt
A tests/temp_bom/rebom.txt
A tests/temp_compression/archive.tar
A tests/temp_compression/archive.tar.bz2
A tests/temp_compression/archive.tar.gz
A tests/temp_compression/archive.tar.xz
A tests/temp_compression/archive.tar.zst
A tests/temp_compression/basic_concat.txt
A tests/temp_compression/bzip2.txt
A tests/temp_compression/decompression_target.tar
A tests/temp_compression/decompression_target.tar.bz2
A tests/temp_compression/decompression_target.tar.gz
A tests/temp_compression/decompression_target.tar.xz
A tests/temp_compression/decompression_target.tar.zst
A tests/temp_compression/explicit.tar
A tests/temp_compression/explicit.tar.bz2
A tests/temp_compression/explicit.tar.gz
A tests/temp_compression/explicit.tar.xz
A tests/temp_compression/explicit.tar.zst
A tests/temp_compression/explicit_concat.txt
A tests/temp_compression/gzip.txt
A tests/temp_compression/implicit.tar
A tests/temp_compression/implicit.tar.bz2
A tests/temp_compression/implicit.tar.gz
A tests/temp_compression/implicit.tar.xz
A tests/temp_compression/implicit.tar.zst
A tests/temp_compression/implicit_concat.txt
A tests/temp_compression/tar.txt
A tests/temp_compression/xz.txt
A tests/temp_compression/zstd.txt
A tests/temp_enumerate/0001.a
A tests/temp_enumerate/0002.b
A tests/temp_enumerate/0003.c
A tests/temp_enumerate/0004.d
A tests/temp_enumerate/0005.e
A tests/which_test.sh
D tlog
D unittest.sh
D untar
D weather
D whichcat
D whiched
D whichhead
D whichvi
D whisper
A Makefile => Makefile +85 -0
@@ 0,0 1,85 @@

BIN_DIR?=/usr/local/bin
LIB_DIR?=/usr/local/lib

install: clean
	install -m755 src/mylib.bash $(LIB_DIR)/mylib.bash
	install -m755 src/myparse.bash $(LIB_DIR)/myparse.bash
	install -m755 src/myminiparse.sh $(LIB_DIR)/myminiparse.sh
	install -m755 src/unittest-color.awk $(LIB_DIR)/unittest-color.awk
	install -m755 src/check-x $(BIN_DIR)/check-x
	install -m755 src/ctdir $(BIN_DIR)/ctdir
	install -m755 src/debom $(BIN_DIR)/debom
	install -m755 src/epub $(BIN_DIR)/epub
	install -m755 src/enumerate $(BIN_DIR)/enumerate
	install -m755 src/mkbak $(BIN_DIR)/mkbak
	install -m755 src/mktar $(BIN_DIR)/mktar
	install -m755 src/rand $(BIN_DIR)/rand
	install -m755 src/rebom $(BIN_DIR)/rebom
	install -m755 src/rmtar $(BIN_DIR)/rmtar
	install -m755 src/rmzip $(BIN_DIR)/rmzip
	install -m755 src/tarcat $(BIN_DIR)/tarcat
	install -m755 src/unittest $(BIN_DIR)/unittest
	install -m755 src/untar $(BIN_DIR)/untar
	install -m755 src/whichcat $(BIN_DIR)/whichcat
	install -m755 src/whiched $(BIN_DIR)/whiched
	install -m755 src/whichhead $(BIN_DIR)/whichhead
	install -m755 src/whichvi $(BIN_DIR)/whichvi
	install -m755 src/whisper $(BIN_DIR)/whisper
	install -m755 src/wttr $(BIN_DIR)/wttr

uninstall:
	rm $(LIB_DIR)/mylib.bash
	rm $(LIB_DIR)/myparse.bash
	rm $(LIB_DIR)/myminiparse.sh
	rm $(LIB_DIR)/unittest-color.awk
	rm $(BIN_DIR)/check-x
	rm $(BIN_DIR)/ctdir
	rm $(BIN_DIR)/debom
	rm $(BIN_DIR)/epub
	rm $(BIN_DIR)/enumerate
	rm $(BIN_DIR)/mkbak
	rm $(BIN_DIR)/mktar
	rm $(BIN_DIR)/rand
	rm $(BIN_DIR)/rebom
	rm $(BIN_DIR)/rmtar
	rm $(BIN_DIR)/rmzip
	rm $(BIN_DIR)/tarcat
	rm $(BIN_DIR)/unittest
	rm $(BIN_DIR)/untar
	rm $(BIN_DIR)/whichcat
	rm $(BIN_DIR)/whiched
	rm $(BIN_DIR)/whichhead
	rm $(BIN_DIR)/whichvi
	rm $(BIN_DIR)/whisper
	rm $(BIN_DIR)/wttr

test: clean static-test-files
	shellcheck src/*.bash src/*.sh $(LIB_DIR)/mylib.bash $(LIB_DIR)/myparse.bash $(LIB_DIR)/myminiparse.sh
	sh tests/compression_test.sh
	sh tests/ctdir_test.sh
	sh tests/which_test.sh
	sh tests/bom_test.sh

static-test-files:
	printf "[1]\n[2]\n[3]\n\n[1]\n[2]\n[3]\n\n[1]\n[2]\n[3]\n\n[1]\n[2]\n[3]\n\n[1]\n[2]\n[3]\n\n" > tests/static/compression_result.txt
	printf "[1]\n[2]\n[3]\n\n" > tests/static/compression_target.txt
	printf "foo bar\n" > tests/static/debom_result.txt
	printf "\xEF\xBB\xBFfoo bar\n" > tests/static/debom_target.txt
	printf "(a)\n(b)\n(c)\n\n" > tests/static/decompression_result.txt
	cp tests/static/decompression_result.txt tests/static/tar.txt
	cp tests/static/decompression_result.txt tests/static/gzip.txt
	cp tests/static/decompression_result.txt tests/static/xz.txt
	cp tests/static/decompression_result.txt tests/static/zstd.txt
	cp tests/static/decompression_result.txt tests/static/bzip2.txt
	cd tests/static/; tar -cf decompression_target.tar tar.txt
	cd tests/static/; tar -czf decompression_target.tar.gz gzip.txt
	cd tests/static/; tar -cJf decompression_target.tar.xz xz.txt
	cd tests/static/; tar --zstd -cf decompression_target.tar.zst zstd.txt
	cd tests/static/; tar -cjf decompression_target.tar.bz2 bzip2.txt
	rm tests/static/tar.txt tests/static/gzip.txt tests/static/xz.txt tests/static/zstd.txt tests/static/bzip2.txt

clean:
	rm -rf tests/temp_compression
	rm -rf tests/temp_bom


M README.md => README.md +31 -169
@@ 1,95 1,46 @@
Set of scripts that I use frequently.
# my-utils

A set of scripts that I use frequently. Written in a mix of shell (some POSIX sh, some bash), sed, awk, and so on. Any dependencies *beyond* the POSIX standard and inter-dependency are noted.


Everything is as POSIX as I can make it. Unfortunately, some utilities are
intrinsically based in arrays or regex matching, which are *not* POSIX.
With scripts that are intended for direct use (i.e., `ctdir`), just try it and
see what happens. With scripts intended to be used in other scripts
(i.e., `is-int.sh`), the filename extensions accurately represent dependencies.

## Scripts

Executable |Description                                                   |Extra Dependencies
:----------|:-------------------------------------------------------------|:------------------------------------------
check-x    |Check if an X11 server is running
ctdir      |Count entries in a target directory(ies)
debom      |Remove BOM from a target file                                 |`bash`
enumerate  |Dumps HTML from an 'epub' e-book archive                      |`bash`, `zipinfo`, `unzip`, `w3m`
epub       |Rename files in current directory into sequential numbers     |`bash`
mkbak      |Create a backup of a target file                              |`bash`
mktar      |Wrapper around `tar` for easier compression                   |`bash` *
rand       |Get a random number within an inclusive range                 |`shuf`
rebom      |Add BOM to a target file                                      |
rmtar      |Delete 'tar' archive files
rmzip      |Delete 'zip' archive files
stop-at    |Re-print until a pattern is matched
tarcat     |Print contents of target archive file(s)                      |*
unittest   |Wrapper around Python's `unittest` module                     |`python3`, GNU or New (AT&T) `awk`
untar      |Wrapper around `tar` for easier decompression                 |*
whichcat   |Print all lines from a program
whiched    |Open a program with your editor
whichhead  |Print the first 10 lines from a program                       |`bash`
whichvi    |Open a program with your visual editor
whisper    |Wrapper around `espeak` to mirror `say` in macOS              |`espeak`
wttr       |Wrapper around `wttr` to fix double-wide runes for some fonts |`wego`

## check-x.sh
\* *While you **technically** won't run into an error, these scripts **do** expect `tar` to support Zstandard, which isn't necessarily POSIX standard.*

Check if an X server is running
*All* scripts support `-h` and `--help` for printing built-in documentation.

Usage: `check-x.sh`
*Almost all* scripts do nothing if no input arguments are given. (The exceptions are `whisper` and `wttr`.)



## ctdir
## Development

Count entries in a target directory.

Usage: `ctdir TARGET [OPTIONS]`

###### Options (inherited from `ls`):
+ `-a`, `--all`: do not ignore entries starting with `.`
+ `-A`, `--almost-all`: do not list implied `.` and `..`
+ `-B`, `--ignore-backups`: do not list implied entries ending with `~`
+ `-F`, `--classify`: append indicator (one of `*/=>@|`)
+ `--file-type`: likewise, except do not append `*`
+ `-R`, `--recursive`: list subdirectories recursively
+ `-h`, `--help`: print help message



## debom

Remove the byte order mark (BOM) from a target file, saving to a new file.

Usage: `debom TARGET [OUTPUT]`

###### Options:
+ `-h`, `--help`: print help message



## filecheck.sh

Check if a target file exists, and prompt user to remove it.

Usage: filecheck.sh TARGET



## is-int.sh

Check if an item is an integer.

Usage: `is-int.sh ITEM`

###### Example:
```bash
$ is-int.sh 1 && echo "Y" || echo "N"
Y
$ is-int.sh a && echo "Y" || echo "N"
N
$ is-int.sh "" && echo "Y" || echo "N"  #handles empty strings
N
```



## match.bash

Check if any items match a pattern.

Usage: `match.bash PATTERN ITEM1 [ITEM2 ...]`

###### Example:
```bash
$ match a abc && echo "Y" || echo "N"
N
$ match a abc abc a && echo "Y" || echo "N"
Y
$ match a* abc && echo "Y" || echo "N"  #handles globbing
Y
```

###### Options:
+ `-h`, `--help`: print help message
These being re-used scripts, there's been a great deal of feature creep, bikeshedding, and over-engineering. That is to say, there's a lot of uncertainty as to how *reliable* these scripts are. To mitigate these concerns, `shellcheck` is a development dependency.





@@ 105,51 56,6 @@ Usage: `rand START END [OPTIONS]`



## safe-rm.bash

Check if a target matches a pattern before removing it.

Usage: `safe-rm.bash TARGET PATTERN`



## tlog

Writes input to a target file while stripping ANSI codes, and returns input as
it was.

Usage: tlog TARGET [OPTIONS]

###### Options:
+ `-a`, `--append`: append to target file instead of overwriting
+ `-h`, `--help`: print help message



## unittest.sh, clean-unittest.awk, and color-unittest.sh

Run Python tests in a target directory while cleaning and colorizing the output.

Usage: `unittest.sh [TARGET] [OPTIONS] | clean-unittest.awk | color-unittest.sh`

###### Options:
+ `-v`, `--verbose`: verbose output
+ `-p`, `--pattern`: pattern to match test files (default: `test*.py`)
+ `-h`, `--help`: print help message



## untar

Uncompress a tarball. Expects standardized file names (i.e., `.tar.gz.gpg`).

Usage: `untar FILE [OPTIONS]`

###### Options:
+ `-h`, `--help`: print help text



## weather

Wrapper around `wego`--fixes spacing. Shows weather for 2 days unless a number


@@ 159,47 65,3 @@ Usage: `weather [NUMBER]`



## whichcat

Print lines from a program to the terminal.

Usage: `whichcat PROGRAM [OPTIONS]`

###### Options (inherited from `cat`)
+ `-n`, `--number`: number all output lines
+ `-s`, `--squeeze-blank`: suppress repeated empty output lines
+ `-h`, `--help`: print help message



## whiched and whichvi

Open a program in your editor or visual editor.

Usage: `whiched PROGRAM [OPTIONS]`

###### Options:
+ `-h`, `--help`: print help message



## whichhead

Print the first 10 lines from a program to the terminal.

Usage: `whichhead PROGRAM [OPTIONS]`

###### Options (inherited from `head`)
+ `-n N`, `--lines=N`: print the first N lines instead of the first 10
+ `-h`, `--help`: print help message



## whisper

Wrapper on `espeak` to mirror macOS's `say` using whisper voice.

Usage: `whisper [TEXT]`




A bash-completion/mybashcompletion.bash => bash-completion/mybashcompletion.bash +45 -0
@@ 0,0 1,45 @@
#!/bin/bash

# epub
# Function tries to find a target archive in arguments already specified, and
# then tries to list the filenames stored inside the archive
_epub_completion() {
  # set fallback completion as default (filenames) with 'complete -o default'
  COMPREPLY=()
  # search for a filename in arguments, skipping the first (executable name)
  for arg in "${COMP_WORDS[@]:1}"; do
    if [[ -f "$arg" ]]; then
      # on finding a filename, try to complete with archive entries
      local list=( $(epub --list "$arg" | sort) )
      if [[ "${#list[@]}" -ge 1 ]]; then
        COMPREPLY=( $(compgen -W "${list[*]}" -- "${COMP_WORDS[$COMP_CWORD]}") )
      else
        # if not an archive, or an archive with no entries, halt completion
        compopt +o default
      fi
      break
    fi
  done
}
# Complete with function, and fallback to filenames
complete -o default -F _epub_completion epub

# rmzip
# Complete with filenames matching pattern '*.zip', and fallback to filenames
complete -o default -f -X '!*.zip' rmzip

# untar, tarcat, rmtar
# Complete with filenames matching pattern
# '*.@(tar|tar.@(gz|xz|zst|bz2)|tar.@(gz|xz|zst|bz2).gpg)', and fallback to
# filenames
complete -o default -f -X '!*.@(tar|tar.@(gz|xz|zst|bz2)|tar.@(gz|xz|zst|bz2).gpg)' tarcat
complete -o default -f -X '!*.@(tar|tar.@(gz|xz|zst|bz2)|tar.@(gz|xz|zst|bz2).gpg)' untar
complete -o default -f -X '!*.@(tar|tar.@(gz|xz|zst|bz2)|tar.@(gz|xz|zst|bz2).gpg)' rmtar

# whichcat, whiched. whichhead, whichvi
# Complete with program names
complete -c whichcat
complete -c whichhead
complete -c whiched
complete -c whichvi


D check-x.sh => check-x.sh +0 -14
@@ 1,14 0,0 @@
#!/bin/sh

# check-x.sh
# ==========
# Usage: check-x.sh
#
# Check if an X server is running

if ! xset q &>/dev/null; then
  exit 1
else
  exit 0
fi


D clean-unittest.awk => clean-unittest.awk +0 -45
@@ 1,45 0,0 @@
#!/usr/bin/awk -f

# clean-unittest.awk
# ==================
# Usage: python -m unittest | clean-unittest.awk
#
# Clean output of Python's unittest

BEGIN { last_testcase="PROBABLYWILLNOTMATCH"; blank_lines=0; first_line=1 }
{
  if ($0 ~ /^ *$/) {
    if (blank_lines == 0 && first_line == 0) {
      print $0
    }
    blank_lines+=1
  }
  else if ($0 ~ /^test/) {
    blank_lines=0
    if ($0 !~ last_testcase) {
      split($0, last_array, "[()]")
      last_testcase=last_array[2]
      print last_testcase
    }
    sub(/ \(.*\) /, " ")
    $0="  " $0
    print $0
  }
  else if ($0 ~ /Ran [0-9]+ tests in [0-9]+.[0-9]+s/) {
    num_tests=$0
  }
  else if ($0 ~ /^FAIL: / || $0 ~ /^ERROR: /) {
    blank_lines=0
    print "\n" $0
  }
  else if ($0 ~ /^OK$/ || $0 ~ /^FAILED /) {
    blank_lines=0
    print num_tests " ... " $0
  }
  else if ($0 !~ /^ *[-=]+ *$/) {
    print $0
  }
  first_line=0
}



D color-unittest.sh => color-unittest.sh +0 -47
@@ 1,47 0,0 @@
#!/bin/sh

# color-unittest.sh
# =================
# Usage: python -m unittest | color-unittest.sh
#
# Colorize output of Python's unittest

grey_grep() {
  GREP_COLOR='1;30' grep --color=always -e "$1"
}

red_grep() {
  GREP_COLOR='1;31' grep --color=always -e "$1"
}

green_grep() {
  GREP_COLOR='1;32' grep --color=always -e "$1"
}

yellow_grep() {
  GREP_COLOR='1;33' grep --color=always -e "$1"
}

blue_grep() {
  GREP_COLOR='1;34' grep --color=always -e "$1"
}

magenta_grep() {
  GREP_COLOR='1;35' grep --color=always -e "$1"
}

cyan_grep() {
  GREP_COLOR='1;36' grep --color=always -e "$1"
}

white_grep() {
  GREP_COLOR='1;37' grep --color=always -e "$1"
}

cat <&0 \
  | green_grep 'ok$\|OK\|' \
  | red_grep 'ERROR:\|ERROR\|FAIL:\|FAIL\|FAILED\|' \
  | red_grep 'failures=[0-9]\+\|' \
  | red_grep 'errors=[0-9]\+\|' \
  | yellow_grep 'skipped=[0-9]\+\|skipped .*\|skipped\|'


D ctdir => ctdir +0 -39
@@ 1,39 0,0 @@
#!/bin/sh

# ctdir
# =====
# Usage: ctdir TARGET [OPTIONS]
#
# Count entries in a directory

help_msg() {
  cat <<-EOF
	Count entries in a target directory
	Usage: ctdir TARGET [OPTIONS]
	Options:
	 -a, --all             do not ignore entries starting with .
	 -A, --almost-all      do not list implied . and ..
	 -B, --ignore-backups  do not list implied entries ending with ~
	 -r, --recursive       list subdirectories recursively
	 -h, --help            print this message
	EOF
  exit 1
}
err_msg() {
  (>&2 echo "$1")
  exit 1
}

for i in "$@"; do
  case $i in
    -h|--help) help_msg;;
  esac
done

LISTING=$(ls -1 "$@" 2>/dev/null | wc -l)
if [ "$LISTING" -eq 0 ]; then
  err_msg "No files as '${*}'"
else
  echo "$LISTING"
fi


D debom => debom +0 -39
@@ 1,39 0,0 @@
#!/bin/bash

# debom
# =====
# Usage: debom TARGET [OUTPUT]
#
# Remove byte order mark (BOM) from a target file, saving to a new file

help_msg() {
  cat <<-EOF
	Remove BOM from a target file, saving to a new file
	Usage: debom TARGET [OUTPUT]
	Options:
	 -h, --help: print this message
	EOF
  exit 1
}

err_msg() {
  (>&2 echo "$1")
  exit 1
}

for i in "$@"; do
  case $i in
    -h|--help) help_msg;;
  esac
done

if [ $# -lt 1 ]; then
  err_msg "Usage: debom TARGET [OUTPUT]"
elif [[ $# -lt 2 ]]; then
  OUTFILE="${1}.new"
else
  OUTFILE="$2"
fi

sed '1s/^\xEF\xBB\xBF//' < "$1" > "$OUTFILE"


D filecheck.sh => filecheck.sh +0 -19
@@ 1,19 0,0 @@
#!/bin/sh

# filecheck.sh
# ============
# Usage: filecheck.sh TARGET
#
# Check if a target file exists, and prompt user to remove it

exec < /dev/tty #clears stdin

if [ -e "$1" ]; then
  printf "File ${1} already exists. Overwrite? (Y/n) "
  read RESP
  case "$RESP" in
    [Yy]* ) echo "Overwriting..."; rm "$1";;
    * )     echo "Quitting..."; exit 1;;
  esac
fi


D is-int.sh => is-int.sh +0 -19
@@ 1,19 0,0 @@
#!/bin/sh

# is-int.sh
# =========
# Usage: is-int.sh ITEM
#
# Check if an item is an integer.

err_msg() {
  (>&2 echo "$1")
  exit 1
}

if [ $# -lt 1 ]; then
  err_msg "Usage: is-int.sh ITEM"
fi

[ "$1" -eq "$1" ] 2> /dev/null && exit 0 || exit 1


D match.bash => match.bash +0 -41
@@ 1,41 0,0 @@
#!/bin/bash

# match.bash
# ==========
# Usage: match.bash PATTERN ITEM1 [ITEM2 ...]
#
# Check if any items match a pattern

help_msg() {
  cat <<-EOF
	Check if any items match a pattern
	Usage: match PATTERN ITEM1 [ITEM2 ...]
	Options:
	 -h, --help: print this message
	EOF
  exit 1
}

err_msg() {
  (>&2 echo "$1")
  exit 1
}

for i in "$@"; do
  case $i in
    -h|--help) help_msg;;
  esac
done

if [[ $# -lt 2 ]]; then
  err_msg "Usage: match.bash PATTERN ITEM1 [ITEM2 ...]"
fi

for item in "${@:2}"; do
  if [[ $item == $1 ]]; then
    exit 0
  fi
done

exit 1


D rand => rand +0 -54
@@ 1,54 0,0 @@
#!/bin/bash

# rand
# ====
# Usage: rand START END [OPTIONS]
#
# Returns a random number within a range (inclusive)

help_msg() {
  cat <<-EOF
	Returns a random number within a range (inclusive)
	Usage: rand START END [OPTIONS]
	Options:
	 -w, --width N  zero-pad number to be N wide
	 -h, --help     print this message
	EOF
  exit 1
}

err_msg() {
  (>&2 echo "$1")
  exit 1
}

START=
END=
WIDTH=1
POSITIONAL=()

while [[ $# -gt 0 ]]; do
  case $1 in
    -h|--help)  help_msg;;
    -w|--width) WIDTH="$2"; shift; shift;;
    *)          POSITIONAL+=("$1"); shift;;
  esac
done

if [[ ${#POSITIONAL[@]} -lt 2 ]]; then
  err_msg "Usage: rand START END"
  exit 1
else
  START="${POSITIONAL[0]}"
  END="${POSITIONAL[1]}"
  if ! is-int.sh "$START"; then
    err_msg "Expected numeric argument (given '${START}')"
  elif ! is-int.sh "$END"; then
    err_msg "Expected numeric argument (given '${END}')"
  elif [[ $START -ge $END ]]; then
    err_msg "Expected ascending range ('${END}' not greater than '${START}')"
  fi
fi

seq -f "%0${WIDTH}g" "$START" "$END" | shuf -n 1


D safe-rm.bash => safe-rm.bash +0 -22
@@ 1,22 0,0 @@
#!/bin/bash

# save_rm.bash
# ============
# Usage: safe_rm.bash TARGET PATTERN
#
# Checks if a target matches a pattern before removing it

err_msg() {
  (>&2 echo "$1")
  exit 1
}

if [[ $# -lt 2 ]]; then
  err_msg "Usage: safe_rm.bash TARGET PATTERN"
fi

PATTERN="^${2}$"
if [[ $1 =~ $PATTERN ]]; then
  rm $1
fi


A src/check-x => src/check-x +19 -0
@@ 0,0 1,19 @@
#!/bin/sh
# shellcheck disable=SC2034

name="check-x"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Check if an X11 server is running
	Usage: check-x
EOF
)

. /usr/local/lib/myminiparse.sh

if ! /usr/bin/xset q >/dev/null 2>&1; then
  exit 1
else
  exit 0
fi


A src/ctdir => src/ctdir +49 -0
@@ 0,0 1,49 @@
#!/bin/sh

name="ctdir"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Count entries in target directory(ies)
	Usage: ctdir TARGETS [..] [OPTIONS]
	Options:
	 -h, --help     print this message and exit
	 -q, --quiet    suppress error messages
	 -v, --version  print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

# error if no directory names given
if [ "$#" -eq 0 ]; then
  (>&2 /usr/bin/printf "Usage: ctdir TARGET [OPTIONS]\n")
  exit 1
elif [ "$#" -eq 1 ] && [ "$quiet" -eq 1 ]; then
  exit 1
fi

# loop through arguments
code=0
for arg; do
  case "$arg" in
  -q|--quiet)
    #ignore these
    ;;

  *)
    # main routine
    if [ ! -d "$arg" ]; then
      if [ "$quiet" -eq 0 ]; then
        (>&2 /usr/bin/printf "%s: No such directory '%s'\n" "$name" "$arg")
      fi
      code=1
    else
      /usr/bin/ls -1A "$arg" 2>/dev/null | wc -l
    fi
    ;;
  esac
done

# exit with stored code
exit "$code"


A src/debom => src/debom +150 -0
@@ 0,0 1,150 @@
#!/bin/bash

name="debom"
version="1.0"
read -r -d '' help_message <<-EOF
	Remove BOM from a target file
	Usage: debom TARGET [OPTIONS]
	Options:
	 -b, --backup          output a backup file, editing TARGET in-place
	 -d, --dump            print to STDOUT
	 -f, --force           overwrite without asking
	 -h, --help            print this message
	 -n FILE, --name FILE  set output filename (Default: TARGET.new, or
	                         TARGET.bak in backup mode)
	 -q, --quiet           suppress error messages and prompts
	 -v, --verbose         show additional messages
	 -V, --version         print version number and exit
EOF

source /usr/local/lib/mylib.bash

positional=()
quiet=0
verbose=0
backup=0
dump=0
force=0
output_fn=""
while [[ $# -gt 0 ]]; do
  case "$1" in

  -b|--backup)
    debug_msg "Setting backup option to 1 (was ${backup})"
    backup=1
    shift
    ;;

  -d|--dump)
    debug_msg "Setting dump option to 1 (was ${dump})"
    dump=1
    shift
    ;;

  -f|--force)
    debug_msg "Setting force option to 1 (was ${force})"
    force=1
    shift
    ;;

  -h|--help)
    help_msg
    shift
    ;;

  -n|--name)
    debug_msg "Setting output filename to ${2} (was ${output_fn})"
    output_fn="$2"
    shift; shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -v|--verbose)
    debug_msg "Setting verbose option to 1 (was ${verbose})"
    verbose=1;
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  *)
    debug_msg "Argument '${1}' added to positional array"
    positional+=("$1")
    shift
    ;;
  esac
done

# error if no filenames given
if [[ ${#positional[@]} -eq 0 ]]; then
  debug_msg "No input filename was given"
  usage_msg
fi
original_fn="${positional[0]}"

# output filename
if [[ "$dump" -eq 1 ]]; then
  debug_msg "Output is STDOUT"
elif [[ -z "$output_fn" ]]; then
  if [[ "$backup" -eq 1 ]]; then
    debug_msg "No output filename was given, so defaulting to 'TARGET.bak'"
    output_fn="${original_fn}.bak"
  else
    debug_msg "No output filename was given, so defaulting to 'TARGET.new'"
    output_fn="${original_fn}.new"
  fi
else
  debug_msg "Output filename given as '${output_fn}'"
fi

# check if files exist
debug_msg "Beginning file existence checks"
if [[ ! -f "$original_fn" ]]; then
  debug_msg "Input file does not exist"
  error_msg "No such file '${original_fn}'"
elif [[ "$dump" -eq 0 && "$force" -eq 0 && -f "$output_fn" ]]; then
  debug_msd "Output file exists; prompting for permission to overwrite"
  if ! prompt_overwrite "${output_fn}"; then
    debug_msg "Could not obtain permission to overwrite output file"
    exit 1
  fi
fi
debug_msg "Finished file existence checks"

# backup routine
if [[ "$dump" -eq 0 && "$backup" -eq 1 ]]; then
  debug_msg "Beginning backup routine"
  backup_fn="${output_fn}"
  if ! /usr/local/bin/mkbak "${original_fn}" --force --name "${backup_fn}"; then
    error_msg "Error occured while executing backup"
    exit 1
  fi
  output_fn=${original_fn}
  original_fn=${backup_fn}
  debug_msg "Finished backup routine"
fi

# main routine
debug_msg "Beginning main routine"
code=0
if [[ "$dump" -eq 1 ]]; then
  if ! /usr/bin/sed '1s/^\xEF\xBB\xBF//' < "${original_fn}"; then
    code=1
  fi
else
  if ! /usr/bin/sed '1s/^\xEF\xBB\xBF//' < "${original_fn}" > "${output_fn}"; then
    code=1
  fi
fi
debug_msg "Finished main routine"

# return stored code
exit "$code"


A src/enumerate => src/enumerate +124 -0
@@ 0,0 1,124 @@
#!/bin/bash

name="enumerate"
version="1.0"
read -r -d '' help_message <<-EOF
	Rename files in current directory into sequential numbers
	Usage: enumerate [OPTIONS]
	Options:
	 -d, --dry-run     print current and intended filenames side-by-side
	 -f P, --filter P  filter targets with filename pattern P
	 -h, --help        print this message
	 -q, --quiet       suppress error messages
	 -s N, --start N   start enumeration at N (Default: 1)
	 -S N, --step N    step enumeration by N (Default: 1)
	 -v, --verbose     show additional messages
	 -V, --version     print version number and exit
	 -w N, --width N   rename files to numbers N wide (Default: 4)
EOF

source /usr/local/lib/mylib.bash

positional=()
execute=0
filter="*"
enum_start=1
enum_step=1
width=4
quiet=0
verbose=0

while [[ $# -gt 0 ]]; do
  case $1 in

  -f|--filter)
    debug_msg "Setting filter option to ${2} (was ${filter})"
    filter="$2"
    shift; shift
    ;;

  -h|--help)
    help_msg
    shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -s|--start)
    if ! is_natural "$2"; then
      error_msg "Cannot set enumeration start to '${2}' (not an integer >= 0)"
    fi
    debug_msg "Setting enumeration start to ${2} (was ${enum_start})"
    enum_start="$2"
    shift; shift
    ;;

  -S|--step)
    if ! is_positive_integer "$2"; then
      error_msg "Cannot set enumeration step to '${2}' (not an integer >= 1)"
    fi
    debug_msg "Setting enumeration step to ${2} (was ${enum_step})"
    enum_step="$2"
    shift; shift
    ;;

  -v|--verbose)
    debug_msg "Setting verbose option to 1 (was ${verbose})"
    verbose=1;
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  -w|--width)
    if ! is_natural "$2"; then
      error_msg "Cannot set filename width to '${2}' (not an integer >= 0)"
    fi
    debug_msg "Setting filename width to ${2} (was ${width})"
    width="$2";
    shift; shift
    ;;

  -x|--execute)
    debug_msg "Setting execution to 1 (was ${execute})"
    execute=1
    shift
    ;;

  *)
    debug_msg "Argument '${1}' added to positional array"
    positional+=("$1")
    shift
    ;;
  esac
done

# main routine
n="$enum_start"
s="$enum_step"
code=0
for original_fn in $(find . -maxdepth 1 -name "$filter" -type f -printf "%f\n" | sort); do
  wide_n=$(printf "%0*d\n" "$width" "$n")
  enum_fn="${wide_n}.$(fn_extension "$original_fn")"
  debug_msg "Widened format of '${n}' is '${wide_n}'"
  debug_msg "Input filename is '${original_fn}'"
  debug_msg "Output filename is '${enum_fn}'"
  if [[ "$execute" -eq 1 ]]; then
    if ! mv "$original_fn" "${enum_fn}"; then
      code=1
    fi
  else
    printf "#mv %s %s\n" "$original_fn" "$enum_fn"
  fi
  n=$((n+s))
done

# return stored code
exit "$code"


A src/epub => src/epub +155 -0
@@ 0,0 1,155 @@
#!/bin/bash

name="epub"
version="1.0"
read -r -d '' help_message <<-EOF
	Dumps HTML from an epub e-book archive
	Usage: epub TARGET [FILENAMES] [OPTIONS]
	Options:
	 -d, --dump            print to STDOUT
	 -h, --help            print this message
	 -l, --list            list HTML entries of archive
	 -q, --quiet           suppress error messages and prompts
	 -v, --verbose         show additional messages
	 -V, --version         print version number and exit
EOF

source /usr/local/lib/mylib.bash

target_archive=""
positional=()
quiet=0
verbose=0
dump=0
list=0
width="$(/usr/bin/tput cols)"
while [[ $# -gt 0 ]]; do
  case "$1" in

  -d|--dump)
    debug_msg "Setting dump option to 1 (was ${dump})"
    dump=1
    shift
    ;;

  -h|--help)
    help_msg
    shift
    ;;

  -l|--list)
    debug_msg "Setting list option to 1 (was ${list})"
    list=1
    shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -v|--verbose)
    debug_msg "Setting verbose option to 1 (was ${verbose})"
    verbose=1;
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  -w|--width)
    if ! is_positive_integer "$2"; then
      error_msg "Cannot set column width to '${2}' (not an integer >= 1)"
    fi
    debug_msg "Setting column width to ${2} (was ${width})"
    width="$2";
    shift; shift
    ;;

  *)
    if [ -z "$target_archive" ]; then
      debug_msg "Setting target filename to '${1}'"
      target_archive="$1"
    else
      debug_msg "Argument '${1}' added to positional array"
      positional+=("$1")
    fi
    shift
    ;;
  esac
done

# error if no filenames given
if [[ -z "$target_archive" ]]; then
  debug_msg "No input filename was given"
  usage_msg
elif [[ ! -f "$target_archive" ]]; then
  error_msg "No such file '${target_archive}'"
fi

# listing subroutine
list_target_archive() {
  if ! /usr/bin/zipinfo -1 "$target_archive" 2>/dev/null; then
    debug_msg "Error in listing subroutine"
    code=1
  fi
}

# extract subroutine
extract_from_target_archive() {
  if ! /usr/bin/unzip -caaqq "$target_archive" "$target_fn"; then
    code=1
  fi
}

# render subroutine
render_html() {
  if ! /usr/bin/w3m -T text/html -cols "$width" -dump; then
    code=1
  fi
}

# dump subroutine
dump_target_archive() {
  if [[ "${#positional[@]}" -eq 0 ]]; then
    for fn in "${list_fn[@]}"; do
      target_fn="$fn"
      extract_from_target_archive | render_html
    done
  else
    for fn in "${positional[@]}"; do
      target_fn="$fn"
      extract_from_target_archive | render_html
    done
  fi
}

# main routine
code=0
if [[ "$list" -eq 1 ]]; then
  list_target_archive | sort
else
  list_fn=( $(list_target_archive | grep -E '\.xml|\.html|\.xhtml' | sort) )

  if [[ "$code" -eq 1 || "${#list_fn[@]}" -eq 0 ]]; then
    error_msg "'${target_archive}' is not a valid archive"
  else
    for target_fn in "${positional[@]}"; do
      if ! contains "$target_fn" "${list_fn[@]}"; then
        error_msg "'${target_fn}' not in archive '${target_archive}'"
      fi
    done
  fi

  if [[ "$dump" -eq 1 ]]; then
    dump_target_archive
  else
    dump_target_archive | ${PAGER:=/usr/bin/less}
  fi
fi

# return stored code
exit "$code"


A src/mkbak => src/mkbak +107 -0
@@ 0,0 1,107 @@
#!/bin/bash

name="mkbak"
version="1.0"
read -r -d '' help_message <<-EOF
	Create a backup of a target file
	Usage: mkbak TARGET [OPTIONS]
	Options:
	 -d, --diff            diff files before asking to overwrite
	 -f, --force           overwrite without asking
	 -h, --help            print this message and exit
	 -n FILE, --name FILE  name of backup file (Default: TARGET.bak)
	 -q, --quiet           suppress error messages and prompts
	 -v, --verbose         show additional messages
	 -V, --version         print version number and exit
EOF

source /usr/local/lib/mylib.bash

positional=()
quiet=0
verbose=0
force=0
backup_fn=""
while [[ $# -gt 0 ]]; do
  case $1 in

  -d|--diff)
    debug_msg "Setting diff option to 1 (was ${diff})"
    diff=1
    shift
    ;;

  -f|--force)
    debug_msg "Setting force option to 1 (was ${force})"
    force=1
    shift
    ;;

  -h|--help)
    help_msg
    shift
    ;;

  -n|--name)
    debug_msg "Setting output filename to ${2} (was ${backup_fn})"
    backup_fn="$2"
    shift; shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -v|--verbose)
    debug_msg "Setting verbose option to 1 (was ${verbose})"
    verbose=1;
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  *)
    debug_msg "Argument '${1}' added to positional array"
    positional+=("$1")
    shift
    ;;
  esac
done

# error if no filenames given
if [[ "${#positional[@]}" -lt 1 ]]; then
  debug_msg "No input filename was given"
  usage_msg
fi
original_fn="${positional[0]}"

# output filename
if [[ -z "$backup_fn" ]]; then
  debug_msg "No output filename was given, so defaulting to 'TARGET.bak'"
  backup_fn="${original_fn}.bak"
fi

# check files
if [[ ! -f "$original_fn" ]]; then
  error_msg "No such file '${original_fn}'"
elif [[ "$force" -eq 0 && -f "$backup_fn" ]]; then
  if /usr/bin/cmp "$backup_fn" "$original_fn" >/dev/null 2>&1; then
    msg "File '${backup_fn}' already exists and is a copy of '${original_fn}'"
  elif [[ $quiet -eq 0 ]]; then
    if [[ "$diff" -eq 1 ]]; then
      /usr/bin/diff --color "$backup_fn" "$original_fn"
    fi
    if ! prompt_overwrite "$backup_fn"; then
      exit 1
    fi
  fi
fi

# main routine
/usr/bin/cp "$original_fn" "$backup_fn"
exit "$?"


A src/mktar => src/mktar +269 -0
@@ 0,0 1,269 @@
#!/bin/bash

name="mktar"
version="1.0"
read -r -d '' help_message <<-EOF
	Wrapper around 'tar' for easier compression
	Usage: mktar FILES [..] [OPTIONS]
	Options:
	 -c, --compress        compress archive with gzip
	 --compress=ALGO       compress archive with [none|gzip|xz|zstd|bzip2]
	 -C, --checksum        create checksum with SHA1
	 -e, --encrypt         encrypt files with GPG
	 -h, --help            print this message
	 -n FILE, --name FILE  name of backup file (Default: archive.tar)
	 -q, --quiet           suppress error messages and prompts
	 -v, --verbose         show additional messages
	 -V, --version         print version number and exit
EOF

source /usr/local/lib/mylib.bash

positional=()
quiet=0
verbose=0
compress=-1
encrypt=-1
checksum=-1
archive_fn=""
while [[ $# -gt 0 ]]; do
  case $1 in

  --compress=xz|--compress=2)
    debug_msg "Setting compress option to 2 (=xz) (was ${compress})"
    compress=2
    shift
    ;;

  --compress=zst|--compress=zstd|--compress=3)
    debug_msg "Setting compress option to 3 (=zstd) (was ${compress})"
    compress=3
    shift
    ;;

  --compress=bz2|--compress=bzip2|--compress=4)
    debug_msg "Setting compress option to 4 (=bzip2) (was ${compress})"
    compress=4
    shift
    ;;

  --compress=gz|--compress=gzip|--compress=1)
    debug_msg "Setting compress option to 1 (=gzip) (was ${compress})"
    compress=1
    shift
    ;;

  --compress=no|--compress=none|--compress=0)
    debug_msg "Setting compress option to 0 (=none) (was ${compress})"
    compress=0
    shift
    ;;

  --compress=*)
    attempted_compression="$(printf "%s\n" "$1" | sed -e 's/^.*=//' )"
    error_msg "Unknown compression '${attempted_compression}'"
    ;;

  -c|--compress)
    debug_msg "Setting compress option to 1 (=gzip) (was ${compress})"
    compress=1
    shift
    ;;

  -C|--checksum)
    debug_msg "Setting checksum option to 1 (=SHA1) (was ${checksum})"
    checksum=1
    shift
    ;;

  -e|--encrypt)
    debug_msg "Setting encrypt option to 1 (=GPG) (was ${encrypt})"
    encrypt=1
    shift
    ;;

  -h|--help)
    help_msg
    shift
    ;;

  -n|--name)
    debug_msg "Setting output filename to ${2} (was ${archive_fn})"
    archive_fn="$2"
    shift; shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -v|--verbose)
    debug_msg "Setting verbose option to 1 (was ${verbose})"
    verbose=1;
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  *)
    debug_msg "Argument '${1}' added to positional array"
    positional+=("$1")
    shift
    ;;
  esac
done

# error if no filenames given
if [[ "${#positional[@]}" -lt 1 ]]; then
  debug_msg "No input filenames were given"
  usage_msg
fi

# determine tar action
if [[ "$compress" -eq 1 ]]; then
  archive_action="tar.gz"
elif [[ "$compress" -eq 2 ]]; then
  archive_action="tar.xz"
elif [[ "$compress" -eq 3 ]]; then
  archive_action="tar.zst"
elif [[ "$compress" -eq 4 ]]; then
  archive_action="tar.bz2"
elif [[ -n "$archive_fn" && "$compress" -eq -1 ]]; then
  case "$archive_fn" in
  *.tar)
    archive_action="tar"
    ;;

  *.tar.gz)
    archive_action="tar.gz"
    ;;

  *.tar.xz)
    archive_action="tar.xz"
    ;;

  *.tar.zst)
    archive_action="tar.zst"
    ;;

  *.tar.bz2)
    archive_action="tar.bz2"
    ;;
  esac
else
  archive_action="tar"
fi
if [[ "$encrypt" -eq 1 ]]; then
  archive_action="${archive_action}.gpg"
elif [[ -n "$archive_fn" && "$encrypt" -eq -1 ]]; then
  case "$archive_fn" in
  *.gpg)
    archive_action="${archive_action}.gpg"
    ;;
  esac
fi

# output filename
if [[ -z "$archive_fn" ]]; then
  archive_fn="archive.${archive_action}"
  checksum_fn="archive.sha1"
  debug_msg "No output filename was given, defaulting to '${archive_fn}'"
else
  checksum_fn="$(fn_basename "$archive_fn").sha1"
fi

# check files
if contains "$archive_fn" "${positional[@]}"; then
  error_msg "Output file cannot also be input file"
elif [[ "$checksum" -eq 1 ]] && contains "$checksum_fn" "${positional[@]}"; then
  error_msg "Output file cannot also be input file"
fi
if ! prompt_overwrite "$archive_fn"; then
  exit 1
elif [[ "$checksum" -eq 1 ]] && ! prompt_overwrite "$checksum_fn"; then
  exit 1
fi
for target in "${positional[@]}";do
  if [[ ! -f "$target" ]]; then
    error_msg "No such file '${target}'"
  fi
done

# main routine
code=0
case "$archive_action" in
tar)
  if ! /usr/bin/tar -cf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.gpg)
  if ! /usr/bin/tar -c "${positional[@]}" | /usr/bin/gpg -c -o "$archive_fn"; then
    code=1
  fi
  ;;

tar.gz)
  if ! /usr/bin/tar -czf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.gz.gpg)
  if ! /usr/bin/tar -cz "${positional[@]}" | /usr/bin/gpg -c -o "$archive_fn"; then
    code=1
  fi
  ;;

tar.xz)
  if ! /usr/bin/tar -cJf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.xz.gpg)
  if ! /usr/bin/tar -cJ "${positional[@]}" | /usr/bin/gpg -c -o "$archive_fn"; then
    code=1
  fi
  ;;

tar.zst)
  if ! /usr/bin/tar --zstd -cf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.zst.gpg)
  if ! /usr/bin/tar --zstd -c "${positional[@]}" | /usr/bin/gpg -c -o "$archive_fn"; then
    code=1
  fi
  ;;

tar.bz2)
  if ! /usr/bin/tar -cjf "$archive_fn" "${positional[@]}"; then
    code=1
  fi
  ;;

tar.bz2.gpg)
  if ! /usr/bin/tar -cj "${positional[@]}" | /usr/bin/gpg -c -o "$archive_fn"; then
    code=1
  fi
  ;;
esac

# checksum routine
if [[ "$checksum" -eq 1 ]]; then
  if ! /usr/bin/sha1sum "$archive_fn" | /usr/bin/awk '{print $1}' > "$checksum_fn"; then
    code=1
  fi
fi

# return stored code
exit "$code"


A src/mylib.bash => src/mylib.bash +248 -0
@@ 0,0 1,248 @@
#!/bin/bash
# shellcheck disable=SC2030,SC2031

# This library gives access to these functions:
# msg MSG                    prints MSG, except if $quiet==1
# prompt MSG                 prints MSG and sets $response to user input
# debug_msg MSG              prints MSG only if $verbose==1
# nonfatal_error_msg MSG     prints MSG to STDERR, except if $quiet==1
# error_msg MSG              prints MSG to STDERR, except if $quiet==1
# help_msg                   prints built-in documentation
# usage_msg                  prints usage instructions to STDERR, except if $quiet==1
# version_msg                prints version to STDERR, except if $quiet==1
# is_integer VALUE           checks if VALUE is an integer
# is_natural VALUE           checks if VALUE is an integer >= 0
# is_positive_integer VALUE  checks if VALUE is an integer
# contains NEEDLE HAYSTACK   checks if NEEDLE is in array HAYSTACK
# prompt_overwrite FILE      prompts user for permission to overwrite FILE
# fn_basename FN             extracts identifiable base from FN
# fn_extension FN            extracts file extension from FN
#
# Through some means, these environment variables should be set:
# name          name of program
# version       version of program
# help_message  built-in documentation
# verbose       0 or 1; should more messages be shown, with debugging prefixes?
# quiet         0 or 1; should messages be suppressed?


# Internal API - precedes a message
# Normal            ->
# Quiet             ->
# Verbose           -> <LEVEL>:<PROGRAM>:
# Verbose AND Quiet -> <LEVEL>:<PROGRAM>:
verbose_prefix() {
  if [[ "${verbose:=0}" -eq 1 ]]; then
    (/usr/bin/printf "%s:%s:" "$1" "$name")
  fi
}


# Internal API - precedes a message
# Normal            -> <PROGRAM>:
# Quiet             ->
# Verbose           -> ERROR:<PROGRAM>:
# Verbose AND Quiet -> ERROR:<PROGRAM>:
error_prefix() {
  if [[ "${verbose:=0}" -eq 1 ]]; then
    (>&2 /usr/bin/printf "ERROR:%s:" "$name")
  elif [[ "${quiet:=0}" -eq 0 ]]; then
    (>&2 /usr/bin/printf "%s: " "$name")
  fi
}


# Internal API - follows a prefix
# Normal            -> <MESSAGE>
# Quiet             ->
# Verbose           -> <MESSAGE>
# Verbose AND Quiet -> <MESSAGE> (NOTE: unsuppressed)
base_msg() {
  if [[ "${quiet:=0}" -eq 0 ]]; then
    /usr/bin/printf "%s\n" "$1"
  elif [[ "${verbose:=0}" -eq 1 ]]; then
    /usr/bin/printf "%s (NOTE: unsuppressed)\n" "$1"
  fi
}


# Internal API
# Normal -> <MESSAGE>
# Quiet  ->
# Note: ignore Verbose
dump_msg() {
  if [[ "${quiet:=0}" -eq 0 ]]; then
    /usr/bin/printf "%s\n" "$1"
  fi
}


# Normal            -> <MESSAGE>
# Quiet             ->
# Verbose           -> INFO:<PROGRAM>:<MESSAGE>
# Verbose AND Quiet -> INFO:<PROGRAM>:<MESSAGE> (NOTE: unsuppressed)
msg() {
  verbose_prefix "INFO"
  base_msg "$1"
}


# Normal            -> <MESSAGE>
# Verbose           -> PROMPT:<PROGRAM>:<MESSAGE>
#                      DEBUG:<PROGRAM>:Received value of <VALUE>
# Note: if Quiet, exit as failure
prompt() {
  if [[ "${quiet:=0}" -eq 0 ]]; then
    verbose_prefix "PROMPT"
    read -r -p "$1" response
    debug_msg "Received value of '${response}'"
  else
    exit 1
  fi
}


# Normal            ->
# Quiet             ->
# Verbose           -> DEBUG:<PROGRAM>:<MESSAGE>
# Verbose AND Quiet -> DEBUG:<PROGRAM>:<MESSAGE>
debug_msg() {
  if [[ "${verbose:=0}" -eq 1 ]]; then
    verbose_prefix "DEBUG"
    /usr/bin/printf "%s\n" "$1"
  fi
}


# Normal            -> <PROGRAM>: <MESSAGE>
# Quiet             ->
# Verbose           -> ERROR:<PROGRAM>:<MESSAGE>
# Verbose AND Quiet -> ERROR:<PROGRAM>:<MESSAGE> (NOTE: unsuppressed)
nonfatal_error_msg() {
  error_prefix
  (>&2 base_msg "$1")
}


# Normal            -> <PROGRAM>: <MESSAGE>
# Quiet             ->
# Verbose           -> ERROR:<PROGRAM>:<MESSAGE>
# Verbose AND Quiet -> ERROR:<PROGRAM>:<MESSAGE> (NOTE: unsuppressed)
# Note: exit as error
error_msg() {
  nonfatal_error_msg "$1"
  exit 1
}


# Normal -> <USAGE MESSAGE>
# Quiet  ->
# Note: exit as error
# Note: ignore Verbose
usage_msg() {
  (>&2 dump_msg "$(/usr/bin/printf "${help_message:=Usage: don\'t}\n" | grep -e 'Usage' | head -n 1)")
  exit 1
}


# Normal -> <HELP MESSAGE>
#           <HELP MESSAGE>
#           <HELP MESSAGE>
# Quiet  ->
# Note: exit as success
# Note: ignore Verbose
help_msg() {
  (dump_msg "${help_message:=git gud}")
  exit 0
}


# Normal -> <PROGRAM> <VERSION>
# Quiet  ->
# Note: exit as success
# Note: ignore Verbose
version_msg() {
  (dump_msg "${name:=my_program} ${version:=X.Y}")
  exit 0
}


# is integer -> 0
# else       -> 1
is_integer() {
  [ "$1" -eq "$1" ] 2>/dev/null && return 0 || return 1
}


# is integer >=0 -> 0
# else           -> 1
is_natural() {
  [ "$1" -ge 0 ] 2>/dev/null && return 0 || return 1
}


# is integer >=1 -> 0
# else           -> 1
is_positive_integer() {
  [ "$1" -ge 1 ] 2>/dev/null && return 0 || return 1
}


# is in array -> 0
# else        -> 1
contains() {
  pattern="$1"; shift
  code=1
  for arg; do
    if [[ "$arg" == "$pattern" ]]; then
      code=0
      break
    fi
  done
  return "$code"
}


# file does not exist OR user input 'Yy*' -> 0
# else                                    -> 1
prompt_overwrite() {
  code=0
  if [[ -f "$1" ]]; then
    prompt "File '${1}' already exists. Overwrite? "
    case "$response" in
    [Yy]*)
      msg "Overwriting..."
      ;;

    *)
      nonfatal_error_msg "Exiting"
      code=1
      ;;
    esac
  fi
  return "$code"
}


# filename begins in . -> original filename
# else                 -> original filename up to first .
fn_basename() {
  if [[ "$1" = ".*" ]]; then
    printf "%s\n" "$1"
  else
    printf "%s\n" "$1" | cut -f 1 -d '.'
  fi
}


# filename begins in . -> original filename
# else                 -> original filename after first .
fn_extension() {
  if [[ "$1" = ".*" ]]; then
    printf "%s\n" "$1"
  else
    printf "%s\n" "$1" | cut -f 2- -d '.'
  fi
}



A src/myminiparse.sh => src/myminiparse.sh +27 -0
@@ 0,0 1,27 @@
#!/bin/sh
# shellcheck disable=SC2034

# A very basic argument parser written for POSIX sh. Only supports printing
# a help message or a version number. Intended for scripts that have no good
# reason to support debugging messages.

quiet=0

for arg; do
  case "$arg" in
  -h|--help)
    /usr/bin/printf "${help_message:=git gud}\n"
    exit 0
    ;;

  -q|--quiet)
    quiet=1
    ;;

  -v|--version)
    /usr/bin/printf "${name:=my_program} ${version:=X.Y}\n"
    exit 0
    ;;
  esac
done


A src/myparse.bash => src/myparse.bash +42 -0
@@ 0,0 1,42 @@
#!/bin/bash

# A basic argument parser written in bash. Depends on and supplies all
# variables expected by mylib.bash. Sufficient for any script that takes
# positional arguments only.

positional=()
quiet=0
verbose=0

while [[ $# -gt 0 ]]; do
  case $1 in

  -h|--help)
    help_msg
    shift
    ;;

  -q|--quiet)
    debug_msg "Setting QUIET option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -v|--verbose)
    debug_msg "Setting VERBOSE option to 1 (was ${verbose})"
    verbose=1;
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  *)
    debug_msg "Argument '${1}' added to positional array"
    positional+=("$1")
    shift
    ;;
  esac
done


A src/rand => src/rand +98 -0
@@ 0,0 1,98 @@
#!/bin/bash

name="rand"
version="1.0"
read -r -d '' help_message <<-EOF
	Get a random number within an inclusive range
	Usage: rand START END [OPTIONS]
	Options:
	 -h, --help        print this message
	 -n N, --number N  print N numbers (Default: 1)
	 -q, --quiet       suppress error messages and prompts
	 -v, --verbose     show additional messages
	 -V, --version     print version number and exit
	 -w N, --width N   print numbers zero-padded to N-wide (Default: 0)
EOF

source /usr/local/lib/mylib.bash

positional=()
quiet=0
verbose=0
width=1
number=1
while [[ $# -gt 0 ]]; do
  case "$1" in

  -h|--help)
    help_msg
    shift
    ;;

  -n|--number)
    if ! is_positive_integer "$2"; then
      error_msg "Cannot set number of outputs to '${2}' (not an integer >= 1)"
    fi
    debug_msg "Setting number of outputs to ${2} (was ${number})"
    number="$2"
    shift; shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -v|--verbose)
    debug_msg "Setting verbose option to 1 (was ${verbose})"
    verbose=1;
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  -w|--width)
    if ! is_natural "$2"; then
      error_msg "Cannot set zero-padding width to '${2}' (not an integer >= 0)"
    fi
    debug_msg "Setting zero-padding width to ${2} (was ${width})"
    width="$2";
    shift; shift
    ;;

  *)
    debug_msg "Argument '${1}' added to positional array"
    positional+=("$1")
    shift
    ;;
  esac
done

# check arguments
if [[ ${#positional[@]} -lt 2 ]]; then
  debug_msg "Expected 2 arguments (given ${#positional[@]})"
  usage_msg
else
  range_start="${positional[0]}"
  range_end="${positional[1]}"
  if ! is_integer "$range_start"; then
    error_msg "Expected integer for range start (given '${range_start}')"
  elif ! is_integer "$range_end"; then
    error_msg "Expected integer for range end (given '${range_end}')"
  elif [[ "$range_start" -ge "$range_end" ]]; then
    error_msg "Expected ascending range (given '${range_start}' and '${range_end}')"
  fi
fi

# main routine
code=0
if ! seq -f "%0${width}g" "$range_start" "$range_end" | shuf -n "$number"; then
  code=1
fi

# return stored code
exit "$code"


A src/rebom => src/rebom +41 -0
@@ 0,0 1,41 @@
#!/bin/sh

name="rebom"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Add BOM to a target file
	Usage: rebom TARGET [OPTIONS]
	 -h, --help            print this message
	 -v, --version         print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

# re-print first non-option argument, then exit
for arg; do
  case "$arg" in
  -q|--quiet)
    #ignore these
    ;;

  *)
    # main routine
    if [ ! -f "$arg" ]; then
      (>&2 printf "%s: No such file '%s'\n" "$name" "$arg")
      exit 1
    else
      /usr/bin/printf "\xEF\xBB\xBF"
      /usr/bin/cat "$1"
      exit $?
    fi
    ;;
  esac
done

# if have not exited, no filename was specified
if [ "$quiet" -eq 0 ]; then
  (>&2 /usr/bin/printf "Usage: rebom TARGET [OPTIONS]\n")
fi
exit 1


A src/rmtar => src/rmtar +41 -0
@@ 0,0 1,41 @@
#!/bin/sh

name="rmtar"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Delete 'tar' archive files
	Usage: rmtar TARGET [..] [OPTIONS]
	Options:
	 -h, --help     print this message and exit
	 -v, --version  print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

# error if no directory names given
if [ "$#" -eq 0 ]; then
  (>&2 /usr/bin/printf "Usage: rmtar TARGET [OPTIONS]\n")
  exit 1
elif [ "$#" -eq 1 ] && [ "$quiet" -eq 1 ]; then
  exit 1
fi

# main routine
code=0
for arg; do
  if [ ! -f "$arg" ]; then
    if [ "$quiet" -eq 0 ]; then
      (>&2 printf "%s: No such file '%s'\n" "$name" "$arg")
    fi
    code=1
  else
    if ! /usr/bin/rm "$arg"; then
      code=1
    fi
  fi
done

# return stored code
exit "$code"


A src/rmzip => src/rmzip +41 -0
@@ 0,0 1,41 @@
#!/bin/sh

name="rmzip"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Delete 'zip' archive files
	Usage: rmzip TARGET [..] [OPTIONS]
	Options:
	 -h, --help     print this message and exit
	 -v, --version  print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

# error if no directory names given
if [ "$#" -eq 0 ]; then
  (>&2 /usr/bin/printf "Usage: rmzip TARGET [OPTIONS]\n")
  exit 1
elif [ "$#" -eq 1 ] && [ "$quiet" -eq 1 ]; then
  exit 1
fi

# main routine
code=0
for arg; do
  if [ ! -f "$arg" ]; then
    if [ "$quiet" -eq 0 ]; then
      (>&2 printf "%s: No such file '%s'\n" "$name" "$arg")
    fi
    code=1
  else
    if ! /usr/bin/rm "$arg"; then
      code=1
    fi
  fi
done

# return stored code
exit "$code"


A src/stop-at => src/stop-at +22 -0
@@ 0,0 1,22 @@
#!/bin/awk -f

# stop-at
# ===========
# Usage: <command> | stop-at -v pattern='^-+$'
#
# Re-print until a pattern is matched

BEGIN {
  if (pattern == "" || inclusive !~ /^[01]?$/) {
    print "Usage: stop-at -v pattern=PATTERN -v inclusive=0|1"
    exit 1
  }
}
{
  if ($0 ~ pattern) {
    if (inclusive==1) print;
    exit 0
  }
  print $0
}


A src/tarcat => src/tarcat +97 -0
@@ 0,0 1,97 @@
#!/bin/sh

name="tarcat"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Print contents of target archive file(s)
	Usage: tarcat [TARGET ..] [OPTIONS]
	Options:
	 -h, --help     print this message and exit
	 -q, --quiet    suppress error messages
	 -v, --version  print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

code=0

for target; do
  if [ ! -f "$target" ]; then
    if [ "$quiet" -eq 0 ]; then
      (>&2 printf "%s: No such file '%s'\n" "$name" "$target")
    fi
    code=1
  else
    case "$target" in
    *tar)
      if ! /usr/bin/tar -xOf "$target"; then
        code=1
      fi
      ;;

    *tar.gpg)
      if ! /usr/bin/gpg -d "$target" | /usr/bin/tar -xO; then
        code=1
      fi
      ;;

    *tar.gz)
      if ! /usr/bin/tar -xzOf "$target"; then
        code=1
      fi
      ;;

    *tar.gz.gpg)
      if ! /usr/bin/gpg -d "$target" | /usr/bin/tar -xzO; then
        code=1
      fi
      ;;

    *tar.xz)
      if ! /usr/bin/tar -xJOf "$target"; then
        code=1
      fi
      ;;

    *tar.xz.gpg)
      if ! /usr/bin/gpg -d "$target" | /usr/bin/tar -xJO; then
        code=1
      fi
      ;;

    *tar.zst)
      if ! /usr/bin/tar --zstd -xOf "$target"; then
        code=1
      fi
      ;;

    *tar.zst.gpg)
      if ! /usr/bin/gpg -d "$target" | /usr/bin/tar --zstd -xO; then
        code=1
      fi
      ;;

    *tar.bz2)
      if ! /usr/bin/tar -xjOf "$target"; then
        code=1
      fi
      ;;

    *tar.bz2.gpg)
      if ! /usr/bin/gpg -d "$target" | /usr/bin/tar -xjO; then
        code=1
      fi
      ;;

    *)
      if ! /usr/bin/tar -xOf "$target"; then
        code=1
      fi
      ;;
    esac
  fi
done

exit "$code"


A src/unittest => src/unittest +80 -0
@@ 0,0 1,80 @@
#!/bin/sh

name="unittest"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Wrapper around Python's 'unittest' module
	Usage: unittest TARGET [OPTIONS]
	Options:
	 -c, --color       print with color
	 -h, --help        print this message and exit
	 -v, --verbose     show verbose 'unittest' output
	 -V, --version     print version number and exit
EOF
)

target=""
color=0
verbose=0
python_executable="/usr/bin/env python3"
for arg; do
  case "$arg" in
  -c|--color)
    color=1
    ;;

  -h|--help)
    printf "%s\n" "$help_message"
    exit 0
    ;;

  -v|--verbose)
    verbose=1
    ;;

  -V|--version)
    printf "%s %s\n" "$name" "$version"
    exit 0
    ;;

  *)
    if [ -z "$target" ]; then
      target="$arg"
    fi
    ;;
  esac
done

# unittest subroutine
unittest_subroutine() {
  if [ "$verbose" -eq 1 ]; then
    if ! $python_executable -m unittest discover -v -s "$target" 2>&1; then
      code=1
    fi
  else
    if ! $python_executable -m unittest discover -s "$target" 2>&1 | /usr/bin/tail -n 1; then
      code=1
    fi
  fi
}

# check directory
if [ -z "${target+x}" ]; then
  (>&2 printf "Usage: unittest TARGET [OPTIONS]")
  exit 1
elif [ ! -d "$target" ]; then
  (>&2 printf "%s: No such directory '%s'\n" "$name" "$target")
  exit 1
fi

# main routine
code=0
if [ "$color" -eq 0 ] || { [ -n "${NO_COLOR+x}" ] && [ "$NO_COLOR" -eq 1 ]; }; then
  unittest_subroutine
else
  unittest_subroutine | /usr/local/lib/unittest-color.awk
fi

# return stored code
exit "$code"


A src/unittest-color.awk => src/unittest-color.awk +74 -0
@@ 0,0 1,74 @@
#!/bin/awk -f

# unittest-color.awk
# ==================
# Color filter for Python's 'unittest'

BEGIN {
  red="\033[31;1m"
  green="\033[32;1m"
  yellow="\033[33m"
  cyan="\033[36m";
  reset="\033[0m";
  dim="\033[2m";

  comma_replacement=reset ","
  brace_replacement=reset ")"
  equal_replacement="=" yellow
}
{
  # color tracebacks
  if ($0 ~ /^ERROR:/) {
    $1=red $1 reset dim cyan
    $0=$0 reset
  }
  else if ($0 ~ /^\s*File ".+", line [1-9][0-9]*, in/) {
    $2=yellow $2 reset dim cyan
    $4=yellow $4 reset dim cyan
    $6=yellow $6 reset dim cyan
    $0="  " dim cyan $0 reset
  }

  # highlight numeric measures
  else if ($0 ~ /^Ran [1-9][0-9]* tests?/) {
    $2=yellow $2 reset
    $5=yellow $5 reset
  }

  # color results
  else if ( $0 ~ /^OK\s*$/ || $0 ~ /^OK \(/ ) {
    $1=green $1 reset
    gsub(/=/, equal_replacement)
    gsub(/,/, comma_replacement)
    gsub(/)/, brace_replacement)
    $0=$0 reset
  }
  else if ($0 ~ /^(ERROR|FAILED) \(/) {
    $1=red $1 reset
    $0=$0 reset
    gsub(/=/, equal_replacement)
    gsub(/,/, comma_replacement)
    gsub(/)/, brace_replacement)
    $0=$0 reset
  }
  else if ($0 ~ /\.\.\. (ok|skipped)/) {
    $2=dim cyan $2
    $0=$0 reset
  }
  else if ($0 ~ /\.\.\. (ERROR|FAIL)/) {
    $2=dim cyan $2
    $NF=reset red $NF
    $0=$0 reset
  }

  # color string diffs
  else if ($0 ~ /^\+ /) {
    $1=green $1 reset
  }
  else if ($0 ~ /^\- /) {
    $1=red $1 reset
  }

  print $0
}


A src/untar => src/untar +105 -0
@@ 0,0 1,105 @@
#!/bin/sh

name="untar"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Wrapper around 'tar' for easier decompression
	Usage: untar TARGET [..] [OPTIONS]
	Options:
	 -h, --help     print this message and exit
	 -q, --quiet    suppress error messages
	 -v, --version  print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

# error if no directory names given
if [ "$#" -eq 0 ]; then
  (>&2 /usr/bin/printf "Usage: untar TARGET [OPTIONS]\n")
  exit 1
elif [ "$#" -eq 1 ] && [ "$quiet" -eq 1 ]; then
  exit 1
fi

# main routine
code=0
for target; do
  if [ ! -f "$target" ]; then
    if [ "$quiet" -eq 0 ]; then
      (>&2 printf "%s: No such file '%s'\n" "$name" "$target")
    fi
    code=1
  else
    case "$target" in
    *tar)
      if ! /usr/bin/tar -xf "$target"; then
        code=1
      fi
      ;;

    *tar.gpg)
      if ! /usr/bin/gpg -d "$target" | /usr/bin/tar -x; then
        code=1
      fi
      ;;

    *tar.gz)
      if ! /usr/bin/tar -xzf "$target"; then
        code=1
      fi
      ;;

    *tar.gz.gpg)
      if ! /usr/bin/gpg -d "$target" | /usr/bin/tar -xz; then
        code=1
      fi
      ;;

    *tar.xz)
      if ! /usr/bin/tar -xJf "$target"; then
        code=1
      fi
      ;;

    *tar.xz.gpg)
      if ! /usr/bin/gpg -d "$target" | /usr/bin/tar -xJ; then
        code=1
      fi
      ;;

    *tar.zst)
      if ! /usr/bin/tar --zstd -xf "$target"; then
        code=1
      fi
      ;;

    *tar.zst.gpg)
      if ! /usr/bin/gpg -d "$target" | /usr/bin/tar --zstd -x; then
        code=1
      fi
      ;;

    *tar.bz2)
      if ! /usr/bin/tar -xjf "$target"; then
        code=1
      fi
      ;;

    *tar.bz2.gpg)
      if ! /usr/bin/gpg -d "$target" | /usr/bin/tar -xj; then
        code=1
      fi
      ;;

    *)
      if ! /usr/bin/tar -xf "$target"; then
        code=1
      fi
      ;;
    esac
  fi
done

exit "$code"


A src/whichcat => src/whichcat +42 -0
@@ 0,0 1,42 @@
#!/bin/sh

name="whichcat"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Print all lines from a program to the terminal
	Usage: whichcat [PROGRAM ..] [OPTIONS]
	Options:
	 -h, --help     print this message
	 -q, --quiet    suppress error messages
	 -v, --version  print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

# loop through input
code=0
for arg; do
  case "$arg" in
  -q|--quiet)
    #ignore these
    ;;

  *)
    # main routine
    target=$(command -v "$arg")
    if [ -z "$target" ]; then
      if [ "$quiet" -eq 0 ]; then
        (>&2 /usr/bin/printf "%s: No such program '%s'\n" "$name" "$arg")
      fi
      code=1
    else
      /usr/bin/cat "$target"
    fi
    ;;
  esac
done

# return stored code
exit "$code"


A src/whiched => src/whiched +46 -0
@@ 0,0 1,46 @@
#!/bin/sh

name="whiched"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Open a program with your editor
	Usage: whiched PROGRAM [OPTIONS]
	Options:
	 -h, --help     print this message
	 -q, --quiet    suppress error messages
	 -v, --version  print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

# open first non-option argument in editor, then exit
for arg; do
  case "$arg" in
  -q|--quiet)
    #ignore these
    ;;

  *)
    # main routine
    target=$(command -v "$arg")
    if [ -z "$target" ]; then
      if [ "$quiet" -eq 0 ]; then
        (>&2 printf "%s: No such directory '%s'\n" "$name" "$target")
      fi
      exit 1
    else
      my_editor=${EDITOR:=/usr/bin/ed}
      $my_editor "$target"
      exit $?
    fi
    ;;
  esac
done

# if have not exited, no program was specified
if [ "$quiet" -eq 0 ]; then
  (>&2 /usr/bin/printf "Usage: whiched PROGRAM [OPTIONS]\n")
fi
exit 1


A src/whichhead => src/whichhead +78 -0
@@ 0,0 1,78 @@
#!/bin/bash

name="whichhead"
version="1.0"
read -r -d '' help_message <<-EOF
	Print the first 10 lines from a program
	Usage: whichhead [PROGRAM ..] [OPTIONS]
	Options:
	 -h, --help        print this message
	 -n N, --number=N  print the first N lines (Default: 10)
	 -q, --quiet       suppress error messages
	 -v, --verbose     show additional messages
	 -V, --version     print version number and exit
EOF

source /usr/local/lib/mylib.bash

positional=()
num_lines=10
quiet=0
verbose=0

while [[ $# -gt 0 ]]; do
  case $1 in

  -h|--help)
    help_msg
    shift
    ;;

  -n|--number)
    if ! is_natural "$2"; then
      error_msg "Cannot set number of lines to '${2}' (not an integer >= 0)"
    fi
    debug_msg "Setting number of lines to ${2} (was ${num_lines})"
    num_lines="$2"
    shift; shift
    ;;

  -q|--quiet)
    debug_msg "Setting quiet option to 1 (was ${quiet})"
    quiet=1
    shift
    ;;

  -v|--verbose)
    debug_msg "Setting verbose option to 1 (was ${verbose})"
    verbose=1;
    shift
    ;;

  -V|--version)
    version_msg
    ;;

  *)
    debug_msg "Argument '${1}' added to positional array"
    positional+=("$1")
    shift
    ;;
  esac
done

# main routine
code=0
for target in "${positional[@]}"; do
  abs_target=$(command -v "$target")
  if [[ -z $abs_target ]]; then
    nonfatal_error_msg "No such program '${target}'"
    code=1
  else
    /usr/bin/head "$abs_target" -n "$num_lines"
  fi
done

# return stored code
exit "$code"


A src/whichvi => src/whichvi +46 -0
@@ 0,0 1,46 @@
#!/bin/sh

name="whichvi"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Open a program with your visual editor
	Usage: whichvi PROGRAM [OPTIONS]
	Options:
	 -h, --help     print this message
	 -q, --quiet    suppress error messages
	 -v, --version  print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

# open first non-option argument in editor, then exit
for arg; do
  case "$arg" in
  -q|--quiet)
    #ignore these
    ;;

  *)
    # main routine
    target=$(command -v "$arg")
    if [ -z "$target" ]; then
      if [ "$quiet" -eq 0 ]; then
        (>&2 printf "%s: No such directory '%s'\n" "$name" "$target")
      fi
      exit 1
    else
      my_visual=${VISUAL:=/usr/bin/ed}
      $my_visual "$target"
      exit $?
    fi
    ;;
  esac
done

# if have not exited, no program was specified
if [ "$quiet" -eq 0 ]; then
  (>&2 /usr/bin/printf "Usage: whichvi PROGRAM [OPTIONS]\n")
fi
exit 1


A src/whisper => src/whisper +24 -0
@@ 0,0 1,24 @@
#!/bin/sh

name="whisper"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Wrapper around 'espeak' to mirror 'say' in macOS
	Usage: mkbak MESSAGE [OPTIONS]
	Options:
	 -h, --help     print this message and exit
	 -v, --version  print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

if [ "$#" -eq 0 ]; then
  my_message="present day <break time='1000ms'/> present time."
else
  my_message="$*"
fi

/usr/bin/espeak -v +whisper -s 8 -m "$my_message" 2>/dev/null
exit "$?"


A src/wttr => src/wttr +17 -0
@@ 0,0 1,17 @@
#!/bin/sh

name="wttr"
version="1.0"
help_message=$(/usr/bin/cat <<-EOF
	Wrapper around 'wego' to fix double-wide runes for some fonts
	Usage: wttr
	Options:
	 -h, --help     print this message and exit
	 -V, --version  print version number and exit
EOF
)

. /usr/local/lib/myminiparse.sh

wego $@ | sed -e 's/[↘↗↖↙]/ &/g'


A tests/bom_test.sh => tests/bom_test.sh +57 -0
@@ 0,0 1,57 @@
#!/bin/sh

# debom

# create temp directory
mkdir -p tests/temp_bom

# exit codes
if ! debom -V >/dev/null 2>&1; then
  printf "Wrong exit code on 'debom -V' - should be 0\n"
  exit 1
elif ! debom -h >/dev/null 2>&1; then
  printf "Wrong exit code on 'debom -h' - should be 0\n"
  exit 1
elif debom >/dev/null 2>&1; then
  printf "Wrong exit code on 'debom' - should be 1\n"
  exit 1
elif debom non-existant >/dev/null 2>&1; then
  printf "Wrong exit code on 'debom non-existant' should be 1\n"
  exit 1
fi

# de-BOM a file
cd tests/temp_bom
debom -d ../static/debom_target.txt > debom.txt
if ! cmp ../static/debom_result.txt debom.txt >/dev/null 2>&1; then
  printf "Failure in de-BOM test\n"
  exit 1
fi
cd ../..

# rebom

# exit codes
if ! rebom -v >/dev/null 2>&1; then
  printf "Wrong exit code on 'rebom -v' - should be 0\n"
  exit 1
elif ! rebom -h >/dev/null 2>&1; then
  printf "Wrong exit code on 'rebom -h' - should be 0\n"
  exit 1
elif rebom >/dev/null 2>&1; then
  printf "Wrong exit code on 'rebom' - should be 1\n"
  exit 1
elif rebom non-existant >/dev/null 2>&1; then
  printf "Wrong exit code on 'rebom non-existant' should be 1\n"
  exit 1
fi

# re-BOM a file
cd tests/temp_bom
rebom ../static/debom_result.txt > rebom.txt
if ! cmp ../static/debom_target.txt rebom.txt >/dev/null 2>&1; then
  printf "Failure in re-BOM test\n"
  exit 1
fi
cd ../..


A tests/compression_test.sh => tests/compression_test.sh +99 -0
@@ 0,0 1,99 @@
#!/bin/sh

# mktar and tarcat

# create temp directory
mkdir -p tests/temp_compression

# compress file with algorithms set by options
cd tests/static
mktar compression_target.txt --compress=none
mktar compression_target.txt --compress=gzip
mktar compression_target.txt --compress=xz
mktar compression_target.txt --compress=zstd
mktar compression_target.txt --compress=bzip2
mv archive* ../temp_compression/
cd ..

# uncompress and combine compressed files
cd temp_compression
tarcat archive* > basic_concat.txt
cd ..

# test
if ! cmp static/compression_result.txt temp_compression/basic_concat.txt >/dev/null 2>&1; then
  printf "Failure in compression tests: basic compression\n"
  exit 1
fi

# compress file with algorithms implicitly set by output filename
cd static
mktar compression_target.txt -n implicit.tar
mktar compression_target.txt -n implicit.tar.gz
mktar compression_target.txt -n implicit.tar.xz
mktar compression_target.txt -n implicit.tar.zst
mktar compression_target.txt -n implicit.tar.bz2
mv implicit* ../temp_compression/
cd ..

# uncompress and combine compressed files
cd temp_compression
tarcat implicit* > implicit_concat.txt
cd ..

# test
if ! cmp static/compression_result.txt temp_compression/implicit_concat.txt >/dev/null 2>&1; then
  printf "Failure in compression tests: implicit compression\n"
  exit 1
fi

# compress file with algorithms set and filenames set
cd static
mktar compression_target.txt --compress=none  -n explicit.tar
mktar compression_target.txt --compress=gzip  -n explicit.tar.gz
mktar compression_target.txt --compress=xz    -n explicit.tar.xz
mktar compression_target.txt --compress=zstd  -n explicit.tar.zst
mktar compression_target.txt --compress=bzip2 -n explicit.tar.bz2
mv explicit* ../temp_compression/
cd ..

# uncompress and combine compressed files
cd temp_compression
tarcat explicit* > explicit_concat.txt
cd ..

# test
if ! cmp static/compression_result.txt temp_compression/explicit_concat.txt >/dev/null 2>&1; then
  printf "Failure in compression tests: explicit compression\n"
  exit 1
fi

# untar

# copy archives to temp directory
cp static/decompression_target* temp_compression/

# decompress files
cd temp_compression
untar decompression_target.tar decompression_target.tar.gz decompression_target.tar.xz decompression_target.tar.zst decompression_target.tar.bz2
cd ..

# test
if ! cmp static/decompression_result.txt temp_compression/tar.txt >/dev/null 2>&1; then
  printf "Failure in decompression tests: tar\n"
  exit 1
elif ! cmp static/decompression_result.txt temp_compression/gzip.txt >/dev/null 2>&1; then
  printf "Failure in decompression tests: gzip\n"
  exit 1
elif ! cmp static/decompression_result.txt temp_compression/xz.txt >/dev/null 2>&1; then
  printf "Failure in decompression tests: xz\n"
  exit 1
elif ! cmp static/decompression_result.txt temp_compression/zstd.txt >/dev/null 2>&1; then
  printf "Failure in decompression tests: zstd\n"
  exit 1
elif ! cmp static/decompression_result.txt temp_compression/bzip2.txt >/dev/null 2>&1; then
  printf "Failure in decompression tests: bzip2\n"
  exit 1
fi



A tests/ctdir_test.sh => tests/ctdir_test.sh +36 -0
@@ 0,0 1,36 @@
#!/bin/sh

# ctdir

# exit codes
if ! ctdir -v >/dev/null 2>&1; then
  printf "Wrong exit code on 'ctdir -v' - should be 0\n"
  exit 1
elif ! ctdir -h >/dev/null 2>&1; then
  printf "Wrong exit code on 'ctdir -h' - should be 0\n"
  exit 1
elif ctdir >/dev/null 2>&1; then
  printf "Wrong exit code on 'ctdir' - should be 1\n"
  exit 1
elif ctdir non-existant >/dev/null 2>&1; then
  printf "Wrong exit code on 'ctdir non-existant' should be 1\n"
  exit 1
elif ! ctdir . >/dev/null 2>&1; then
  printf "Wrong exit code on 'ctdir .' should be 0\n"
  exit 1
fi

# output
num=$(ctdir .)
if [ ! "$num" -eq "$num" ] 2>/dev/null; then
  printf "Did not return a number on 'ctdir .'\n"
  exit 1
fi

# behavior on multiple inputs
lines=$(ctdir . . | wc -l)
if [ "$lines" -ne 2 ] 2>/dev/null; then
  printf "Did not return a number for each input on 'ctdir . .'\n"
  exit 1
fi


A tests/static/compression_result.txt => tests/static/compression_result.txt +20 -0
@@ 0,0 1,20 @@
[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]


A tests/static/compression_target.txt => tests/static/compression_target.txt +4 -0
@@ 0,0 1,4 @@
[1]
[2]
[3]


A tests/static/debom_result.txt => tests/static/debom_result.txt +1 -0
@@ 0,0 1,1 @@
foo bar

A tests/static/debom_target.txt => tests/static/debom_target.txt +1 -0
@@ 0,0 1,1 @@
foo bar

A tests/static/decompression_result.txt => tests/static/decompression_result.txt +4 -0
@@ 0,0 1,4 @@
(a)
(b)
(c)


A tests/static/decompression_target.tar => tests/static/decompression_target.tar +0 -0
A tests/static/decompression_target.tar.bz2 => tests/static/decompression_target.tar.bz2 +0 -0
A tests/static/decompression_target.tar.gz => tests/static/decompression_target.tar.gz +0 -0
A tests/static/decompression_target.tar.xz => tests/static/decompression_target.tar.xz +0 -0
A tests/static/decompression_target.tar.zst => tests/static/decompression_target.tar.zst +0 -0
A tests/temp_bom/debom.txt => tests/temp_bom/debom.txt +1 -0
@@ 0,0 1,1 @@
foo bar

A tests/temp_bom/rebom.txt => tests/temp_bom/rebom.txt +1 -0
@@ 0,0 1,1 @@
foo bar

A tests/temp_compression/archive.tar => tests/temp_compression/archive.tar +0 -0
A tests/temp_compression/archive.tar.bz2 => tests/temp_compression/archive.tar.bz2 +0 -0
A tests/temp_compression/archive.tar.gz => tests/temp_compression/archive.tar.gz +0 -0
A tests/temp_compression/archive.tar.xz => tests/temp_compression/archive.tar.xz +0 -0
A tests/temp_compression/archive.tar.zst => tests/temp_compression/archive.tar.zst +0 -0
A tests/temp_compression/basic_concat.txt => tests/temp_compression/basic_concat.txt +20 -0
@@ 0,0 1,20 @@
[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]


A tests/temp_compression/bzip2.txt => tests/temp_compression/bzip2.txt +4 -0
@@ 0,0 1,4 @@
(a)
(b)
(c)


A tests/temp_compression/decompression_target.tar => tests/temp_compression/decompression_target.tar +0 -0
A tests/temp_compression/decompression_target.tar.bz2 => tests/temp_compression/decompression_target.tar.bz2 +0 -0
A tests/temp_compression/decompression_target.tar.gz => tests/temp_compression/decompression_target.tar.gz +0 -0
A tests/temp_compression/decompression_target.tar.xz => tests/temp_compression/decompression_target.tar.xz +0 -0
A tests/temp_compression/decompression_target.tar.zst => tests/temp_compression/decompression_target.tar.zst +0 -0
A tests/temp_compression/explicit.tar => tests/temp_compression/explicit.tar +0 -0
A tests/temp_compression/explicit.tar.bz2 => tests/temp_compression/explicit.tar.bz2 +0 -0
A tests/temp_compression/explicit.tar.gz => tests/temp_compression/explicit.tar.gz +0 -0
A tests/temp_compression/explicit.tar.xz => tests/temp_compression/explicit.tar.xz +0 -0
A tests/temp_compression/explicit.tar.zst => tests/temp_compression/explicit.tar.zst +0 -0
A tests/temp_compression/explicit_concat.txt => tests/temp_compression/explicit_concat.txt +20 -0
@@ 0,0 1,20 @@
[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]


A tests/temp_compression/gzip.txt => tests/temp_compression/gzip.txt +4 -0
@@ 0,0 1,4 @@
(a)
(b)
(c)


A tests/temp_compression/implicit.tar => tests/temp_compression/implicit.tar +0 -0
A tests/temp_compression/implicit.tar.bz2 => tests/temp_compression/implicit.tar.bz2 +0 -0
A tests/temp_compression/implicit.tar.gz => tests/temp_compression/implicit.tar.gz +0 -0
A tests/temp_compression/implicit.tar.xz => tests/temp_compression/implicit.tar.xz +0 -0
A tests/temp_compression/implicit.tar.zst => tests/temp_compression/implicit.tar.zst +0 -0
A tests/temp_compression/implicit_concat.txt => tests/temp_compression/implicit_concat.txt +20 -0
@@ 0,0 1,20 @@
[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]

[1]
[2]
[3]


A tests/temp_compression/tar.txt => tests/temp_compression/tar.txt +4 -0
@@ 0,0 1,4 @@
(a)
(b)
(c)


A tests/temp_compression/xz.txt => tests/temp_compression/xz.txt +4 -0
@@ 0,0 1,4 @@
(a)
(b)
(c)


A tests/temp_compression/zstd.txt => tests/temp_compression/zstd.txt +4 -0
@@ 0,0 1,4 @@
(a)
(b)
(c)


A tests/temp_enumerate/0001.a => tests/temp_enumerate/0001.a +0 -0
A tests/temp_enumerate/0002.b => tests/temp_enumerate/0002.b +0 -0
A tests/temp_enumerate/0003.c => tests/temp_enumerate/0003.c +0 -0
A tests/temp_enumerate/0004.d => tests/temp_enumerate/0004.d +0 -0
A tests/temp_enumerate/0005.e => tests/temp_enumerate/0005.e +0 -0
A tests/which_test.sh => tests/which_test.sh +92 -0
@@ 0,0 1,92 @@
#!/bin/sh

# whichcat

# exit codes
if ! whichcat -v >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichcat -v' - should be 0\n"
  exit 1
elif ! whichcat -h >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichcat -h' - should be 0\n"
  exit 1
elif ! whichcat >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichcat' - should be 0\n"
  exit 1
elif whichcat non-existant >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichcat non-existant' - should be 1\n"
  exit 1
elif ! whichcat whichcat >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichcat whichcat' - should be 0\n"
  exit 1
fi

# behavior on multiple inputs
lines_once=$(whichcat whichcat | wc -l)
lines_twice=$(whichcat whichcat whichcat | wc -l)
if [ "$lines_once" -ge "$lines_twice" ] 2>/dev/null; then
  printf "Did not print output for each input on 'whichcat whichcat whichcat'\n"
  exit 1
fi

# whichhead

# exit codes
if ! whichhead -V >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichhead -v' - should be 0\n"
  exit 1
elif ! whichhead -h >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichhead -V' - should be 0\n"
  exit 1
elif ! whichhead >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichhead' - should be 0\n"
  exit 1
elif whichhead non-existant >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichhead non-existant' - should be 1\n"
  exit 1
elif ! whichhead whichhead >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichhead whichhead' - should be 0\n"
  exit 1
fi

# behavior on multiple inputs
lines_once=$(whichhead whichhead | wc -l)
lines_twice=$(whichhead whichhead whichhead | wc -l)
if [ "$lines_once" -ge "$lines_twice" ] 2>/dev/null; then
  printf "Did not print output for each input on 'whichhead whichhead whichhead'\n"
  exit 1
fi

# whiched

# exit codes
if ! whiched -v >/dev/null 2>&1; then
  printf "Wrong exit code on 'whiched -v' - should be 0\n"
  exit 1
elif ! whiched -h >/dev/null 2>&1; then
  printf "Wrong exit code on 'whiched -V' - should be 0\n"
  exit 1
elif whiched >/dev/null 2>&1; then
  printf "Wrong exit code on 'whiched' - should be 1\n"
  exit 1
elif whiched non-existant >/dev/null 2>&1; then
  printf "Wrong exit code on 'whiched non-existant' - should be 1\n"
  exit 1
fi

# whichvi

# exit codes
if ! whichvi -v >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichvi -v' - should be 0\n"
  exit 1
elif ! whichvi -h >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichvi -V' - should be 0\n"
  exit 1
elif whichvi >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichvi' - should be 1\n"
  exit 1
elif whichvi non-existant >/dev/null 2>&1; then
  printf "Wrong exit code on 'whichvi non-existant' - should be 1\n"
  exit 1
fi


D tlog => tlog +0 -46
@@ 1,46 0,0 @@
#!/bin/bash

# tlog
# ====
# Usage: tlog TARGET [OPTIONS]
#
# Writes input to a target file while stripping ANSI codes, and returns input
# as it was.

help_msg() {
  cat <<-EOF
	Usage: tlog FILENAME [OPTIONS]
	Options:
	 -a, --append  append to target file instead of overwriting
	 -h, --help    print this message
	EOF
  exit 1
}

err_msg() {
  (>&2 echo "$1")
  exit 1
}

if [[ $# -lt 1 ]]; then
  err_msg "Usage: tlog TARGET [OPTIONS]"
fi

APPEND=0
LOGFILE=
while [[ $# -gt 0 ]]; do
  case $1 in
    -a|--append) APPEND=1; shift;;
    -h|--help)   help_msg;;
    *)           LOGFILE=$1; shift;;
  esac
done

if [[ -z $LOGFILE ]]; then
  err_msg "Usage: tlog TARGET [OPTIONS]"
elif [[ $APPEND -eq 1 ]]; then
  tee >(sed 's/\x1b\[[0-9;]*[mK]//g' >> "$LOGFILE")
else
  tee >(sed 's/\x1b\[[0-9;]*[mK]//g' > "$LOGFILE")
fi


D unittest.sh => unittest.sh +0 -32
@@ 1,32 0,0 @@
#!/bin/sh

# unittest.sh
# ===========
# Usage: unittest.sh [TARGET] [OPTIONS]
#
# Run Python tests in a target directory

help_msg() {
  cat <<-EOF
	Run Python tests in a target directory
	Usage: unittest.sh [TARGET] [OPTIONS]
	Options:
	 -v, --verbose: verbose output
	 -p, --pattern: pattern to match test files
	 -h, --help: print this message
	EOF
  exit 1
}

for i in "$@"; do
  case $i in
    -h|--help) help_msg;;
  esac
done

DIR=${1:-.}
OPTS=${2}
python3 -m unittest discover -s $DIR $OPTS 2>&1 \
  | clean-unittest.awk \
  | color-unittest.sh


D untar => untar +0 -60
@@ 1,60 0,0 @@
#!/bin/sh

# untar
# =====
# Usage: untar FILE [OPTIONS]
#
# Uncompress a tarball. Expects standardized file names (i.e., '.tar.gz.gpg')

help_msg() {
  cat <<-EOF
	Uncompress a tarball
	Usage: untar FILE [OPTIONS]
	Options:
	 -h, --help: print this message
	EOF
  exit 1
}

err_msg() {
  # Takes one argument:
  #   error message
  (>&2 echo "$1")
  exit 1
}

gpg_then_tar() {
  # Takes two arguments:
  #   path to target file
  #   tar options
  TEMP=$(basename "$1" ".gpg")
  gpg -o "$TEMP" -d "$1" || err_msg "Failed to decrypt '${1}'"
  tar "${2}" "$TEMP"
}

for i in "$@"; do
  case $i in
    -h|--help) help_msg;;
  esac
done

if [ "$#" -lt 1 ]; then
  err_msg "Usage: untar FILE [OPTIONS]"
elif ! test -f "$1"; then
  err_msg "No file '${1}'"
  exit
fi

case "$1" in
  *.tar)         tar -xvf "$1";;
  *.tar.gz)      tar -xzvf "$1";;
  *.tar.gz.gpg)  gpg_then_tar "$1" "-xzvf";;
  *.tar.bz2)     tar -xjvf "$1";;
  *.tar.bz2.gpg) gpg_then_tar "$1" "-xjvf";;
  *.tar.xz)      tar -xJvf "$1";;
  *.tar.xz.gpg)  gpg_then_tar "$1" "-xJvf";;
  *.tar.zst)     tar --zstd -xvf "$1";;
  *.tar.zst.gpg) gpg_then_tar "$1" "--zstd -xvf";;
  *)             err_msg "Not a known file extension '${1}'"
esac


D weather => weather +0 -11
@@ 1,11 0,0 @@
#!/bin/sh

# weather
# =======
# Usage weather [NUMBER]
#
# Wrapper aroung `wego`. Show weather for 2 days unless a number is specified.
# Location should be set in `$WEGORC`.

wego ${1:-2} | sed -e 's/[↘↗↖↙]/ &/g' -e '2,7d'


D whichcat => whichcat +0 -42
@@ 1,42 0,0 @@
#!/bin/sh

# whichcat
# ========
# USAGE: whichcat PROGRAM
#
# Print all lines of a program

help_msg() {
  cat <<-EOF
	Print all lines from a program to the terminal
	Usage: whichcat PROGRAM [OPTIONS]
	Options:
	 -n, --number: number all output lines
	 -s, --squeeze-blank: suppress repeated empty output lines
	 -h, --help: print this message
	EOF
  exit 1
}

err_msg() {
  (>&2 echo "$1")
  exit 1
}

for i in "$@"; do
  case $i in
    -h|--help) help_msg;;
  esac
done

if [ "$#" -lt 1 ]; then
  err_msg "Usage: whichcat PROGRAM [OPTIONS]"
fi

BIN=$(which "$1" 2>/dev/null)
if [ -z "$BIN" ]; then
  err_msg "No program '${1}'"
fi

cat "$BIN"


D whiched => whiched +0 -41
@@ 1,41 0,0 @@
#!/bin/sh

# whiched
# =======
# USAGE: whiched PROGRAM
#
# Open a program in `$EDITOR`

help_msg() {
  cat <<-EOF
	Open a program with your editor
	Usage: whiched PROGRAM [OPTIONS]
	Options:
	 -h, --help: print this message
	EOF
  exit 1
}

err_msg() {
  (>&2 echo "$1")
  exit 1
}

for i in "$@"; do
  case $i in
    -h|--help) help_msg;;
  esac
done

if [ "$#" -lt 1 ]; then
  err_msg "Usage: whiched PROGRAM [OPTIONS]"
fi

BIN=$(which "$1" 2>/dev/null)
if [ -z "$BIN" ]; then
  err_msg "No program '${1}'"
fi

ED=${EDITOR:-ed}
"$ED" "$BIN"


D whichhead => whichhead +0 -42
@@ 1,42 0,0 @@
#!/bin/sh

# whichhead
# =========
# USAGE: whichhead PROGRAM [OPTIONS]
#
# Print the first lines of a program

help_msg() {
  cat <<-EOF
	Print the first 10 lines from a program
	Usage: whichhead PROGRAM [OPTIONS]
	Options:
	 -n N, --lines=N: print the first N lines
	 -h, --help: print this message
	EOF
  exit 1
}

err_msg() {
  (>&2 echo "$1")
  exit 1
}

for i in "$@"; do
  case $i in
    -h|--help) help_msg;;
  esac
done

if [ "$#" -lt 1 ]; then
  err_msg "Usage: whichhead PROGRAM [OPTIONS]"
fi

BIN=$(which "$1" 2>/dev/null)
if [ -z "$BIN" ]; then
  err_msg "No program '${1}'"
fi

OPTS="${@:2}"
head "$BIN" ${OPTS[@]}


D whichvi => whichvi +0 -41
@@ 1,41 0,0 @@
#!/bin/sh

# whichvi
# =======
# USAGE: whichvi PROGRAM
#
# Open a program in `$VISUAL`

help_msg() {
  cat <<-EOF
	Open a program with your visual editor
	Usage: whichvi PROGRAM [OPTIONS]
	Options:
	 -h, --help: print this message
	EOF
  exit 1
}

err_msg() {
  (>&2 echo "$1")
  exit 1
}

for i in "$@"; do
  case $i in
    -h|--help) help_msg;;
  esac
done

if [ "$#" -lt 1 ]; then
  err_msg "Usage: whichvi PROGRAM [OPTIONS]"
fi

BIN=$(which "$1" 2>/dev/null)
if [ -z "$BIN" ]; then
  err_msg "No program '${1}'"
fi

VIS=${VISUAL:-vi}
"$VIS" "$BIN"


D whisper => whisper +0 -16
@@ 1,16 0,0 @@
#!/bin/sh

# whisper
# =======
# Usage: whisper [TEXT]
#
# Wrapper on `espeak` to mirror macOS's `say` using whisper voice.

if [ $# -lt 1 ]; then
  TEXT="present day <break time='1000ms'/> present time."
else
  TEXT=$*
fi

espeak -v +whisper -s 8 -m "$TEXT" 2>/dev/null