123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664 |
- #!/bin/bash
-
- shellfu import exit
- shellfu import inigrep
- shellfu import pretty
-
- #
- # Saturnin - __MKIT_PROJ_TAGLINE__
- #
- # Saturnin is a Shellfu module that can help you build consistent and easy
- # to use command line-based toolkit. The idea is that you have a set of
- # scripts that together form a particular tool, which you then integrate
- # under a single meta-command (similar to git, apt, systemctl and many
- # others...).
- #
- # Saturnin's main goal is to help you with the integration so that you can
- # fully concentrate on development of the scripts, while Saturnin will
- # provide the common part of command-line interface. Adding to that,
- # Saturnin also provides several extra features as config management,
- # hooks or support for command line completion.
- #
- # It's possible to integrate any kind of programs within Saturnin, although
- # extra features are currently only available for Shellfu/Bash scripts.
- #
- # Starting your toolkit
- # =====================
- #
- # The starting point of a Saturnin-powered toolkit is the main meta-command,
- # which sets all mandatory variables and calls saturnin__main(). You could
- # implement that one yourself, but it's **strongly recommended** to start
- # off by creating a copy of 'app.skel' from 'saturnin-demo':
- #
- # https://github.com/AloisMahdal/saturnin-demo/
- #
- # In fact, it's best to actually copy the *whole* project, which serves
- # as skeleton of not just the meta-command (by Saturnin) but also MKit
- # install/build system, which provides features like installation
- # scripts, SemVer-compatible versioning and packaging templates for both
- # RPM and DEB:
- #
- # git clone https://github.com/AloisMahdal/saturnin-demo
- # mv saturnin-demo myproject
- # cd myproject
- # rm -rf .git
- # git init
- #
- # You will need to edit few files, for starters:
- #
- # edit mkit.ini # your main project info
- # edit packaging/template.spec # file lists, Requires and description
- # edit packaging/debian/control # ..also for Debian
- # git add .
- # git commit -m "Initial commit"
- #
- # ...but the reward is sweet:
- #
- # make install # to install to current system
- #
- # make rpmstuff # to get .spec file and source tarball
- # make debstuff # to get debian directory
- # make vbump # to seal off release
- # make release # to seal off release
- #
-
- #
- # Git commit hash of application source tree
- #
- # This is supposed to be set by your build scripts when building your
- # application. The string is returned by calling your meta-command with
- # option --saturnin-app-git-hash.
- #
- # Look for 'satcmd' template for a working example.
- #
- SATURNIN_APP_GIT_HASH=${SATURNIN_APP_GIT_HASH:-}
-
- #
- # Your application version
- #
- # This is supposed to be set by your build scripts when building your
- # application. The string is returned by calling your meta-command with
- # options --version, --version-semver or --saturnin-app-version.
- #
- # Look for 'satcmd' template for a working example.
- #
- SATURNIN_APP_VERSION=${SATURNIN_APP_VERSION:-}
-
- #
- # Path to user cache
- #
- SATURNIN_CACHE_HOME=${SATURNIN_CACHE_HOME:-}
-
- #
- # Path where saturnin__conf should look for files
- #
- # If filename does not contain slash, it is looked up in each (or all,
- # based on strategy--see saturnin__conf() doc) path in this list. The
- # list is colon-separated and non-dirs as well as empty strings are
- # silently ignored.
- #
- SATURNIN_CONF_PATH="${SATURNIN_CONF_PATH:-}"
-
- #
- # Expected config filename extension (for guessing from path head)
- #
- # If no filename to read is given, saturnin__conf() will guess filename
- # as the path head plus this suffix (e.g. `foo.ini` for `saturnin__conf
- # foo.bar.baz`)
- #
- SATURNIN_CONF_SUFFIX="${SATURNIN_CONF_SUFFIX:-.ini}"
-
- #
- # Directory where to look for subcommands
- #
- # Files here starting with $SATURNIN_LIBEXEC_PREFIX are considered
- # subcommands
- #
- SATURNIN_LIBEXEC="${SATURNIN_LIBEXEC:-}"
-
- #
- # Subcommand file prefix
- #
- # This is recommended to be set to meta-command name plus dash. For
- # example, if your meta-command is `mykit`, this should be set to
- # `mykit-`.
- #
- SATURNIN_LIBEXEC_PREFIX="${SATURNIN_LIBEXEC_PREFIX:-}"
-
- #
- # Meta-command help mode
- #
- # This controls what is displayed when user calls meta-command with --help
- # argument. Following formats are supported:
- #
- # +HELPFILE
- # =HELPFILE
- #
- # In both cases, HELPFILE must be absolute path to a file containing
- # human-readable description of the meta-command, which will be directly
- # presented to user.
- #
- # If the leading character is '=' (equal sign), the help text consists of,
- # and only of the HELPFILE. If the character is '+' (plus sign), the help
- # text is pre-pended with auto-generated usage message.
- #
- # If the value is empty, only the auto-generated usage message is printed.
- #
- SATURNIN_META_HELP="${SATURNIN_META_HELP:-}"
-
- #
- # Current running subcommand
- #
- # The value is set and exported into sub-command binary at moment of
- # saturnin__main() call.Z
- #
- SATURNIN_SUBCOMMAND="${SATURNIN_SUBCOMMAND:-}"
-
- saturnin__bug() {
- #
- # Warn about bug in your software
- #
- # Issue warning using warn() from pretty but also add application
- # version. This is useful when an assertion in your application fails
- # such that it is certain that there is a bug inside it.
- #
- # In such cases, it makes sense to print also version information to
- # help users with reporting.
- #
- local msg # message core
- for msg in "$@";
- do
- warn "bug: $msg"
- done
- warn "bug in $(basename "$0") version: $SATURNIN_APP_VERSION"
- }
-
- saturnin__conf() {
- #
- # inigrep smart loader
- #
- # Usage:
- # saturnin__conf [-j] [inigrep-query] [-- [file]..]
- #
- #
- # File arguments
- # ==============
- #
- # If omitted, *file* argument is inferred by taking part of kpath name
- # before first dot and appending value of `$SATURNIN_CONF_SUFFIX`,
- # (".ini" by default).
- #
- # Each *file* argument is then processed as follows:
- #
- # * `-` (single dash) is interpreted as reading from standard input.
- #
- # * If argument contains slash, it is expanded as a regular path
- # (relative or absolute).
- #
- # * Otherwise, it is taken as filename and searched for in directories
- # given in `$SATURNIN_CONF_PATH`. (This can yield more than one
- # path, which is equivalent as if all paths were provided.)
- #
- # Not all files expanded based on `$SATURNIN_CONF_PATH` are read by
- # default; reading is governed by "merge strategy": the default
- # strategy "first" reads only the first existing file.
- #
- # "join" strategy on the other hand, means that any files are simply
- # concatenated and prefixed with comment (visible only in raw mode)
- # containing path to the file.
- #
- # This means that if a section is queried that is present in both
- # files, it is effectively concatenated as well.
- #
- # Following calls are equivalent
- #
- # saturnin__conf foo.bar.baz
- # saturnin__conf foo.bar.baz foo.ini
- #
- # and both result in reading of key *baz* from section *foo.bar* in file
- # *foo.ini*, which is selected from *SATURNIN_CONF_PATH*. Should there
- # be more foo.ini's, the first is selected. Using `-j` switch
- #
- # saturnin__conf -j foo.bar.baz
- #
- # would cause all foo.ini's on *SATURNIN_CONF_PATH* be concatenated
- # instead.
- #
- local ig_mode # retrieval mode
- local ig_query # keypath or section name (when listing keys)
- local ig_limit # line limit
- local files=() # file specification
- local Strategy=first # merge strategy
- while true; do case $1:$2 in
- "":*) break ;;
- -j:*) Strategy=join; shift 1 ;;
- -1:*) ig_limit=$1; shift 1 ;;
- -e:*.*) ig_mode=$1; ig_query=$2; shift 2; break ;;
- -r:*.*) ig_mode=$1; ig_query=$2; shift 2; break ;;
- -K:*) ig_mode=$1; ig_query=$2; shift 2; break ;;
- -S:*) ig_mode=$1; ig_query=""; shift 1; break ;;
- -P:*) ig_mode=$1; ig_query=""; shift 1; break ;;
- .*:*) _saturnin__conf_usage -w "bad syntax: $*" ;;
- *.*:*) ig_mode=-e; ig_query=$1; shift 1; break ;;
- --help:*) _saturnin__conf_usage -e 0 ;;
- *) _saturnin__conf_usage -w "bad syntax: $*" ;;
- esac done
- test -n "$ig_mode" || _saturnin__conf_usage -w "could not determine inigrep mode"
- debug -v ig_limit ig_query ig_mode Strategy
- if test -n "$*";
- then
- files=("$@")
- elif test -n "$ig_query";
- then
- files=("${ig_query%%.*}$SATURNIN_CONF_SUFFIX")
- else
- _saturnin__conf_usage -w "dunno what to load"
- fi
- debug -v files
- #shellcheck disable=SC2086
- _saturnin__conf__load "${files[@]}" | inigrep $ig_limit $ig_mode "$ig_query"
- return "${PIPESTATUS[0]}"
- }
-
- saturnin__conf_find() {
- #
- # Find all existing instances of sub-path $1 on $SATURNIN_CONF_PATH
- #
- # Usage:
- #
- # saturnin__conf_find SUBPATH
- #
- # Go through all elements of $SATURNIN_CONF_PATH, looking for file or
- # directory, whose path is formed by joining SUBPATH to element of
- # $SATURNIN_CONF_PATH. Print each existing path, ignore rest.
- #
- # For example, with following setup:
- #
- # SATURNIN_CONF_PATH=foo:bar:baz
- # mkdir -p foo/one bar/one
- # mkdir -p bar/two/slashes
- #
- # call
- #
- # saturnin__conf_find one
- #
- # would print `foo/one` and `bar/one`, while
- #
- # saturnin__conf_find two/slashes
- #
- # would print `bar/two/slashes`.
- #
- # If at least one path was found, return zero. Otherwise, return one,
- # or more in case of error.
- #
- local file=$1 # sub-path to find
- local trydir # each item of $SATURNIN_CONF_PATH
- local trypath # each combined path
- debug -v SATURNIN_CONF_PATH
- echos "$SATURNIN_CONF_PATH" \
- | tr ':' '\n' \
- | while read -r trydir;
- do
- test -n "$trydir" || continue
- trypath="$trydir/$file"
- test -e "$trypath" || continue
- echos "$trypath"
- done \
- | grep .
- }
-
- saturnin__get() {
- #
- # Show Saturnin internal info by key $1 and exit
- #
- # Key $1 can be whole `--saturnin-get-stuff` argument or just the part
- # after `--saturnin-get-`.
- #
- # This is aimed to help debugging and testing the app (or Saturnin
- # itself) by showing packaging and deployment related info.
- #
- local key=${1#--saturnin-get-} # internal info key
- case "$key" in
- saturnin-conf-path) echo "$SATURNIN_CONF_PATH" ;;
- saturnin-version) echo "__MKIT_PROJ_VERSION__" ;;
- app-git-hash) echo "$SATURNIN_APP_GIT_HASH" ;;
- app-version) echo "$SATURNIN_APP_VERSION" ;;
- cache-home) echo "$SATURNIN_CACHE_HOME" ;;
- libexec) echo "$SATURNIN_LIBEXEC" ;;
- libexec-prefix) echo "$SATURNIN_LIBEXEC_PREFIX" ;;
- subcommands) saturnin__lssc ;;
- *) warn "unknown devel key: $key"
- exit "$EXIT_USAGE" ;;
- esac
- exit "$EXIT_OK"
- }
-
- saturnin__lssc() {
- #
- # List subcommands
- #
- find "$SATURNIN_LIBEXEC" \
- -mindepth 1 \
- -maxdepth 1 \
- -executable \
- -name "$SATURNIN_LIBEXEC_PREFIX*" \
- | sed -e "s|^.*/||; s|^$SATURNIN_LIBEXEC_PREFIX||" \
- | sort
- }
-
- saturnin__main() {
- #
- # Main meta-command entry function
- #
- # After setting all mandatory environment variables, call this from your
- # main meta-command script.
- #
- local subcommand # subcommand to execute (first non-option)
- test -n "$SATURNIN_CACHE_HOME" || die "SATURNIN_CACHE_HOME is not set"
- test -n "$SATURNIN_LIBEXEC" || die "SATURNIN_LIBEXEC is not set"
- test -n "$SATURNIN_LIBEXEC_PREFIX" || die "SATURNIN_LIBEXEC_PREFIX is not set"
- while true; do case $1 in
- -D|--full-debug) export PRETTY_DEBUG=true
- export PRETTY_DEBUG_EXCLUDE=""
- shift ;;
- -d|--debug) export PRETTY_DEBUG=true; shift ;;
- -v|--verbose) export PRETTY_VERBOSE=true; shift ;;
- -h|--help) saturnin__help; exit ;;
- --version) saturnin__version; exit ;;
- -V|--version-semver) saturnin__get app-version ;;
- --saturnin-get-*) saturnin__get "$1" ;;
- -*) saturnin__usage -w "unknown argument: $1" ;;
- --*) saturnin__usage -w "unknown argument: $1" ;;
- --) shift; break ;;
- "") saturnin__usage -w "too few arguments" ;;
- *) break; ;;
- esac done
- subcommand="$1"; shift
- PRETTY_DEBUG_EXCLUDE="" debug -v SATURNIN_APP_VERSION BASH_VERSION
- debug -v SATURNIN_CONF_PATH
- case "$subcommand" in
- conf) saturnin__conf "$@" ;;
- *) saturnin__runsc "$subcommand" "$@" ;;
- esac
- }
-
- saturnin__help() {
- #
- # Print meta-command help text
- #
- # See $SATURNIN_META_HELP for details.
- #
- local introline # introduction line
- introline=$(basename "$0")
- test -n "$SATURNIN_APP_TAGLINE" \
- && introline+=" - $SATURNIN_APP_TAGLINE"
- case "$SATURNIN_META_HELP" in
- "")
- echo "$introline"$'\n' >&2
- saturnin__usage -E -e 0
- ;;
- +/*)
- echo "$introline"$'\n' >&2
- saturnin__usage -E
- echo >&2
- _saturnin__cat_helpfile "${SATURNIN_META_HELP:1}"
- ;;
- =/*)
- _saturnin__cat_helpfile "${SATURNIN_META_HELP:1}"
- ;;
- *)
- echo "$introline"$'\n' >&2
- saturnin__usage -E
- saturnin__bug "malformed SATURNIN_META_HELP: $SATURNIN_META_HELP"
- return 3
- ;;
- esac
- }
-
- saturnin__conf_mkpath() {
- #
- # Compose new value for $SATURNIN_CONF_PATH from locations $@
- #
- # Usage:
- #
- # saturnin__conf_mkpath DIR [DIR..]
- #
- # Go through each DIR and print it, unless it ends with "/ini.d",
- # in which case list its subdirectories, sorted by C locale (this allows
- # for modular configuration).
- #
- # Non-existent or non-directory items are silently ignored.
- #
- local location # one location argument
- local path # one path listed
- for location in "$@";
- do
- test -d "$location" || continue
- case "$location" in
- */ini.d) # modular location--sort subfolders
- find -L "$location" -mindepth 1 -maxdepth 1 -type d \
- | LC_ALL=C sort
- ;;
- *)
- echo "$location"
- ;;
- esac
- done \
- | _saturnin__nl2colon
- }
-
- saturnin__runhook() {
- #
- # Run custom hook named $1 from respective configuration section
- #
- # Will load joined multi-line key "hook.$SATURNIN_SUBCOMMAND.$1" and
- # unless syntax check fails, execute it as Bash code (in separate
- # process).
- #
- local name="$1" # hook name
- local code # ... code
- test -n "$SATURNIN_SUBCOMMAND" || {
- warn "unknown subcommand, ignoring hook: $name"
- return 0
- }
- code="$(saturnin__conf -j "hook.$SATURNIN_SUBCOMMAND.$name")"
- debug -v SATURNIN_SUBCOMMAND code name
- bash -n <<<"$code" || {
- warn "syntax errors, ignoring hook: $name"
- return 0
- }
- bash <<<"$code"
- }
-
- saturnin__runsc() {
- #
- # Run subcommand $1 with arguments $2..
- #
- local subcommand="$1"; shift # subcommand to run
- local binpath # path to subcommand's binary
- binpath+="$SATURNIN_LIBEXEC/"
- binpath+="$SATURNIN_LIBEXEC_PREFIX$subcommand"
- debug -v binpath
- debug "\$*='$*'"
- test -x "$binpath" \
- || saturnin__usage "invalid sub-command: $subcommand"
- SATURNIN_SUBCOMMAND="$subcommand" "$binpath" "$@"
- }
-
- saturnin__usage() {
- #
- # Show usage message and exit
- #
- #shellcheck disable=SC2046
- mkusage "$@" \
- "[options] COMMAND [ARG...]" \
- -o \
- "-D, --full-debug turn on gory debugging" \
- "-V, --version show version and exit" \
- "-d, --debug turn on debugging" \
- "-h, --help show this help message and exit"\
- "-v, --verbose turn on verbosity" \
- -c \
- $(saturnin__lssc)
- }
-
- saturnin__version() {
- #
- # Print human-readable version info
- #
- # Basic version info is already stored in $SATURNIN_APP_VERSION,
- # this function prints more descriptive paragraph including Saturnin's
- # own version.
- #
- echo -n "$(basename "$0")"
- test -n "$SATURNIN_APP_TAGLINE" \
- && echo -n " ($SATURNIN_APP_TAGLINE)"
- echo -n " $SATURNIN_APP_VERSION"
- test -n "$SATURNIN_APP_CODENAME" \
- && echo -n " - $SATURNIN_APP_CODENAME"
- echo
- echo -n "Powered by Saturnin (__MKIT_PROJ_TAGLINE__)"
- echo -n " __MKIT_PROJ_VERSION__"
- echo -n " - __MKIT_PROJ_CODENAME__"
- echo
- return "$EXIT_OK"
- }
-
- saturnin__wraphook() {
- #
- # Wrap command $@ in hooks 'pre' and 'post'
- #
- # Run pre hook, then command $@, then post hook. Always exit with
- # status of the payload command, even if hooks fail. Ignore post-hook
- # if payload command failed.
- #
- local es=0 # exit status of this function
- saturnin__runhook pre
- "$@" || return $?
- es=$?
- saturnin__runhook post
- return $es
- }
-
-
- # # that what you see below this line #
- # INTERNAL # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
- # # use in your code to anger the divine #
-
- _saturnin__cat_helpfile() {
- #
- # Print helpfile $1
- #
- local helpfile=$1 # path to help file
- cat "$helpfile" >&2 && return 0
- saturnin__bug "cannot print help file: $helpfile"
- return 3
- }
-
- _saturnin__conf__merge() {
- #
- # Take paths and applying merge strategy, load file(s)
- #
- local path # every path
- local found=false # 'true' if we got any path
- while read -r path;
- do
- found=true
- case $Strategy in
- first)
- debug "winner: $path"
- cat "$path"
- cat >/dev/null # throw away rest of paths
- ;;
- join)
- echo "# file: ${path/$HOME/~}"
- cat "$path" 2>/dev/null
- ;;
- esac
- done
- $found
- }
-
- _saturnin__conf__load() {
- #
- # Print contents of files specified in $@
- #
- # Each argument means possible file candidate. If candidate contains
- # slash, it's treated as file path and is printed directly. If it's
- # single dash, standard input is copied.
- #
- # In all other cases, filename is searched in all elements of variable
- # SATURNIN_CONF_PATH; output then depends on chosen $Strategy: with
- # 'first' strategy, first existing file is printed, with 'join'
- # strategy, all existing files are printed.
- #
- local arg # each passed argument
- local es=0 # exit status of this function
- for arg in "$@";
- do
- case $arg in
- -|*/*) # stdin, or path (with slash)
- cat "$arg" || es=3
- ;;
- *) # name given, find all its incarnations
- saturnin__conf_find "$arg" \
- | _saturnin__conf__merge; es=$?
- ;;
- esac
- done
- return $es
- }
-
- _saturnin__conf_usage() {
- #
- # Show usage message, passing $@ to mkusage() and exit
- #
- PRETTY_USAGE="self=${0##*/} conf" \
- mkusage "$@" \
- "[options] [-e] SECTION.KEY [FNAME]" \
- "[options] -r SECTION.KEY [FNAME]" \
- "[options] -K SECTION [FNAME]" \
- "[options] -P FNAME" \
- "[options] -S FNAME" \
- -- \
- "Use inigrep to query config files." \
- -o \
- "-j join all files before applying query" \
- "-1 ensure single line is returned" \
- -c \
- "-e use normal mode (default)" \
- "-r use raw mode (preserves RHS whitespace and some comments)"\
- "-K list available keys in SECTION" \
- "-S list available sections in FNAME" \
- "-P list available keypaths (SECTION.KEY) in FNAME" \
- -- \
- "FNAME is filename, which is then searched on all paths specified"\
- "in SATURNIN_CONF_PATH and depending on -j parameter, first one" \
- "wins or all are joined. If FNAME contains slash, this search is"\
- "not done and FNAME is taken as path to file that is then" \
- "queried." \
- "" \
- "If FNAME is omitted, it is inferred from SECTION (e.g. 'foo.ini'"\
- "if 'foo.bar' was section name; note that section name itself may"\
- "contain dot)."
- }
-
- _saturnin__nl2colon() {
- #
- # Convert newline-based list of paths to colon:based:list
- #
- # Empty paths must not be included in the resulting list, so we need to
- # drop them and also get the colons right.
- #
- local idx=0 # current item index (zero-based)
- local path # each path on stdin
- while read -r path;
- do
- test -z "$path" && continue
- test $idx -gt 0 && echo -n ':'
- echo -n "$path"
- ((idx++))
- done
- }
-
- #shellfu module-version=__MKIT_PROJ_VERSION__
|