shell dot on steroids https://pagure.io/shellfu

pretty.sh 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. #!/bin/bash
  2. shellfu import exit
  3. #
  4. # Application debug mode
  5. #
  6. PRETTY_DEBUG=${PRETTY_DEBUG:-}
  7. #
  8. # Comma-separated list of module/function names to exclude from debuging
  9. #
  10. # For the sake of readability of your debug dumps, you can set this
  11. # variable to comma-separated list of module or function names that
  12. # you don't expect to get useful info from.
  13. #
  14. # If the caller has a qualified name (`modname__funcname()`, according
  15. # to Shellfu naming scheme it's possible to use just module name here
  16. # to mute all debug from that module (including internal functions).
  17. # Otherwise, full function name should be listed.
  18. #
  19. PRETTY_DEBUG_EXCLUDE=${PRETTY_DEBUG_EXCLUDE:-}
  20. #
  21. # Application verbosity mode
  22. #
  23. PRETTY_VERBOSE=${PRETTY_VERBOSE:-}
  24. #
  25. # Name of pretty-printer module
  26. #
  27. # Friendly name of module used for decorating output. For
  28. # example, if the value is NAME, a _pretty_NAME module must
  29. # exist and be importable. Otherwise pretty will fall back
  30. # to plain, which is also the default value.
  31. #
  32. PRETTY=${PRETTY:-plain}
  33. #
  34. # Usage display mode
  35. #
  36. # How to determine program name for purposes of usage messages.
  37. #
  38. # If empty, use basename of "$0". Set to 'subcommand' to remove first
  39. # dash (so that `path/to/foo-bar becomes `foo bar` which may make more
  40. # sense in meta-command scenrarios). Set to self=NAME to force NAME.
  41. #
  42. PRETTY_USAGE=${PRETTY_USAGE:-}
  43. __shellfu_pretty__init() {
  44. #
  45. # Import proper submodule
  46. #
  47. if shellfu try_import "_pretty_${PRETTY}"; then
  48. shellfu import "_pretty_${PRETTY}"
  49. return 0
  50. else
  51. warn "falling back to _pretty_plain"
  52. PRETTY=plain
  53. shellfu import "_pretty_${PRETTY}"
  54. fi
  55. }
  56. ##--------------------##
  57. ## PRINTING FRONT-END ##
  58. ##--------------------##
  59. debug() {
  60. #
  61. # You already know what it does
  62. #
  63. # BTW, following are equivalent:
  64. #
  65. # debug "var1=$var1" "var2=$var2" "result=$result"
  66. # debug -v var1 var2 result
  67. #
  68. test "$PRETTY_DEBUG" == true || return 0
  69. _pretty__echo "$@"
  70. }
  71. debug_pipe() {
  72. #
  73. # Debug the whole pipe.
  74. #
  75. while IFS= read -r line; do
  76. debug "|$1: '$line'"
  77. echos "$line"
  78. done
  79. }
  80. die() {
  81. #
  82. # A fatal error
  83. #
  84. _pretty__echo -t
  85. _pretty__echo "$@"
  86. exit_error
  87. }
  88. echos() {
  89. #
  90. # Safer version of echo able to echo "-n"
  91. #
  92. # Traditional echo is broken in that it does not
  93. # distinguish between string to print and its own switches
  94. # (-e, -E or -n), leading to unexpected behavior.
  95. #
  96. # This echo version circumvents this by using printf.
  97. #
  98. printf -- '%s\n' "$@"
  99. }
  100. mkusage() {
  101. #
  102. # Echo out usage patterns and (by default) `exit 2`
  103. #
  104. # mkusage [-w MSG] [-e STATUS] [-E] pattern [patern...]
  105. #
  106. # Each pattern is prefixed by "usage: " and a resolved
  107. # script/function name to produce traditional usage hint.
  108. # By default, will exit with status *EXIT_USAGE*, you
  109. # can use `-e STATUS` to specify other number. `-k` can be
  110. # used as shorthand for `-e 0` (e.g. if user asked for
  111. # the patterns)
  112. #
  113. # Use `-E` to prevent exiting; in that case, the status
  114. # that would be fed to `exit` will be used as exit status
  115. # of this function.
  116. #
  117. # Optionally, you can add -w MSG to add clarifying message
  118. # (presented as warning) to help user understand what is
  119. # wrong with arguments they have passed. Alternatively,
  120. # -m SUBJ, -M SUBJ, -u SUBJ, and -U SUBJ are shorthands for
  121. # error messages describing missing positional argument (-m),
  122. # missing value to a parametrized argument (-M), unknown
  123. # argument (-u) or unknown command (-U). Using these
  124. # shorthands you don't need to provide whole message, but
  125. # only subject in question; rest of the message is provided
  126. # by pretty.sh.
  127. #
  128. # Use "--" to delimit end of arguments processed by mkusage.
  129. #
  130. # Recommended usage is to define usage() in your script and
  131. # use this in its body. That way you only need to define
  132. # usage patterns once and skip to them from any place where
  133. # you detect incorrect usage. Optionally, this usage()
  134. # function can pass first argument as -w for clarification
  135. # messages.
  136. #
  137. local es=$EXIT_USAGE # our exit status
  138. local doexit=true # should we exit?
  139. local cmsg # clarification message
  140. while true; do case "$1" in
  141. -e) es="$2"; shift 2 || return 2 ;;
  142. -E) doexit=false; shift ;;
  143. -k) es=$EXIT_OK; shift ;;
  144. -w) cmsg="$2"; shift 2 || return 2 ;;
  145. -m) cmsg="no $2?"; shift 2 || return 2 ;;
  146. -M) cmsg="missing value for: $2"; shift 2 || return 2 ;;
  147. -u) cmsg="unknown argument: $2"; shift 2 || return 2 ;;
  148. -U) cmsg="unknown command: $2"; shift 2 || return 2 ;;
  149. --) shift; break ;;
  150. *) break ;;
  151. esac done
  152. _pretty__echo -u "$@";
  153. if test -n "$cmsg"; then
  154. test -n "$*" && echo >&2
  155. warn "bad usage: $cmsg"
  156. fi
  157. $doexit && exit "$es"
  158. return "$es"
  159. }
  160. mkhelp() {
  161. #
  162. # Echo out help text
  163. #
  164. # mkhelp [-e STATUS] [-E] arg...
  165. #
  166. # By default, will exit with status *EXIT_OK*, you
  167. # can use `-e STATUS` to specify other number.
  168. #
  169. # Use `-E` to prevent exiting; in that case, the status
  170. # that would be fed to `exit` will be used as exit status
  171. # of this function.
  172. #
  173. # Use "--" to delimit end of arguments processed by mkhelp
  174. #
  175. local es=$EXIT_OK
  176. local doexit=true
  177. while true; do case "$1" in
  178. -e) es="$2"; shift 2 || return 2 ;;
  179. -E) doexit=false; shift ;;
  180. --) shift; break ;;
  181. *) break ;;
  182. esac done
  183. _pretty__echo "$@"
  184. $doexit && exit "$es"
  185. return "$es"
  186. }
  187. think() {
  188. #
  189. # If verbose is on, think loud
  190. #
  191. # Use "-l" to split every parameter to separate line, (useful
  192. # or longer warnings)
  193. #
  194. test "$PRETTY_VERBOSE" == true || return 0
  195. _pretty__echo "$@"
  196. }
  197. warn() {
  198. #
  199. # Warn them
  200. #
  201. # Use "-l" to split every parameter to separate line, (useful
  202. # or longer warnings)
  203. #
  204. _pretty__echo "$@"
  205. }
  206. ##----------##
  207. ## BACK-END ##
  208. ##----------##
  209. _pretty__cat() {
  210. #
  211. # `cat` but without starting a process
  212. #
  213. # Used to avoid spanning new process where stream handler is chosen
  214. # based on some logic
  215. #
  216. while IFS= read -r line;
  217. do echos "$line"; done
  218. }
  219. _pretty__get_caller() {
  220. #
  221. # Get first user function and negative index from stack
  222. #
  223. local fname
  224. local nidx="${#FUNCNAME[@]}"
  225. for fname in "${FUNCNAME[@]}"; do
  226. (( nidx-- ))
  227. _pretty__is_internal && continue
  228. _pretty__is_frontend && continue
  229. test "$fname" = "usage" && continue
  230. echos "$nidx $fname"
  231. return
  232. done
  233. }
  234. _pretty__get_frontend() {
  235. #
  236. # Get entry point function name from stack
  237. #
  238. local fname
  239. for fname in "${FUNCNAME[@]}"; do
  240. _pretty__is_internal && continue
  241. _pretty__is_frontend && echos "$fname" && return 0
  242. echo "do not call _pretty_* directly: $fname" >&2
  243. return "$EXIT_USAGE"
  244. done
  245. }
  246. _pretty__is_excluded() {
  247. #
  248. # True if $caller is excluded based on PRETTY_DEBUG_EXCLUDE
  249. #
  250. # Check PRETTY_DEBUG_EXCLUDE to see if $caller (using only module name
  251. # part, if possible) should be muted from debugging.
  252. #
  253. local listed # item listed in PRETTY_DEBUG_EXCLUDE
  254. local name # module part of caller's name
  255. local qualified # is caller "qualified" (ac. to shellfu scheme)?
  256. name="$caller"
  257. case "$name" in
  258. __*__*) qualified=true ;;
  259. __*) qualified=false ;;
  260. *__*) qualified=true ;;
  261. *) qualified=false ;;
  262. esac
  263. if $qualified; then
  264. # we'll use only the module part of the name
  265. name=${name#_} # drop one "internal" prefix
  266. name=${name#_} # drop yet another one
  267. name=${name%%__*} # drop funcname
  268. fi
  269. for listed in ${PRETTY_DEBUG_EXCLUDE//,/ }; do
  270. test "$name" = "$listed" && return 0
  271. done
  272. return 1
  273. }
  274. _pretty__is_frontend() {
  275. #
  276. # True if $fname is one of our "frontends"
  277. #
  278. case "$fname" in
  279. debug) return 0 ;;
  280. debug_pipe) return 0 ;;
  281. die) return 0 ;;
  282. mkhelp) return 0 ;;
  283. think) return 0 ;;
  284. mkusage) return 0 ;;
  285. warn) return 0 ;;
  286. esac
  287. return 1
  288. }
  289. _pretty__is_internal() {
  290. #
  291. # True if $fname is our internal function
  292. #
  293. case "$fname" in
  294. _pretty__*) return 0 ;;
  295. *) return 1 ;;
  296. esac
  297. }
  298. _pretty__echo() {
  299. #
  300. # A smarter echo backend
  301. #
  302. # A smarter backend for debug, warn, think, die and
  303. # mkusage.
  304. #
  305. # -c cmd echo output of a command
  306. # -f file echo output of a file (- for stdin)
  307. # -l line [line...] echo each line separately
  308. # -t add stack trace to output
  309. # -u patt [patt...] convert each patt to usage pattern
  310. # -v var [var...] show contents of each var
  311. #
  312. local frontend # who (of pretty.sh) was called (=> prettyprinter choice)
  313. local caller # which user's function (or script) called it
  314. # ^ ^ eg. if user calls 'debug hello' from function 'foo', then
  315. # : :.. * frontend is 'debug'
  316. # :......... * and caller is 'foo'.
  317. local caller_nidx # negative stack index of caller
  318. local caller_is_main # true if caller was main script or main() in it
  319. local provider # which provider (_pretty__echo_*()) to use
  320. frontend="$(_pretty__get_frontend)" || exit_usage
  321. read -r caller_nidx caller <<<"$(_pretty__get_caller)"
  322. test "$frontend" = debug && _pretty__is_excluded "$caller" && return 0
  323. #shellcheck disable=SC2034
  324. case $caller_nidx:$caller in
  325. 0:*) caller_is_main=true; caller="${0##*/}" ;;
  326. 1:main) caller_is_main=true; caller="${0##*/}" ;;
  327. *:usage) frontend=mkusage ;;
  328. *) caller_is_main=false ;;
  329. esac
  330. while true; do case $1 in
  331. -c|--cmd) provider=cmd; shift; break ;;
  332. -f|--files) provider=files; shift; break ;;
  333. -l|--lines) provider=lines; shift; break ;;
  334. -t|--trace) provider=trace; shift; break ;;
  335. -u|--usage) provider=usage; shift; break ;;
  336. -v|--vars) provider=vars; shift; break ;;
  337. *) provider=args; break ;;
  338. esac done
  339. _pretty__echo_$provider "$@" \
  340. | _pretty__$frontend >&2
  341. }
  342. _pretty__echo_args() {
  343. #
  344. # The simplest (but safe) printing of args
  345. #
  346. echos "$*"
  347. }
  348. _pretty__echo_cmd() {
  349. #
  350. # Print command line, launch it and report exit status
  351. #
  352. local es
  353. echo "-- begin command $* --"
  354. "$@"; es=$?
  355. echo "-- end command ($es) $* --"
  356. }
  357. _pretty__echo_files() {
  358. #
  359. # Print names and contents of existing files
  360. #
  361. local fp
  362. for fp in "$@"; do
  363. if test "$fp" = "-"; then
  364. echo "-- begin pipe --"
  365. cat
  366. echo "-- end pipe --"
  367. elif test -s "$fp" || test "$fp" = "/dev/stdin"; then
  368. echo "-- begin file $fp --"
  369. cat "$fp"
  370. echo "-- end file $fp --"
  371. fi
  372. done
  373. }
  374. _pretty__echo_lines() {
  375. #
  376. # Echo each argument as a separate line
  377. #
  378. local l;
  379. for l in "$@"; do _pretty__echo_args "$l"; done
  380. }
  381. _pretty__echo_trace() {
  382. #
  383. # Print "decorated" call trace (only in debug mode)
  384. #
  385. test "$PRETTY_DEBUG" == true || return 0
  386. local depth
  387. echo "== trace =="
  388. for depth in $(seq 0 ${#FUNCNAME}); do
  389. caller "$depth" || break
  390. done \
  391. | tail -n +3 \
  392. | sed -e '
  393. s/^\([^ ]\+\) \([^ ]\+\) \(.*\)/\3:\1:\2()/
  394. # ^line^, ^func^, ^file^
  395. 1 s/^/ -> /g
  396. 2,$ s/^/ /
  397. ' \
  398. | tac
  399. }
  400. _pretty__echo_help() {
  401. local oldverbose="$PRETTY_VERBOSE"
  402. think -l "$@"
  403. PRETTY_VERBOSE=$oldverbose
  404. }
  405. _pretty__echo_usage() {
  406. #
  407. # Compose conventional usage guide
  408. #
  409. # The default mode treats each argument as usage pattern
  410. # (see below for details). Additional formatting can be
  411. # conveniently achieved by switching to other modes, which
  412. # automatically brings necessary headers and indentations
  413. # where needed.
  414. #
  415. # * option mode (`-o`) prints "options:" header and
  416. # indents next arguments,
  417. #
  418. # * command mode (`-c`) prints "commands:" header and
  419. # indents next arguments,
  420. #
  421. # * indent mode (`-i`) just indents next arguments,
  422. #
  423. # * plain mode (`--`) prints empty line (new paragraph)
  424. # and turns indentations off.
  425. #
  426. # * usage mode (`-u`, active by default), prints
  427. # "usage:" header, indents next arguments and prefixes
  428. # them with name of the script. See also $PRETTY_USAGE.
  429. #
  430. # A special case of usage mode is when only single
  431. # argument is passed to this function; then instead
  432. # printing "usage:" header on separate string, it is
  433. # joined with the argument to single line.
  434. #
  435. # In order to help avoid (rare) conflict between mkusage()
  436. # switches and your usage patterns, the very first argument,
  437. # and each argument that comes right after one of these
  438. # switches are guarranteed not to be interpreted as switch.
  439. #
  440. local self # the script name
  441. local mode=usage # mode
  442. local esc=1 # escape (take next argument as literal)
  443. local arg # argument to iterate
  444. case "$PRETTY_USAGE" in
  445. self=*) self=${PRETTY_USAGE#self=} ;;
  446. subcommand) self="${0##*/}"; self="${self/-/ }" ;;
  447. *) self="$caller" ;;
  448. esac
  449. case $# in
  450. 0) return 0 ;;
  451. 1) echo "usage: $self $1"; return 0 ;;
  452. esac
  453. echo usage:
  454. for arg in "$@"; do
  455. case $esc:$arg in
  456. 0:--) shift; mode=plain; esc=1; echo ;;
  457. 0:-c) shift; mode=indent; esc=1; echo; echo commands: ;;
  458. 0:-i) shift; mode=indent; esc=1 ;;
  459. 0:-o) shift; mode=indent; esc=1; echo; echo options: ;;
  460. 0:-u) shift; mode=usage; esc=1 ;;
  461. *) esc=0
  462. case $mode in
  463. usage) echo " $self $arg" ;;
  464. indent) echo " $arg" ;;
  465. plain) echos "$arg" ;;
  466. esac
  467. ;;
  468. esac
  469. done
  470. }
  471. _pretty__echo_vars() {
  472. #
  473. # Report value of each named variable
  474. #
  475. local varname
  476. local declare_str
  477. for varname in "$@"; do
  478. if ! _pretty__is_word "$varname"; then
  479. warn "unsafe value skipped: $varname";
  480. continue
  481. fi
  482. if declare_str=$(declare -p "$varname" 2>/dev/null); then
  483. _pretty__echo "${declare_str#declare ?? }"
  484. else
  485. _pretty__echo "$varname #Unset"
  486. fi
  487. done
  488. }
  489. _pretty__is_word() {
  490. #
  491. # Check if $1 contains only alphanumeric chars or _
  492. #
  493. local tainted="$1"
  494. local clean
  495. clean=$(tr -c -d '_[:alnum:]' <<< "$tainted")
  496. test "$tainted" = "$clean"
  497. }