#!/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://pagure.io/shellfu/blob/master/f/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. # # See also $SFDOC_HIDE_REGEX. # SFDOC_SHOW_HIDDEN=${SFDOC_SHOW_HIDDEN:-false} # # Regex to override what is hidden # # Override definition of which objects are considered "private", thus hidden # by default. Regular expression is used as Extended Regular Expressions, # is anchored on both sides and case-sensitive, e setting 'foo|bAr' matches # only objects whose entire name is either 'foo' or 'bAr'. # # See also $SFDOC_SHOW_HIDDEN. # SFDOC_HIDE_REGEX=${SFDOC_HIDE_REGEX:-_.*} 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 debug -v SFDOC_HIDE_REGEX case $what in m) grep -vE "^$SFDOC_HIDE_REGEX$" ;; v) grep -vE ":$SFDOC_HIDE_REGEX$" ;; f) grep -vE ":$SFDOC_HIDE_REGEX$" ;; *) 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 = ; 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"; then # it's our wanted object 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/^# //;' }