简体   繁体   中英

Bash Script “Usage” output formatting

What is a good way to output help text for a bash script to get the columns lines to be lined up properly?

something like:

 Usage: mycommand [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 like to use cat for this:

usage.sh :

#!/bin/bash

cat <<EOF
Usage: $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 option
EOF

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

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 option
EOF

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.

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 accepts
local 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 above
local 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 get
local options_offset=3
# Put here the offset descriptions will get after the longest option
local descriptions_offset_after_longest_option=5
# Put here the maximum length of descriptions spanning
local maximum_descriptions_length=80

# ---------------------------------
# Up until here is the configuration
# ---------------------------------

# First we print the classic Usage message
echo "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=1
for (( i = 0; i < ${#options[@]}; i++)); do
    if [[ $max_option_length -lt ${#options[$i]} ]]; then
        max_option_length=${#options[$i]}
    fi
done
# 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM