Skip to content

Instantly share code, notes, and snippets.

@alganet
Last active December 25, 2025 23:22
Show Gist options
  • Select an option

  • Save alganet/99b9f2e87d66e73ba47c94ffdc60d40a to your computer and use it in GitHub Desktop.

Select an option

Save alganet/99b9f2e87d66e73ba47c94ffdc60d40a to your computer and use it in GitHub Desktop.
um.sh - A simple, extensible, literate auto-documenting standard for automation
#!/bin/sh
# ISC License
# Copyright (c) 2025, Alexandre Gomes Gaigalas <alganet@gmail.com>
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
UM_INFO="um version 0.1.0, Copyright (c) Alexandre Gomes Gaigalas"
UM_MAIN="$PWD/$0"
UM_PATH="${UM_PATH:-"${UM_MAIN%/*}/ums"}"
# um.sh
# =======
#
# Welcome to um.sh! A literate program for humans and machines alike.
#
# It is written in portable shell script (bash, zsh, dash, etc) with comments
# in concise english markdown.
#
# um is very simple. It's made of functions prefixed with `um_` and a tiny
# dispatcher to invoke them.
#
# Here is `um_showinfo`:
um_showinfo () {
/bin/cat <<-@
version: $UM_INFO
main: $UM_MAIN
path: $UM_PATH
@
}
# You can invoke it by running `sh um.sh showinfo`.
#
# All ums follow a strict style:
#
# - use descriptive yet concise names
# - the declaration MUST be exactly `um_<name> () {`
# - indentation MUST be tabs to enable `<<-` style heredocs
# - each line SHOULD be at most 80 characters long
# - use only simple UNIX commands
#
# That's it! Now, with those simple style rules, we can compose some
# interesting new ums.
um_list () {
local um_file=
local um_name=
um_file="${1:-"$UM_MAIN"}"
while read -r um_src_line
do
case "$um_src_line" in "um_"*' () {')
um_name="${um_src_line#"um_"}"
um_name="${um_name%' () {'}"
printf '%s ' "$um_name"
;;
esac
done < "$um_file"
printf '\n'
}
# Cool! Now, if we run `sh um.sh list`, we can see available ums.
#
# The um system is extensible. It can load ums from `UM_PATH`.
# Let's make a function to find files in there:
um_umfiles () {
! test -d "$UM_PATH" || find "$UM_PATH" -name '*.sh'
}
# ums can also be private, when it doesn't make sense to invoke them
# directly. Let's make a private function to load all umfiles:
_um_load_umfiles () {
local um_files=
local um_file=
um_files="$(um_umfiles)"
for um_file in $um_files
do . "$um_file"
done
}
# This one will be used at the end of the script, before we run the dispatcher.
#
# We call these extra files "umfiles". They follow some rules too:
#
# - umfiles MUST be named like `foo.um.sh`, no underscores, no subdirs
# - umfiles MUST follow the same style conventions as um.sh
# - umfiles MUST NOT redefine existing ums
# - ums inside umfiles must have their own namespace (e.g., `um_foo_`)
# - um naming should be enough, umfiles MUST NOT have a special help um
#
# By now, you must also have noticed some additional style conventions:
#
# - in addition to the um prefix, private functions MUST be start with `_`
# - all variables inside ums MUST be local and initialized
# - extended ums can be loaded from shell files in UM_PATH
#
# Hey, maybe we can also have a function to list all ums in all umfiles!
um_listall () {
um_list
um_umfiles |
while read -r um_file
do um_list "$um_file"
done
}
# That seems useful! By running `sh um.sh listall`, we can see all ums,
# including those loaded from UM_PATH.
#
# Let's reuse that to create a help function:
um_help () {
printf 'usage: %s <um>\n\n' "$0"
printf 'ums:\n'
um_listall
}
# Let's also add a version um for completeness:
um_version () {
printf '%s\n' "$UM_INFO"
}
# ---
# Code below is needed only in the main um.sh and MUST NOT be in umfiles.
set -euf
IFS='
'
um_main="${1:-}"
um_main="${um_main##*-}"
test $# -lt 1 || shift
_um_load_umfiles
um_"${um_main:-help}" "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment