Showing a Bash Spinner for Long Running Tasks

by Louis Marascio on February 27, 2011

This post is part of a series: Bash Tips and Tricks.

I’ve mentioned before that I like to show users of my scripts useful information. This can be a complex progress bar, a simple progress meter, or it can be an animation to let the user know the script hasn’t hung. These animations are typically called Throbbers are exist purely to tell the user to continue patiently waiting. Sometimes folks like me call Throbbers Spinners for two reasons. First, a very common throbber animation type is the spinning wheel. Second, the word throbber sounds oddly sexual, and sort of creeps me out if I say it too many times in a sentence (Just kidding. Well, no, not really.)

This tip will show you to create a spinner for your Bash scripts. If you have a long running process and don’t want to try to tell the user approximately how much of that process is left to run, showing them a spinner is a great alternative.

Implementing the Spinner

The throbber, er I mean spinner, is implemented as a loop that shifts a string during each iteration.

    local pid=$1
    local delay=0.75
    local spinstr='|/-\'
    while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do
        local temp=${spinstr#?}
        printf " [%c]  " "$spinstr"
        local spinstr=$temp${spinstr%"$temp"}
        sleep $delay
        printf "\b\b\b\b\b\b"
    printf "    \b\b\b\b"

Let’s dissect the spinner function so to illustrate how it works.

local pid=$1
local delay=0.75
local spinstr='|/-\'

This is pretty self-explanatory. The function has one input argument and two internal variables that control how it works.

  1. The input argument is assigned to the local variable pid. This is the process ID of the background task for which we are showing the spinner.

  2. The next local variable, delay, is how long each frame of the spinner animation stays visible for. We’ve set it to 75% of 1 second or 750ms. A smaller number will make the spinner rotate faster while a larger number will make it slower.

  3. The final local variable is called spinstr. This is a string for which each character is a frame in our spinner animation. As you can see, the string is 4 characters long, therefore we have four frames in our animation.

Moving along we get to the meat of the function, the primary loop.

while [ "$(ps a | awk '{print $1}' | grep -w $pid)" ]; do

The loop condition does three things:

  1. ps a will show all processes.

  2. awk '{print $1}' will extract the pid column of the process list.

  3. grep -w $pid will look for the process ID of our background task in the list of PIDs printed by awk.

The loop is conditioned on the return value of grep, the last command in our pipe chain. If grep finds a match in our PID list it will return 0, otherwise it will return 1.

The next several lines do the hard work of displaying our animation. I’ve created two images to help illustrate what’s going on.

First, I remove the first character from the string and save the remaining characters into a temp.

local temp=${spinstr#?}

Then I use printf to output the first character of spinstr, which contains our animation. Only the first character is output because I use the %c format string.

printf " [%c]  " "$spinstr"

These two steps are illustrated below.

Bash Spinner Steps 1 and 2

Finally, I shift spinstr by constructing a new string that contains the value of temp and all characters from spinstr that aren’t in temp.

local spinstr=$temp${spinstr%"$temp"}

The first part of this, the character rotation, appears as step 3 and the last part, the assignment to spinstr appears as step 4.

Bash Spinner Steps 3 and 4

Using the Bash Spinner

When you have a task to run that will take a large (or unknown) amount of time invoke it in a background subshell like this:

(a_long_running_task) &

Then, immediately following that invocation, call the spinner and pass it the PID of the subshell you invoked.

spinner $!

The $! is a bash internal variable for the PID of the last job run in the background. In this case, it will give us the PID of the bash shell executing our long running task.

When it’s all said and done you’ll have a nice and simple Bash spinner like the one below.

A Bash Spinner

  • Василий Алексеенко

     show progress long tasks – pv

  • Louis Marascio

     Thanks for sharing. I love pipe viewer but it isn’t always the best way to show progress to your users. Computationally expensive tasks, for example, w/o any pipe IO come to mind. It is a great tool, however, any is worthy of it’s own post.

  • Guest

    spinner(){ local pid=$1 local delay=0.25 while [ $(ps -eo pid | grep $pid) ]; do for i in | / – \; do printf ‘ [%c]bbbb’ $i sleep $delay done done printf ‘bbbb’}

  • Louis Marascio

    Nice! Thanks for sharing.

  • Haus

    I’m interested in using your spinner. Under what license is it available?

  • Louis Marascio

    Haus, it is in the public domain. You may use it for whatever purpose you wish, commercial or otherwise.

  • Haus

    Awesome. Thanks!

  • blzysh

    Awesome – Thanks :)

  • guest

    Thank you for sharing, my little bash script now has a spinner :)

  • Iskren

    Another way to check if a process is alive without invoking external commands is …. while [ -d /proc/$PID ]; do ….

  • JPetrucci

    Added informational text after the spinner:


    spinner() { local pid=$1 local delay=0.175 local spinstr=’|/-’ local infotext=$2 while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do local temp=${spinstr#?} printf ” [%c] %s” “$spinstr” “$infotext” local spinstr=$temp${spinstr%”$temp”} sleep $delay printf “bbbbbb” for i in $(seq 1 ${#infotext}); do printf “b” done done printf ” bbbb” } sleep 30 & spinner $! “Doing something big…”

  • Rodrigo Bariviera

    Could use tput to hide the cursor while loop tput civis; # hide cursor tput cnorm; # show cursor

    spinner() { local pid=$1 local delay=0.75 local spinstr=’|/-’ tput civis; while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do local temp=${spinstr#?} printf ” [%c] ” “$spinstr” local spinstr=$temp${spinstr%”$temp”} sleep $delay printf “bbbbbb” done printf ” bbbb” tput cnorm; }

  • Paul

    How do you get the text in front of the spinner in the above gif?

  • Anders S

    Nice stuff. But how do you handle errors? I have a exit 1 inside the subshell, but the main script just continues…

  • Barrabash

    Nice question…

  • Robert Casey

    Very cool. Adapted this to my bash while loop as it digests return lines from a web service. Left credit to you in the comments. Thanks!

  • Systems Rebooter

    How do you get the text in front of the spinner in the above gif?

  • Clay

    You need to add an extra backslash on the local spinstr declaration, otherwise bash thinks you’re escaping the ‘ local spinstr=’|/-’ Should be: local spinstr=’|/-\’

  • Ozy

    Why complicating the whole stuff ?

    spinstr=’|/-’ while kill -0 $pid > /dev/null 2>&1; do printf ” [%c] bbbbbb” “$spinstr” spinstr=${spinstr#?}${spinstr%%???} sleep $delay done

    kill -0 is a portable way to check for a pid. spinstr gets shifted on every run.

  • fanoush

    Or you can leave spinstr alone and increment index variable and use bash substring:

    tback=”$(tput cub1)” spinstr=’|/-’ spinpos=0 while true; do echo -n “${spinstr:spinpos:1}$tback” spinpos=$(((spinpos+1)%4)) done

Previous post:

Next post: