28

How can I show spinner till command line finish it is job? In other words, If I am running a script and I want to show spinner while this script is running and the spinner disappears when the script finish it is job.

Bellow is a common spinner code:

i=1
sp="/-\|"
echo -n ' '
while true
do
printf "\b${sp:i++%${#sp}:1}"
done

How can I link the previous spinner code to a command to let it show spinner while the command is running and the spinner disappears when the command finish it is job? If I include the command inside the loop it will loop with the spinner so what is the solution in this case?

6 Answers 6

34

Have your while loop watch for your real command to exit. I'll assume a Linux environment that has /proc entries for each PID, but you could slice it other ways:

#!/bin/bash
# your real command here, instead of sleep
sleep 7 &
PID=$!
i=1
sp="/-\|"
echo -n ' '
while [ -d /proc/$PID ]
do
  printf "\b${sp:i++%${#sp}:1}"
done
3
  • 10
    This is a busy loop that will eat up cpu resources. I'd suggest having a delay of some kind in your while loop.
    – ACase
    Jul 20, 2016 at 14:36
  • This makes sense and seems to be the simplest method, but it doesn't work for me (on MacOS), so I'm trying to make it work. What does -d /proc/$PID do?
    – matharden
    Jul 7, 2020 at 11:04
  • -d /proc/$PID, as part of the test command [ ... ], checks whether /proc/$PID exists as a directory (after expanding the $PID variable, of course). It's a quick way in Linux to see if a process exists with that PID.
    – Jeff Schaller
    Jul 7, 2020 at 11:49
33

Here's another fancy spinner which you can use like this:

spinner ping google.com
echo "ping exited with exit code $?"

spinner sleep 10
echo "sleep exited with exit code $?"

# or, to check out the themes quickly
while spinner sleep 1; do echo; done

It has 12 themes and picks one randomly.

#!/bin/bash
# Shows a spinner while another command is running. Randomly picks one of 12 spinner styles.
# @args command to run (with any parameters) while showing a spinner. 
#       E.g. ‹spinner sleep 10›

function shutdown() {
  tput cnorm # reset cursor
}
trap shutdown EXIT

function cursorBack() {
  echo -en "\033[$1D"
  # Mac compatible, but goes back to first column always. See comments
  #echo -en "\r"
}

function spinner() {
  # make sure we use non-unicode character type locale 
  # (that way it works for any locale as long as the font supports the characters)
  local LC_CTYPE=C

  local pid=$1 # Process Id of the previous running command

  case $(($RANDOM % 12)) in
  0)
    local spin='⠁⠂⠄⡀⢀⠠⠐⠈'
    local charwidth=3
    ;;
  1)
    local spin='-\|/'
    local charwidth=1
    ;;
  2)
    local spin="▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"
    local charwidth=3
    ;;
  3)
    local spin="▉▊▋▌▍▎▏▎▍▌▋▊▉"
    local charwidth=3
    ;;
  4)
    local spin='←↖↑↗→↘↓↙'
    local charwidth=3
    ;;
  5)
    local spin='▖▘▝▗'
    local charwidth=3
    ;;
  6)
    local spin='┤┘┴└├┌┬┐'
    local charwidth=3
    ;;
  7)
    local spin='◢◣◤◥'
    local charwidth=3
    ;;
  8)
    local spin='◰◳◲◱'
    local charwidth=3
    ;;
  9)
    local spin='◴◷◶◵'
    local charwidth=3
    ;;
  10)
    local spin='◐◓◑◒'
    local charwidth=3
    ;;
  11)
    local spin='⣾⣽⣻⢿⡿⣟⣯⣷'
    local charwidth=3
    ;;
  esac

  local i=0
  tput civis # cursor invisible
  while kill -0 $pid 2>/dev/null; do
    local i=$(((i + $charwidth) % ${#spin}))
    printf "%s" "${spin:$i:$charwidth}"

    cursorBack 1
    sleep .1
  done
  tput cnorm
  wait $pid # capture exit code
  return $?
}

("$@") &

spinner $!
13
  • Very interesting, but not working at all on macOS zsh.
    – kitsune
    Jul 11, 2020 at 8:41
  • 2
    Of course I passed the script under chmod! :D However, the case 1) works fine, but other cases, for example 11) seems that cursorBack() doesn't work, so I get a long line of characters printed in succession.
    – kitsune
    Jul 13, 2020 at 8:29
  • 2
    Man.. this is a beauty.. thank you for the inspiration!!
    – TacB0sS
    Nov 21, 2020 at 0:36
  • 2
    I love this implementation! To call it within another script save this script to spinner.sh and in your file you can source it with source $(dirname "$0")/spinner.sh then call it sleep 10 & spinner $! or long_fn & spinner $!
    – saNiks
    Aug 9, 2022 at 15:49
  • 1
    Thank you for such an awesome collection of spinners! I had the same issue as posted above on mac where the spinner text just stacked up, but I got this working properly by replacing the cursorBack function with echo -en "\r" Dec 13, 2023 at 1:39
