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

sfdoc.sh 15KB

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