Working Saturnin-based meta-command

saturnin.sh 14KB

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