21

This shell script should do what you're looking for:

#!/usr/bin/env bash

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

("$@") &
show_spinner "$!"

Assuming you store the shell script in a file named spinner, you can invoke it like this to display a spinner while the command sleep 10 is running:

$ spinner sleep 10
1
3

If you want a lowest common denominator spinner that works with /bin/sh and doesn't rely on the extended bash parameter substitution this should work:

#!/bin/sh

# The command you are waiting on goes between the ( ) here
# The example below returns a non zero return code

(sleep 20 ; /bin/false) &

pid=$! ; i=0
while ps -a | awk '{print $1}' | grep -q "${pid}"
do
    c=`expr ${i} % 4`
    case ${c} in
       0) echo "/\c" ;;
       1) echo "-\c" ;;
       2) echo "\\ \b\c" ;;
       3) echo "|\c" ;;
    esac
    i=`expr ${i} + 1`
    # change the speed of the spinner by altering the 1 below
    sleep 1
    echo "\b\c"
done

# Collect the return code from the background process

wait ${pid}
ret=$?

# You can report on any errors due to a non zero return code here

exit ${ret}
1

There are some fancy spinners and I bet every answer is spot on, especially @Jeff Schaller's, but personally as a developer I like being able to read the code and know exactly what's going on. I wanted a bash script to copy all my git repos into a temp zip when I start up my terminal and I also wanted a cool spinner to go with it and I'm not sure if my code is the most compact, but it definitely works well and is simple enough to read.

I'm not the most knowledgeable in bash, but I think the biggest problems are

  • running while loop in the background (could be costly to CPU, but who knows?)
  • I can still move my cursor around and that is annoying
  • and if I wanted more than one process to happen I'm not exactly sure how to go about that
function runCommand() { 
    load &                                             # calls the loading function
    local whilePID=$!                                  # gets the pid for the loop
    tar -czf ${zipFileToUpdate} ${directoryToBackUp} & # backs up files
    local backupPID=$!                                 # get's back up pid
    wait $backupPID                                    # waits for backup id
    kill $whilePID                                     # kills the while loop
    echo -ne "done"                                    # echos done and removes the spinner
}

function load() { # just a function to hold the spinner loop, here you can put whatever
    while true; do
        echo -ne "/\r"
        sleep .1
        echo -ne "-\r"
        sleep .1
        echo -ne "\ \r"
        sleep .1
        echo -ne "|\r"
        sleep .1
    done
}

runCommand

^ To touch on the last issue I mentioned about multiple commands, I think I would personally put all my commands in a function and then run the function in the background, but they may have a bunch of different PID's

EXP:

function allCommands() {
    command1;
    command2;
    command3;
    ...;
}

Then in the runCommands() function

function runCommand() { 
    load &                                             # calls the loading function
    local whilePID=$!                                  # gets the pid for the loop
    allCommands &                                      # run function w/ all cmds
    local allCmdPID=$!
    ...
}

The variable allCmdPID probably won't be the same as the commands switch and you'll likely wait for the first command and then terminate the loading loop, all while the other commands are still running. Possible work around is:

  • getting an array of commands in for loop
  • get pid of the command
  • wait for it
  • then move on to next command

But that all seems very tedious.

1
  • 1
    Just a comment about "running while loop in the background (could be costly to CPU, but who knows?)": It really depends. In the code you posted, it issues a sleep .1, which greatly reduces the "cost" because the kernel's process scheduler puts the process to sleep, sending the while loop in to limbo, and services other processes until it is time to wake the process and resume where it left off. This is the basis of event driven programming; Enter an event loop, and a some point in the loop, you call a blocking function that blocks the loop until an event of interest happens.
    – C. M.
    Jul 27, 2021 at 17:34
0

So while all the above answer work.. I thought to add mine:

DEFAULT_SpinnerFrames=("—" "\\" "|" "/")

## @function: spinner(action, label, &spinnerFramesRef[])
##
## @description: Perform an action asynchronously and display
## spinner till action is completed
##
## @param action: The action the execute
## @param label: The label to display while waiting
## @param spinnerRef: In case you feel like a custom spinner, pass a ref to an array of strings
spinner() {
  local frameRef
  local action="${1}"
  local label="${2} "
  local spinnerRef="${3-DEFAULT_SpinnerFrames}"
  local spinnerFrames=$(eval "echo \${!${spinnerRef}[@]}")

  spinnerRun() {
    while true; do
      for frame in ${spinnerFrames[@]}; do
        frameRef="${spinnerRef}[${frame}]"
        echo "${label}${!frameRef}"
        tput cuu1 tput el
        sleep 0.2
      done
    done
    echo -e "\r"
  }

  spinnerRun &
  local spinnerPid=$!
  ${action}
  kill "${spinnerPid}"
}

github tests

1
  • tput cuu1 tput el on a single line looks odd, what does this do?
    – tripleee
    Dec 8, 2021 at 8:54

You must log in to answer this question.