#!/bin/bash . "$(sfpath)" || exit 3 shellfu import pretty usage() { mkusage \ "[options] MODULE" \ "[options] --ls [MODULE]" \ "[options] --ls{var|fun} MODULE" \ "[options] --lsmod" \ "[options] --export FMT MODULE" \ -c \ "-l, --ls [MODULE] list contents of MODULE if specified," \ " otherwise of all modules" \ "-L, --lsmod show list of modules" \ " --lsfun MODULE show list of functions in MODULE" \ " --lsvar MODULE show list of variables in MODULE" \ "-e, --export FMT MODULE export MODULE documentation in" \ " format FMT: 'markdown', 'manpage' and" \ " 'pod' are supported" \ -o \ "-d, --debug Turn on debugging" \ "-a, --all Don't ignore hidden (starting with" \ " underscore) modules or objects" \ "--encoding Override encoding of the source text" \ " (default: utf8)" \ "--name Override module name (useful when" \ " exporting from a file and the filename" \ " is not helpful)" \ "-I PTH, --include PTH Include path PTH when searching" \ " for modules; can be specified multiple" \ " times." \ -- \ "Without command specified, will try to display documentation" \ "in man(1) pager." } export_as_manpage() { # # Export module doc as manpage # local module=$1 local mfile mfile=$(select_mfile "$module") || return 3 export_as_pod "$module" \ | pod2man \ --name "${RealModuleName:-${module^^}}" \ --center "User Contributed Shellfu Documentation" \ --section 3x \ --date "$(date -I -r "$mfile")" \ --release "shellfu v$(sfpath --version-semver)" \ --utf8 -q \` } export_as_markdown() { # # Export module doc as Markdown # local module=$1 local module_name=${RealModuleName:-$module} local variable local function local vheader_done=false local fheader_done=false debug -v module echo "$module_name" echo "$module_name" | tr -c '\n' '[=*]' echo get_doc "$module" | md_escapes echo for variable in $(sf_list v "$module" | cut -d. -f2); do debug -v variable $vheader_done || { echo echo Variables echo --------- vheader_done=true } echo echo "### \`\$$variable\` ###" echo "" get_doc "$module" "v:$variable" | md_escapes echo done for function in $(sf_list f "$module" | cut -d. -f2); do debug -v function $fheader_done || { echo echo Functions echo --------- fheader_done=true } echo echo "### \`$function()\` ###" echo "" get_doc "$module" "f:$function" | md_escapes echo done } export_as_pod() { # # Export module doc as POD # local module=$1 local variable local function local vheader_done=false local fheader_done=false local indented=false debug -v module echo "true <<'=cut'" echo "=pod" echo echo "=head1 NAME" echo echo "${RealModuleName:-$module} - $(get_doc "$module" | head -1)" echo echo "=head1 DESCRIPTION" echo get_doc "$module" echo for variable in $(sf_list v "$module" | 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 get_doc "$module" "v:$variable" echo done $indented && { echo "=back" echo } for function in $(sf_list f "$module" | 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 get_doc "$module" "f:$function" echo done $indented && { echo "=back" echo } echo "=encoding $Encoding" echo "=cut" } filter_body() { # # Filter part $1 of body on stdin # local part=$1 case "$part" in f:*) filter_fun "${part:2}" ;; v:*) filter_var "${part:2}" ;; *) warn "bug: invalid part specification $part" ;; esac } filter_doc() { # # Filter docstring for part $1 from object body on stdin # local part=$1 case $part in "") filter_mdoc ;; v:*) filter_vdoc ;; f:*) filter_fdoc ;; *) warn "bug: invalid part specification: $part" ;; esac } 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; ' } 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; }' | strip_doc } 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 ' | strip_doc } 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.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 } 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 '^#' | strip_doc } get_doc() { # # Show doc for part $2 of module $1 # local module=$1 local part=$2 get_part "$module" "$part" | filter_doc "$part" } get_part() { # # Print part $2 (f|v:name) of module $1 # # 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 module=$1 local part=$2 local mfile mfile=$(select_mfile "$module") || return 3 test -n "$mfile" || return 1 test -n "$part" || { cat "$mfile"; return $?; } <"$mfile" filter_body "$part" } 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"; } ' } select_mfile() { # # Find and/or verify file that holds module $1 # local module=$1 local mfile case $module in */*) mfile="$module" ;; *) mfile=$(shellfu _select_mfile "$module") ;; esac test -n "$mfile" || { warn "no such module found: $module"; return 3; } test -f "$mfile" || { warn "no such file found: $mfile"; return 3; } echo "$mfile" } sf_list() { # # List all objects of type $1 in module $2 # local otype=$1 local module=$2 local mfile mfile=$(select_mfile "$module") || return 3 case $otype in f) grep -HE '^[[:alnum:]_]+\(\) \{' "$mfile" \ | sed -e 's|(.*||; s|\.sh:|.|; s|^.*/||' ;; v) grep -HE '^[[:alnum:]_]+=' "$mfile" \ | sed -e 's|=.*||; s|\.sh:|.|; s|^.*/||' ;; *) warn "bug: invalid object type: $otype" ;; esac \ | sf_flt_hidden } sf_list_modules() { # # List all module files # shellfu _list_mfiles \ | sed 's|\.sh$||; s|.*/||;' \ | sf_flt_hidden } sf_flt_hidden() { $ShowHidden && cat && return grep -v ^_ | grep -v \\._ } 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/^# //;' } main() { local ShowHidden # show hidden objects? local action # what to do local format # export format local module # module name or path/to/module.sh local m # module helper var local RealModuleName # name to override eg. if accessing via filename local Encoding # encoding ShowHidden=false action=man format=markdown Encoding=utf8 #shellcheck disable=SC2034 while true; do case "$1" in -d|--debug) PRETTY_DEBUG=true; shift ;; -a|--all) ShowHidden=true; shift ;; -I|--include) SHELLFU_PATH="$2:$SHELLFU_PATH"; shift 2 || usage ;; -l|--ls) action=lsx; shift; break ;; -L|--lsmod) action=lsm; shift; break ;; --lsvar) action=lsv; shift; break ;; --lsfun) action=lsf; shift; break ;; -e|--export) action=exp; format="$2"; shift 2 || usage; break ;; --encoding) Encoding="$2"; shift 2 || usage ;; --name) RealModuleName="$2"; shift 2 || usage ;; -*) usage ;; *) break ;; esac done module="$1"; shift debug -v SHELLFU_INCLUDE SHELLFU_PATH debug -v action ShowHidden format module RealModuleName case $action:$module in lsx:*|lsm:*) true ;; *:) usage ;; esac case $action in exp) # --export grep -qw "$format" <</dev/null || return 3 export_as_manpage "$module" \ | man -l - ;; esac } main "$@"