123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- #!/bin/bash
-
- shellfu import pretty
-
- #
- # Explore installed Shellfu modules
- #
- # Modules following Shellfu coding style[1] and installed at one of
- # directories listed by $SHELLFU_PATH can be listed and explored using
- # functions in this module.
- #
- # [1]: https://github.com/AloisMahdal/shellfu/blob/master/notes/style.md
- #
- # Note that this module exists in order to allow developers implement commands
- # similar to sfdoc, which is preferred choice for normal use as it provides
- # more feature and better user experience.
- #
-
- #
- # Show hidden objects (modules, functions, variables)?
- #
- # An object (module, function or variable) is considered hidden if its name starts
- # with `_` (underscore). These are ignored by default. Set this to 'true' to show
- # them.
- #
- SFDOC_SHOW_HIDDEN=${SFDOC_SHOW_HIDDEN:-false}
-
- sfdoc__export() {
- #
- # Export docs of module $2 from file $3 as format $3
- #
- # Usage:
- #
- # sfdoc__export FMT NAME PATH [ENCODING]
- #
- # Exports docs of module file at PATH, in format FMT, displaying
- # NAME as "natural" name of the module. Supported FMTs are
- # 'manpage', 'markdown' and 'pod'. ENCODING is currently used by
- # 'pod' format only and is 'utf8' by default.
- #
- local format=$1 # format to export in
- local MName=$2 # name to use in headers
- local MPath=$3 # path to file to export
- local Encoding=${4:-utf8} # file encoding
- local MVer # module version (if found)
- local exfun # export function
- local usage="usage: sfdoc__export FMT NAME PATH [ENCODING]"
- test -n "$format" || { warn "$usage"; return 2; }
- test -n "$MPath" || { warn "$usage"; return 2; }
- test -n "$MName" || { warn "$usage"; return 2; }
- exfun="__sfdoc__export_as_$format"
- type -t "$exfun" >/dev/null || {
- warn "unknown format: $format"
- return 2
- }
- MVer="v$(shellfu _read_directive "module-version" "$MPath" 2>/dev/null)"
- test "$MVer" == "v" && MVer="(no version)"
- debug -v format MName MPath Encoding MVer
- "$exfun"
- }
-
- sfdoc__ls() {
- #
- # List all objects of type $1 in module file $2
- #
- # Usage:
- #
- # sfdoc__ls TYPE PATH
- #
- # TYPE can be 'f' for functions or 'v' for variables. PATH
- # must be path to a module following Shellfu coding style
- # (see DESCRIPTION section).
- #
- local otype=$1
- local mpath=$2
- case $otype in
- f)
- grep -HE '^[[:alnum:]_]+\(\) \{' "$mpath" \
- | sed -e 's|(.*||; s|\.sh:|.|; s|^.*/||'
- ;;
- v)
- grep -HE '^[[:alnum:]_]+=' "$mpath" \
- | sed -e 's|=.*||; s|\.sh:|.|; s|^.*/||'
- ;;
- *)
- warn "bug: invalid object type: $otype"
- ;;
- esac \
- | __sfdoc__filter_hidden "$otype"
- }
-
- sfdoc__ls_m() {
- #
- # List all installed modules
- #
- # An installed module is a shell file named ending with '.sh' and
- # placed in one of directories listed in comma-separated list
- # in environment variable $SHELLFU_PATH.
- #
- shellfu _list_mfiles \
- | sed 's|\.sh$||; s|.*/||;' \
- | __sfdoc__filter_hidden m
- }
-
- __sfdoc__export_as_manpage() {
- #
- # Export module $Module doc as manpage
- #
- __sfdoc__export_as_pod \
- | pod2man \
- --name "${MName^^}" \
- --center "User Contributed Shellfu Documentation" \
- --section 3x \
- --date "$(date -I -r "$MPath")" \
- --release "$MName $MVer" \
- --utf8 -q \`
- }
-
- __sfdoc__export_as_markdown() {
- #
- # Export module $Module doc as Markdown
- #
- local variable
- local function
- local vheader_done=false
- local fheader_done=false
- debug -v module
- echo "$MName"
- echo "$MName" | tr -c '\n' '[=*]'
- echo
- __sfdoc__get_doc | __sfdoc__md_escapes
- echo
- for variable in $(sfdoc__ls v "$MPath" | cut -d. -f2);
- do
- debug -v variable
- $vheader_done || {
- echo
- echo Variables
- echo ---------
- vheader_done=true
- }
- echo
- echo "### \`\$$variable\` ###"
- echo ""
- __sfdoc__get_doc "v:$variable" | __sfdoc__md_escapes
- echo
- done
- for function in $(sfdoc__ls f "$MPath" | cut -d. -f2);
- do
- debug -v function
- $fheader_done || {
- echo
- echo Functions
- echo ---------
- fheader_done=true
- }
- echo
- echo "### \`$function()\` ###"
- echo ""
- __sfdoc__get_doc "f:$function" | __sfdoc__md_escapes
- echo
- done
- }
-
- __sfdoc__export_as_pod() {
- #
- # Export module $Module doc as POD
- #
- local variable
- local function
- local vheader_done=false
- local fheader_done=false
- local indented=false
- echo "true <<'=cut'"
- echo "=pod"
- echo
- echo "=encoding $Encoding"
- echo
- echo "=head1 NAME"
- echo
- echo "$MName - $(__sfdoc__get_doc | head -1)"
- echo
- echo "=head1 DESCRIPTION"
- echo
- __sfdoc__get_doc
- echo
- for variable in $(sfdoc__ls v "$MPath" | cut -d. -f2);
- do
- debug -v variable
- $vheader_done || {
- echo
- echo "=head1 VARIABLES"
- vheader_done=true
- echo
- echo "=over 8"
- echo
- indented=true
- }
- echo
- echo "=item I<\$$variable>"
- echo
- __sfdoc__get_doc "v:$variable"
- echo
- done
- $indented && {
- echo "=back"
- echo
- }
- for function in $(sfdoc__ls f "$MPath" | cut -d. -f2);
- do
- debug -v function
- $fheader_done || {
- echo
- echo "=head1 FUNCTIONS"
- fheader_done=true
- echo
- echo "=over 8"
- echo
- indented=true
- }
- echo
- echo "=item I<$function()>"
- echo
- __sfdoc__get_doc "f:$function"
- echo
- done
- $indented && {
- echo "=back"
- echo
- }
- echo "=cut"
- }
-
- __sfdoc__filter_body() {
- #
- # Filter part $1 of body on stdin
- #
- local part=$1
- case "$part" in
- f:*) __sfdoc__filter_fun "${part:2}" ;;
- v:*) __sfdoc__filter_var "${part:2}" ;;
- *) warn "bug: invalid part specification $part" ;;
- esac
- }
-
- __sfdoc__filter_doc() {
- #
- # Filter docstring for part $1 from object body on stdin
- #
- local part=$1
- case $part in
- "") __sfdoc__filter_mdoc ;;
- v:*) __sfdoc__filter_vdoc ;;
- f:*) __sfdoc__filter_fdoc ;;
- *) warn "bug: invalid part specification: $part" ;;
- esac
- }
-
- __sfdoc__filter_fun() {
- #
- # From module body on stdin, filter out function named $1
- #
- # Function definition should look like:
- #
- # foo_fun() {
- # foo=$1
- # }
- #
- # That is,
- #
- # * no `function` keyword,
- # * name starts the first line,
- # * name is followed by '() {' with no additional spaces,
- # * function end is denoted by line with a single '}'.
- #
- local fn_name=$1
- name=$fn_name perl -we '
- undef $/;
- my $name = $ENV{name};
- my ($fbody) = <> =~ m/^($name\(\) \{.*?^\}$)/ms;
- print "$fbody\n" if defined $fbody;
- '
- }
-
- __sfdoc__filter_fdoc() {
- #
- # Filter docstring from function body on stdin
- #
- # Look for:
- #
- # 1. line "^ #$" to open docstring - this must be
- # the first one after function name (`fun() {`)
- # 2. block of consecutive docstring lines (i.e. matching
- # "^ # " or "^ #$"
- # 3. last "^ #$" line to close docstring.
- # 4. Next line must not match any of the patterns above.
- #
- # For example, stdin like this:
- #
- # myfun() {
- # #
- # # Do my thing with foo $1
- # #
- # # Detailed description, possibly spanning
- # # several paragraph.
- # #
- # # The format here should be Markdown.
- # #
- # local foo=$1
- # printf %p "${foo:4}"
- # }
- #
- # would be read as:
- #
- # Do my thing with foo $1
- #
- # Detailed description, possibly spanning
- # several paragraph.
- #
- # The format here should be Markdown.
- #
- # However if we added following line right before the `local`
- # declaration
- #
- # #FIXME: TODO/FIXME lines are not dropped properly
- #
- # it **will not** be considered part of the docstring. This is
- # to allow for special comments tools like TODO/FIXME or
- # data for lint-like tools that are not part of function
- # description.
- #
- perl -we '
- my $isdoc;
- while (<>) {
- if (m/^ #$/) {
- $isdoc = 1;
- } elsif (m/^ [^#]/) {
- exit;
- }
- next unless $isdoc;
- s/^ //;
- print;
- }' | __sfdoc__strip_doc
- }
-
- __sfdoc__filter_hidden() {
- #
- # From objects on stdin, print only visible unless $SFDOC_SHOW_HIDDEN
- #
- local what=$1
- $SFDOC_SHOW_HIDDEN && cat && return
- case $what in
- m) grep -v ^_ ;;
- v) grep -v \\._ ;;
- f) grep -v \\._ ;;
- *) warn "bug: invalid object type: $otype" ;;
- esac
- }
-
- __sfdoc__filter_mdoc() {
- #
- # From module body on stdin, filter module doc
- #
- # Example:
- #
- # #!/bin/bash
- # #
- # # legal stuff
- #
- # shellfu import irrelevant_stuff
- #
- # #
- # # Module is cool this
- # #
- # # Ok so this is my
- # # "module", heh...
- # #
- #
- perl -we '
- sub ok_chunk {
- my $chunk = shift;
- my @lines = split "\n", $_;
- chomp @lines;
- return 0 unless (defined $lines[0] and $lines[0] eq "#");
- return 0 unless $lines[$#lines] eq "#";
- foreach (@lines) {
- return 0 unless (m/^#$/ or m/^# /);
- }
- return 1
- }
- my @chunks;
- my @lines;
- $/ = "\n\n";
- @chunks = <STDIN>;
- foreach (@chunks) {
- if (ok_chunk "$_") {
- s/\n+$//;
- print "$_\n";
- exit 0;
- }
- }
- exit 1
- ' | __sfdoc__strip_doc
- }
-
- __sfdoc__filter_var() {
- #
- # From module body on stdin, filter variable definition named $1
- #
- # Look for:
- #
- # 1. Empty line, followed by line with single hash sign
- # 2. Set of consecutive docstring lines (start with '# ')
- # 3. Another "#" followed by one or more variable definitions (start
- # with $name or \w+, followed by equal sign), at least one of which
- # must be $1
- # 4. Empty line or EOF
- #
- # and return what is found.
- #
- # For example:
- #
- # #
- # # My variable
- # #
- # my_var=foo
- #
- # is found if $1 is 'my_var'
- #
- # A more advanced example:
- #
- # #
- # # Bar variables
- # #
- # # These serve similar purpose; the difference is
- # # obvious
- # #
- # bar_1=one
- # bar_2=two
- # bar_3=three
- #
- # is found if $1 is either 'bar_1', 'bar_2' or 'bar_3'. This allows
- # for a docstring being shared among closely related variables.
- #
- local oname=$1 # object name
- local state # void, edge, dstr, vstr, junk
- local o_end # object ended; is complete
- local cache # buffer of hope
- # parse and keep each var docstring in cache;
- # then decide whether or not print the cache
- cache=$(mktemp -t shellfu_doc.__sfdoc__filter_var.XXXXXXXX)
- state=junk
- o_end=false
- while read -r line;
- do
- case $state:$line in
- # looks like void
- void:) state=void; o_end=false ;;
- edge:) state=void; o_end=true ;;
- dstr:) state=void; o_end=true ;;
- vstr:) state=void; o_end=true ;;
- junk:) state=void; o_end=false ;;
- # looks like edge
- void:"#") state=edge; o_end=false ;;
- edge:"#") state=dstr; o_end=false ;;
- dstr:"#") state=dstr; o_end=false ;;
- vstr:"#") state=dstr; o_end=false ;;
- junk:"#") state=junk; o_end=false ;;
- # looks like docstring
- void:"# "*) state=junk; o_end=false ;;
- edge:"# "*) state=dstr; o_end=false ;;
- dstr:"# "*) state=dstr; o_end=false ;;
- vstr:"# "*) state=dstr; o_end=false ;;
- junk:"# "*) state=junk; o_end=false ;;
- # looks like junk
- void:"#"*) state=junk; o_end=false ;;
- edge:"#"*) state=junk; o_end=false ;;
- dstr:"#"*) state=junk; o_end=false ;;
- vstr:"#"*) state=junk; o_end=false ;;
- junk:"#"*) state=junk; o_end=false ;;
- # looks like variable string
- void:*=*) state=vstr; o_end=false ;;
- edge:*=*) state=junk; o_end=false ;;
- dstr:*=*) state=vstr; o_end=false ;;
- vstr:*=*) state=vstr; o_end=false ;;
- junk:*=*) state=junk; o_end=false ;;
- # looks like something else
- *) state=junk; o_end=false ;;
- esac
- case $state in
- edge|dstr|vstr) echo "$line" >> "$cache" ;;
- esac
- if $o_end;
- then
- if grep -q "^$oname=" "$cache"; # it's our wanted object
- then
- cat "$cache"
- rm "$cache"
- return 0
- fi
- rm "$cache"
- fi
- done
- rm "$cache"
- return 1
- }
-
- __sfdoc__filter_vdoc() {
- #
- # From variable definition body on stdin, filter doc
- #
- # Example:
- #
- # #
- # # Bar variables
- # #
- # # These serve similar purpose; the difference is
- # # obvious
- # #
- # bar_1=one
- # bar_2=two
- # bar_3=three
- #
- grep '^#' | __sfdoc__strip_doc
- }
-
- __sfdoc__get_doc() {
- #
- # Show doc for part $2 of module $1
- #
- local part=$1
- __sfdoc__get_part "$part" | __sfdoc__filter_doc "$part"
- }
-
- __sfdoc__get_part() {
- #
- # Print part $1 (f|v:name) of module $MPath
- #
- # Part has format
- #
- # TYPE:NAME
- #
- # where TYPE is 'v' for variables and f for functions, and
- # NAME is name of the part.
- #
- # If part is not specified, whole module file is printed.
- #
- local part=$1
- test -n "$part" || { cat "$MPath"; return $?; }
- <"$MPath" __sfdoc__filter_body "$part"
- }
-
- __sfdoc__md_escapes() {
- #
- # Do additional escapes for Markdown
- #
- # In docstrings, references to variables are often done
- # without backticks; add these to prevent interpreting
- # underscores in variable names as cursive.
- #
- perl -ne '
- my @bigwords = ();
- my @newwords = ();
- if (m|^ |) {
- print; # probably code -- leave be
- } else {
- @bigwords = split " ";
- foreach (@bigwords) {
- if (m|`\w+\(\)`|) {
- push @newwords, $_; # function: escaped
- } elsif (m|(.*)\b(\w+)\(\)(.*)|) {
- push @newwords, "$1`$2()`$3"; # function: needs escape
- } elsif (m|`\$\w+`|) {
- push @newwords, $_; # variable: escaped
- } elsif (m|(.*)\$(\w+)(.*)|) {
- push @newwords, "$1`\$$2`$3"; # variable: needs escape
- } else {
- push @newwords, $_; # a normal word
- }
- }
- print join " ", @newwords;
- print "\n";
- }
- '
- }
-
- __sfdoc__strip_doc() {
- #
- # Strip doc out of Shellfu docstring
- #
- # From e.g.
- #
- # #
- # # foo
- # #
- # # bar
- # # baz
- # #
- #
- # where first and last line are mandatory and each line
- # must be either '#' or '# *', get:
- #
- # foo
- #
- # bar
- # baz
- #
- # just as when you're parsing this text by eyes.
- #
- head -n -1 \
- | tail -n +2 \
- | sed -e 's/^#$//; s/^# //;'
- }
|