123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 |
- #!/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 = <STDIN>;
- 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" <<<manpage,markdown,pod \
- || die "unknown format: $format"
- "export_as_$format" "$module"
- ;;
- lsm) # --lsmod
- sf_list_modules | sort
- ;;
- lsv) # --lsvar MODULE
- sf_list v "$module" | sort
- ;;
- lsf) # --lsfun MODULE
- sf_list f "$module" | sort
- ;;
- lsx) # --ls [MODULE]
- case $module in
- "") sf_list_modules ;;
- *) echo "$module" ;;
- esac \
- | while read -r m;
- do
- sf_list v "$m"
- sf_list f "$m"
- done \
- | sort
- ;;
- man)
- select_mfile "$module" >/dev/null || return 3
- export_as_manpage "$module" \
- | man -l -
- ;;
- esac
- }
-
- main "$@"
|