Bash Script "Usage" output formatting Bash Script "Usage" output formatting bash bash

Bash Script "Usage" output formatting


I like to use cat for this:

usage.sh:

#!/bin/bashcat <<EOFUsage: $0 [options]-h| --help           this is some help text.                     this is more help text.-1|--first-option    this is my first option-2|--second-option   this is my second optionEOF

This will output:

Usage: usage.sh [options]-h| --help           this is some help text.                     this is more help text.-1|--first-option    this is my first option-2|--second-option   this is my second option


I think that a truly perfect solution for this kind of task should be more complicated than that. In most shells, the environment variable echo ${COLUMNS} can be used to know in a script the width of the terminal window.

I've created a simple usage function for a script I wrote which takes ${COLUMNS} into consideration. It is explained as much as possible in the comments:

# Put here all the options your script acceptslocal options=(    '-c,--config <FILE>'    '-l,--list'    '-r,--run'    '-v,--verbose'    '-n,--dry-run'    '-h,--help')# Put here the corresponding descriptions for every option you specified in the array abovelocal descriptions=(    "Use the given configuration file instead of the default one"    "List all program related files. if used with \`--verbose\`, the full contents are printed"    "Try to process all the files"    "Turn on verbose output"    "don't store files like alwyas but show only what actions would have been taken if $(basename "$0") would have run normally (with or without --run), implies --verbose"    "display help")# Put here the offset options will getlocal options_offset=3# Put here the offset descriptions will get after the longest optionlocal descriptions_offset_after_longest_option=5# Put here the maximum length of descriptions spanninglocal maximum_descriptions_length=80# ---------------------------------# Up until here is the configuration# ---------------------------------# First we print the classic Usage messageecho "Usage: $(basename "$0") [OPTION]..."# In the next loop, we put in ${max_option_length} the length of the# longest option. This way, we'll be able to calculate the offset when# printing long descriptions that should span over several lines.local max_option_length=1for (( i = 0; i < ${#options[@]}; i++)); do    if [[ $max_option_length -lt ${#options[$i]} ]]; then        max_option_length=${#options[$i]}    fidone# We put in the following variable the total offset of descriptions# after new-lines.local descriptions_new_line_offset=$((${max_option_length} + ${options_offset} + ${descriptions_offset_after_longest_option}))# The next loop is the main loop where we actually print the options with# the corresponding descriptions.for (( i = 0; i < ${#options[@]}; i++)); do    # First, we print the option and the offset we chose above right before it    printf -- '%*s' ${options_offset}    printf -- '%s' "${options[$i]}"    # Here we start tracking through out this loop the current index of the    # char on the terminal window. This is necessary because in the process    # of printing the descriptions' words we'll be able to know how not to    # span over the defined maximum length or not to split words when    # hitting ${COLUMNS}    local current_char_index=$((${options_offset} + ${#options[$i]}))    # We calculate the offset which should be given between the current    # option and the start of it's description. This is different for every    # option because every option has a different length but they all must    # be aligned according to the longest option's length and the offsets    # we chose above    local current_description_offset=$((${max_option_length} - ${#options[$i]} + ${descriptions_offset_after_longest_option}))    # We print this offset before printing the description    printf -- '%*s' ${current_description_offset}    # Updating the current_char_index    current_char_index=$((${current_char_index} + ${current_description_offset}))    # We put in a temporary variable the current description from the array    local current_description="${descriptions[$i]}"    # We divide the current_description to an array with the description's    # words as the array's elements. This is necessary so we can print the    # description without spliting words    IFS=' ' read -r -a description_words <<< "${current_description}"    # We start a loop for every word in the descriptions words array    for (( j = 0; j < ${#description_words[@]}; j++)); do        # We update the current char index before actually printing the        # next word in the description because of the condition right        # afterwards        current_char_index=$((${current_char_index} + ${#description_words[$j]} + 1))        # We check if the index we will reach will hit the maximum limit we        # chose in the beginning or the number of ${COLUMNS} our terminal        # gives us        if [[ ${current_char_index} -le ${COLUMNS} ]] && [[ ${current_char_index} -le ${maximum_descriptions_length} ]]; then            # If we don't hit our limit, print the current word            printf -- '%s ' ${description_words[$j]}        else            # If we've hit our limit, print a new line            printf -- '\n'            # Print a number of spaces equals to the offset we need to give            # according to longest option we have and the other offsets we            # defined above            printf -- '%*s' ${descriptions_new_line_offset}            # print the next word in the new line            printf -- '%s ' ${description_words[$j]}            # Update the current char index            current_char_index=$((${descriptions_new_line_offset} + ${#description_words[$j]}))        fi    done    # print a new line between every option and it's description    printf '\n'done


Heredocs also have a tab indented option. This allows you to preface each line of code with any number of tabs - and those will be "eaten up" on output, left justifying your output. Note that the trailing 'EOF' (in this example) MUST be fully left indented - the 'EOF' can not be tab indented.

Be careful of any editors that are converting tab characters to spaces (eg "vi" option is "expandtab" which converts tabs to spaces). Unfortunately, multiple spaces are not "eaten up" like tabs are. If you use 'expandtab' (or similar options) for code formatting, then the heredoc tab indent method is not likely to be useful to you.

In the example below, the "<<-" is the syntax for a heredoc to honor the tab indents.

Example:

    cat <<-EOF    usage.sh [options]    -h| --help           this is some help text.                         this is more help text.    -1|--first-option    this is my first option    -2|--second-option   this is my second optionEOF

Note that there are "tabs" in front of the "cat", and subsequent lines - HTML formatting here is likely not going to allow you to cut-n-paste the example.

Note how the terminating "EOF" is left justified.