Creating a Bash Spinner

Do you know what this sequence of characters "/-\|/-\|" is for? A text based spinner. Still confused, read on.

You've probably seen some long running console program that shows a spinner while it runs. A spinner being the aforementioned sequence of characters output one after the other at the same place on the screen with a pause between each character. The code in this article creates a spinner which runs in a separate process. By doing this the spinner spins at a constant rate and doesn't pause when your script pauses. It also eliminates the need to sprinkle spinner output messages throughout your program.

In addition the spinner reads from a log file created by the main process and displays the last line from the log file next to the spinner. When the log file goes away the spinner exits.

This is the script for the main process, called runner.sh:

#!/bin/bash

logfile=/tmp/mylog

echo >$logfile
trap "rm -f $logfile" EXIT

# Output message to log file.
function log_msg()
{
    echo "$*" >>$logfile
}


# Start spinner
sh spinner.sh &

# Perform really long task.
i=0
log_msg "Starting a really long job"
while [[ $i -lt 100 ]]
do
    sleep 1
    let i+=5
    log_msg "$i% complete"
done

sleep 1
echo

The function at the top is to output a message to the log file. The log file is initialized to an empty file at the top of the program and the output function appends to it.

Before entering its main loop, the main process starts the spinner in the background. After that it just pauses a bit, outputs a status message and then repeats till it's 100% complete.

The spinner process follows:

#!/bin/bash

logfile=/tmp/mylog
logsize=0
spinpause=0.10
linelen=0


# Output last line from log file.
function lastout()
{
    local line=$(tail -n 1 $logfile 2>/dev/null)
    if [[ "$line" ]]; then
        echo -n "     $line"

        # Erase any extra from last line.
        local len
        let len=$linelen-${#line}
        while [[ $len -gt 0 ]]
        do
            echo -n " "
            let len--
        done
        linelen=${#line}
    fi
}

# Output a spin character.
function spinout()
{
    local spinchar="$1"
    local sz
    local ll
    if [[ -f $logfile ]]; then
        echo -n -e "\r$spinchar"
        sleep $spinpause

        # Check for new message.
        sz=$(stat --printf '%s' $logfile 2>/dev/null)
        if [[ $sz -gt $logsize ]]; then
            lastout
            logsize=$sz
        fi
    fi
}

if [[ -f $logfile ]]; then
    logsize=$(stat --printf '%s' $logfile 2>/dev/null)
    if [[ $logsize -gt 0 ]]; then
        echo -n " "
        lastout
    fi

    while [[ -f $logfile ]]
    do
        spinout "/"
        spinout "-"
        spinout "\\"
        spinout "|"
        spinout "/"
        spinout "-"
        spinout "\\"
        spinout "|"
    done
    echo
fi

The spinner contains two functions. The first function outputs the last line from the log file. Since the log file line is always placed on the same screen line it's possible that the new log line is shorter than the last log line. So, after outputting the new line the function outputs spaces up to the length of the previous log line so that it is completely erased.

The second function outputs a spinner character and pauses for the inter-character pause time. It precedes the spinner character with a carriage return so that everything remains on the same line. After outputting the spinner character it checks to see if the log file has grown in size since the last time it output a line. If it has, it then outputs a new line.

The main loop of the spinner outputs each of the spinner characters, one after the other.

Watch the attached video the see it run.

Mitch Frazier is an embedded systems programmer at Emerson Electric Co. Mitch has been a contributor to and a friend of Linux Journal since the early 2000s.

Load Disqus comments