From e08a602e71ba1f9c9bb8e1c493cb4109fc22e952 Mon Sep 17 00:00:00 2001 From: Andre Noll Date: Mon, 17 Apr 2017 16:07:31 +0200 Subject: [PATCH] subcommand: Implement roff and html output for com_man(). This implements the -m option of the internal man subcommand to specify the output format for the manual page (text, roff, or html). html is generated by running the html postprocessor of groff. Another option, -b, is introduced to let the user specify the browser for html output. The commit introduces new public variable $gsu_package whose value is shown at the bottom left of the man page. The documentation is updated accordingly, and the example subcommand of README.md has been enhaced to illustrate how subcommand options should be formatted to look nice in both html and roff output. --- README.md | 36 +++++-- common | 2 + subcommand | 295 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 315 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 58d28a0..0d2c9e2 100644 --- a/README.md +++ b/README.md @@ -127,28 +127,34 @@ Example: echo 'hello world' } -The subcommand documentation consists of three parts: +The subcommand documentation consists of the following parts: - The summary. One line of text, - the usage/synopsis string, -- free text section. +- free text section 1, +- options (if any), +- free text section 2. -The three parts should be separated by lines consisting of two `#` characters -only. Example: +The last three parts are optional. All parts should be separated by +lines consisting of two `#` characters only. Example: com_world() ## ## Print the string "hello world" to stdout. ## - ## Usage: world + ## Usage: world [-v] ## ## Any arguments to this function are ignored. ## + ## -v: Enable verbose mode + ## ## Warning: This subcommand may cause the top most line of your terminal to ## disappear and may cause DATA LOSS in your scrollback buffer. Use with ## caution. { - echo 'hello world' + printf 'hello world' + [[ "$1" == '-v' ]] && printf '!' + printf '\n' } Replace `hello` with the above and try: @@ -318,12 +324,17 @@ Example: ___HTML output___ -The output of the auto-generated man subcommand is a suitable input for the -grutatxt plain text to html converter. Hence +The auto-generated man subcommand produces plain text, html, or +roff output. + + ./hello man -m html > index.html - ./hello man | grutatxt > index.html +is all it takes to produce an html page for your +application. Similarly, -is all it takes to produce an html page for your application. + ./hello man -m roff > hello.1 + +creates a manual page. ___Interactive completion___ @@ -698,6 +709,10 @@ Defaults to `~/.${gsu_name}.rc`. - `$gsu_config_var_prefix`. Used by the config module to set up the variables defined in `$gsu_options`. +- `$gsu_package`. Text shown at the bottom left of the man page, +usually the name and version number of the software package. Defaults +to `$gsu_name`. + License ------- gsu is licensed under the GNU LESSER GENERAL PUBLIC LICENSE (LGPL), version 3. @@ -712,4 +727,3 @@ References ---------- - [bash](http://www.gnu.org/software/bash/bash.html) - [dialog](http://www.invisible-island.net/dialog/dialog.html) -- [grutatxt](http://triptico.com/software/grutatxt.html) diff --git a/common b/common index ec7b8e2..4b69b23 100644 --- a/common +++ b/common @@ -14,11 +14,13 @@ E_GSU_BAD_BOOL bad value for boolian option E_GSU_BAD_OPTION_TYPE invalid option type E_GSU_BAD_ARG_COUNT invalid number of arguments E_GSU_EDITOR failed to execute editor +E_GSU_INVAL invalid argument E_GSU_MKDIR failed to create directory E_GSU_GETOPTS getopts error E_GSU_DIALOG dialog error E_GSU_MKTEMP mktemp error E_GSU_MENU_TREE invalid menu tree +E_GSU_XCMD external command failed ${gsu_errors:-} " local a b i=0 diff --git a/subcommand b/subcommand index 7318c9e..a07f414 100644 --- a/subcommand +++ b/subcommand @@ -278,29 +278,310 @@ complete_prefs() gsu_complete_options "$com_prefs_options" "$@" } +_gsu_man_options='m:b:' + +complete_man() +{ + gsu_complete_options "$_gsu_man_options" "$@" + ((ret > 0)) && return + gsu_cword_is_option_parameter "$_gsu_man_options" "$@" + [[ "$result" == 'm' ]] && printf 'roff\ntext\nhtml\n' +} + _gsu_man_txt=' Print the manual. -Usage: man +Usage: man [-m ] [-b ] + +-m: Set output format (text, roff or html). Default: text. +-b: Use the specified browser. Implies html mode. + +If stdout is not associated with a terminal device, the command +dumps the man page to stdout and exits. -If stdout associated with a terminal device, output is piped to -$PAGER. If $PAGER is unset, less(1) is assumed. +Otherwise, it tries to display the manual page as follows. In text +mode, plain text is piped to $PAGER. In roff mode, the roff output +is filtered through nroff, then piped to $PAGER. For both formats, +if $PAGER is unset, less(1) is assumed. + +In html mode, html output is written to a temporary file, and this +file is displayed as a page in the web browser. If -b is not given, +the command stored in the $BROWSER environment variable is executed +with the path to the temporary file as an argument. If $BROWSER is +unset, elinks(1) is assumed. + +It is recommended to specify the output format with -m as the default +mode might change in future versions of gsu. ' +_gsu_read_line() +{ + local -n p="$1" + local l OIFS="$IFS" + + IFS= + read -r l || return + IFS="$OIFS" + p="$l" +} + +_gsu_change_roffify_state() +{ + local -n statep="$1" + local new_state="$2" + local old_state="$statep" + + [[ "$old_state" == "$new_state" ]] && return 0 + + case "$old_state" in + text);; + example) printf '.EE\n';; + enum) printf '.RE\n';; + esac + case "$new_state" in + text);; + example) printf '.EX\n';; + enum) printf '.RS 2\n';; + esac + + statep="$new_state" + return 1 +} + +_gsu_print_protected_roff_line() +{ + local line="$1" + local -i n=0 + + while [[ "${line:$n:1}" == ' ' ]]; do + let n++ + done + line="${line:$n}" + printf '\\&%s\n' "${line//\\/\\\\}" +} + +_gsu_roffify_maindoc() +{ + local state='text' TAB=' ' + local line next_line + local -i n + + _gsu_read_line 'line' || return + while _gsu_read_line next_line; do + if [[ "$next_line" =~ ^(----|====|~~~~) ]]; then # heading + printf '.SS %s\n' "$line" + _gsu_read_line line || return + _gsu_change_roffify_state 'state' 'text' + continue + fi + if [[ "${line:0:1}" == "$TAB" ]]; then # example + _gsu_change_roffify_state 'state' 'example' + printf '%s\n' "$line" + line="$next_line" + continue + fi + n=0 + while [[ "${line:$n:1}" == ' ' ]]; do + let n++ + done + line=${line:$n}; + if [[ "${line:0:1}" == '*' ]]; then # enum + line=${line#\*} + _gsu_change_roffify_state 'state' 'enum' + printf '\n\(bu %s\n' "$line" + line="$next_line" + continue + fi + if [[ "$line" =~ ^$ ]]; then # new paragraph + _gsu_change_roffify_state 'state' 'text' + printf '.PP\n' + else + _gsu_print_protected_roff_line "$line" + fi + line="$next_line" + done + _gsu_print_protected_roff_line "$line" +} + +_gsu_extract_maindoc() +{ + sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' -e 's/^# *//' -e 's/^#//g' "$0" +} + +_gsu_roffify_cmds() +{ + local line cmd= desc= state='text' TAB=' ' + + while _gsu_read_line line; do + if [[ "${line:0:1}" != '#' ]]; then # com_foo() + line="${line#com_}" + cmd="${line%()}" + continue + fi + line="${line####}" + if [[ "$line" =~ ^[[:space:]]*$ ]]; then + printf '.PP\n' + _gsu_change_roffify_state 'state' 'text' + continue + fi + if [[ -n "$cmd" ]]; then # desc or usage + if [[ -z "$desc" ]]; then # desc + desc="$line" + continue + fi + # usage + _gsu_change_roffify_state 'state' 'text' + printf '\n.SS %s \\- %s\n' "$cmd" "$desc" + printf '\n.I %s\n' "$line" + cmd= + desc= + continue + fi + line="${line# }" + if [[ "${line:0:1}" == "$TAB" ]]; then + _gsu_change_roffify_state 'state' 'example' + _gsu_print_protected_roff_line "$line" + continue + fi + if [[ "$line" == -*:* ]]; then + _gsu_change_roffify_state 'state' 'enum' + printf '.PP\n.B %s:\n' "${line%%:*}" + _gsu_print_protected_roff_line "${line#*:}" + continue + fi + _gsu_print_protected_roff_line "$line" + done +} + +_gsu_roffify_autocmd() +{ + local cmd="$1" help_txt="$2" + + { + printf 'com_%s()\n' "$cmd" + sed -e 's/^/## /g' <<< "$help_txt" + } | _gsu_roffify_cmds +} + +_gsu_roff_man() +{ + local name="$1" sect_num="$2" month_year="$3" pkg="$4" sect_name="$5" + local purpose="$6" + local ere + + cat << EOF +.TH "${name^^}" "$sect_num" "$month_year" "$pkg" "$sect_name" +.SH NAME +$name \- $purpose +.SH SYNOPSIS +.B $name +\fI\,\/\fR [\fI\,\/\fR] [\fI\,\/\fR] +.SH DESCRIPTION +EOF + _gsu_extract_maindoc | _gsu_roffify_maindoc + + printf '\n.SH "GENERIC SUBCOMMANDS"\n' + printf 'The following commands are automatically created by gsu\n' + _gsu_roffify_autocmd "help" "$_gsu_help_txt" + _gsu_roffify_autocmd "man" "$_gsu_man_txt" + _gsu_roffify_autocmd "prefs" "$_gsu_prefs_txt" + _gsu_roffify_autocmd "complete" "$_gsu_complete_txt" + + printf '\n.SH "LIST OF SUBCOMMANDS"\n' + printf 'Each command has its own set of options as described below.\n' + + _gsu_get_command_regex + ere="$result" + # only consider lines in the comment of the function + sed -nEe '/'"$ere"'/,/^[^#]/p' "$0" | _gsu_roffify_cmds +} + +_gsu_file_mtime() +{ + local file="$1" + result="$(find "$file" -printf '%TB %TY' 2>/dev/null)" # GNU + (($? == 0)) && [[ -n "$result" ]] && return + result="$(stat -f %Sm -t '%B %Y' "$file" 2>/dev/null)" # BSD + (($? == 0)) && [[ -n "$result" ]] && return + result="$(date '+%B %Y' 2>/dev/null)" # POSIX + (($? == 0)) && [[ -n "$result" ]] && return + result='[unknown date]' +} + com_man() { local equal_signs="==================================================" local minus_signs="--------------------------------------------------" - local com num pager='cat' + local filter='cat' pager='cat' browser=${BROWSER:-elinks} tmpfile= + local com num isatty pipeline - _gsu_isatty && pager="${PAGER:-less}" + gsu_getopts "$_gsu_man_options" + eval "$result" + ((ret < 0)) && return + if [[ -n "$o_b" ]]; then + o_m='html' + browser="$o_b" + elif [[ -z "$o_m" ]]; then + o_m='text' + fi + + _gsu_isatty && isatty='true' || isatty='false' + if [[ "$o_m" == 'roff' ]]; then + if [[ "$isatty" == 'true' ]]; then + filter='nroff -Tutf8 -mandoc' + pager="${PAGER:-less}" + fi + elif [[ "$o_m" == 'text' ]]; then + if [[ "$isatty" == 'true' ]]; then + pager="${PAGER:-less}" + fi + elif [[ "$o_m" == 'html' ]]; then + filter='groff -T html -m man' + if [[ "$isatty" == 'true' ]]; then + gsu_make_tempfile "gsu_html_man.XXXXXX.html" + ((ret < 0)) && return || tmpfile="$result" + trap "rm -f $tmpfile" RETURN EXIT + fi + fi [[ "$pager" == 'less' ]] && export LESS=${LESS-RI} + case "$o_m" in + roff|html) + _gsu_file_mtime "$0" + _gsu_roff_man "$gsu_name" '1' "$result" \ + "${gsu_package-${gsu_name^^}(1)}" \ + "User Commands" "${gsu_banner_txt}" \ + | $filter | { + if [[ -n "$tmpfile" ]]; then + cat > "$tmpfile" + else + $pager + fi + } + if (($? != 0)); then + ret=-$E_GSU_XCMD + result="filter: $filter" + return + fi + if [[ -n "$tmpfile" ]]; then + ret=-$E_GSU_XCMD + result="$browser" + "$browser" "$tmpfile" || return + fi + ret=$GSU_SUCCESS + return + ;; + text) ;; + "") ;; + *) + ret=-$E_GSU_INVAL + result="$o_m" + return + esac { echo "$gsu_name (_${gsu_banner_txt}_) manual" echo "${equal_signs:0:${#gsu_name} + ${#gsu_banner_txt} + 16}" echo - - sed -e '1,/^#\{70,\}/d' -e '/^#\{70,\}/,$d' "$0" -e 's/^# *//' + _gsu_extract_maindoc echo "----" echo echo "$gsu_name usage" -- 2.39.2