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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. #!/bin/bash
  2. shellfu import pretty
  3. #
  4. # Explore installed Shellfu modules
  5. #
  6. # Modules following Shellfu coding style[1] and installed at one of
  7. # directories listed by $SHELLFU_PATH can be listed and explored using
  8. # functions in this module.
  9. #
  10. # [1]: https://github.com/AloisMahdal/shellfu/blob/master/notes/style.md
  11. #
  12. # Note that this module exists in order to allow developers implement commands
  13. # similar to sfdoc, which is preferred choice for normal use as it provides
  14. # more feature and better user experience.
  15. #
  16. #
  17. # Show hidden objects (modules, functions, variables)?
  18. #
  19. # An object (module, function or variable) is considered hidden if its name starts
  20. # with `_` (underscore). These are ignored by default. Set this to 'true' to show
  21. # them.
  22. #
  23. SFDOC_SHOW_HIDDEN=${SFDOC_SHOW_HIDDEN:-false}
  24. sfdoc__export() {
  25. #
  26. # Export docs of module $2 from file $3 as format $3
  27. #
  28. # Usage:
  29. #
  30. # sfdoc__export FMT NAME PATH [ENCODING]
  31. #
  32. # Exports docs of module file at PATH, in format FMT, displaying
  33. # NAME as "natural" name of the module. Supported FMTs are
  34. # 'manpage', 'markdown' and 'pod'. ENCODING is currently used by
  35. # 'pod' format only and is 'utf8' by default.
  36. #
  37. local format=$1 # format to export in
  38. local MName=$2 # name to use in headers
  39. local MPath=$3 # path to file to export
  40. local Encoding=${4:-utf8} # file encoding
  41. local MVer # module version (if found)
  42. local exfun # export function
  43. local usage="usage: sfdoc__export FMT NAME PATH [ENCODING]"
  44. test -n "$format" || { warn "$usage"; return 2; }
  45. test -n "$MPath" || { warn "$usage"; return 2; }
  46. test -n "$MName" || { warn "$usage"; return 2; }
  47. exfun="__sfdoc__export_as_$format"
  48. type -t "$exfun" >/dev/null || {
  49. warn "unknown format: $format"
  50. return 2
  51. }
  52. MVer="v$(shellfu _read_directive "module-version" "$MPath" 2>/dev/null)"
  53. test "$MVer" == "v" && MVer="(no version)"
  54. debug -v format MName MPath Encoding MVer
  55. "$exfun"
  56. }
  57. sfdoc__ls() {
  58. #
  59. # List all objects of type $1 in module file $2
  60. #
  61. # Usage:
  62. #
  63. # sfdoc__ls TYPE PATH
  64. #
  65. # TYPE can be 'f' for functions or 'v' for variables. PATH
  66. # must be path to a module following Shellfu coding style
  67. # (see DESCRIPTION section).
  68. #
  69. local otype=$1
  70. local mpath=$2
  71. case $otype in
  72. f)
  73. grep -HE '^[[:alnum:]_]+\(\) \{' "$mpath" \
  74. | sed -e 's|(.*||; s|\.sh:|.|; s|^.*/||'
  75. ;;
  76. v)
  77. grep -HE '^[[:alnum:]_]+=' "$mpath" \
  78. | sed -e 's|=.*||; s|\.sh:|.|; s|^.*/||'
  79. ;;
  80. *)
  81. warn "bug: invalid object type: $otype"
  82. ;;
  83. esac \
  84. | __sfdoc__filter_hidden "$otype"
  85. }
  86. sfdoc__ls_m() {
  87. #
  88. # List all installed modules
  89. #
  90. # An installed module is a shell file named ending with '.sh' and
  91. # placed in one of directories listed in comma-separated list
  92. # in environment variable $SHELLFU_PATH.
  93. #
  94. shellfu _list_mfiles \
  95. | sed 's|\.sh$||; s|.*/||;' \
  96. | __sfdoc__filter_hidden m
  97. }
  98. __sfdoc__export_as_manpage() {
  99. #
  100. # Export module $Module doc as manpage
  101. #
  102. __sfdoc__export_as_pod \
  103. | pod2man \
  104. --name "${MName^^}" \
  105. --center "User Contributed Shellfu Documentation" \
  106. --section 3x \
  107. --date "$(date -I -r "$MPath")" \
  108. --release "$MName $MVer" \
  109. --utf8 -q \`
  110. }
  111. __sfdoc__export_as_markdown() {
  112. #
  113. # Export module $Module doc as Markdown
  114. #
  115. local variable
  116. local function
  117. local vheader_done=false
  118. local fheader_done=false
  119. debug -v module
  120. echo "$MName"
  121. echo "$MName" | tr -c '\n' '[=*]'
  122. echo
  123. __sfdoc__get_doc | __sfdoc__md_escapes
  124. echo
  125. for variable in $(sfdoc__ls v "$MPath" | cut -d. -f2);
  126. do
  127. debug -v variable
  128. $vheader_done || {
  129. echo
  130. echo Variables
  131. echo ---------
  132. vheader_done=true
  133. }
  134. echo
  135. echo "### \`\$$variable\` ###"
  136. echo ""
  137. __sfdoc__get_doc "v:$variable" | __sfdoc__md_escapes
  138. echo
  139. done
  140. for function in $(sfdoc__ls f "$MPath" | cut -d. -f2);
  141. do
  142. debug -v function
  143. $fheader_done || {
  144. echo
  145. echo Functions
  146. echo ---------
  147. fheader_done=true
  148. }
  149. echo
  150. echo "### \`$function()\` ###"
  151. echo ""
  152. __sfdoc__get_doc "f:$function" | __sfdoc__md_escapes
  153. echo
  154. done
  155. }
  156. __sfdoc__export_as_pod() {
  157. #
  158. # Export module $Module doc as POD
  159. #
  160. local variable
  161. local function
  162. local vheader_done=false
  163. local fheader_done=false
  164. local indented=false
  165. echo "true <<'=cut'"
  166. echo "=pod"
  167. echo
  168. echo "=encoding $Encoding"
  169. echo
  170. echo "=head1 NAME"
  171. echo
  172. echo "$MName - $(__sfdoc__get_doc | head -1)"
  173. echo
  174. echo "=head1 DESCRIPTION"
  175. echo
  176. __sfdoc__get_doc
  177. echo
  178. for variable in $(sfdoc__ls v "$MPath" | cut -d. -f2);
  179. do
  180. debug -v variable
  181. $vheader_done || {
  182. echo
  183. echo "=head1 VARIABLES"
  184. vheader_done=true
  185. echo
  186. echo "=over 8"
  187. echo
  188. indented=true
  189. }
  190. echo
  191. echo "=item I<\$$variable>"
  192. echo
  193. __sfdoc__get_doc "v:$variable"
  194. echo
  195. done
  196. $indented && {
  197. echo "=back"
  198. echo
  199. }
  200. for function in $(sfdoc__ls f "$MPath" | cut -d. -f2);
  201. do
  202. debug -v function
  203. $fheader_done || {
  204. echo
  205. echo "=head1 FUNCTIONS"
  206. fheader_done=true
  207. echo
  208. echo "=over 8"
  209. echo
  210. indented=true
  211. }
  212. echo
  213. echo "=item I<$function()>"
  214. echo
  215. __sfdoc__get_doc "f:$function"
  216. echo
  217. done
  218. $indented && {
  219. echo "=back"
  220. echo
  221. }
  222. echo "=cut"
  223. }
  224. __sfdoc__filter_body() {
  225. #
  226. # Filter part $1 of body on stdin
  227. #
  228. local part=$1
  229. case "$part" in
  230. f:*) __sfdoc__filter_fun "${part:2}" ;;
  231. v:*) __sfdoc__filter_var "${part:2}" ;;
  232. *) warn "bug: invalid part specification $part" ;;
  233. esac
  234. }
  235. __sfdoc__filter_doc() {
  236. #
  237. # Filter docstring for part $1 from object body on stdin
  238. #
  239. local part=$1
  240. case $part in
  241. "") __sfdoc__filter_mdoc ;;
  242. v:*) __sfdoc__filter_vdoc ;;
  243. f:*) __sfdoc__filter_fdoc ;;
  244. *) warn "bug: invalid part specification: $part" ;;
  245. esac
  246. }
  247. __sfdoc__filter_fun() {
  248. #
  249. # From module body on stdin, filter out function named $1
  250. #
  251. # Function definition should look like:
  252. #
  253. # foo_fun() {
  254. # foo=$1
  255. # }
  256. #
  257. # That is,
  258. #
  259. # * no `function` keyword,
  260. # * name starts the first line,
  261. # * name is followed by '() {' with no additional spaces,
  262. # * function end is denoted by line with a single '}'.
  263. #
  264. local fn_name=$1
  265. name=$fn_name perl -we '
  266. undef $/;
  267. my $name = $ENV{name};
  268. my ($fbody) = <> =~ m/^($name\(\) \{.*?^\}$)/ms;
  269. print "$fbody\n" if defined $fbody;
  270. '
  271. }
  272. __sfdoc__filter_fdoc() {
  273. #
  274. # Filter docstring from function body on stdin
  275. #
  276. # Look for:
  277. #
  278. # 1. line "^ #$" to open docstring - this must be
  279. # the first one after function name (`fun() {`)
  280. # 2. block of consecutive docstring lines (i.e. matching
  281. # "^ # " or "^ #$"
  282. # 3. last "^ #$" line to close docstring.
  283. # 4. Next line must not match any of the patterns above.
  284. #
  285. # For example, stdin like this:
  286. #
  287. # myfun() {
  288. # #
  289. # # Do my thing with foo $1
  290. # #
  291. # # Detailed description, possibly spanning
  292. # # several paragraph.
  293. # #
  294. # # The format here should be Markdown.
  295. # #
  296. # local foo=$1
  297. # printf %p "${foo:4}"
  298. # }
  299. #
  300. # would be read as:
  301. #
  302. # Do my thing with foo $1
  303. #
  304. # Detailed description, possibly spanning
  305. # several paragraph.
  306. #
  307. # The format here should be Markdown.
  308. #
  309. # However if we added following line right before the `local`
  310. # declaration
  311. #
  312. # #FIXME: TODO/FIXME lines are not dropped properly
  313. #
  314. # it **will not** be considered part of the docstring. This is
  315. # to allow for special comments tools like TODO/FIXME or
  316. # data for lint-like tools that are not part of function
  317. # description.
  318. #
  319. perl -we '
  320. my $isdoc;
  321. while (<>) {
  322. if (m/^ #$/) {
  323. $isdoc = 1;
  324. } elsif (m/^ [^#]/) {
  325. exit;
  326. }
  327. next unless $isdoc;
  328. s/^ //;
  329. print;
  330. }' | __sfdoc__strip_doc
  331. }
  332. __sfdoc__filter_hidden() {
  333. #
  334. # From objects on stdin, print only visible unless $SFDOC_SHOW_HIDDEN
  335. #
  336. local what=$1
  337. $SFDOC_SHOW_HIDDEN && cat && return
  338. case $what in
  339. m) grep -v ^_ ;;
  340. v) grep -v \\._ ;;
  341. f) grep -v \\._ ;;
  342. *) warn "bug: invalid object type: $otype" ;;
  343. esac
  344. }
  345. __sfdoc__filter_mdoc() {
  346. #
  347. # From module body on stdin, filter module doc
  348. #
  349. # Example:
  350. #
  351. # #!/bin/bash
  352. # #
  353. # # legal stuff
  354. #
  355. # shellfu import irrelevant_stuff
  356. #
  357. # #
  358. # # Module is cool this
  359. # #
  360. # # Ok so this is my
  361. # # "module", heh...
  362. # #
  363. #
  364. perl -we '
  365. sub ok_chunk {
  366. my $chunk = shift;
  367. my @lines = split "\n", $_;
  368. chomp @lines;
  369. return 0 unless (defined $lines[0] and $lines[0] eq "#");
  370. return 0 unless $lines[$#lines] eq "#";
  371. foreach (@lines) {
  372. return 0 unless (m/^#$/ or m/^# /);
  373. }
  374. return 1
  375. }
  376. my @chunks;
  377. my @lines;
  378. $/ = "\n\n";
  379. @chunks = <STDIN>;
  380. foreach (@chunks) {
  381. if (ok_chunk "$_") {
  382. s/\n+$//;
  383. print "$_\n";
  384. exit 0;
  385. }
  386. }
  387. exit 1
  388. ' | __sfdoc__strip_doc
  389. }
  390. __sfdoc__filter_var() {
  391. #
  392. # From module body on stdin, filter variable definition named $1
  393. #
  394. # Look for:
  395. #
  396. # 1. Empty line, followed by line with single hash sign
  397. # 2. Set of consecutive docstring lines (start with '# ')
  398. # 3. Another "#" followed by one or more variable definitions (start
  399. # with $name or \w+, followed by equal sign), at least one of which
  400. # must be $1
  401. # 4. Empty line or EOF
  402. #
  403. # and return what is found.
  404. #
  405. # For example:
  406. #
  407. # #
  408. # # My variable
  409. # #
  410. # my_var=foo
  411. #
  412. # is found if $1 is 'my_var'
  413. #
  414. # A more advanced example:
  415. #
  416. # #
  417. # # Bar variables
  418. # #
  419. # # These serve similar purpose; the difference is
  420. # # obvious
  421. # #
  422. # bar_1=one
  423. # bar_2=two
  424. # bar_3=three
  425. #
  426. # is found if $1 is either 'bar_1', 'bar_2' or 'bar_3'. This allows
  427. # for a docstring being shared among closely related variables.
  428. #
  429. local oname=$1 # object name
  430. local state # void, edge, dstr, vstr, junk
  431. local o_end # object ended; is complete
  432. local cache # buffer of hope
  433. # parse and keep each var docstring in cache;
  434. # then decide whether or not print the cache
  435. cache=$(mktemp -t shellfu_doc.__sfdoc__filter_var.XXXXXXXX)
  436. state=junk
  437. o_end=false
  438. while read -r line;
  439. do
  440. case $state:$line in
  441. # looks like void
  442. void:) state=void; o_end=false ;;
  443. edge:) state=void; o_end=true ;;
  444. dstr:) state=void; o_end=true ;;
  445. vstr:) state=void; o_end=true ;;
  446. junk:) state=void; o_end=false ;;
  447. # looks like edge
  448. void:"#") state=edge; o_end=false ;;
  449. edge:"#") state=dstr; o_end=false ;;
  450. dstr:"#") state=dstr; o_end=false ;;
  451. vstr:"#") state=dstr; o_end=false ;;
  452. junk:"#") state=junk; o_end=false ;;
  453. # looks like docstring
  454. void:"# "*) state=junk; o_end=false ;;
  455. edge:"# "*) state=dstr; o_end=false ;;
  456. dstr:"# "*) state=dstr; o_end=false ;;
  457. vstr:"# "*) state=dstr; o_end=false ;;
  458. junk:"# "*) state=junk; o_end=false ;;
  459. # looks like junk
  460. void:"#"*) state=junk; o_end=false ;;
  461. edge:"#"*) state=junk; o_end=false ;;
  462. dstr:"#"*) state=junk; o_end=false ;;
  463. vstr:"#"*) state=junk; o_end=false ;;
  464. junk:"#"*) state=junk; o_end=false ;;
  465. # looks like variable string
  466. void:*=*) state=vstr; o_end=false ;;
  467. edge:*=*) state=junk; o_end=false ;;
  468. dstr:*=*) state=vstr; o_end=false ;;
  469. vstr:*=*) state=vstr; o_end=false ;;
  470. junk:*=*) state=junk; o_end=false ;;
  471. # looks like something else
  472. *) state=junk; o_end=false ;;
  473. esac
  474. case $state in
  475. edge|dstr|vstr) echo "$line" >> "$cache" ;;
  476. esac
  477. if $o_end;
  478. then
  479. if grep -q "^$oname=" "$cache"; # it's our wanted object
  480. then
  481. cat "$cache"
  482. rm "$cache"
  483. return 0
  484. fi
  485. rm "$cache"
  486. fi
  487. done
  488. rm "$cache"
  489. return 1
  490. }
  491. __sfdoc__filter_vdoc() {
  492. #
  493. # From variable definition body on stdin, filter doc
  494. #
  495. # Example:
  496. #
  497. # #
  498. # # Bar variables
  499. # #
  500. # # These serve similar purpose; the difference is
  501. # # obvious
  502. # #
  503. # bar_1=one
  504. # bar_2=two
  505. # bar_3=three
  506. #
  507. grep '^#' | __sfdoc__strip_doc
  508. }
  509. __sfdoc__get_doc() {
  510. #
  511. # Show doc for part $2 of module $1
  512. #
  513. local part=$1
  514. __sfdoc__get_part "$part" | __sfdoc__filter_doc "$part"
  515. }
  516. __sfdoc__get_part() {
  517. #
  518. # Print part $1 (f|v:name) of module $MPath
  519. #
  520. # Part has format
  521. #
  522. # TYPE:NAME
  523. #
  524. # where TYPE is 'v' for variables and f for functions, and
  525. # NAME is name of the part.
  526. #
  527. # If part is not specified, whole module file is printed.
  528. #
  529. local part=$1
  530. test -n "$part" || { cat "$MPath"; return $?; }
  531. <"$MPath" __sfdoc__filter_body "$part"
  532. }
  533. __sfdoc__md_escapes() {
  534. #
  535. # Do additional escapes for Markdown
  536. #
  537. # In docstrings, references to variables are often done
  538. # without backticks; add these to prevent interpreting
  539. # underscores in variable names as cursive.
  540. #
  541. perl -ne '
  542. my @bigwords = ();
  543. my @newwords = ();
  544. if (m|^ |) {
  545. print; # probably code -- leave be
  546. } else {
  547. @bigwords = split " ";
  548. foreach (@bigwords) {
  549. if (m|`\w+\(\)`|) {
  550. push @newwords, $_; # function: escaped
  551. } elsif (m|(.*)\b(\w+)\(\)(.*)|) {
  552. push @newwords, "$1`$2()`$3"; # function: needs escape
  553. } elsif (m|`\$\w+`|) {
  554. push @newwords, $_; # variable: escaped
  555. } elsif (m|(.*)\$(\w+)(.*)|) {
  556. push @newwords, "$1`\$$2`$3"; # variable: needs escape
  557. } else {
  558. push @newwords, $_; # a normal word
  559. }
  560. }
  561. print join " ", @newwords;
  562. print "\n";
  563. }
  564. '
  565. }
  566. __sfdoc__strip_doc() {
  567. #
  568. # Strip doc out of Shellfu docstring
  569. #
  570. # From e.g.
  571. #
  572. # #
  573. # # foo
  574. # #
  575. # # bar
  576. # # baz
  577. # #
  578. #
  579. # where first and last line are mandatory and each line
  580. # must be either '#' or '# *', get:
  581. #
  582. # foo
  583. #
  584. # bar
  585. # baz
  586. #
  587. # just as when you're parsing this text by eyes.
  588. #
  589. head -n -1 \
  590. | tail -n +2 \
  591. | sed -e 's/^#$//; s/^# //;'
  592. }