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

pretty.sh 13KB

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