saturnin.sh.skel 16KB

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