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

inigrep.sh 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. #!/bin/bash
  2. shellfu import pretty
  3. #
  4. # grep for (some) INIs
  5. #
  6. # inigrep is designed to read a particular simplistic dialect of
  7. # INI configuration files. In a sense, it can be considered as
  8. # a "grep for INIs", in that rather than parsing the file into
  9. # a typed memory structure for later access, it passes file each
  10. # time a query is done, and spits out relevant parts; treating
  11. # everything as text. Hence, it's not intended as replacement
  12. # for a full-blown configuration system but rather a quick & dirty
  13. # "swiss axe" for quick & dirty scripts.
  14. #
  15. # That's not to say that you cannot do things nicely; but don't
  16. # count on speed -- well since you're using bash you obviously
  17. # don't -- and compliance -- simple things are simple, but there
  18. # are a bit unusual pitfalls.
  19. #
  20. #
  21. # The format by examples
  22. # ----------------------
  23. #
  24. # The most basic example understood by inigrep is identical
  25. # to most INI formats:
  26. #
  27. # # Let's call this simple.ini
  28. #
  29. # [foo]
  30. # bar = baz
  31. # qux = quux
  32. #
  33. # [corge]
  34. # grault = graply
  35. #
  36. # Structure here is obvious: two sections named `foo` and `corge`,
  37. # the first one has two key/value pairs and the other has one pair.
  38. #
  39. # Getting values from this file is trivial:
  40. #
  41. # inigrep foo.bar simple.ini
  42. # inigrep foo.qux simple.ini
  43. # inigrep corge.grault simple.ini
  44. #
  45. # would list `baz`, `quux` and `graply`.
  46. #
  47. # This is where 80% of use cases are covered.
  48. #
  49. #
  50. # Multi-line
  51. # ----------
  52. #
  53. # Multi-line values are rather unusual but very simple:
  54. #
  55. # [lipsum]
  56. #
  57. # latin = Lorem ipsum dolor sit amet, consectetur adipiscing
  58. # latin = elit, sed do eiusmod tempor incididunt ut labore et
  59. # latin = dolore magna aliqua. Ut enim ad minim veniam, quis
  60. # latin = ...
  61. #
  62. # english = [32] But I must explain to you how all this mistaken
  63. # english = idea of denouncing of a pleasure and praising pain
  64. # english = was born and I will give you a complete account of
  65. # english = ...
  66. #
  67. # This file can be read as:
  68. #
  69. # inigrep lipsum.latin lipsum.ini
  70. # inigrep lipsum.english lipsum.ini
  71. #
  72. #
  73. # Exploration
  74. # -----------
  75. #
  76. # Other than basic value retrieval, inigrep allows you to look around
  77. # first. For example, to list all keypaths available in a file:
  78. #
  79. # inigrep -P simple.ini
  80. #
  81. # In case of simple.ini, this would print:
  82. #
  83. # foo.bar
  84. # foo.qux
  85. # corge.grault
  86. #
  87. # Similarly:
  88. #
  89. # inigrep -S simple.ini
  90. #
  91. # would list just the section names:
  92. #
  93. # foo
  94. # corge
  95. #
  96. # and
  97. #
  98. # inigrep -K foo simple.ini
  99. #
  100. # would list all keys from section 'foo'
  101. #
  102. # bar
  103. # qux
  104. #
  105. _inigrep__fltcmt() {
  106. #
  107. # Just strip comments
  108. #
  109. perl -ne '
  110. chomp;
  111. next if m/^[[:space:]]*[#;]/;
  112. s/ *#.*//;
  113. s/ *;.*//;
  114. print "$_\n" if $_;
  115. '
  116. }
  117. _inigrep__fltkey() {
  118. #
  119. # Filter values from `key = value pairs`
  120. #
  121. # In listing mode, prints *key names* instead
  122. #
  123. local line # every line
  124. local key # key name part
  125. local value # name part
  126. while IFS= read -r line; do # ' key = val '
  127. line="${line##*([[:space:]])}" # line='key = val '
  128. key="${line%%*([[:space:]])=*}" # key='key'
  129. value="${line##$key*([[:space:]])=}" # value=' val '
  130. $StrictMode || value="${value##*([[:space:]])}" # value='val'
  131. test "$key" = "$WantedKey" || continue
  132. $ListingMode || echos "$value"
  133. $ListingMode && echos "$line"
  134. done
  135. }
  136. _inigrep__fltsct() {
  137. #
  138. # Filter out only the wanted section body
  139. #
  140. # In listing mode prints also section name
  141. #
  142. local sct_ok=false # is this the section we want?
  143. local sct_name # wanted section name
  144. local line # current line
  145. while IFS= read -r line;
  146. do
  147. if [[ $line =~ ^[[:space:]]*\[[^]]*\].* ]]; then # IOW: 'spaces[name]anything'
  148. # hack out only the name between '[' and ']'
  149. sct_name=$line
  150. sct_name=${sct_name##*([[:space:]])[} # drop leading space
  151. sct_name=${sct_name%%]*} # and trailing anything
  152. if test "$sct_name" == "$WantedSection"; then
  153. sct_ok=true; # our section
  154. $ListingMode && echos "[$sct_name]"; # inform lister
  155. else
  156. sct_ok=false
  157. fi
  158. continue
  159. else
  160. $sct_ok || continue
  161. $StrictMode || line="${line##*([[:space:]])}"
  162. echos "$line"
  163. fi
  164. done
  165. }
  166. _inigrep__fltlst() {
  167. #
  168. # Filter only section/keys/path names from the stream
  169. #
  170. local line="" # current line
  171. local key="__NOKEY__" # current key
  172. local sct="__NOSCT__" # current section
  173. while read -r line; do
  174. case "$line" in
  175. \[*\]) sct=${line#[}; sct=${sct%]}; key=__NOKEY__ ;;
  176. *=*) key="${line%%*([[:space:]])=*}" ;; # key='key'
  177. *) continue ;; # whatever
  178. esac
  179. case "$Engine:$sct:$key" in
  180. lspth:__NOSCT__:*) echos "$sct.$key" ;;
  181. lspth:*:__NOKEY__) continue ;;
  182. lspth:*:*) echos "$sct.$key" ;;
  183. lssct:__NOSCT__:*) echos "$sct" ;;
  184. lssct:*:__NOKEY__) echos "$sct" ;;
  185. lssct:*:*) continue ;;
  186. lskey:__NOSCT__:__NOKEY__) continue ;;
  187. lskey:__NOSCT__:*) echos "$key" ;;
  188. lskey:*:__NOKEY__) continue ;;
  189. lskey:*:*) echos "$key" ;;
  190. esac
  191. done
  192. }
  193. _inigrep__query() {
  194. #
  195. # Query INI stream composed of files at $@ or stdin
  196. #
  197. # Compose filter pipe of potential parts and run it.
  198. #
  199. local pipe="" # filter pipe
  200. local ListingMode=false # listing mode on?
  201. local StrictMode=false # strict mode on?
  202. local ifile # current input file
  203. local restore_shopt # shopt backup (executable code)
  204. case $Engine in
  205. raw) StrictMode=true ;;
  206. ls*) ListingMode=true ;;
  207. esac
  208. # which will come first? prefix all and strip the first prefix
  209. #
  210. $StrictMode || pipe+=" | _inigrep__fltcmt"
  211. test -n "$WantedSection" && pipe+=" | _inigrep__fltsct"
  212. test -n "$WantedKey" && pipe+=" | _inigrep__fltkey"
  213. $ListingMode && pipe+=" | _inigrep__fltlst"
  214. $StrictMode || pipe+=" | grep ."
  215. pipe="${pipe# | }"
  216. test -z "$pipe" && pipe="cat" # possible with raw mode and dot wildcard
  217. debug -v pipe
  218. # pray and run
  219. #
  220. restore_shopt=$(shopt -p extglob)
  221. shopt -s extglob
  222. for ifile in "$@"; do
  223. case "$ifile" in
  224. -) eval "$pipe" ;;
  225. *) <"$ifile" eval "$pipe" ;;
  226. esac
  227. done
  228. $restore_shopt
  229. }
  230. _inigrep__validate_ks() {
  231. #
  232. # Check $WantedSection ans $WantedKey for invalid chars and warn
  233. #
  234. case $WantedKey in
  235. *\\*) warn "invalid char '\\' in key name: $WantedKey"; return 1 ;;
  236. *[*) warn "invalid char '[' in key name: $WantedKey"; return 1 ;;
  237. *=*) warn "invalid char '=' in key name: $WantedKey"; return 1 ;;
  238. esac
  239. case $WantedSection in
  240. *]*) warn "invalid char ']' in section name: $WantedSection"; return 1 ;;
  241. esac
  242. }
  243. inigrep() {
  244. #
  245. # Query INI file(s) for data
  246. #
  247. # Usage:
  248. #
  249. # inigrep [-1] [section].[key] [file]...
  250. # inigrep [-1] -e|--basic [section].[key] [file]...
  251. # inigrep [-1] -r|--raw [section].[key] [file]...
  252. # inigrep [-1] -K|--lskey section [file]...
  253. # inigrep [-1] -S|--lssct [file]...
  254. # inigrep [-1] -P|--lspth [file]...
  255. #
  256. # First form implies the basic engine.
  257. #
  258. # Option *-r* switches to raw mode, in which comments,
  259. # empty lines and whitespace are all preserved in applicable
  260. # contexts.
  261. #
  262. # Key *keypath* consists of section name and key name delimited
  263. # by dot. Note that keypath may contain dots but key may not.
  264. #
  265. # If *file* is not given or is a single dash, standard input
  266. # is read. Note that standard input is not duplicated in case
  267. # of multiple dashes.
  268. #
  269. # If suitable key is found at multiple lines, all values
  270. # are printed, which allows for creating multi-line values.
  271. # Providing *-1* argument, however, always prints only one
  272. # line.
  273. #
  274. # Options -K, -S and -P can be used for inspecting file structure;
  275. # -K needs an argument of section name and will list all keys from
  276. # that section. -S will list all sections and -P will list all
  277. # existing keypaths.
  278. #
  279. #
  280. # #### Examples ####
  281. #
  282. # Having INI file such as
  283. #
  284. # [foo]
  285. # bar=baz
  286. # quux=qux
  287. # quux=qux2
  288. # quux=qux3
  289. #
  290. # * `inigrep foo.bar ./file.ini` gives "bar".
  291. # * `inigrep foo.quux ./file.ini` gives three lines "qux", "qux2"
  292. # and "qux3".
  293. # * `inigrep -P ./file.ini` gives two lines "foo.bar" and "foo.quux".
  294. # * `inigrep -K foo ./file.ini` gives two lines "bar" and "quux".
  295. # * `inigrep -S ./file.ini` gives "foo".
  296. #
  297. local kpath="" # desired key path
  298. local one_line=false # limit to single line
  299. local Engine=ini # engine: ini,raw,lspth,lssct,lskey
  300. local WantedSection="" # desired section name (in case of listing)
  301. local WantedKey="" # desired key name
  302. while true; do case $1 in
  303. -1|--one-line) one_line=true; shift ;;
  304. -e|--basic) Engine=ini; kpath="$2"; shift; break ;;
  305. -r|--raw) Engine=raw; kpath="$2"; shift; break ;;
  306. -K|--lskey) Engine=lskey; kpath="$2."; shift; break ;;
  307. -P|--lspth) Engine=lspth; kpath="."; break ;;
  308. -S|--lssct) Engine=lssct; kpath="."; break ;;
  309. *) break ;;
  310. esac done
  311. test -n "$kpath" || kpath="$1" && shift
  312. test -z "$*" && set -- -
  313. debug -v one_line Engine kpath
  314. debug "\$*='$*'"
  315. WantedKey="${kpath##*.}"
  316. WantedSection="${kpath%.$WantedKey}"
  317. _inigrep__validate_ks || return 2
  318. debug -v WantedKey WantedSection
  319. if $one_line; then
  320. _inigrep__query "$@" | head -1
  321. else
  322. _inigrep__query "$@"
  323. fi
  324. }