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

sfdoc.sh 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  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. 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); do
  126. debug -v variable
  127. $vheader_done || {
  128. echo
  129. echo Variables
  130. echo ---------
  131. vheader_done=true
  132. }
  133. echo
  134. echo "### \`\$$variable\` ###"
  135. echo ""
  136. __sfdoc__get_doc "v:$variable" | __sfdoc__md_escapes
  137. echo
  138. done
  139. for function in $(sfdoc__ls f "$MPath" | cut -d: -f2); do
  140. debug -v function
  141. $fheader_done || {
  142. echo
  143. echo Functions
  144. echo ---------
  145. fheader_done=true
  146. }
  147. echo
  148. echo "### \`$function()\` ###"
  149. echo ""
  150. __sfdoc__get_doc "f:$function" | __sfdoc__md_escapes
  151. echo
  152. done
  153. }
  154. __sfdoc__export_as_pod() {
  155. #
  156. # Export module $Module doc as POD
  157. #
  158. local variable
  159. local function
  160. local vheader_done=false
  161. local fheader_done=false
  162. local indented=false
  163. echo "true <<'=cut'"
  164. echo "=pod"
  165. echo
  166. echo "=encoding $Encoding"
  167. echo
  168. echo "=head1 NAME"
  169. echo
  170. echo "$MName - $(__sfdoc__get_doc | head -1)"
  171. echo
  172. echo "=head1 DESCRIPTION"
  173. echo
  174. __sfdoc__get_doc
  175. echo
  176. for variable in $(sfdoc__ls v "$MPath" | cut -d: -f2); do
  177. debug -v variable
  178. $vheader_done || {
  179. echo
  180. echo "=head1 VARIABLES"
  181. vheader_done=true
  182. echo
  183. echo "=over 8"
  184. echo
  185. indented=true
  186. }
  187. echo
  188. echo "=item I<\$$variable>"
  189. echo
  190. __sfdoc__get_doc "v:$variable"
  191. echo
  192. done
  193. $indented && {
  194. echo "=back"
  195. echo
  196. }
  197. for function in $(sfdoc__ls f "$MPath" | cut -d: -f2); do
  198. debug -v function
  199. $fheader_done || {
  200. echo
  201. echo "=head1 FUNCTIONS"
  202. fheader_done=true
  203. echo
  204. echo "=over 8"
  205. echo
  206. indented=true
  207. }
  208. echo
  209. echo "=item I<$function()>"
  210. echo
  211. __sfdoc__get_doc "f:$function"
  212. echo
  213. done
  214. $indented && {
  215. echo "=back"
  216. echo
  217. }
  218. echo "=cut"
  219. }
  220. __sfdoc__filter_body() {
  221. #
  222. # Filter part $1 of body on stdin
  223. #
  224. local part=$1
  225. case "$part" in
  226. f:*) __sfdoc__filter_fun "${part:2}" ;;
  227. v:*) __sfdoc__filter_var "${part:2}" ;;
  228. *) warn "bug: invalid part specification $part" ;;
  229. esac
  230. }
  231. __sfdoc__filter_doc() {
  232. #
  233. # Filter docstring for part $1 from object body on stdin
  234. #
  235. local part=$1
  236. case $part in
  237. "") __sfdoc__filter_mdoc ;;
  238. v:*) __sfdoc__filter_vdoc ;;
  239. f:*) __sfdoc__filter_fdoc ;;
  240. *) warn "bug: invalid part specification: $part" ;;
  241. esac
  242. }
  243. __sfdoc__filter_fun() {
  244. #
  245. # From module body on stdin, filter out function named $1
  246. #
  247. # Function definition should look like:
  248. #
  249. # foo_fun() {
  250. # foo=$1
  251. # }
  252. #
  253. # That is,
  254. #
  255. # * no `function` keyword,
  256. # * name starts the first line,
  257. # * name is followed by '() {' with no additional spaces,
  258. # * function end is denoted by line with a single '}'.
  259. #
  260. local fn_name=$1
  261. name=$fn_name perl -we '
  262. undef $/;
  263. my $name = $ENV{name};
  264. my ($fbody) = <> =~ m/^($name\(\) \{.*?^\}$)/ms;
  265. print "$fbody\n" if defined $fbody;
  266. '
  267. }
  268. __sfdoc__filter_fdoc() {
  269. #
  270. # Filter docstring from function body on stdin
  271. #
  272. # Look for:
  273. #
  274. # 1. line "^ #$" to open docstring - this must be
  275. # the first one after function name (`fun() {`)
  276. # 2. block of consecutive docstring lines (i.e. matching
  277. # "^ # " or "^ #$"
  278. # 3. last "^ #$" line to close docstring.
  279. # 4. Next line must not match any of the patterns above.
  280. #
  281. # For example, stdin like this:
  282. #
  283. # myfun() {
  284. # #
  285. # # Do my thing with foo $1
  286. # #
  287. # # Detailed description, possibly spanning
  288. # # several paragraph.
  289. # #
  290. # # The format here should be Markdown.
  291. # #
  292. # local foo=$1
  293. # printf %p "${foo:4}"
  294. # }
  295. #
  296. # would be read as:
  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. # However if we added following line right before the `local`
  306. # declaration
  307. #
  308. # #FIXME: TODO/FIXME lines are not dropped properly
  309. #
  310. # it **will not** be considered part of the docstring. This is
  311. # to allow for special comments tools like TODO/FIXME or
  312. # data for lint-like tools that are not part of function
  313. # description.
  314. #
  315. perl -we '
  316. my $isdoc;
  317. while (<>) {
  318. if (m/^ #$/) {
  319. $isdoc = 1;
  320. } elsif (m/^ [^#]/) {
  321. exit;
  322. }
  323. next unless $isdoc;
  324. s/^ //;
  325. print;
  326. }' | __sfdoc__strip_doc
  327. }
  328. __sfdoc__filter_hidden() {
  329. #
  330. # From objects on stdin, print only visible unless $SFDOC_SHOW_HIDDEN
  331. #
  332. local what=$1
  333. $SFDOC_SHOW_HIDDEN && cat && return
  334. case $what in
  335. m) grep -v ^_ ;;
  336. v) grep -v :_ ;;
  337. f) grep -v :_ ;;
  338. *) warn "bug: invalid object type: $otype" ;;
  339. esac
  340. }
  341. __sfdoc__filter_mdoc() {
  342. #
  343. # From module body on stdin, filter module doc
  344. #
  345. # Example:
  346. #
  347. # #!/bin/bash
  348. # #
  349. # # legal stuff
  350. #
  351. # shellfu import irrelevant_stuff
  352. #
  353. # #
  354. # # Module is cool this
  355. # #
  356. # # Ok so this is my
  357. # # "module", heh...
  358. # #
  359. #
  360. perl -we '
  361. sub ok_chunk {
  362. my $chunk = shift;
  363. my @lines = split "\n", $_;
  364. chomp @lines;
  365. return 0 unless (defined $lines[0] and $lines[0] eq "#");
  366. return 0 unless $lines[$#lines] eq "#";
  367. foreach (@lines) {
  368. return 0 unless (m/^#$/ or m/^# /);
  369. }
  370. return 1
  371. }
  372. my @chunks;
  373. my @lines;
  374. $/ = "\n\n";
  375. @chunks = <STDIN>;
  376. foreach (@chunks) {
  377. if (ok_chunk "$_") {
  378. s/\n+$//;
  379. print "$_\n";
  380. exit 0;
  381. }
  382. }
  383. exit 1
  384. ' | __sfdoc__strip_doc
  385. }
  386. __sfdoc__filter_var() {
  387. #
  388. # From module body on stdin, filter variable definition named $1
  389. #
  390. # Look for:
  391. #
  392. # 1. Empty line, followed by line with single hash sign
  393. # 2. Set of consecutive docstring lines (start with '# ')
  394. # 3. Another "#" followed by one or more variable definitions (start
  395. # with $name or \w+, followed by equal sign), at least one of which
  396. # must be $1
  397. # 4. Empty line or EOF
  398. #
  399. # and return what is found.
  400. #
  401. # For example:
  402. #
  403. # #
  404. # # My variable
  405. # #
  406. # my_var=foo
  407. #
  408. # is found if $1 is 'my_var'
  409. #
  410. # A more advanced example:
  411. #
  412. # #
  413. # # Bar variables
  414. # #
  415. # # These serve similar purpose; the difference is
  416. # # obvious
  417. # #
  418. # bar_1=one
  419. # bar_2=two
  420. # bar_3=three
  421. #
  422. # is found if $1 is either 'bar_1', 'bar_2' or 'bar_3'. This allows
  423. # for a docstring being shared among closely related variables.
  424. #
  425. local oname=$1 # object name
  426. local state # void, edge, dstr, vstr, junk
  427. local o_end # object ended; is complete
  428. local cache # buffer of hope
  429. # parse and keep each var docstring in cache;
  430. # then decide whether or not print the cache
  431. cache=$(mktemp -t shellfu_doc.__sfdoc__filter_var.XXXXXXXX)
  432. state=junk
  433. o_end=false
  434. while read -r line; do
  435. case $state:$line in
  436. # looks like void
  437. void:) state=void; o_end=false ;;
  438. edge:) state=void; o_end=true ;;
  439. dstr:) state=void; o_end=true ;;
  440. vstr:) state=void; o_end=true ;;
  441. junk:) state=void; o_end=false ;;
  442. # looks like edge
  443. void:"#") state=edge; o_end=false ;;
  444. edge:"#") state=dstr; o_end=false ;;
  445. dstr:"#") state=dstr; o_end=false ;;
  446. vstr:"#") state=dstr; o_end=false ;;
  447. junk:"#") state=junk; o_end=false ;;
  448. # looks like docstring
  449. void:"# "*) state=junk; o_end=false ;;
  450. edge:"# "*) state=dstr; o_end=false ;;
  451. dstr:"# "*) state=dstr; o_end=false ;;
  452. vstr:"# "*) state=dstr; o_end=false ;;
  453. junk:"# "*) state=junk; o_end=false ;;
  454. # looks like junk
  455. void:"#"*) state=junk; o_end=false ;;
  456. edge:"#"*) state=junk; o_end=false ;;
  457. dstr:"#"*) state=junk; o_end=false ;;
  458. vstr:"#"*) state=junk; o_end=false ;;
  459. junk:"#"*) state=junk; o_end=false ;;
  460. # looks like variable string
  461. void:*=*) state=vstr; o_end=false ;;
  462. edge:*=*) state=junk; o_end=false ;;
  463. dstr:*=*) state=vstr; o_end=false ;;
  464. vstr:*=*) state=vstr; o_end=false ;;
  465. junk:*=*) state=junk; o_end=false ;;
  466. # looks like something else
  467. *) state=junk; o_end=false ;;
  468. esac
  469. case $state in
  470. edge|dstr|vstr) echo "$line" >> "$cache" ;;
  471. esac
  472. if $o_end; then
  473. if grep -q "^$oname=" "$cache"; then # it's our wanted object
  474. cat "$cache"
  475. rm "$cache"
  476. return 0
  477. fi
  478. rm "$cache"
  479. fi
  480. done
  481. rm "$cache"
  482. return 1
  483. }
  484. __sfdoc__filter_vdoc() {
  485. #
  486. # From variable definition body on stdin, filter doc
  487. #
  488. # Example:
  489. #
  490. # #
  491. # # Bar variables
  492. # #
  493. # # These serve similar purpose; the difference is
  494. # # obvious
  495. # #
  496. # bar_1=one
  497. # bar_2=two
  498. # bar_3=three
  499. #
  500. grep '^#' | __sfdoc__strip_doc
  501. }
  502. __sfdoc__get_doc() {
  503. #
  504. # Show doc for part $2 of module $1
  505. #
  506. local part=$1
  507. __sfdoc__get_part "$part" | __sfdoc__filter_doc "$part"
  508. }
  509. __sfdoc__get_part() {
  510. #
  511. # Print part $1 (f|v:name) of module $MPath
  512. #
  513. # Part has format
  514. #
  515. # TYPE:NAME
  516. #
  517. # where TYPE is 'v' for variables and f for functions, and
  518. # NAME is name of the part.
  519. #
  520. # If part is not specified, whole module file is printed.
  521. #
  522. local part=$1
  523. test -n "$part" || { cat "$MPath"; return $?; }
  524. <"$MPath" __sfdoc__filter_body "$part"
  525. }
  526. __sfdoc__md_escapes() {
  527. #
  528. # Do additional escapes for Markdown
  529. #
  530. # In docstrings, references to variables are often done
  531. # without backticks; add these to prevent interpreting
  532. # underscores in variable names as cursive.
  533. #
  534. perl -ne '
  535. my @bigwords = ();
  536. my @newwords = ();
  537. if (m|^ |) {
  538. print; # probably code -- leave be
  539. } else {
  540. @bigwords = split " ";
  541. foreach (@bigwords) {
  542. if (m|`\w+\(\)`|) {
  543. push @newwords, $_; # function: escaped
  544. } elsif (m|(.*)\b(\w+)\(\)(.*)|) {
  545. push @newwords, "$1`$2()`$3"; # function: needs escape
  546. } elsif (m|`\$\w+`|) {
  547. push @newwords, $_; # variable: escaped
  548. } elsif (m|(.*)\$(\w+)(.*)|) {
  549. push @newwords, "$1`\$$2`$3"; # variable: needs escape
  550. } else {
  551. push @newwords, $_; # a normal word
  552. }
  553. }
  554. print join " ", @newwords;
  555. print "\n";
  556. }
  557. '
  558. }
  559. __sfdoc__strip_doc() {
  560. #
  561. # Strip doc out of Shellfu docstring
  562. #
  563. # From e.g.
  564. #
  565. # #
  566. # # foo
  567. # #
  568. # # bar
  569. # # baz
  570. # #
  571. #
  572. # where first and last line are mandatory and each line
  573. # must be either '#' or '# *', get:
  574. #
  575. # foo
  576. #
  577. # bar
  578. # baz
  579. #
  580. # just as when you're parsing this text by eyes.
  581. #
  582. head -n -1 \
  583. | tail -n +2 \
  584. | sed -e 's/^#$//; s/^# //;'
  585. }