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

shellfu-doc 19KB

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