saturnin.sh.skel 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  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. saturnin__bug() {
  62. #
  63. # Warn about bug in your software
  64. #
  65. # Issue warning using warn() from pretty but also add application
  66. # version. This is useful when an assertion in your application fails
  67. # such that it is certain that there is a bug inside it.
  68. #
  69. # In such cases, it makes sense to print also version information to
  70. # help users with reporting.
  71. #
  72. local msg
  73. for msg in "$@";
  74. do
  75. warn "bug: $msg"
  76. done
  77. warn "bug in $(basename "$0") version: $SATURNIN_APP_VERSION"
  78. }
  79. saturnin__conf() {
  80. #
  81. # inigrep smart loader
  82. #
  83. # Usage:
  84. # saturnin__conf [-j] [inigrep-query] [-- [file]..]
  85. #
  86. #
  87. # File arguments
  88. # ==============
  89. #
  90. # If omitted, *file* argument is inferred by taking part of kpath name
  91. # before first dot and appending value of `$SATURNIN_CONF_SUFFIX`,
  92. # (".ini" by default).
  93. #
  94. # Each *file* argument is then processed as follows:
  95. #
  96. # * `-` (single dash) is interpreted as reading from standard input.
  97. #
  98. # * If argument contains slash, it is expanded as a regular path
  99. # (relative or absolute).
  100. #
  101. # * Otherwise, it is taken as filename and searched for in directories
  102. # given in `$SATURNIN_CONF_PATH`. (This can yield more than one
  103. # path, which is equivalent as if all paths were provided.)
  104. #
  105. # Not all files expanded based on `$SATURNIN_CONF_PATH` are read by
  106. # default; reading is governed by "merge strategy": the default
  107. # strategy "first" reads only the first existing file.
  108. #
  109. # "join" strategy on the other hand, means that any files are simply
  110. # concatenated and prefixed with comment (visible only in raw mode)
  111. # containing path to the file.
  112. #
  113. # This means that if a section is queried that is present in both
  114. # files, it is effectively concatenated as well.
  115. #
  116. # Following calls are equivalent
  117. #
  118. # saturnin__conf foo.bar.baz
  119. # saturnin__conf foo.bar.baz foo.ini
  120. #
  121. # and both result in reading of key *baz* from section *foo.bar* in file
  122. # *foo.ini*, which is selected from *SATURNIN_CONF_PATH*. Should there
  123. # be more foo.ini's, the first is selected. Using `-j` switch
  124. #
  125. # saturnin__conf -j foo.bar.baz
  126. #
  127. # would cause all foo.ini's on *SATURNIN_CONF_PATH* be concatenated
  128. # instead.
  129. #
  130. local ig_mode # retrieval mode
  131. local ig_query # keypath or section name (when listing keys)
  132. local ig_limit # line limit
  133. local files=() # file specification
  134. local Strategy=first # merge strategy
  135. while true; do case $1:$2 in
  136. "":*) break ;;
  137. -j:*) Strategy=join; shift 1 ;;
  138. -1:*) ig_limit=$1; shift 1 ;;
  139. -e:*.*) ig_mode=$1; ig_query=$2; shift 2; break ;;
  140. -r:*.*) ig_mode=$1; ig_query=$2; shift 2; break ;;
  141. -K:*) ig_mode=$1; ig_query=$2; shift 2; break ;;
  142. -S:*) ig_mode=$1; ig_query=""; shift 1; break ;;
  143. -P:*) ig_mode=$1; ig_query=""; shift 1; break ;;
  144. .*:*) warn "bad syntax: $*"; _saturnin__conf_usage ;;
  145. *.*:*) ig_mode=-e; ig_query=$1; shift 1; break ;;
  146. --help:*) _saturnin__conf_usage -e 0 ;;
  147. *) warn "bad syntax: $*"; _saturnin__conf_usage ;;
  148. esac done
  149. test -n "$ig_mode" || { warn "could not determine inigrep mode"; _saturnin__conf_usage; }
  150. debug -v ig_limit ig_query ig_mode Strategy
  151. if test -n "$*";
  152. then
  153. files=("$@")
  154. elif test -n "$ig_query";
  155. then
  156. files=("${ig_query%%.*}$SATURNIN_CONF_SUFFIX")
  157. else
  158. warn "dunno what to load"
  159. _saturnin__conf_usage
  160. fi
  161. debug -v files
  162. #shellcheck disable=SC2086
  163. _saturnin__conf__load "${files[@]}" | inigrep $ig_limit $ig_mode "$ig_query"
  164. return "${PIPESTATUS[0]}"
  165. }
  166. saturnin__conf_find() {
  167. #
  168. # Find all existing instances of sub-path $1 on $SATURNIN_CONF_PATH
  169. #
  170. # Go through all elements of $SATURNIN_CONF_PATH, looking for file on
  171. # sub-path $1. Print each existing path, ignore rest.
  172. #
  173. # If at least one path was found, return zero. Otherwise, return one,
  174. # or more in case of error.
  175. #
  176. local file=$1 # sub-path to find
  177. local trydir # each item of $SATURNIN_CONF_PATH
  178. local trypath # each combined path
  179. debug -v SATURNIN_CONF_PATH
  180. echos "$SATURNIN_CONF_PATH" \
  181. | tr ':' '\n' \
  182. | while read -r trydir;
  183. do
  184. test -n "$trydir" || continue
  185. trypath="$trydir/$file"
  186. test -e "$trypath" || continue
  187. echos "$trypath"
  188. done \
  189. | grep .
  190. }
  191. saturnin__get() {
  192. #
  193. # Show Saturnin internal info by key $1 and exit
  194. #
  195. # Key $1 can be whole `--saturnin-get-stuff` argument or just the part
  196. # after `--saturnin-get-`.
  197. #
  198. # This is aimed to help debugging and testing the app (or Saturnin
  199. # itself) by showing packaging and deployment related info.
  200. #
  201. local key=${1#--saturnin-get-}
  202. case "$key" in
  203. saturnin-conf-path) echo "$SATURNIN_CONF_PATH" ;;
  204. app-git-hash) echo "$SATURNIN_APP_GIT_HASH" ;;
  205. app-version) echo "$SATURNIN_APP_VERSION" ;;
  206. cache-home) echo "$SATURNIN_CACHE_HOME" ;;
  207. libexec) echo "$SATURNIN_LIBEXEC" ;;
  208. libexec-prefix) echo "$SATURNIN_LIBEXEC_PREFIX" ;;
  209. *) warn "unknown devel key: $key"
  210. exit "$EXIT_USAGE" ;;
  211. esac
  212. exit "$EXIT_OK"
  213. }
  214. saturnin__lssc() {
  215. #
  216. # List subcommands
  217. #
  218. echo conf
  219. find "$SATURNIN_LIBEXEC" \
  220. -mindepth 1 \
  221. -maxdepth 1 \
  222. -executable \
  223. | sed -e "s|^.*/||; s|^$SATURNIN_LIBEXEC_PREFIX||" \
  224. | sort
  225. }
  226. saturnin__main() {
  227. #
  228. # Main meta-command entry function
  229. #
  230. # After setting all mandatory environment variables, call this from your
  231. # main meta-command script.
  232. #
  233. local subcommand
  234. test -n "$SATURNIN_CACHE_HOME" || die "SATURNIN_CACHE_HOME is not set"
  235. test -n "$SATURNIN_LIBEXEC" || die "SATURNIN_LIBEXEC is not set"
  236. test -n "$SATURNIN_LIBEXEC_PREFIX" || die "SATURNIN_LIBEXEC_PREFIX is not set"
  237. while true; do case $1 in
  238. -d|--debug) export PRETTY_DEBUG=true; shift ;;
  239. -v|--verbose) export PRETTY_VERBOSE=true; shift ;;
  240. -h|--help) saturnin__usage -e 0; exit ;;
  241. --version) saturnin__version; exit ;;
  242. --version-semver) saturnin__get app-version ;;
  243. --saturnin-get-*) saturnin__get "$1" ;;
  244. -*) saturnin__usage; ;;
  245. --*) saturnin__usage; ;;
  246. --) shift; break ;;
  247. "") saturnin__usage; ;;
  248. *) break; ;;
  249. esac done
  250. subcommand="$1"; shift
  251. debug -v SATURNIN_APP_VERSION SATURNIN_CONF_PATH
  252. case "$subcommand" in
  253. conf) saturnin__conf "$@" ;;
  254. _ls_sc) saturnin__lssc ;;
  255. _lsfun) shellfu-get lsfun ;;
  256. _lsmod) shellfu-get lsmod ;;
  257. *) saturnin__runsc "$subcommand" "$@" ;;
  258. esac
  259. }
  260. saturnin__conf_mkpath() {
  261. #
  262. # Assemble SATURNIN_CONF_PATH from locations $@
  263. #
  264. # For each location, print colon-delimited list of directories. If
  265. # location ends with "/ini.d", list of subfolders, sorted by C locale is
  266. # printed--this allows for modular configuration. Otherwise the
  267. # location is printed. Non-existent or non-directory locations are
  268. # silently ignored.
  269. #
  270. local location # one location argument
  271. local path # one path listed
  272. for location in "$@";
  273. do
  274. test -d "$location" || continue
  275. case "$location" in
  276. */ini.d) # modular location--sort subfolders
  277. find -L "$location" -mindepth 1 -maxdepth 1 -type d \
  278. | LC_ALL=C sort
  279. ;;
  280. *)
  281. echo "$location"
  282. ;;
  283. esac
  284. done \
  285. | _saturnin__nl2colon
  286. }
  287. saturnin__runhook() {
  288. #
  289. # Run custom hook named $1 from respective configuration section
  290. #
  291. # Will load joined multi-line key "hook.$SATURNIN_SUBCOMMAND.$1" and
  292. # unless syntax check fails, execute it as Bash code (in separate
  293. # process).
  294. #
  295. local hname="$1"
  296. local hook_code
  297. test -n "$SATURNIN_SUBCOMMAND" || {
  298. warn "unknown subcommand, ignoring hook: $hname"
  299. return 0
  300. }
  301. hook_code="$(saturnin__conf -j "hook.$SATURNIN_SUBCOMMAND.$hname")"
  302. debug -v SATURNIN_SUBCOMMAND hook_code hname
  303. bash -n <<<"$hook_code" || {
  304. warn "syntax errors, ignoring hook: $hname"
  305. return 0
  306. }
  307. eval "$hook_code"
  308. }
  309. saturnin__runsc() {
  310. #
  311. # Run subcommand $SATURNIN_SUBCOMMAND
  312. #
  313. local subcommand="$1"; shift
  314. local binpath # path to subcommand's binary
  315. binpath+="$SATURNIN_LIBEXEC/"
  316. binpath+="$SATURNIN_LIBEXEC_PREFIX$subcommand"
  317. debug -v binpath
  318. debug "\$*='$*'"
  319. test -x "$binpath" || {
  320. warn "invalid sub-command: $subcommand"
  321. saturnin__usage
  322. }
  323. SATURNIN_SUBCOMMAND="$subcommand" "$binpath" "$@"
  324. }
  325. saturnin__usage() {
  326. #
  327. # Show usage message and exit
  328. #
  329. #shellcheck disable=SC2046
  330. mkusage "$@" \
  331. "[options] COMMAND [ARG...]" \
  332. -o \
  333. "-d, --debug turn on debugging" \
  334. "-h, --help show this help message and exit"\
  335. "-v, --verbose turn on verbosity" \
  336. "--version show version and exit" \
  337. -c \
  338. $(saturnin__lssc)
  339. }
  340. saturnin__version() {
  341. #
  342. # Print version info
  343. #
  344. local tagline=${SATURNIN_APP_TAGLINE:-Some app with default tagline}
  345. local maybe_codename=""
  346. test -n "$SATURNIN_APP_CODENAME" && maybe_codename=" - $SATURNIN_APP_CODENAME"
  347. echo "$(basename "$0") ($tagline) $SATURNIN_APP_VERSION$maybe_codename"
  348. return "$EXIT_OK"
  349. }
  350. saturnin__wraphook() {
  351. #
  352. # Wrap command $@ in hooks 'pre' and 'post'
  353. #
  354. # Run pre hook, then command $@, then post hook. Always exit with
  355. # status of the payload command, even if hooks fail. Ignore post-hook
  356. # if payload command failed.
  357. #
  358. local es=0
  359. saturnin__runhook pre
  360. "$@" || return $?
  361. es=$?
  362. saturnin__runhook post
  363. return $es
  364. }
  365. # # that what you see below this line #
  366. # INTERNAL # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
  367. # # use in your code to anger the divine #
  368. _saturnin__conf__merge() {
  369. #
  370. # Take paths and applying merge strategy, load file(s)
  371. #
  372. local path
  373. local found=false
  374. while read -r path;
  375. do
  376. found=true
  377. case $Strategy in
  378. first)
  379. debug "winner: $path"
  380. cat "$path"
  381. cat >/dev/null # throw away rest of paths
  382. ;;
  383. join)
  384. echo "# file: ${path/$HOME/~}"
  385. cat "$path" 2>/dev/null
  386. ;;
  387. esac
  388. done
  389. $found
  390. }
  391. _saturnin__conf__load() {
  392. #
  393. # Print contents of files specified in $@
  394. #
  395. # Each argument means possible file candidate. If candidate contains
  396. # slash, it's treated as file path and is printed directly. If it's
  397. # single dash, standard input is copied.
  398. #
  399. # In all other cases, filename is searched in all elements of variable
  400. # SATURNIN_CONF_PATH; output then depends on chosen $Strategy: with
  401. # 'first' strategy, first existing file is printed, with 'join'
  402. # strategy, all existing files are printed.
  403. #
  404. local arg es
  405. es=0
  406. for arg in "$@";
  407. do
  408. case $arg in
  409. -|*/*) # stdin, or path (with slash)
  410. cat "$arg" || es=3
  411. ;;
  412. *) # name given, find all its incarnations
  413. saturnin__conf_find "$arg" \
  414. | _saturnin__conf__merge; es=$?
  415. ;;
  416. esac
  417. done
  418. return $es
  419. }
  420. _saturnin__conf_usage() {
  421. #
  422. # Show usage message and exit
  423. #
  424. PRETTY_USAGE="self=${0##*/} conf" \
  425. mkusage "$@" \
  426. "[options] [-e] SECTION.KEY [FNAME]" \
  427. "[options] -r SECTION.KEY [FNAME]" \
  428. "[options] -K SECTION [FNAME]" \
  429. "[options] -P FNAME" \
  430. "[options] -S FNAME" \
  431. -- \
  432. "Use inigrep to query config files." \
  433. -o \
  434. "-j join all files before applying query" \
  435. "-1 ensure single line is returned" \
  436. -c \
  437. "-e use normal mode (default)" \
  438. "-r use raw mode (preserves RHS whitespace and some comments)"\
  439. "-K list available keys in SECTION" \
  440. "-S list available sections in FNAME" \
  441. "-P list available keypaths (SECTION.KEY) in FNAME" \
  442. -- \
  443. "FNAME is filename, which is then searched on all paths specified"\
  444. "in SATURNIN_CONF_PATH and depending on -j parameter, first one" \
  445. "wins or all are joined. If FNAME contains slash, this search is"\
  446. "not done and FNAME is taken as path to file that is then" \
  447. "queried." \
  448. "" \
  449. "If FNAME is omitted, it is inferred from SECTION (e.g. 'foo.ini'"\
  450. "if 'foo.bar' was section name; note that section name itself may"\
  451. "contain dot)."
  452. }
  453. _saturnin__nl2colon() {
  454. #
  455. # Convert newline-based list of paths to colon:based:list
  456. #
  457. # Empty paths must not be included in the resulting list, so we need to
  458. # drop them and also get the colons right.
  459. #
  460. local idx=0 # current item index (zero-based)
  461. local path
  462. while read -r path;
  463. do
  464. test -z "$path" && continue
  465. test $idx -gt 0 && echo -n ':'
  466. echo -n "$path"
  467. ((idx++))
  468. done
  469. }
  470. #shellfu module-version: __MKIT_PROJ_VERSION__