X-Git-Url: http://git.tuebingen.mpg.de/?p=gsu.git;a=blobdiff_plain;f=subcommand;h=a07f4145c69adb6fa319dbdaa9c97e3b0a2d5cde;hp=bded36d1514a09a4c3ed61dc72d11095e470d7b8;hb=e08a602e71ba1f9c9bb8e1c493cb4109fc22e952;hpb=521b9d7584ff363b9af24f33e0afb1e954da854a diff --git a/subcommand b/subcommand index bded36d..a07f414 100644 --- a/subcommand +++ b/subcommand @@ -1,9 +1,10 @@ #!/bin/bash -# (C) 2006-2011 Andre Noll +# Copyright (C) 2006 Andre Noll +# Licensed under the LGPL, version 3. See COPYING and COPYING.LESSER. if [[ "$(type -t _gsu_setup)" != "function" ]]; then gsu_dir=${gsu_dir:-${BASH_SOURCE[0]%/*}} - . $gsu_dir/common || exit 1 + . "$gsu_dir/common" || exit 1 _gsu_setup fi @@ -24,7 +25,7 @@ _gsu_usage() # exactly one parenthesized subexpression for matching the command name. _gsu_get_command_regex() { - local cmd="${1:-[-a-zA-Z_0-9]+}" + local cmd="${1:-[-a-zA-Z_0-9]{1,\}}" result="^com_($cmd)\(\)" } @@ -45,19 +46,135 @@ _gsu_available_commands() # otherwise delete it d - ' $0 + ' "$0" } | sort | tr '\n' ' ')" } +# Check number of arguments. +# +# Usage: gsu_check_arg_count [] +# +# Check that is between and inclusively. +# If only ist given, num2 is assumed to be infinity. +# +# Examples: +# 0 0 no argument allowed +# 1 1 exactly one argument required +# 0 2 at most two arguments admissible +# 2 at least two arguments required +gsu_check_arg_count() +{ + ret=-$E_GSU_BAD_ARG_COUNT + if (($# == 2)); then # only num1 is given + result="at least $2 args required, $1 given" + (($1 < $2)) && return + ret=$GSU_SUCCESS + return + fi + # num1 and num2 given + result="need at least $2 args, $1 given" + (($1 < $2)) && return + result="need at most $3 args, $1 given" + (($1 > $3)) && return + ret=$GSU_SUCCESS +} + +# Wrapper for the bash getopts builtin. +# +# Aborts on programming errors such as missing or invalid option string. On +# success $result contains shell code that can be eval'ed. For each defined +# option x, the local variable o_x will be created when calling eval "$result". +# o_x contains true/false for options without argument and either the empty +# string or the given argument for options that take an argument. +# +# Example: +# gsu_getopts abc:x:y +# eval "$result" +# (($ret < 0)) && return +# +# [[ "$o_a" = 'true' ]] && echo 'The -a flag was given' +# [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c" +gsu_getopts() +{ + local i c tab=' ' cr=' +' + + gsu_check_arg_count $# 1 1 + if (($ret < 0)); then + gsu_err_msg + exit 1 + fi + + ret=-$E_GSU_GETOPTS + result="invalid optstring $1" + if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then + gsu_err_msg + exit 1 + fi + + for ((i=0; i < ${#1}; i++)); do + c=${1:$i:1} + case "$c" in + [a-zA-Z:]);; + *) + ret=-$E_GSU_GETOPTS + result="invalid character $c in optstring" + gsu_err_msg + exit 1 + esac + done + result="local _gsu_getopts_opt" + for ((i=0; i < ${#1}; i++)); do + c1=${1:$i:1} + c2=${1:$(($i + 1)):1} + result+=" o_$c1=" + if [[ "$c2" = ":" ]]; then + let i++ + else + result+="false" + fi + done + result+=" + OPTIND=1 + while getopts $1 _gsu_getopts_opt \"\$@\"; do + case \"\$_gsu_getopts_opt\" in +" + for ((i=0; i < ${#1}; i++)); do + c1=${1:$i:1} + c2=${1:$(($i + 1)):1} + result+="$tab$tab$c1) o_$c1=" + if [[ "$c2" = ":" ]]; then + result+="\"\$OPTARG\"" + let i++ + else + result+="true" + fi + result+=";;$cr" + done + result+=" + *) + ret=-\$E_GSU_GETOPTS + result=\"invalid option given\" + return + ;; + esac + done + shift \$((\$OPTIND - 1)) +" + ret=$GSU_SUCCESS +} + _gsu_print_available_commands() { - local cmd + local cmd cmds local -i count=0 + _gsu_available_commands + cmds="$result" printf 'Available commands:\n' - for cmd in $gsu_cmds; do + for cmd in $cmds; do printf '%s' "$cmd" - let count++ + let ++count if (($count % 4)); then printf '\t' ((${#cmd} < 8)) && printf '\t' @@ -68,6 +185,8 @@ _gsu_print_available_commands() printf '\n' } +# Print all options of the given optstring to stdout if the word in the current +# command line begins with a hyphen character. gsu_complete_options() { local opts="$1" cword="$2" cur opt @@ -88,19 +207,23 @@ gsu_complete_options() done } -export gsu_prefs_txt=" +com_prefs_options='e' + +_gsu_prefs_txt=" Print the current preferences. Usage: prefs [-e] -If -e is given, the config file is opened with the default editor. Without -options, the command prints out a list of all cmt config variables, together -with their current value and the default value." -_com_prefs() +If -e is given, the config file is opened with the default editor. +Without options, the command prints out a list of all config variables, +together with their current value and the default value. +" + +com_prefs() { local i conf="${gsu_config_file:=${HOME:-}/.$gsu_name.rc}" - gsu_getopts "e" + gsu_getopts "$com_prefs_options" eval "$result" (($ret < 0)) && return gsu_check_arg_count $# 0 0 @@ -132,41 +255,335 @@ _com_prefs() printf "# optional" ;; esac - printf " $option_type: $description" + printf "%s: %s" "$option_type" "$description" if [[ "$required" != "yes" && "$required" != "true" ]]; then - printf " [$default_value]" + printf " [%s]" "$default_value" fi echo [[ -n "$help_text" ]] && sed -e '/^[ ]*$/d; s/^[ ]*/# /g' <<< "$help_text" - printf "$name=$val" + printf "%s=%s" "$name" "$val" [[ "$val" == "$default_value" ]] && printf " # default" echo done } +_gsu_isatty() +{( + exec 3<&1 + stty 0<&3 &> /dev/null +)} + complete_prefs() { - gsu_complete_options "e" "$@" + 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' } -export gsu_man_txt=" +_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. + +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 +} -_com_man() +_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 + local filter='cat' pager='cat' browser=${BROWSER:-elinks} tmpfile= + local com num isatty pipeline + + 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 echo "$gsu_name usage" echo "${minus_signs:0:${#gsu_name} + 6}" printf "\t" @@ -178,19 +595,20 @@ _com_man() echo "Available commands:" _gsu_available_commands - for com in $result; do + for com in $result; do num=${#com} (($num < 4)) && num=4 - echo "${minus_signs:0:$num}" - echo "$com" - echo "${minus_signs:0:$num}" - $0 help $com + echo "${minus_signs:0:$num}" + echo "$com" + echo "${minus_signs:0:$num}" + "$0" help "$com" echo - done - ret=$GSU_SUCCESS + done + } | $pager + ret=$GSU_SUCCESS } -export gsu_help_txt=" +_gsu_help_txt=" Print online help. Usage: help [command] @@ -198,7 +616,7 @@ Usage: help [command] Without arguments, print the list of available commands. Otherwise, print the help text for the given command." -export gsu_complete_txt=" +_gsu_complete_txt=" Command line completion. Usage: complete [ ...] @@ -212,9 +630,9 @@ written to stdout. This can be used from the completion function of the subcommand. " -_com_help() +com_help() { - local a b ere tab=' ' + local ere tab=' ' _gsu_get_command_regex ere="$result" @@ -223,11 +641,11 @@ _com_help() gsu_short_msg "### $gsu_name -- $gsu_banner_txt ###" _gsu_usage 2>&1 { - printf "com_help()\n$gsu_help_txt" | head -n 4; echo "--" - printf "com_man()\n$gsu_man_txt" | head -n 4; echo "--" - printf "com_prefs()\n$gsu_prefs_txt" | head -n 4; echo "--" - printf "com_complete()\n$gsu_complete_txt" | head -n 4; echo "--" - grep -EA 2 "$ere" $0 + printf "com_help()\n%s" "$_gsu_help_txt" | head -n 4; echo "--" + printf "com_man()\n%s" "$_gsu_man_txt" | head -n 4; echo "--" + printf "com_prefs()\n%s" "$_gsu_prefs_txt" | head -n 4; echo "--" + printf "com_complete()\n%s" "$_gsu_complete_txt" | head -n 4; echo "--" + grep -EA 2 "$ere" "$0" } | grep -v -- '--' \ | sed -En "/$ere/"'!d # remove everything but the command name @@ -255,29 +673,29 @@ _com_help() return fi if test "$1" = "help"; then - echo "$gsu_help_txt" + echo "$_gsu_help_txt" ret=$GSU_SUCCESS return fi if test "$1" = "man"; then - echo "$gsu_man_txt" + echo "$_gsu_man_txt" ret=$GSU_SUCCESS return fi if test "$1" = "prefs"; then - echo "$gsu_prefs_txt" + echo "$_gsu_prefs_txt" ret=$GSU_SUCCESS return fi if test "$1" = "complete"; then - echo "$gsu_complete_txt" + echo "$_gsu_complete_txt" ret=$GSU_SUCCESS return fi ret=$GSU_SUCCESS _gsu_get_command_regex "$1" ere="$result" - if ! grep -Eq "$ere" $0; then + if ! grep -Eq "$ere" "$0"; then _gsu_print_available_commands result="$1" ret=-$E_GSU_BAD_COMMAND @@ -300,7 +718,7 @@ _com_help() :p p } - ' $0 + ' "$0" } complete_help() @@ -309,92 +727,7 @@ complete_help() echo "$result" } -# Wrapper for the bash getopts builtin. -# -# Aborts on programming errors such as missing or invalid option string. On -# success $result contains shell code that can be eval'ed. For each defined -# option x, the local variable o_x will be created when calling eval "$result". -# o_x contains true/false for options without argument and either the emtpy -# string or the given argument for options that take an argument. -# -# Example: -# gsu_getopts abc:x:y -# eval "$result" -# (($ret < 0)) && return -# -# [[ "$o_a" = 'true' ]] && echo 'The -a flag was given' -# [[ -n "$o_c" ]] && echo "The -c option was given with arg $o_c" -gsu_getopts() -{ - local i c tab=' ' cr=' -' - - gsu_check_arg_count $# 1 1 - if (($ret < 0)); then - gsu_err_msg - exit 1 - fi - - ret=-$E_GSU_GETOPTS - result="invalid optstring $1" - if [[ -z "$1" ]] || grep -q '::' <<< "$1" ; then - gsu_err_msg - exit 1 - fi - - for ((i=0; i < ${#1}; i++)); do - c=${1:$i:1} - case "$c" in - [a-zA-Z:]);; - *) - ret=-$E_GSU_GETOPTS - result="invalid character $c in optstring" - gsu_err_msg - exit 1 - esac - done - result="local _gsu_getopts_opt" - for ((i=0; i < ${#1}; i++)); do - c1=${1:$i:1} - c2=${1:$(($i + 1)):1} - result+=" o_$c1=" - if [[ "$c2" = ":" ]]; then - let i++ - else - result+="false" - fi - done - result+=" - OPTIND=1 - while getopts $1 _gsu_getopts_opt \"\$@\"; do - case \"\$_gsu_getopts_opt\" in -" - for ((i=0; i < ${#1}; i++)); do - c1=${1:$i:1} - c2=${1:$(($i + 1)):1} - result+="$tab$tab$c1) o_$c1=" - if [[ "$c2" = ":" ]]; then - result+="\"\$OPTARG\"" - let i++ - else - result+="true" - fi - result+=";;$cr" - done - result+=" - *) - ret=-\$E_GSU_GETOPTS - result=\"invalid option given\" - return - ;; - esac - done - shift \$((\$OPTIND - 1)) -" - ret=$GSU_SUCCESS -} - -_com_complete() +com_complete() { local cmd n cword local -a words @@ -424,8 +757,8 @@ EOF words=("$@") cmd="${words[1]}" ret=$GSU_SUCCESS # It's not an error if no completer was defined - [[ "$(type -t complete_$cmd)" != "function" ]] && return - complete_$cmd "$cword" "${words[@]}" + [[ "$(type -t "complete_$cmd")" != "function" ]] && return + "complete_$cmd" "$cword" "${words[@]}" # ignore errors, they would only clutter the completion output ret=$GSU_SUCCESS } @@ -441,7 +774,8 @@ EOF # gsu_cword_is_option_parameter() { - local opts="$1" cword="$2" prev i n + local opts="$1" cword="$2" + local opt prev i n local -a words result= @@ -458,7 +792,7 @@ gsu_cword_is_option_parameter() opt="${opts:$i:1}" [[ "${opts:$(($i + 1)):1}" != ":" ]] && continue let i++ - [[ "$prev" != "-$opt" ]] && continue + [[ ! "$prev" =~ ^-.*$opt$ ]] && continue result="$opt" return done @@ -524,8 +858,7 @@ gsu_get_unnamed_arg_num() gsu() { local i - _gsu_available_commands - gsu_cmds="$result" + if (($# == 0)); then _gsu_usage _gsu_print_available_commands @@ -533,61 +866,17 @@ gsu() fi arg="$1" shift - # check internal commands - if [[ "$arg" = "help" || "$arg" = "man" || "$arg" = "prefs" || "$arg" = "complete" ]]; then - _com_$arg "$@" + if [[ "$(type -t "com_$arg")" == 'function' ]]; then + "com_$arg" "$@" if (("$ret" < 0)); then gsu_err_msg exit 1 fi exit 0 fi - - # external commands - for i in $gsu_cmds; do - if test "$arg" = "$i"; then - com_$arg "$@" - if (("$ret" < 0)); then - gsu_err_msg - exit 1 - fi - exit 0 - fi - done - ret=-$E_GSU_BAD_COMMAND result="$arg" gsu_err_msg _gsu_print_available_commands exit 1 } - -# Check number of arguments. -# -# Usage: gsu_check_arg_count [] -# -# Check that is between and inclusively. -# If only ist given, num2 is assumed to be infinity. -# -# Examples: -# 0 0 no argument allowed -# 1 1 exactly one argument required -# 0 2 at most two arguments admissible -# 2 at least two arguments reqired -# -gsu_check_arg_count() -{ - ret=-$E_GSU_BAD_ARG_COUNT - if (($# == 2)); then # only num1 is given - result="at least $2 args required, $1 given" - (($1 < $2)) && return - ret=$GSU_SUCCESS - return - fi - # num1 and num2 given - result="need at least $2 args, $1 given" - (($1 < $2)) && return - result="need at most $3 args, $1 given" - (($1 > $3)) && return - ret=$GSU_SUCCESS -}