saturnin.sh.skel 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. #!/bin/bash
  2. shellfu import exit
  3. shellfu import inigrep
  4. shellfu import pretty
  5. #
  6. # Git commit hash of application source tree
  7. #
  8. # This is supposed to be set by your build scripts when building your
  9. # application. The string is returned by calling your meta-command with
  10. # option --saturnin-app-git-hash.
  11. #
  12. # Look for 'satcmd' template for a working example.
  13. #
  14. SATURNIN_APP_GIT_HASH=${SATURNIN_APP_GIT_HASH:-}
  15. #
  16. # Your application version
  17. #
  18. # This is supposed to be set by your build scripts when building your
  19. # application. The string is returned by calling your meta-command with
  20. # options --version, --version-semver or --saturnin-app-version.
  21. #
  22. # Look for 'satcmd' template for a working example.
  23. #
  24. SATURNIN_APP_VERSION=${SATURNIN_APP_VERSION:-}
  25. #
  26. # Path to user cache
  27. #
  28. SATURNIN_CACHE_HOME=${SATURNIN_CACHE_HOME:-}
  29. #
  30. # Path where saturnin__conf should look for files
  31. #
  32. # If filename does not contain slash, it is looked up in each (or all,
  33. # based on strategy--see saturnin__conf() doc) path in this list. The
  34. # list is colon-separated and non-dirs as well as empty strings are
  35. # silently ignored.
  36. #
  37. SATURNIN_CONF_PATH="${SATURNIN_CONF_PATH:-}"
  38. #
  39. # Expected config filename extension (for guessing from path head)
  40. #
  41. # If no filename to read is given, saturnin__conf() will guess filename
  42. # as the path head plus this suffix (e.g. `foo.ini` for `saturnin__conf
  43. # foo.bar.baz`)
  44. #
  45. SATURNIN_CONF_SUFFIX="${SATURNIN_CONF_SUFFIX:-.ini}"
  46. #
  47. # Directory where to look for subcommands
  48. #
  49. # Files here starting with $SATURNIN_LIBEXEC_PREFIX are considered
  50. # subcommands
  51. #
  52. SATURNIN_LIBEXEC="${SATURNIN_LIBEXEC:-}"
  53. #
  54. # Subcommand file prefix
  55. #
  56. # This is recommended to be set to meta-command name plus dash. For
  57. # example, if your meta-command is `mykit`, this should be set to
  58. # `mykit-`.
  59. #
  60. SATURNIN_LIBEXEC_PREFIX="${SATURNIN_LIBEXEC_PREFIX:-}"
  61. #
  62. # Meta-command help mode
  63. #
  64. # This controls what is displayed when user calls meta-command with --help
  65. # argument. Following formats are supported:
  66. #
  67. # +HELPFILE
  68. # =HELPFILE
  69. #
  70. # In both cases, HELPFILE must be absolute path to a file containing
  71. # human-readable description of the meta-command, which will be directly
  72. # presented to user.
  73. #
  74. # If the leading character is '=' (equal sign), the help text consists of,
  75. # and only of the HELPFILE. If the character is '+' (plus sign), the help
  76. # text is pre-pended with auto-generated usage message.
  77. #
  78. # If the value is empty, only the auto-generated usage message is printed.
  79. #
  80. SATURNIN_META_HELP="${SATURNIN_META_HELP:-}"
  81. saturnin__bug() {
  82. #
  83. # Warn about bug in your software
  84. #
  85. # Issue warning using warn() from pretty but also add application
  86. # version. This is useful when an assertion in your application fails
  87. # such that it is certain that there is a bug inside it.
  88. #
  89. # In such cases, it makes sense to print also version information to
  90. # help users with reporting.
  91. #
  92. local msg # message core
  93. for msg in "$@";
  94. do
  95. warn "bug: $msg"
  96. done
  97. warn "bug in $(basename "$0") version: $SATURNIN_APP_VERSION"
  98. }
  99. saturnin__conf() {
  100. #
  101. # inigrep smart loader
  102. #
  103. # Usage:
  104. # saturnin__conf [-j] [inigrep-query] [-- [file]..]
  105. #
  106. #
  107. # File arguments
  108. # ==============
  109. #
  110. # If omitted, *file* argument is inferred by taking part of kpath name
  111. # before first dot and appending value of `$SATURNIN_CONF_SUFFIX`,
  112. # (".ini" by default).
  113. #
  114. # Each *file* argument is then processed as follows:
  115. #
  116. # * `-` (single dash) is interpreted as reading from standard input.
  117. #
  118. # * If argument contains slash, it is expanded as a regular path
  119. # (relative or absolute).
  120. #
  121. # * Otherwise, it is taken as filename and searched for in directories
  122. # given in `$SATURNIN_CONF_PATH`. (This can yield more than one
  123. # path, which is equivalent as if all paths were provided.)
  124. #
  125. # Not all files expanded based on `$SATURNIN_CONF_PATH` are read by
  126. # default; reading is governed by "merge strategy": the default
  127. # strategy "first" reads only the first existing file.
  128. #
  129. # "join" strategy on the other hand, means that any files are simply
  130. # concatenated and prefixed with comment (visible only in raw mode)
  131. # containing path to the file.
  132. #
  133. # This means that if a section is queried that is present in both
  134. # files, it is effectively concatenated as well.
  135. #
  136. # Following calls are equivalent
  137. #
  138. # saturnin__conf foo.bar.baz
  139. # saturnin__conf foo.bar.baz foo.ini
  140. #
  141. # and both result in reading of key *baz* from section *foo.bar* in file
  142. # *foo.ini*, which is selected from *SATURNIN_CONF_PATH*. Should there
  143. # be more foo.ini's, the first is selected. Using `-j` switch
  144. #
  145. # saturnin__conf -j foo.bar.baz
  146. #
  147. # would cause all foo.ini's on *SATURNIN_CONF_PATH* be concatenated
  148. # instead.
  149. #
  150. local ig_mode # retrieval mode
  151. local ig_query # keypath or section name (when listing keys)
  152. local ig_limit # line limit
  153. local files=() # file specification
  154. local Strategy=first # merge strategy
  155. while true; do case $1:$2 in
  156. "":*) break ;;
  157. -j:*) Strategy=join; shift 1 ;;
  158. -1:*) ig_limit=$1; shift 1 ;;
  159. -e:*.*) ig_mode=$1; ig_query=$2; shift 2; break ;;
  160. -r:*.*) ig_mode=$1; ig_query=$2; shift 2; break ;;
  161. -K:*) ig_mode=$1; ig_query=$2; shift 2; break ;;
  162. -S:*) ig_mode=$1; ig_query=""; shift 1; break ;;
  163. -P:*) ig_mode=$1; ig_query=""; shift 1; break ;;
  164. .*:*) _saturnin__conf_usage -w "bad syntax: $*" ;;
  165. *.*:*) ig_mode=-e; ig_query=$1; shift 1; break ;;
  166. --help:*) _saturnin__conf_usage -e 0 ;;
  167. *) _saturnin__conf_usage -w "bad syntax: $*" ;;
  168. esac done
  169. test -n "$ig_mode" || _saturnin__conf_usage -w "could not determine inigrep mode"
  170. debug -v ig_limit ig_query ig_mode Strategy
  171. if test -n "$*";
  172. then
  173. files=("$@")
  174. elif test -n "$ig_query";
  175. then
  176. files=("${ig_query%%.*}$SATURNIN_CONF_SUFFIX")
  177. else
  178. _saturnin__conf_usage -w "dunno what to load"
  179. fi
  180. debug -v files
  181. #shellcheck disable=SC2086
  182. _saturnin__conf__load "${files[@]}" | inigrep $ig_limit $ig_mode "$ig_query"
  183. return "${PIPESTATUS[0]}"
  184. }
  185. saturnin__conf_find() {
  186. #
  187. # Find all existing instances of sub-path $1 on $SATURNIN_CONF_PATH
  188. #
  189. # Usage:
  190. #
  191. # saturnin__conf_find SUBPATH
  192. #
  193. # Go through all elements of $SATURNIN_CONF_PATH, looking for file or
  194. # directory, whose path is formed by joining SUBPATH to element of
  195. # $SATURNIN_CONF_PATH. Print each existing path, ignore rest.
  196. #
  197. # For example, with following setup:
  198. #
  199. # SATURNIN_CONF_PATH=foo:bar:baz
  200. # mkdir -p foo/one bar/one
  201. # mkdir -p bar/two/slashes
  202. #
  203. # call
  204. #
  205. # saturnin__conf_find one
  206. #
  207. # would print `foo/one` and `bar/one`, while
  208. #
  209. # saturnin__conf_find two/slashes
  210. #
  211. # would print `bar/two/slashes`.
  212. #
  213. # If at least one path was found, return zero. Otherwise, return one,
  214. # or more in case of error.
  215. #
  216. local file=$1 # sub-path to find
  217. local trydir # each item of $SATURNIN_CONF_PATH
  218. local trypath # each combined path
  219. debug -v SATURNIN_CONF_PATH
  220. echos "$SATURNIN_CONF_PATH" \
  221. | tr ':' '\n' \
  222. | while read -r trydir;
  223. do
  224. test -n "$trydir" || continue
  225. trypath="$trydir/$file"
  226. test -e "$trypath" || continue
  227. echos "$trypath"
  228. done \
  229. | grep .
  230. }
  231. saturnin__get() {
  232. #
  233. # Show Saturnin internal info by key $1 and exit
  234. #
  235. # Key $1 can be whole `--saturnin-get-stuff` argument or just the part
  236. # after `--saturnin-get-`.
  237. #
  238. # This is aimed to help debugging and testing the app (or Saturnin
  239. # itself) by showing packaging and deployment related info.
  240. #
  241. local key=${1#--saturnin-get-} # internal info key
  242. case "$key" in
  243. saturnin-conf-path) echo "$SATURNIN_CONF_PATH" ;;
  244. saturnin-version) echo "__MKIT_PROJ_VERSION__" ;;
  245. app-git-hash) echo "$SATURNIN_APP_GIT_HASH" ;;
  246. app-version) echo "$SATURNIN_APP_VERSION" ;;
  247. cache-home) echo "$SATURNIN_CACHE_HOME" ;;
  248. libexec) echo "$SATURNIN_LIBEXEC" ;;
  249. libexec-prefix) echo "$SATURNIN_LIBEXEC_PREFIX" ;;
  250. subcommands) saturnin__lssc ;;
  251. *) warn "unknown devel key: $key"
  252. exit "$EXIT_USAGE" ;;
  253. esac
  254. exit "$EXIT_OK"
  255. }
  256. saturnin__lssc() {
  257. #
  258. # List subcommands
  259. #
  260. find "$SATURNIN_LIBEXEC" \
  261. -mindepth 1 \
  262. -maxdepth 1 \
  263. -executable \
  264. -name "$SATURNIN_LIBEXEC_PREFIX*" \
  265. | sed -e "s|^.*/||; s|^$SATURNIN_LIBEXEC_PREFIX||" \
  266. | sort
  267. }
  268. saturnin__main() {
  269. #
  270. # Main meta-command entry function
  271. #
  272. # After setting all mandatory environment variables, call this from your
  273. # main meta-command script.
  274. #
  275. local subcommand # subcommand to execute (first non-option)
  276. test -n "$SATURNIN_CACHE_HOME" || die "SATURNIN_CACHE_HOME is not set"
  277. test -n "$SATURNIN_LIBEXEC" || die "SATURNIN_LIBEXEC is not set"
  278. test -n "$SATURNIN_LIBEXEC_PREFIX" || die "SATURNIN_LIBEXEC_PREFIX is not set"
  279. while true; do case $1 in
  280. -D|--full-debug) export PRETTY_DEBUG=true
  281. export PRETTY_DEBUG_EXCLUDE=""
  282. shift ;;
  283. -d|--debug) export PRETTY_DEBUG=true; shift ;;
  284. -v|--verbose) export PRETTY_VERBOSE=true; shift ;;
  285. -h|--help) saturnin__help; exit ;;
  286. --version) saturnin__version; exit ;;
  287. -V|--version-semver) saturnin__get app-version ;;
  288. --saturnin-get-*) saturnin__get "$1" ;;
  289. -*) saturnin__usage -w "unknown argument: $1" ;;
  290. --*) saturnin__usage -w "unknown argument: $1" ;;
  291. --) shift; break ;;
  292. "") saturnin__usage -w "too few arguments" ;;
  293. *) break; ;;
  294. esac done
  295. subcommand="$1"; shift
  296. debug -v SATURNIN_APP_VERSION SATURNIN_CONF_PATH
  297. case "$subcommand" in
  298. conf) saturnin__conf "$@" ;;
  299. *) saturnin__runsc "$subcommand" "$@" ;;
  300. esac
  301. }
  302. saturnin__help() {
  303. #
  304. # Print meta-command help text
  305. #
  306. # See $SATURNIN_META_HELP for details.
  307. #
  308. local introline # introduction line
  309. introline=$(basename "$0")
  310. test -n "$SATURNIN_APP_TAGLINE" \
  311. && introline+=" - $SATURNIN_APP_TAGLINE"
  312. case "$SATURNIN_META_HELP" in
  313. "")
  314. echo "$introline"$'\n' >&2
  315. saturnin__usage -E -e 0
  316. ;;
  317. +/*)
  318. echo "$introline"$'\n' >&2
  319. saturnin__usage -E
  320. echo >&2
  321. _saturnin__cat_helpfile "${SATURNIN_META_HELP:1}"
  322. ;;
  323. =/*)
  324. _saturnin__cat_helpfile "${SATURNIN_META_HELP:1}"
  325. ;;
  326. *)
  327. echo "$introline"$'\n' >&2
  328. saturnin__usage -E
  329. saturnin__bug "malformed SATURNIN_META_HELP: $SATURNIN_META_HELP"
  330. return 3
  331. ;;
  332. esac
  333. }
  334. saturnin__conf_mkpath() {
  335. #
  336. # Assemble SATURNIN_CONF_PATH from locations $@
  337. #
  338. # For each location, print colon-delimited list of directories. If
  339. # location ends with "/ini.d", list of subfolders, sorted by C locale is
  340. # printed--this allows for modular configuration. Otherwise the
  341. # location is printed. Non-existent or non-directory locations are
  342. # silently ignored.
  343. #
  344. local location # one location argument
  345. local path # one path listed
  346. for location in "$@";
  347. do
  348. test -d "$location" || continue
  349. case "$location" in
  350. */ini.d) # modular location--sort subfolders
  351. find -L "$location" -mindepth 1 -maxdepth 1 -type d \
  352. | LC_ALL=C sort
  353. ;;
  354. *)
  355. echo "$location"
  356. ;;
  357. esac
  358. done \
  359. | _saturnin__nl2colon
  360. }
  361. saturnin__runhook() {
  362. #
  363. # Run custom hook named $1 from respective configuration section
  364. #
  365. # Will load joined multi-line key "hook.$SATURNIN_SUBCOMMAND.$1" and
  366. # unless syntax check fails, execute it as Bash code (in separate
  367. # process).
  368. #
  369. local name="$1" # hook name
  370. local code # ... code
  371. test -n "$SATURNIN_SUBCOMMAND" || {
  372. warn "unknown subcommand, ignoring hook: $name"
  373. return 0
  374. }
  375. code="$(saturnin__conf -j "hook.$SATURNIN_SUBCOMMAND.$name")"
  376. debug -v SATURNIN_SUBCOMMAND code name
  377. bash -n <<<"$code" || {
  378. warn "syntax errors, ignoring hook: $name"
  379. return 0
  380. }
  381. bash <<<"$code"
  382. }
  383. saturnin__runsc() {
  384. #
  385. # Run subcommand $1 with arguments $2..
  386. #
  387. local subcommand="$1"; shift # subcommand to run
  388. local binpath # path to subcommand's binary
  389. binpath+="$SATURNIN_LIBEXEC/"
  390. binpath+="$SATURNIN_LIBEXEC_PREFIX$subcommand"
  391. debug -v binpath
  392. debug "\$*='$*'"
  393. test -x "$binpath" \
  394. || saturnin__usage "invalid sub-command: $subcommand"
  395. SATURNIN_SUBCOMMAND="$subcommand" "$binpath" "$@"
  396. }
  397. saturnin__usage() {
  398. #
  399. # Show usage message and exit
  400. #
  401. #shellcheck disable=SC2046
  402. mkusage "$@" \
  403. "[options] COMMAND [ARG...]" \
  404. -o \
  405. "-D, --full-debug turn on gory debugging" \
  406. "-V, --version show version and exit" \
  407. "-d, --debug turn on debugging" \
  408. "-h, --help show this help message and exit"\
  409. "-v, --verbose turn on verbosity" \
  410. -c \
  411. $(saturnin__lssc)
  412. }
  413. saturnin__version() {
  414. #
  415. # Print human-readable version info
  416. #
  417. # Basic version info is already stored in $SATURNIN_APP_VERSION,
  418. # this function prints more descriptive paragraph including Saturnin's
  419. # own version.
  420. #
  421. echo -n "$(basename "$0")"
  422. test -n "$SATURNIN_APP_TAGLINE" \
  423. && echo -n " ($SATURNIN_APP_TAGLINE)"
  424. echo -n " $SATURNIN_APP_VERSION"
  425. test -n "$SATURNIN_APP_CODENAME" \
  426. && echo -n " - $SATURNIN_APP_CODENAME"
  427. echo
  428. echo -n "Powered by Saturnin (__MKIT_PROJ_TAGLINE__)"
  429. echo -n " __MKIT_PROJ_VERSION__"
  430. echo -n " - __MKIT_PROJ_CODENAME__"
  431. echo
  432. return "$EXIT_OK"
  433. }
  434. saturnin__wraphook() {
  435. #
  436. # Wrap command $@ in hooks 'pre' and 'post'
  437. #
  438. # Run pre hook, then command $@, then post hook. Always exit with
  439. # status of the payload command, even if hooks fail. Ignore post-hook
  440. # if payload command failed.
  441. #
  442. local es=0 # exit status of this function
  443. saturnin__runhook pre
  444. "$@" || return $?
  445. es=$?
  446. saturnin__runhook post
  447. return $es
  448. }
  449. # # that what you see below this line #
  450. # INTERNAL # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
  451. # # use in your code to anger the divine #
  452. _saturnin__cat_helpfile() {
  453. #
  454. # Print helpfile $1
  455. #
  456. local helpfile=$1 # path to help file
  457. cat "$helpfile" >&2 && return 0
  458. saturnin__bug "cannot print help file: $helpfile"
  459. return 3
  460. }
  461. _saturnin__conf__merge() {
  462. #
  463. # Take paths and applying merge strategy, load file(s)
  464. #
  465. local path # every path
  466. local found=false # 'true' if we got any path
  467. while read -r path;
  468. do
  469. found=true
  470. case $Strategy in
  471. first)
  472. debug "winner: $path"
  473. cat "$path"
  474. cat >/dev/null # throw away rest of paths
  475. ;;
  476. join)
  477. echo "# file: ${path/$HOME/~}"
  478. cat "$path" 2>/dev/null
  479. ;;
  480. esac
  481. done
  482. $found
  483. }
  484. _saturnin__conf__load() {
  485. #
  486. # Print contents of files specified in $@
  487. #
  488. # Each argument means possible file candidate. If candidate contains
  489. # slash, it's treated as file path and is printed directly. If it's
  490. # single dash, standard input is copied.
  491. #
  492. # In all other cases, filename is searched in all elements of variable
  493. # SATURNIN_CONF_PATH; output then depends on chosen $Strategy: with
  494. # 'first' strategy, first existing file is printed, with 'join'
  495. # strategy, all existing files are printed.
  496. #
  497. local arg # each passed argument
  498. local es=0 # exit status of this function
  499. for arg in "$@";
  500. do
  501. case $arg in
  502. -|*/*) # stdin, or path (with slash)
  503. cat "$arg" || es=3
  504. ;;
  505. *) # name given, find all its incarnations
  506. saturnin__conf_find "$arg" \
  507. | _saturnin__conf__merge; es=$?
  508. ;;
  509. esac
  510. done
  511. return $es
  512. }
  513. _saturnin__conf_usage() {
  514. #
  515. # Show usage message, passing $@ to mkusage() and exit
  516. #
  517. PRETTY_USAGE="self=${0##*/} conf" \
  518. mkusage "$@" \
  519. "[options] [-e] SECTION.KEY [FNAME]" \
  520. "[options] -r SECTION.KEY [FNAME]" \
  521. "[options] -K SECTION [FNAME]" \
  522. "[options] -P FNAME" \
  523. "[options] -S FNAME" \
  524. -- \
  525. "Use inigrep to query config files." \
  526. -o \
  527. "-j join all files before applying query" \
  528. "-1 ensure single line is returned" \
  529. -c \
  530. "-e use normal mode (default)" \
  531. "-r use raw mode (preserves RHS whitespace and some comments)"\
  532. "-K list available keys in SECTION" \
  533. "-S list available sections in FNAME" \
  534. "-P list available keypaths (SECTION.KEY) in FNAME" \
  535. -- \
  536. "FNAME is filename, which is then searched on all paths specified"\
  537. "in SATURNIN_CONF_PATH and depending on -j parameter, first one" \
  538. "wins or all are joined. If FNAME contains slash, this search is"\
  539. "not done and FNAME is taken as path to file that is then" \
  540. "queried." \
  541. "" \
  542. "If FNAME is omitted, it is inferred from SECTION (e.g. 'foo.ini'"\
  543. "if 'foo.bar' was section name; note that section name itself may"\
  544. "contain dot)."
  545. }
  546. _saturnin__nl2colon() {
  547. #
  548. # Convert newline-based list of paths to colon:based:list
  549. #
  550. # Empty paths must not be included in the resulting list, so we need to
  551. # drop them and also get the colons right.
  552. #
  553. local idx=0 # current item index (zero-based)
  554. local path # each path on stdin
  555. while read -r path;
  556. do
  557. test -z "$path" && continue
  558. test $idx -gt 0 && echo -n ':'
  559. echo -n "$path"
  560. ((idx++))
  561. done
  562. }
  563. #shellfu module-version: __MKIT_PROJ_VERSION__