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

sfdoc.sh 17KB

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