Skip to main content

4. An example

Let's try to improve the program sys_info.sh, that we started to build in a previous lesson, by adding some parameters and option to it. We want to be able to:

  • Tell it to save the output to a specific file (instead of sending it to stdout), by using the options -f file or --file file.
  • Tell it to ask interactively for a filename for saving the output. This option should be specified by -i or --interactive.
  • Use the options -h or --help to make the program output information about its usage.
  1. There is a small git repo on the archive sys_info.tgz, let's open it:

    tar xfz sys_info.tgz
    cd sys_info/
    ls -al
    git log --oneline
    git tag
  2. Let's get first the initial version of the script (that was developed in a previous lesson):

    git checkout -q 1.initial
    git status
    vim sys_info.sh
    sys_info.initial.sh
    #!/bin/bash

    # Program to output a system information page

    TITLE="System Information Report For $HOSTNAME"
    CURRENT_TIME=$(date +"%x %r %Z")
    TIMESTAMP="Generated on $CURRENT_TIME, by $USER"

    report_uptime() {
    cat <<- _EOF_
    <h2>System Uptime</h2>
    <pre>$(uptime)</pre>
    _EOF_
    return
    }

    report_disk_space() {
    cat <<- _EOF_
    <h2>Disk Space Utilization</h2>
    <pre>$(df -h .)</pre>
    _EOF_
    return
    }

    report_home_space() {
    if [[ "$(id -u)" -eq 0 ]]; then
    cat <<- _EOF_
    <h2>Home Space Utilization (All Users)</h2>
    <pre>$(du -hs /home/*)</pre>
    _EOF_
    else
    cat <<- _EOF_
    <h2>Home Space Utilization ($PWD)</h2>
    <pre>$(du -hs "$PWD")</pre>
    _EOF_
    fi
    return
    }


    cat << _EOF_
    <html>
    <head>
    <title>$TITLE</title>
    </head>
    <body>
    <h1>$TITLE</h1>
    <p>$TIMESTAMP</p>
    $(report_uptime)
    $(report_disk_space)
    $(report_home_space)
    </body>
    </html>
    _EOF_
    ./sys_info.sh
  3. Let's see some modifications and improvements to it:

    Enclose in a function the last part (that generates the HTML page):

    git checkout -q 2.write_html_page
    git diff 1.initial
    git diff 1.initial
    diff --git a/sys_info.sh b/sys_info.sh
    index 4f261db..6291299 100755
    --- a/sys_info.sh
    +++ b/sys_info.sh
    @@ -37,18 +37,23 @@ report_home_space() {
    return
    }

    +write_html_page () {
    + cat <<- _EOF_
    + <html>
    + <head>
    + <title>$TITLE</title>
    + </head>
    + <body>
    + <h1>$TITLE</h1>
    + <p>$TIMESTAMP</p>
    + $(report_uptime)
    + $(report_disk_space)
    + $(report_home_space)
    + </body>
    + </html>
    + _EOF_
    +}
    +
    +# output html page
    +write_html_page

    -cat << _EOF_
    -<html>
    - <head>
    - <title>$TITLE</title>
    - </head>
    - <body>
    - <h1>$TITLE</h1>
    - <p>$TIMESTAMP</p>
    - $(report_uptime)
    - $(report_disk_space)
    - $(report_home_space)
    - </body>
    -</html>
    -_EOF_

    Add a function that displays the usage of the program:

    git checkout -q 3.usage
    git diff 2.write_html_page
    git diff 2.write_html_page
    diff --git a/sys_info.sh b/sys_info.sh
    index 6291299..b58cf06 100755
    --- a/sys_info.sh
    +++ b/sys_info.sh
    @@ -1,7 +1,23 @@
    #!/bin/bash
    -
    # Program to output a system information page

    +usage () {
    + cat <<- _EOF_
    + $PROGNAME: usage:
    +
    + $PROGNAME (-f | --file) <file>
    + Output the report to the given file.
    +
    + $PROGNAME (-i | --interactive)
    + Get the output file interactively from the keyboard.
    +
    + $PROGNAME [-h | --help]
    + Display this help message.
    + _EOF_
    + return
    +}
    +
    +PROGNAME="$(basename "$0")"
    TITLE="System Information Report For $HOSTNAME"
    CURRENT_TIME=$(date +"%x %r %Z")
    TIMESTAMP="Generated on $CURRENT_TIME, by $USER"
  4. Add some code that reads the command line options:

    git checkout -q 4.process_options
    git diff 3.usage
    git diff 3.usage
    diff --git a/sys_info.sh b/sys_info.sh
    index b58cf06..37ddc1b 100755
    --- a/sys_info.sh
    +++ b/sys_info.sh
    @@ -17,6 +17,30 @@ usage () {
    return
    }

    +# process command line options
    +interactive=''
    +filename=''
    +while [[ -n "$1" ]]; do
    + case "$1" in
    + -f | --file)
    + shift
    + filename="$1"
    + ;;
    + -i | --interactive)
    + interactive=1
    + ;;
    + -h | --help)
    + usage
    + exit
    + ;;
    + *)
    + usage >&2
    + exit 1
    + ;;
    + esac
    + shift
    +done
    +
    PROGNAME="$(basename "$0")"
    TITLE="System Information Report For $HOSTNAME"
    CURRENT_TIME=$(date +"%x %r %Z")

    We use a while loop and shift to process all the options. Inside the loop we use case to match the option with one of those that are expected. If the option is -f (or --file), we interpret the next parameter as a filename and set it to the variable filename. If the option is -i (or --interactive), we set the variable interactive to 1 (otherwise it will remain empty).

    Notice that the actions corresponding to -h | --help) and *) are very similar, they display the usage and exit the program. However the later case is considered an error, because there is an unknown/unsupported option, so the usage is sent to stderr (>&2) and the program exits with code 1 (error).

  5. If the option -i or (--interactive) is supplied, the program should get a file name interactively (from the keyboard). Let's see the code that does that:

    git checkout -q 5.interactive
    git diff 4.process_options
    git diff 4.process_options
    diff --git a/sys_info.sh b/sys_info.sh
    index 37ddc1b..f8a2fba 100755
    --- a/sys_info.sh
    +++ b/sys_info.sh
    @@ -41,6 +41,32 @@ while [[ -n "$1" ]]; do
    shift
    done

    +# interactive mode
    +if [[ -n "$interactive" ]]; then
    + while true; do
    + read -p "Enter name of output file: " filename
    + if [[ -e "$filename" ]]; then
    + read -p "'$filename' exists. Overwrite? [y/n/q] > "
    + case "$REPLY" in
    + Y|y)
    + break
    + ;;
    + Q|q)
    + echo "Program terminated."
    + exit
    + ;;
    + *)
    + continue
    + ;;
    + esac
    + elif [[ -z "$filename" ]]; then
    + continue
    + else
    + break
    + fi
    + done
    +fi
    +
    PROGNAME="$(basename "$0")"
    TITLE="System Information Report For $HOSTNAME"
    CURRENT_TIME=$(date +"%x %r %Z")

    This code is executed only if the global variable interactive is not empty. There is an infinite while loop that tries to read the name of the file into to global variable filename. We check that the given value is not empty and that such a file does not exist already. If there is already such a file, we ask again whether we can overwrite the file or not.

    We use the loop so that we can ask again for another file name if the given one is not suitable, and we repeat until we have a suitable file name (stored in the variable filename).

  6. Now let's see the code that outputs the HTML page:

    git checkout -q 6.output_html_page
    git diff 5.interactive
    git diff 5.interactive
    diff --git a/sys_info.sh b/sys_info.sh
    index f8a2fba..8c3e576 100755
    --- a/sys_info.sh
    +++ b/sys_info.sh
    @@ -121,5 +121,13 @@ write_html_page () {
    }

    # output html page
    -write_html_page
    -
    +if [[ -n "$filename" ]]; then
    + if touch "$filename" && [[ -f "$filename" ]]; then
    + write_html_page > "$filename"
    + else
    + echo "$PROGNAME: Cannot write file '$filename'" >&2
    + exit 1
    + fi
    +else
    + write_html_page
    +fi

    If the variable filename is empty, then the HTML page will be sent to the stdout, same as before. Otherwise the program will try to send it to the given file (using redirection). The program also makes sure that we can write to the file, by trying to create an empty file first.

  7. Finally, let's study the latest version of the program.

    git checkout master
    vim sys_info.sh
    sys_info.final.sh
    #!/bin/bash
    # Program to output a system information page

    PROGNAME="$(basename "$0")"

    usage () {
    cat <<- _EOF_
    Usage:
    $PROGNAME
    Output the report to the stdout.

    $PROGNAME (-f | --file) <file>
    Output the report to the given file.

    $PROGNAME (-i | --interactive)
    Get the output file interactively from the keyboard.

    $PROGNAME (-h | --help)
    Display this help message.
    _EOF_
    return
    }

    main () {
    # global aux vars
    interactive=''
    filename=''

    process_options "$@"
    interactive_mode
    output_html_page
    }

    process_options () {
    # process command line options
    while [[ -n "$1" ]]; do
    case "$1" in
    -f | --file)
    shift
    filename="$1"
    ;;
    -i | --interactive)
    interactive=1
    ;;
    -h | --help)
    usage
    exit
    ;;
    *)
    usage >&2
    exit 1
    ;;
    esac
    shift
    done
    }

    interactive_mode () {
    # interactive mode
    if [[ -n "$interactive" ]]; then
    while true; do
    read -p "Enter name of output file: " filename
    if [[ -e "$filename" ]]; then
    read -p "'$filename' exists. Overwrite? [y/n/q] > "
    case "$REPLY" in
    Y|y)
    break
    ;;
    Q|q)
    echo "Program terminated."
    exit
    ;;
    *)
    continue
    ;;
    esac
    elif [[ -z "$filename" ]]; then
    continue
    else
    break
    fi
    done
    fi
    }

    output_html_page () {
    # output html page
    if [[ -n "$filename" ]]; then
    if touch "$filename" && [[ -f "$filename" ]]; then
    write_html_page > "$filename"
    else
    echo "$PROGNAME: Cannot write file '$filename'" >&2
    exit 1
    fi
    else
    write_html_page
    fi
    }

    write_html_page () {
    local TITLE="System Information Report For $HOSTNAME"
    local CURRENT_TIME=$(date +"%x %r %Z")
    local TIMESTAMP="Generated on $CURRENT_TIME, by $USER"

    cat <<- _EOF_
    <html>
    <head>
    <title>$TITLE</title>
    </head>
    <body>
    <h1>$TITLE</h1>
    <p>$TIMESTAMP</p>
    $(report_uptime)
    $(report_disk_space)
    $(report_home_space)
    </body>
    </html>
    _EOF_
    }

    report_uptime() {
    cat <<- _EOF_
    <h2>System Uptime</h2>
    <pre>$(uptime)</pre>
    _EOF_
    return
    }

    report_disk_space() {
    cat <<- _EOF_
    <h2>Disk Space Utilization</h2>
    <pre>$(df -h .)</pre>
    _EOF_
    return
    }

    report_home_space() {
    if [[ "$(id -u)" -eq 0 ]]; then
    cat <<- _EOF_
    <h2>Home Space Utilization (All Users)</h2>
    <pre>$(du -hs /home/*)</pre>
    _EOF_
    else
    cat <<- _EOF_
    <h2>Home Space Utilization ($PWD)</h2>
    <pre>$(du -hs "$PWD")</pre>
    _EOF_
    fi
    return
    }

    # call the main function
    main "$@"

    :set tabstop=8

    Notice that we have placed almost all the code inside a function. There is a function main() that calls some other functions, then these functions call some other ones, and so on.

    The main() function is called at the very end of the program, like this:

    main "$@"

    This makes sure that all the parameters given to the program are passed to the main function. The main function in turn passes all of them to the function process_options, like this:

    process_options "$@"

Loading asciinema cast...