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 Answers6

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
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
  • 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 '16 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 07 '20 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 07 '20 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 ;;

local spin='-\|/'
local charwidth=1
;;
local spin="▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"
local charwidth=3
;;
local spin="▉▊▋▌▍▎▏▎▍▌▋▊▉"
local charwidth=3
;;
local spin='←↖↑↗→↘↓↙'
local charwidth=3
;;
local spin='▖▘▝▗'
local charwidth=3
;;
local spin='┤┘┴└├┌┬┐'
local charwidth=3
;;
local spin='◢◣◤◥'
local charwidth=3
;;
local spin='◰◳◲◱'
local charwidth=3
;;
local spin='◴◷◶◵'
local charwidth=3
;;
local spin='◐◓◑◒'
local charwidth=3
;;
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 $!

  • Very interesting, but not working at all on macOS zsh. – kitsune Jul 11 '20 at 08:41
  • it is a bash script. If you chmod +x it and run it via ./script it'll be run with /bin/bash (even if your shell runs zsh). I just tried it (with Linux though) and I don't see why it shouldn't work with MacOS. – Jonas Eberle Jul 12 '20 at 09:59
  • 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 '20 at 08:29
  • Seems not to be a zsh or bash problem I guess. I think your terminal does not know what to do with `echo -en "\033[$1D"`` - maybe we could find a sequence for "cursorBack" which is more portable even for unusual terminal settings? – Jonas Eberle Jul 14 '20 at 11:56
  • 2
    Man.. this is a beauty.. thank you for the inspiration!! – TacB0sS Nov 21 '20 at 00:36
  • How could you make it work inside another script? e.g. some script has slow function foo() { ...} and INSIDE that script you'd invoke it via spinner $foo. But that returns an error. – lonix Apr 13 '21 at 12:53
  • Call bash functions without $. spinner foo for functions might work, although I have only used it inside scripts on the long-running command directly. – Jonas Eberle Apr 13 '21 at 14:46
  • 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 09 '22 at 15:49
  • Very cool! But I want to show a message while the spinner is running: * torgling the flidgets ... ... anybody got a version like that? – Sasgorilla Jul 21 '23 at 16:33
  • @Sasgorilla you can adapt it to do that. Look at the printf and cursorBack lines. – Jonas Eberle Jul 24 '23 at 07:35
  • 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"

    – justFatLard Dec 13 '23 at 01:39
  • Thanks @justFatLard for finding that more portable way. I have edited and added your solution. It has a little problem that it always jumps to the first column, though. I often use them like this: printf 'doing something '; spinner sleep 3; printf '✅ OK\n' – Jonas Eberle Dec 13 '23 at 11:24
  • Gotcha, I hadn't tried using it that way yet, but it appears the main problem is that the cursorBack call needs to go before the printf call in the while loop. It still overwrites the first portion of the text but that can very easily be mitigated by a newline or a bit more work to do something fancy with spaces could work too. After only moving the cursorBack call I was able to use your example successfully by appending a newline to your first call: printf 'doing something\n'; spinner sleep 3; printf '✅ OK\n' – justFatLard Dec 13 '23 at 18:17
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
jsears
  • 311
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
    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 '21 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

TacB0sS
  • 421