Add README.
authorAndre Noll <maan@tuebingen.mpg.de>
Sat, 30 Aug 2014 12:46:37 +0000 (14:46 +0200)
committerAndre Noll <maan@tuebingen.mpg.de>
Fri, 7 Nov 2014 10:36:19 +0000 (11:36 +0100)
Up to now the API of gsu was completely undocumented. This commit
tries to improve on this. It adds a README file which explains the
public functions and variables of the three gsu modules (subcommand,
config and gui). Most features are illustrated by examples.

The README file is formatted in the grutatxt markup language, so it
can easily be converted to a nice html document.

Many thanks to George Wang who read an early draft of this document
and contributed many improvements.

README [new file with mode: 0644]

diff --git a/README b/README
new file mode 100644 (file)
index 0000000..cd59e95
--- /dev/null
+++ b/README
@@ -0,0 +1,709 @@
+The global subcommand utility
+=============================
+gsu is a small library of bash functions intended to ease the task of
+writing and documenting large shell scripts with multiple subcommands,
+each providing different functionality. gsu is known to work on Linux,
+FreeBSD, NetBSD and MacOS.
+
+This document describes how to install and use the gsu library.
+
+Setting up gsu
+--------------
+gsu is very easy to install:
+
+Requirements
+~~~~~~~~~~~~
+gsu is implemented in bash, and thus gsu depends on bash. Bash version
+3 is required, version 4 is recommended. Besides bash, gsu depends
+only on programs which are usually installed on any Unix system (awk,
+grep, sort, ...). Care has been taken to not rely on GNU specific
+behavior of these programs, so it should work on non GNU systems
+(MacOS, *BSD) as well. The gui module depends on the dialog utility.
+
+Download
+~~~~~~~~
+All gsu modules are contained in a git repository. Get a copy with
+
+       git clone git://git.tuebingen.mpg.de/gsu.git
+
+There is also a http://ilm.eb.local/gitweb/?p=gsu;a=summary (gitweb) page.
+
+Installation
+~~~~~~~~~~~~
+gsu consists of several independent modules which are all located
+at the top level directory of the git repository. gsu requires no
+installation beyond downloading. In particular it is not necessary
+to make the downloaded files executable.  The library modules can
+be sourced directly, simply tell your application where to find
+it. The examples of this document assume that gsu is installed in
+`/usr/local/lib/gsu' but this is not mandatory.`~/.gsu' is another
+reasonable choice.
+
+Conventions
+-----------
+Public and private functions and variables
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Although there is no way in bash to annotate symbols (functions
+and variables) as private or public, gsu distinguishes between the
+two. The `gsu_*' name space is reserved for public symbols while all
+private symbols start with `_gsu'.
+
+Private symbols are meant for internal use only. Applications should
+never use them directly because name and semantics might change
+between gsu versions.
+
+The public symbols, on the other hand, define the gsu API. This API
+must not change in incompatible ways that would break existing
+applications.
+
+$ret and $result
+~~~~~~~~~~~~~~~~
+All public gsu functions set the $ret variable to an integer value to
+indicate success or failure. As a convention, $ret < 0 means failure
+while a non-negative value indicates success.
+
+The $result variable contains either the result of a function (if any)
+or further information in the error case. A negative value of $ret is
+in fact an error code similar to the errno variable used in C programs.
+It can be turned into a string that describes the error. The public
+gsu_err_msg() function can be used to pretty-print a suitable error
+message provided $ret and $result are set appropriately.
+
+The subcommand module
+---------------------
+This gsu module provides helper functions to ease the repetitious task
+of writing applications which operate in several related modes, where
+each mode of operation corresponds to a subcommand of the application.
+
+With gsu, for each subcommand one must only write a _command handler_
+which is simply a function that implements the subcommand. All
+processing is done by the gsu library. Functions starting with the
+string `com_' are automatically recognized as subcommand handlers.
+
+The startup part of the script has to source the subcommand file of
+gsu and must then call
+
+       gsu "$@"
+
+Minimal example:
+
+       #!/bin/bash
+       com_world()
+       {
+               echo 'hello world'
+       }
+       . /usr/local/lib/gsu/subcommand || exit 1
+       gsu "$@"
+
+Save this code in a file called `hello' (adjusting the installation
+directory if necessary), make it executable (`chmod +x hello') and try
+
+       ./hello
+       ./hello world
+       ./hello invalid
+
+Here, we have created a bash script ("hello") that has a single "mode"
+of operation, specified by the subcommand "world".
+
+gsu automatically generates several reserved subcommands, which should
+not be specified: `help, man, prefs, complete'.
+
+Command handler structure
+~~~~~~~~~~~~~~~~~~~~~~~~~
+For the automatically generated help and man subcommands to work
+properly, all subcommand handlers must be documented. In order to be
+recognized as subcommand help text, comments must be prefixed with
+two `#' characters and the subcommand documentation must be located
+between the function "declaration", com_world() in the example above,
+and the opening brace that starts the function body.
+
+Example:
+
+       com_world()
+       ##
+       ##
+       ##
+       {
+               echo 'hello world'
+       }
+
+The subcommand documentation consists of three parts:
+
+ - The summary. One line of text,
+ - the usage/synopsis string,
+ - free text section.
+
+The three parts should be separated by lines consisting of two # characters
+only. Example:
+
+       com_world()
+       ##
+       ## Print the string "hello world" to stdout.
+       ##
+       ## Usage: world
+       ##
+       ## Any arguments to this function are ignored.
+       ##
+       ## 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'
+       }
+
+Replace 'hello' with the above and try:
+
+       ./hello help
+       ./hello help world
+       ./hello help invalid
+       ./hello man
+
+to check the automatically generated help and man subcommands.
+
+Error codes
+~~~~~~~~~~~
+As mentioned above, all public functions of gsu return an error code
+in the $ret variable. A negative value indicates failure, and in this
+case $result contains more information about the error. The same
+convention applies for subcommand handlers: gsu will automatically
+print an error message to stderr if a subcommand handler returns with
+$ret set to a negative value.
+
+To allow for error codes defined by the application, the $gsu_errors
+variable must be set before calling gsu(). Each non-empty line in this
+variable should contain an identifier and error string. Identifiers
+are written in upper case and start with `E_'. For convenience the
+$GSU_SUCCESS variable is defined to non-negative value. Subcommand
+handlers should set $ret to $GSU_SUCCESS on successful return.
+
+To illustrate the $gsu_errors variable, assume the task is to
+print all mount points which correspond to an ext3 file system in
+`/etc/fstab'. We'd like to catch two possible errors: (a) the file
+does not exist or is not readable, and (b) it contains no ext3 entry.
+A possible implementation of the ext3 subcommand could look like this
+(documentation omitted):
+
+       #!/bin/bash
+
+       gsu_errors='
+               E_NOENT         No such file or directory
+               E_NOEXT3        No ext3 file system detected
+       '
+
+       com_ext3()
+       {
+               local f='/etc/fstab'
+               local ext3_lines
+
+               if [[ ! -r "$f" ]]; then
+                       ret=-$E_NOENT
+                       result="$f"
+                       return
+               fi
+               ext3_lines=$(awk '{if ($3 == "ext3") print $2}' "$f")
+               if [[ -z "$ext3_lines" ]]; then
+                       ret=-$E_NOEXT3
+                       result="$f"
+                       return
+               fi
+               printf 'ext3 mount points:\n%s\n' "$ext3_lines"
+               ret=$GSU_SUCCESS
+       }
+
+Printing diagnostic output
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+gsu provides a couple of convenience functions for output. All
+functions write to stderr.
+
+ - *gsu_msg()*. Writes the name of the application and the given text.
+
+ - *gsu_short_msg()*. Like gsu_msg(), but does not print the application name.
+
+ - *gsu_date_msg()*. Writes application name, date, and the given text.
+
+ - *gsu_err_msg()*. Prints an error message according to $ret and $result.
+
+Subcommands with options
+~~~~~~~~~~~~~~~~~~~~~~~~
+Bash's getopts builtin provides a way to define and parse command line
+options, but it is cumbersome to use because one must loop over all
+given arguments and check the OPTIND and OPTARG variables during each
+iteration. The gsu_getopts() function makes this repetitive task easier.
+
+gsu_getopts() takes a single argument: the optstring which contains
+the option characters to be recognized. As usual, if a character is
+followed by a colon, the option is expected to have an argument. On
+return $result contains bash code that should be eval'ed to parse the
+position parameters $1, $2, ... of the subcommand according to the
+optstring.
+
+The shell code returned by gsu_getopts() creates a local variable $o_x
+for each defined option `x'. It contains `true/false' for options
+without argument and either the empty string or the given argument for
+options that take an argument.
+
+To illustrate gsu_getopts(), assume the above com_ext3() subcommand
+handler is to be extended to allow for arbitrary file systems, and
+that it should print either only the mount point as before or the
+full line of `/etc/fstab', depending on whether the verbose switch
+`-v' was given at the command line.
+
+Hence our new subcommand handler must recognize two options: `-t' for
+the file system type and `-v'. Note that `-t' takes an argument but `-v'
+does not. Hence we shall use the optstring `t:v' as the argument for
+gsu_getopts() as follows:
+
+       com_fs()
+       {
+               local f='/etc/fstab'
+               local fstype fstab_lines
+               local -i awk_field=2
+
+               gsu_getopts 't:v'
+               eval "$result"
+               (($ret < 0)) && return
+
+               [[ -z "$o_t" ]] && o_t='ext3' # default to ext3 if -t is not given
+               [[ "$o_v" == 'true' ]] && awk_field=0 # $0 is the whole line
+               fstab_lines=$(awk -v fstype="$o_t" -v n="$awk_field" \
+                       '{if ($3 == fstype) print $n}' "$f")
+               printf '%s entries:\n%s\n' "$o_t" "$fstab_lines"
+               ret=$GSU_SUCCESS
+       }
+
+Another repetitive task is to check the number of non-option arguments
+and to report an error if this number turns out to be invalid for
+the subcommand in question. The gsu_check_arg_count() function performs
+this check and sets $ret and $result as appropriate. This function
+takes three arguments: the actual argument count and the minimal and
+maximal number of non-option arguments allowed. The last argument may
+be omitted in which case any number of arguments is considered valid.
+
+Our com_world() subcommand handler above ignored any given
+arguments. Let's assume we'd like to handle this case and
+print an error message if one or more arguments are given. With
+gsu_check_arg_count() this can be achieved as follows:
+
+       com_world()
+       {
+               gsu_check_arg_count $# 0 0 # no arguments allowed
+               (($ret < 0)) && return
+               echo 'hello world'
+       }
+
+Global documentation
+~~~~~~~~~~~~~~~~~~~~
+Besides the documentation for subcommands, one might also want to
+include an overall description of the application which provides
+general information that is not related to any particular subcommand.
+
+If such a description is included at the top of the script, the
+automatically generated man subcommand will print it. gsu recognizes
+the description only if it is enclosed by two lines consisting of at
+least 70 # characters.
+
+Example:
+
+       #/bin/bash
+
+       #######################################################################
+       # gsu-based hello - a cumbersome way to write a hello world program
+       # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+       # It not only requires one to download and install some totally weird
+       # git repo, it also takes about 50 lines of specially written code
+       # to perform what a simple echo 'hello world' would do equally well.
+       #######################################################################
+
+HTML output
+~~~~~~~~~~~
+The output of the auto-generated man subcommand is a suitable input for the
+grutatxt plain text to html converter. Hence
+
+       ./hello man | grutatxt > index.html
+
+is all it takes to produce an html page for your application.
+
+Interactive completion
+~~~~~~~~~~~~~~~~~~~~~~
+The auto-generated `complete' subcommand provides interactive bash
+completion.  To activate completion for the hello program, it is
+enough to put the following into your `~/.bashrc':
+
+       _hello()
+       {
+               eval $(hello complete 2>/dev/null)
+       }
+       complete -F _hello hello
+
+This will give you completion for the first argument of the hello
+program: the subcommand.
+
+In order to get subcommand-sensitive completion you must provide a
+_completer_ in your application for each subcommand that is to support
+completion. Like subcommand handlers, completers are recognized by name:
+If a function xxx_complete() is defined, gsu will call it on the
+attempt to complete the `xxx' subcommand at the subcommand line. gsu
+has a few functions to aid you in writing a completer.
+
+Let's have a look at the completer for the above `fs' subcommand.
+
+       complete_fs()
+       {
+               local f='/etc/fstab'
+               local optstring='t:v'
+
+               gsu_complete_options $optstring "$@"
+               (($ret > 0)) && return
+
+               gsu_cword_is_option_parameter $optstring "$@"
+               [[ "$result" == 't' ]] && awk '{print $3}' "$f"
+       }
+
+Completers are always called with $1 set to the index into the array
+of words in the current command line when tab completion was attempted
+(see `COMP_CWORD' in the bash manual). These words are passed to the
+completer as $2, $3,...
+
+gsu_complete_options() receives the option string as $1, the word
+index as $2 and the individual words as $3, $4,... Hence we may simply
+pass the $optstring and `"$@"'. gsu_complete_options() checks if the
+current word begins with `-', i.e., whether an attempt to complete
+an option was performed. If yes gsu_complete_options() prints all
+possible command line options and sets $ret to a positive value.
+
+The last two lines of complete_fs() check whether the word preceding
+the current word is an option that takes an argument. If it is,
+that option is returned in $result, otherwise $result is the empty
+string. Hence, if we are completing the argument to `-t', the awk
+command is executed to print all file system types of /etc/fstab as
+the possible completions.
+
+See the comments to gsu_complete_options(),
+gsu_cword_is_option_parameter() and gsu_get_unnamed_arg_num()
+(which was not covered here) in the `subcommand' file for a more
+detailed description.
+
+The gui module
+--------------
+This module can be employed to create interactive dialog boxes from a
+bash script. It depends on the dialog(1) utility which is available on
+all Unix systems. On Debian and Ubuntu Linux it can be installed with
+
+       apt-get install dialog
+
+The core of the gui module is the gsu_gui() function which receives
+a _menu tree_ as its single argument. The menu tree defines a tree
+of menus for the user to navigate with the cursor keys. As for a
+file system tree, internal tree nodes represent folders. Leaf nodes,
+on the other hand, correspond to _actions_. Pressing enter activates a
+node. On activation, for internal nodes a new menu with the contents of
+the subfolder is shown. For leaf nodes the associated _action handler_
+is executed.
+
+Hence the application has to provide a menu tree and an action handler
+for each leaf node defined in the tree. The action handler is simply a
+function which is named according to the node. In most cases the action
+handler will run dialog(1) to show some dialog box on its own. Wrappers
+for some widgets of dialog are provided by the gui module, see below.
+
+Menu trees
+~~~~~~~~~~
+The concept of a menu tree is best illustrated by an example. Assume
+we'd like to write a system utility for the not-so-commandline-affine
+Linux sysadmin next door. For the implementation we confine ourselves
+with giving some insight in the system by running lean system commands
+like `df' to show the list of file system, or `dmesg' to print the
+contents of the kernel log buffer. Bash code which defines the menu
+tree could look like this:
+
+       menu_tree='
+               load_average
+               processes
+               hardware/
+                       cpu
+                       scsi
+               storage/
+                       df
+                       mdstat
+               log/
+                       syslog
+                       dmesg
+       '
+
+In this tree, `hardware/', `block_devices/' and `log/' are the only
+internal nodes. Note that these are written with a trailing slash
+character while the leaf nodes have no slash at the end. All entries
+of the menu tree must be indented by tab characters.
+
+Action handlers
+~~~~~~~~~~~~~~~
+Action handlers are best explained via example:
+
+Our application, let's call it `lsi' for _lean system information_,
+must provide action handlers for all leaf nodes. Here is the action
+handler for the `df' node:
+
+       lsi_df()
+       {
+               gsu_msgbox "$(df -h)"
+       }
+
+The function name `lsi_df' is derived from the name of the script
+(`lsi') and the name of the leaf node (`df'). The function simply
+passes the output of the `df(1)' command as the first argument to
+the public gsu function gsu_msgbox() which runs dialog(1) to display
+a message box that shows the given text.
+
+gsu_msgbox() is suitable for small amounts of output. For essentially
+unbounded output like log files that can be arbitrary large, it is
+better to use gsu_textbox() instead which takes a path to the file
+that contains the text to show.
+
+To illustrate gsu_input_box() function, assume the action handler
+for the `processes' leaf node should ask for a username, and display
+all processes owned by the given user. This could be implemented
+as follows.
+
+       lsi_processes()
+       {
+               local username
+
+               gsu_inputbox 'Enter username' "$LOGNAME"
+               (($ret != 0)) && return
+               username="$result"
+               gsu_msgbox "$(pgrep -lu "$username")"
+       }
+
+Once all other action handlers have been defined, the only thing left
+to do is to source the gsu gui module and to call gsu_gui():
+
+       . /usr/local/lib/gsu/gui || exit 1
+       gsu_gui "$menu_tree"
+
+Example
+~~~~~~~
+The complete lsi script below can be used as a starting point
+for your own gsu gui application. If you cut and paste it, be
+sure to not turn tab characters into space characters.
+
+       #!/bin/bash
+
+       menu_tree='
+               load_average
+               processes
+               hardware/
+                       cpu
+                       scsi
+               storage/
+                       df
+                       mdstat
+               log/
+                       syslog
+                       dmesg
+       '
+
+       lsi_load_average()
+       {
+               gsu_msgbox "$(cat /proc/loadavg)"
+       }
+
+       lsi_processes()
+       {
+               local username
+
+               gsu_inputbox 'Enter username' "$LOGNAME"
+               (($ret < 0)) && return
+               username="$result"
+               gsu_msgbox "$(pgrep -lu "$username")"
+       }
+
+       lsi_cpu()
+       {
+               gsu_msgbox "$(lscpu)"
+       }
+
+       lsi_scsi()
+       {
+               gsu_msgbox "$(lsscsi)"
+       }
+
+       lsi_df()
+       {
+               gsu_msgbox "$(df -h)"
+       }
+
+       lsi_mdstat()
+       {
+               gsu_msgbox "$(cat /proc/mdstat)"
+       }
+
+       lsi_dmesg()
+       {
+               local tmp="$(mktemp)" || exit 1
+
+               trap "rm -f $tmp" EXIT
+               dmesg > $tmp
+               gsu_textbox "$tmp"
+       }
+
+       lsi_syslog()
+       {
+               gsu_textbox '/var/log/syslog'
+       }
+
+       . /usr/local/lib/gsu/gui || exit 1
+       gsu_gui "$menu_tree"
+
+The config module
+-----------------
+Some applications need config options which are not related to
+any particular subcommand, like the URL of a web service, the path
+to some data directory, or a default value which is to be used by
+several subcommands. Such options do not change frequently and are
+hence better stored in a configuration file rather than passed to
+every subcommand that needs the information.
+
+The config module of gsu makes it easy to maintain such options and
+performs routine tasks like reading and checking the values given in
+the config file, or printing out the current configuration. It can
+be used stand-alone, or in combination with either the subcommand or
+the gui module.
+
+Defining config options
+~~~~~~~~~~~~~~~~~~~~~~~
+To use the config module, you must define the $gsu_options bash array.
+Each config option is represented by one slot in this array. Here is
+an example which defines two options:
+
+ gsu_options=(
+ "
+       name=fs_type
+       option_type=string
+       default_value=ext3
+       required=false
+       description='file system type to consider'
+       help_text='
+               This option is used in various contexts. All
+               subcommands which need a file system type
+               use the value specified here as the default.
+       '
+ "
+ "
+       name=limit
+       option_type=num
+       default_value=3
+       required=no
+       description='print at most this many lines of output'
+ "
+ )
+
+Each config option consists of the following fields:
+
+ - *name*. This must be a valid bash variable name. Hence no special
+ characters are allowed.
+
+ - *option_type*. Only `string' and `num' are supported but additional
+ types might be supported in future versions. While string variables
+ may have arbitrary content, only integers are accepted for variables
+ of type `num'.
+
+ - *default_value*. The value to use if the option was not specified.
+
+ - *required*. Whether gsu considers it an error if the option was
+ not specified. It does not make sense to set this to `true' and set
+ *default_value* at the same time.
+
+ - *description*. Short description of the variable. It is printed by
+ the `prefs' subcommand.
+
+ - *help_text*. Optional long description, also printed by `prefs'.
+
+To enable the config module you must source the config module of gsu
+after $gsu_options has been defined:
+
+       . /usr/local/lib/gsu/config || exit 1
+
+Passing config options to the application
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+There are two ways to pass the value of an option to a gsu application:
+environment variable and config file. The default config file is
+~/.$gsu_name.rc where $gsu_name is the basename of the application,
+but this can be changed by setting $gsu_config_file. Thus, the
+following two statements are equivalent
+
+       fs_type=xfs hello fs
+       echo 'fs_type=xfs' > ~/.hello.rc && hello fs
+
+If an option is set both in the environment and in the config file,
+the environment takes precedence.
+
+Checking config options
+~~~~~~~~~~~~~~~~~~~~~~~
+The gsu config module defines two public functions for this purpose:
+gsu_check_options() and gsu_check_options_or_die(). The latter function
+exits on errors while the former function only sets $ret and $result
+as appropriate and lets the application deal with the error. The best
+place to call one of these functions is after sourcing the config
+module but before calling gsu() or gsu_gui().
+
+Using config values
+~~~~~~~~~~~~~~~~~~~
+The name of an option as specified in $gsu_options (`fs_type' in
+the example above) is what users of your application may specify at
+the command line or in the config file. This leads to a mistake that
+is easy to make and difficult to debug: The application might use a
+variable name which is also a config option.
+
+To reduce the chance for this to happen, gsu_check_options() creates
+a different set of variables for the application where each variable
+is prefixed with ${gsu_name}. For example, if $gsu_options as above
+is part of the hello script, $hello_fs_type and $hello_limit are
+defined after gsu_check_options() returned successfully. Only the
+prefixed variants are guaranteed to contain the proper value, so this
+variable should be used exclusively in the application. The
+prefix may be changed by setting $gsu_config_var_prefix before calling
+gsu_check_options().
+
+com_prefs()
+~~~~~~~~~~~
+For scripts which source both the subcommand and the config module, the
+auto-generated 'prefs' subcommand prints out the current configuration
+and exits. The description and help text of the option as specified
+in the `description' and `help_text' fields of $gsu_options are shown
+as comments in the output. Hence this output can be used as a template
+for the config file.
+
+List of public variables
+------------------------
+  - *$gsu_dir*. Where gsu is installed. If unset, gsu guesses
+  its installation directory by examining the $BASH_SOURCE array.
+
+  - *$gsu_name*. The name of the application. Defaults to $0 with
+  all leading directories removed.
+
+  - *$gsu_banner_txt*. Used by both the subcommand and the gui
+  module. It is printed by the man subcommand, and as the title for
+  dialog windows.
+
+  - *$gsu_errors*. Identifier/text pairs for custom error reporting.
+
+  - *$gsu_config_file*. The name of the config file of the application.
+  Defaults to `~/.${gsu_name}.rc'.
+
+  - *$gsu_options*.
+
+  - *$gsu_config_var_prefix*. Used by the config module to set up
+  the variables defined in $gsu_options.
+
+License
+-------
+Contact
+-------
+Send beer, pizza, patches, improvements, bug reports, flames,
+(in this order), to Andre Noll `<maan@tuebingen.mpg.de>'.
+
+References
+----------
+ - http://www.gnu.org/software/bash/bash.html (bash)
+ - http://www.invisible-island.net/dialog/dialog.html (dialog)
+ - http://triptico.com/software/grutatxt.html (grutatxt)