#!/bin/bash shellfu import exit # # Application debug mode # PRETTY_DEBUG=${PRETTY_DEBUG:-} # # Comma-separated list of module/function names to exclude from debuging # # For the sake of readability of your debug dumps, you can set this # variable to comma-separated list of module or function names that # you don't expect to get useful info from. # # If the caller has a qualified name (`modname__funcname()`, according # to Shellfu naming scheme it's possible to use just module name here # to mute all debug from that module (including internal functions). # Otherwise, full function name should be listed. # PRETTY_DEBUG_EXCLUDE=${PRETTY_DEBUG_EXCLUDE:-} # # Application verbosity mode # PRETTY_VERBOSE=${PRETTY_VERBOSE:-} # # Name of pretty-printer module # # Friendly name of module used for decorating output. For # example, if the value is NAME, a _pretty_NAME module must # exist and be importable. Otherwise pretty will fall back # to plain, which is also the default value. # PRETTY=${PRETTY:-plain} # # Usage display mode # # How to determine program name for purposes of usage messages. # # If empty, use basename of "$0". Set to 'subcommand' to remove first # dash (so that `path/to/foo-bar becomes `foo bar` which may make more # sense in meta-command scenrarios). Set to self=NAME to force NAME. # PRETTY_USAGE=${PRETTY_USAGE:-} __shellfu_pretty__init() { # # Import proper submodule # if shellfu try_import "_pretty_${PRETTY}"; then shellfu import "_pretty_${PRETTY}" return 0 else warn "falling back to _pretty_plain" PRETTY=plain shellfu import "_pretty_${PRETTY}" fi } ##--------------------## ## PRINTING FRONT-END ## ##--------------------## debug() { # # You already know what it does # # BTW, following are equivalent: # # debug "var1=$var1" "var2=$var2" "result=$result" # debug -v var1 var2 result # test "$PRETTY_DEBUG" == true || return 0 _pretty__echo "$@" } debug_pipe() { # # Debug the whole pipe. # while IFS= read -r line; do debug "|$1: '$line'" echos "$line" done } die() { # # A fatal error # _pretty__echo -t _pretty__echo "$@" exit_error } echos() { # # Safer version of echo able to echo "-n" # # Traditional echo is broken in that it does not # distinguish between string to print and its own switches # (-e, -E or -n), leading to unexpected behavior. # # This echo version circumvents this by using printf. # printf -- '%s\n' "$@" } mkusage() { # # Echo out usage patterns and (by default) `exit 2` # # mkusage [-w MSG] [-e STATUS] [-E] pattern [patern...] # # Each pattern is prefixed by "usage: " and a resolved # script/function name to produce traditional usage hint. # By default, will exit with status *EXIT_USAGE*, you # can use `-e STATUS` to specify other number. `-k` can be # used as shorthand for `-e 0` (e.g. if user asked for # the patterns) # # Use `-E` to prevent exiting; in that case, the status # that would be fed to `exit` will be used as exit status # of this function. # # Optionally, you can add -w MSG to add clarifying message # (presented as warning) to help user understand what is # wrong with arguments they have passed. Alternatively, # -m SUBJ, -M SUBJ, -u SUBJ, and -U SUBJ are shorthands for # error messages describing missing positional argument (-m), # missing value to a parametrized argument (-M), unknown # argument (-u) or unknown command (-U). Using these # shorthands you don't need to provide whole message, but # only subject in question; rest of the message is provided # by pretty.sh. # # Use "--" to delimit end of arguments processed by mkusage. # # Recommended usage is to define usage() in your script and # use this in its body. That way you only need to define # usage patterns once and skip to them from any place where # you detect incorrect usage. Optionally, this usage() # function can pass first argument as -w for clarification # messages. # local es=$EXIT_USAGE # our exit status local doexit=true # should we exit? local cmsg # clarification message while true; do case "$1" in -e) es="$2"; shift 2 || return 2 ;; -E) doexit=false; shift ;; -k) es=$EXIT_OK; shift ;; -w) cmsg="$2"; shift 2 || return 2 ;; -m) cmsg="no $2?"; shift 2 || return 2 ;; -M) cmsg="missing value for: $2"; shift 2 || return 2 ;; -u) cmsg="unknown argument: $2"; shift 2 || return 2 ;; -U) cmsg="unknown command: $2"; shift 2 || return 2 ;; --) shift; break ;; *) break ;; esac done _pretty__echo -u "$@"; if test -n "$cmsg"; then test -n "$*" && echo >&2 warn "bad usage: $cmsg" fi $doexit && exit "$es" return "$es" } mkhelp() { # # Echo out help text # # mkhelp [-e STATUS] [-E] arg... # # By default, will exit with status *EXIT_OK*, you # can use `-e STATUS` to specify other number. # # Use `-E` to prevent exiting; in that case, the status # that would be fed to `exit` will be used as exit status # of this function. # # Use "--" to delimit end of arguments processed by mkhelp # local es=$EXIT_OK local doexit=true while true; do case "$1" in -e) es="$2"; shift 2 || return 2 ;; -E) doexit=false; shift ;; --) shift; break ;; *) break ;; esac done _pretty__echo "$@" $doexit && exit "$es" return "$es" } think() { # # If verbose is on, think loud # # Use "-l" to split every parameter to separate line, (useful # or longer warnings) # test "$PRETTY_VERBOSE" == true || return 0 _pretty__echo "$@" } warn() { # # Warn them # # Use "-l" to split every parameter to separate line, (useful # or longer warnings) # _pretty__echo "$@" } ##----------## ## BACK-END ## ##----------## _pretty__cat() { # # `cat` but without starting a process # # Used to avoid spanning new process where stream handler is chosen # based on some logic # while IFS= read -r line; do echos "$line"; done } _pretty__get_caller() { # # Get first user function and negative index from stack # local fname local nidx="${#FUNCNAME[@]}" for fname in "${FUNCNAME[@]}"; do (( nidx-- )) _pretty__is_internal && continue _pretty__is_frontend && continue test "$fname" = "usage" && continue echos "$nidx $fname" return done } _pretty__get_frontend() { # # Get entry point function name from stack # local fname for fname in "${FUNCNAME[@]}"; do _pretty__is_internal && continue _pretty__is_frontend && echos "$fname" && return 0 echo "do not call _pretty_* directly: $fname" >&2 return "$EXIT_USAGE" done } _pretty__is_excluded() { # # True if $caller is excluded based on PRETTY_DEBUG_EXCLUDE # # Check PRETTY_DEBUG_EXCLUDE to see if $caller (using only module name # part, if possible) should be muted from debugging. # local listed # item listed in PRETTY_DEBUG_EXCLUDE local name # module part of caller's name local qualified # is caller "qualified" (ac. to shellfu scheme)? name="$caller" case "$name" in __*__*) qualified=true ;; __*) qualified=false ;; *__*) qualified=true ;; *) qualified=false ;; esac if $qualified; then # we'll use only the module part of the name name=${name#_} # drop one "internal" prefix name=${name#_} # drop yet another one name=${name%%__*} # drop funcname fi for listed in ${PRETTY_DEBUG_EXCLUDE//,/ }; do test "$name" = "$listed" && return 0 done return 1 } _pretty__is_frontend() { # # True if $fname is one of our "frontends" # case "$fname" in debug) return 0 ;; debug_pipe) return 0 ;; die) return 0 ;; mkhelp) return 0 ;; think) return 0 ;; mkusage) return 0 ;; warn) return 0 ;; esac return 1 } _pretty__is_internal() { # # True if $fname is our internal function # case "$fname" in _pretty__*) return 0 ;; *) return 1 ;; esac } _pretty__echo() { # # A smarter echo backend # # A smarter backend for debug, warn, think, die and # mkusage. # # -c cmd echo output of a command # -f file echo output of a file (- for stdin) # -l line [line...] echo each line separately # -t add stack trace to output # -u patt [patt...] convert each patt to usage pattern # -v var [var...] show contents of each var # local frontend # who (of pretty.sh) was called (=> prettyprinter choice) local caller # which user's function (or script) called it # ^ ^ eg. if user calls 'debug hello' from function 'foo', then # : :.. * frontend is 'debug' # :......... * and caller is 'foo'. local caller_nidx # negative stack index of caller local caller_is_main # true if caller was main script or main() in it local provider # which provider (_pretty__echo_*()) to use frontend="$(_pretty__get_frontend)" || exit_usage read -r caller_nidx caller <<<"$(_pretty__get_caller)" test "$frontend" = debug && _pretty__is_excluded "$caller" && return 0 #shellcheck disable=SC2034 case $caller_nidx:$caller in 0:*) caller_is_main=true; caller="${0##*/}" ;; 1:main) caller_is_main=true; caller="${0##*/}" ;; *:usage) frontend=mkusage ;; *) caller_is_main=false ;; esac while true; do case $1 in -c|--cmd) provider=cmd; shift; break ;; -f|--files) provider=files; shift; break ;; -l|--lines) provider=lines; shift; break ;; -t|--trace) provider=trace; shift; break ;; -u|--usage) provider=usage; shift; break ;; -v|--vars) provider=vars; shift; break ;; *) provider=args; break ;; esac done _pretty__echo_$provider "$@" \ | _pretty__$frontend >&2 } _pretty__echo_args() { # # The simplest (but safe) printing of args # echos "$*" } _pretty__echo_cmd() { # # Print command line, launch it and report exit status # local es echo "-- begin command $* --" "$@"; es=$? echo "-- end command ($es) $* --" } _pretty__echo_files() { # # Print names and contents of existing files # local fp for fp in "$@"; do if test "$fp" = "-"; then echo "-- begin pipe --" cat echo "-- end pipe --" elif test -s "$fp" || test "$fp" = "/dev/stdin"; then echo "-- begin file $fp --" cat "$fp" echo "-- end file $fp --" fi done } _pretty__echo_lines() { # # Echo each argument as a separate line # local l; for l in "$@"; do _pretty__echo_args "$l"; done } _pretty__echo_trace() { # # Print "decorated" call trace (only in debug mode) # test "$PRETTY_DEBUG" == true || return 0 local depth echo "== trace ==" for depth in $(seq 0 ${#FUNCNAME}); do caller "$depth" || break done \ | tail -n +3 \ | sed -e ' s/^\([^ ]\+\) \([^ ]\+\) \(.*\)/\3:\1:\2()/ # ^line^, ^func^, ^file^ 1 s/^/ -> /g 2,$ s/^/ / ' \ | tac } _pretty__echo_help() { local oldverbose="$PRETTY_VERBOSE" think -l "$@" PRETTY_VERBOSE=$oldverbose } _pretty__echo_usage() { # # Compose conventional usage guide # # The default mode treats each argument as usage pattern # (see below for details). Additional formatting can be # conveniently achieved by switching to other modes, which # automatically brings necessary headers and indentations # where needed. # # * option mode (`-o`) prints "options:" header and # indents next arguments, # # * command mode (`-c`) prints "commands:" header and # indents next arguments, # # * indent mode (`-i`) just indents next arguments, # # * plain mode (`--`) prints empty line (new paragraph) # and turns indentations off. # # * usage mode (`-u`, active by default), prints # "usage:" header, indents next arguments and prefixes # them with name of the script. See also $PRETTY_USAGE. # # A special case of usage mode is when only single # argument is passed to this function; then instead # printing "usage:" header on separate string, it is # joined with the argument to single line. # # In order to help avoid (rare) conflict between mkusage() # switches and your usage patterns, the very first argument, # and each argument that comes right after one of these # switches are guarranteed not to be interpreted as switch. # local self # the script name local mode=usage # mode local esc=1 # escape (take next argument as literal) local arg # argument to iterate case "$PRETTY_USAGE" in self=*) self=${PRETTY_USAGE#self=} ;; subcommand) self="${0##*/}"; self="${self/-/ }" ;; *) self="$caller" ;; esac case $# in 0) return 0 ;; 1) echo "usage: $self $1"; return 0 ;; esac echo usage: for arg in "$@"; do case $esc:$arg in 0:--) shift; mode=plain; esc=1; echo ;; 0:-c) shift; mode=indent; esc=1; echo; echo commands: ;; 0:-i) shift; mode=indent; esc=1 ;; 0:-o) shift; mode=indent; esc=1; echo; echo options: ;; 0:-u) shift; mode=usage; esc=1 ;; *) esc=0 case $mode in usage) echo " $self $arg" ;; indent) echo " $arg" ;; plain) echos "$arg" ;; esac ;; esac done } _pretty__echo_vars() { # # Report value of each named variable # local varname local declare_str for varname in "$@"; do if ! _pretty__is_word "$varname"; then warn "unsafe value skipped: $varname"; continue fi if declare_str=$(declare -p "$varname" 2>/dev/null); then _pretty__echo "${declare_str#declare ?? }" else _pretty__echo "$varname #Unset" fi done } _pretty__is_word() { # # Check if $1 contains only alphanumeric chars or _ # local tainted="$1" local clean clean=$(tr -c -d '_[:alnum:]' <<< "$tainted") test "$tainted" = "$clean" }