#!/bin/bash shellfu import exit shellfu import inigrep shellfu import pretty # # 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}" 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 # STDIN. # # * 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 ;; .*:*) warn "bad syntax: $*"; _saturnin__conf_usage ;; *.*:*) ig_mode=-e; ig_query=$1; shift 1; break ;; --help:*) _saturnin__conf_usage -e 0 ;; *) warn "bad syntax: $*"; _saturnin__conf_usage ;; esac done test -n "$ig_mode" || { warn "could not determine inigrep mode"; _saturnin__conf_usage; } 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 warn "dunno what to load" _saturnin__conf_usage fi debug -v files #shellcheck disable=SC2086 _saturnin__conf__load "${files[@]}" | inigrep $ig_limit $ig_mode "$ig_query" return "${PIPESTATUS[0]}" } 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-} case "$key" in shellfu-path) echo "$SHELLFU_PATH" ;; saturnin-conf-path) echo "$SATURNIN_CONF_PATH" ;; 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" ;; *) warn "unknown devel key: $key" exit "$EXIT_USAGE" ;; esac exit "$EXIT_OK" } saturnin__lssc() { # # List subcommands # echo conf find "$SATURNIN_LIBEXEC" \ -mindepth 1 \ -maxdepth 1 \ -executable \ | sed -e "s|^.*/||; s|^$SATURNIN_LIBEXEC_PREFIX||" \ | sort } saturnin__main() { local subcommand while true; do case $1 in -d|--debug) export PRETTY_DEBUG=true; shift ;; -v|--verbose) export PRETTY_VERBOSE=true; shift ;; -h|--help) saturnin__usage -e 0; exit ;; --version) saturnin__version; exit ;; --version-semver) saturnin__get app-version ;; --saturnin-get-*) saturnin__get "$1" ;; -*) saturnin__usage; ;; --*) saturnin__usage; ;; --) shift; break ;; "") saturnin__usage; ;; *) break; ;; esac done subcommand="$1"; shift debug -v SHELLFU_PATH SATURNIN_LIBEXEC SATURNIN_CONF_PATH case "$subcommand" in conf) saturnin__conf "$@" ;; _ls_sc) saturnin__lssc ;; _lsfun) shellfu-get lsfun ;; _lsmod) shellfu-get lsmod ;; *) saturnin__runsc "$subcommand" "$@" ;; esac } saturnin__conf_mkpath() { # # Assemble SATURNIN_CONF_PATH from locations $@ # # For each location, print colon-delimited list of # directories. If location ends with "/ini.d", list of # subfolders, sorted by C locale is printed--this allows # for modular configuration. Otherwise the location # is printed. Non-existent or non-directory locations # 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 # local hname="$1" local hook_code test -n "$SATURNIN_SUBCOMMAND" || { warn "unknown subcommand, ignoring hook: $hname" return 0 } hook_code="$(saturnin__conf -j "hook.$SATURNIN_SUBCOMMAND.$hname")" debug -v SATURNIN_SUBCOMMAND hook_code hname bash -n <<<"$hook_code" || { warn "syntax errors, ignoring hook: $hname" return 0 } eval "$hook_code" } saturnin__runsc() { # # Run subcommand $SATURNIN_SUBCOMMAND # local subcommand="$1"; shift local binpath # path to subcommand's binary binpath+="$SATURNIN_LIBEXEC/" binpath+="$SATURNIN_LIBEXEC_PREFIX$subcommand" debug -v binpath debug "\$*='$*'" test -x "$binpath" || { warn "invalid sub-command: $subcommand" saturnin__usage } SATURNIN_SUBCOMMAND="$subcommand" "$binpath" "$@" } saturnin__usage() { # # Show usage message and exit # #shellcheck disable=SC2046 mkusage "$@" \ "[options] COMMAND [ARG...]" \ -o \ "-d, --debug turn on debugging" \ "-h, --help show this help message and exit"\ "-v, --verbose turn on verbosity" \ "--version show version and exit" \ -c \ $(saturnin__lssc) } saturnin__version() { # # Print version info # local tagline=${SATURNIN_APP_TAGLINE:-Some app with default tagline} local maybe_codename="" test -n "$SATURNIN_APP_CODENAME" && maybe_codename=" - $SATURNIN_APP_CODENAME" echo "$(basename "$0") ($tagline) $SATURNIN_APP_VERSION$maybe_codename" return "$EXIT_OK" } saturnin__wraphook() { # # Wrap command "$@" in hooks # # Run pre hook, then "$@", then post hook. Always exit # with status of "$@", even if hooks fail. Ignore # post-hook if "$@" failed. # local es=0 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__conf__merge() { # # Take paths and applying merge strategy, load file(s) # local path local found=false while read -r path; do test -f "$path" || continue 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 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 trydir trypath es es=0 for arg in "$@"; do case $arg in -|*/*) # stdin, or path (with slash) cat "$arg" || es=3 ;; *) # name given, find all its incarnations debug -v SATURNIN_CONF_PATH echos "$SATURNIN_CONF_PATH" \ | tr ':' '\n' \ | while read -r trydir; do test -n "$trydir" || continue trypath="$trydir/$arg" echos "$trypath" done \ | _saturnin__conf__merge; es=$? ;; esac done return $es } _saturnin__conf_usage() { # # Show usage message 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 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 while read -r path; do test -z "$path" && continue test $idx -gt 0 && echo -n ':' echo -n "$path" ((idx++)) done }