Skip to main content

1. Looping with for

  1. With for we can loop a list of words:

    for i in A B C D; do echo $i; done
    for i in {A..D}; do echo $i; done
    for i in *.sh; do echo "$i"; done

    In these cases it is using shell expansions.

    The last file expansion may fail if there are no files like that, in which case shell will return just *.sh (instead of a list of matching files). To protect against this, we can rewrite the last example like this:

    for i in *.sh; do [[ -e "$i" ]] && echo "$i"; done
  2. Let's see an example that finds the longest word in a file:

    longest-word.sh
    #!/bin/bash

    # longest-word: find longest string in a file

    while [[ -n "$1" ]]; do
    if [[ -r "$1" ]]; then
    max_word=
    max_len=0
    for i in $(strings "$1"); do
    len="$(echo -n "$i" | wc -c)"
    if (( len > max_len )); then
    max_len="$len"
    max_word="$i"
    fi
    done
    echo "$1: '$max_word' ($max_len characters)"
    fi
    shift
    done
    vim longest-word.sh

    Actually it can take one or more files as parameters and process each of them. This is implemented by the while loop:

    while [[ -n "$1" ]]; do
    if [[ -r "$1" ]]; then
    # . . . . .
    # . . . . .
    fi
    shift
    done

    The if checks that the file is readable (otherwise it is skipped).

    The list of words of the file is produced by the command strings "$1". We use a command substitution to use this list in the for statement:

    for i in $(strings "$1"); do

    Notice that we are not surrounding $(strings "$1") with double quotes, otherwise it will be treated as a single string, which is not what we want.

    Let's try it:

    echo *
    ./longest-word.sh *
  3. Let's see a slightly modified version of the previous example:

    longest-word2.sh
    #!/bin/bash

    # longest-word2: find longest string in a file

    for i; do
    if [[ -r "$i" ]]; then
    max_word=
    max_len=0
    for j in $(strings "$i"); do
    len="$(echo -n "$j" | wc -c)"
    if (( len > max_len )); then
    max_len="$len"
    max_word="$j"
    fi
    done
    echo "$i: '$max_word' ($max_len characters)"
    fi
    done
    vim longest-word2.sh

    The modification consists on replacing the while loop with a for loop like this:

    for i; do
    if [[ -r "$i" ]]; then
    # . . . . .
    # . . . . .
    fi
    done

    And since we are using the variable i for the outer loop, in the inner loop we have replaced i with j.

    When we omit the list of words, for will use by default the positional parameters.

    echo *
    ./longest-word2.sh *
  4. for has also another form, which is similar to that of C (and many other languages):

    for ((i=0; i<5; i=i+1)); do echo $i; done

    As you know, the construct ((...)) is used for arithmetic expressions, and inside it we don't use a $ in front of the variables.

  5. Let's also make a small modification to the program sys_info.sh that we saw in the last lesson:

    sys_info.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 () {
    local format="%8s%10s%10s\n"
    local i dir_list total_files total_dirs total_size user_name
    if [[ "$(id -u)" -eq 0 ]]; then
    dir_list=/home/*
    user_name="All Users"
    else
    dir_list="$PWD"
    user_name="$USER"
    fi
    echo "<h2>Home Space Utilization ($user_name)</h2>"
    for i in $dir_list; do
    total_files="$(find "$i" -type f | wc -l)"
    total_dirs="$(find "$i" -type d | wc -l)"
    total_size="$(du -sh "$i" | cut -f 1)"
    echo "<H3>$i</H3>"
    echo "<pre>"
    printf "$format" "Dirs" "Files" "Size"
    printf "$format" "----" "-----" "----"
    printf "$format" "$total_dirs" "$total_files" "$total_size"
    echo "</pre>"
    done
    return
    }

    # call the main function
    main "$@"

    vim sys_info.sh

    Only report_home_space () (the last function) has been modified. It provides more detail for each user’s home directory and includes the total number of files and subdirectories in each. We also use some local variables and use printf (instead of echo) to format some of the output.

    ./sys_info.sh > report.html
    lynx report.html
Loading asciinema cast...