79

Using a bash only script, how can you provide a bash progress indicator?

For example, when I run a command from bash - while that command is executing - let the user know that something is still happening.

brandonscript
  • 68,675
  • 32
  • 163
  • 220
Pez Cuckow
  • 14,048
  • 16
  • 80
  • 130
  • See also http://stackoverflow.com/questions/238073/how-to-add-a-progress-bar-to-a-shell-script which concentrates more on the drawing part. – tripleee Dec 03 '15 at 20:52
  • 3
    [Pipe Viewer](http://www.ivarch.com/programs/pv.shtml) can be injected into a pipeline to generate a status bar, spinner, &c. automatically. – Charles Duffy Feb 18 '17 at 18:32
  • 1
    Check out [`eta`](https://github.com/aioobe/eta)! – aioobe Jun 08 '19 at 00:13

12 Answers12

99

In this example using SCP, I'm demonstrating how to grab the process id (pid) and then do something while that process is running.

This displays a simple spinnng icon.

/usr/bin/scp me@website.com:file somewhere 2>/dev/null &
pid=$! # Process Id of the previous running command

spin[0]="-"
spin[1]="\\"
spin[2]="|"
spin[3]="/"

echo -n "[copying] ${spin[0]}"
while [ kill -0 $pid ]
do
  for i in "${spin[@]}"
  do
        echo -ne "\b$i"
        sleep 0.1
  done
done

William Pursell's solution

/usr/bin/scp me@website.com:file somewhere 2>/dev/null &
pid=$! # Process Id of the previous running command

spin='-\|/'

i=0
while kill -0 $pid 2>/dev/null
do
  i=$(( (i+1) %4 ))
  printf "\r${spin:$i:1}"
  sleep .1
done
diff_sky
  • 561
  • 4
  • 8
Pez Cuckow
  • 14,048
  • 16
  • 80
  • 130
  • 2
    +1 This is really good: A third of the LOC other solutions use. – Eugen Rieck Sep 19 '12 at 15:52
  • 3
    You need to run `scp` in the background, or it completes before the rest of this script runs. (Backgrounding is also required for `$!` to have any meaning). – chepner Sep 19 '12 at 16:03
  • 8
    It's much simpler to do: `s='-\|/'; i=0; while kill -0 $pid; do i=$(( (i+1) %4 )); printf "\r${s:$i:1}"; sleep .1; done` – William Pursell Sep 19 '12 at 16:04
  • @chepner Good point, I just missed off the `&`. @WilliamPursell Yes, though not sure how clear that is! – Pez Cuckow Sep 19 '12 at 16:06
  • However, this doesn't really indicate progress. It's just a flashy way of showing that `$pid` hasn't completed yet, which is readily observed by keeping the process in the foreground in the first place. – chepner Sep 19 '12 at 16:27
  • I did something similar in `Python`, which I had to fire up another `thread` to print the progress while at the same time keep the task going, never thought could just put the `process` in background and `wait` in `bash`. :) – procleaf Sep 21 '12 at 08:54
  • Very nice and it works. But is there a way to also get the exit code of the finished job after the spinner is done? – Thalis K. Oct 12 '14 at 08:35
  • The spinner interferes with variable assignment, e.g. foo=$(callALongFunction) 2>/dev/null & (spinner code) will prevent callALongFunction from assigning any value to foo. Removing the spinner fixes this. Any ideas how to avoid this issue? – DavidD Nov 11 '14 at 13:58
  • @DavidD You can write to stderr if you are not capturing that or modify the logic to use something like `exec 3>$(tty);` `echo -en "$1" | tee >(cat - >&3);` `exec 3>&-;` This will allow you to clone standard out on fd 3 so you can see whats printed on the screen and still capture the output in a variable within another script. –  Mar 24 '15 at 21:15
  • 4
    `while [ kill -0 $pid ]` causes `line 102: [: -0: binary operator expected` error for me. I have to use `while kill -0 $pid;` instead! – PHP Learner Aug 22 '15 at 12:34
  • Can we use that without showing the command at the end of the loop? – Apollo Feb 26 '20 at 15:50
31

If you have a way to estimate percentage done, such as the current number of files processed and total number, you can make a simple linear progress meter with a little math and assumptions about screen width.

count=0
total=34
pstr="[=======================================================================]"

while [ $count -lt $total ]; do
  sleep 0.5 # this is work
  count=$(( $count + 1 ))
  pd=$(( $count * 73 / $total ))
  printf "\r%3d.%1d%% %.${pd}s" $(( $count * 100 / $total )) $(( ($count * 1000 / $total) % 10 )) $pstr
done

Or instead of a linear meter you could estimate time remaining. It's about as accurate as other similar things.

count=0
total=34
start=`date +%s`

while [ $count -lt $total ]; do
  sleep 0.5 # this is work
  cur=`date +%s`
  count=$(( $count + 1 ))
  pd=$(( $count * 73 / $total ))
  runtime=$(( $cur-$start ))
  estremain=$(( ($runtime * $total / $count)-$runtime ))
  printf "\r%d.%d%% complete ($count of $total) - est %d:%0.2d remaining\e[K" $(( $count*100/$total )) $(( ($count*1000/$total)%10)) $(( $estremain/60 )) $(( $estremain%60 ))
done
printf "\ndone\n"
evil otto
  • 10,348
  • 25
  • 38
  • 1
    How can I estimate the files that are still to copy? You mean take a count of the current files at the start and then every loop count how many are on the remote system? (genuinely interested rather than saying you're wrong) – Pez Cuckow Sep 20 '12 at 10:57
  • It depends completely on your task. For instance, if you're using scp to copy a bunch of files, you could call it separately for each file rather than using `*`. Or as you suggest you could check on the remote system for the number of files or file sizes, although that could be fairly expensive to do. – evil otto Sep 20 '12 at 17:24
  • @evil otto. Thank you very much. Kindly where in the previous code we should call the command that we need to watch it is progress? – goro Aug 24 '15 at 15:36
  • @goro - in my example the command that is doing work is "sleep 0.5", so that would be your `scp` command. – evil otto Aug 24 '15 at 21:10
26

Referred from here is a nice spinner function (with slight modification), will help your cursor to stay in original position also.

spinner()
{
    local pid=$!
    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"
    done
    printf "    \b\b\b\b"
}

with usage:

(a_long_running_task) &
spinner
checksum
  • 6,415
  • 4
  • 36
  • 34
23

This is a pretty easy technique:
(just replace sleep 20 with whatever command you want to indicate is running)

#!/bin/bash

sleep 20 & PID=$! #simulate a long process

echo "THIS MAY TAKE A WHILE, PLEASE BE PATIENT WHILE ______ IS RUNNING..."
printf "["
# While process is running...
while kill -0 $PID 2> /dev/null; do 
    printf  "▓"
    sleep 1
done
printf "] done!"

The output looks like this:

> THIS MAY TAKE A WHILE, PLEASE BE PATIENT WHILE ______ IS RUNNING...
> [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] done!

It adds a (high density dotted) every second until the process is complete.

cosbor11
  • 14,709
  • 10
  • 54
  • 69
11

Here a simple one-liner, that I use:

while true; do for X in '-' '/' '|' '\'; do echo -en "\b$X"; sleep 0.1; done; done 
Tamas Szoke
  • 5,426
  • 4
  • 24
  • 39
fipsbox
  • 126
  • 1
  • 3
  • 1
    Example usage: echo doing something that takes ten seconds...; sleep 10 & while [ "$(ps a | awk '{print $1}' | grep $!)" ] ; do for X in '-' '/' '|' '\'; do echo -en "\b$X"; sleep 0.1; done; done – fipsbox Dec 22 '15 at 13:36
  • This rotatis in endless loop – alper Feb 16 '23 at 14:27
6

Here's my attempt. I'm new to bash scripts so some of this code may be terrible :)

Example Output:

In Progress Done

The Code:

progressBarWidth=20

# Function to draw progress bar
progressBar () {

  # Calculate number of fill/empty slots in the bar
  progress=$(echo "$progressBarWidth/$taskCount*$tasksDone" | bc -l)  
  fill=$(printf "%.0f\n" $progress)
  if [ $fill -gt $progressBarWidth ]; then
    fill=$progressBarWidth
  fi
  empty=$(($fill-$progressBarWidth))

  # Percentage Calculation
  percent=$(echo "100/$taskCount*$tasksDone" | bc -l)
  percent=$(printf "%0.2f\n" $percent)
  if [ $(echo "$percent>100" | bc) -gt 0 ]; then
    percent="100.00"
  fi

  # Output to screen
  printf "\r["
  printf "%${fill}s" '' | tr ' ' ▉
  printf "%${empty}s" '' | tr ' ' ░
  printf "] $percent%% - $text "
}



## Collect task count
taskCount=33
tasksDone=0

while [ $tasksDone -le $taskCount ]; do

  # Do your task
  (( tasksDone += 1 ))

  # Add some friendly output
  text=$(echo "somefile-$tasksDone.dat")

  # Draw the progress bar
  progressBar $taskCount $taskDone $text

  sleep 0.01
done

echo

You can see the source here: https://gist.github.com/F1LT3R/fa7f102b08a514f2c535

f1lt3r
  • 2,176
  • 22
  • 26
6

Here is an example of an 'activity indicator,' for an internet connection speed test via the linux 'speedtest-cli' command:

## BASH shell
printf '\n\tInternet speed test:  '

# http://stackoverflow.com/questions/12498304/using-bash-to-display-a-progress-working-indicator

spin[0]="-"
spin[1]="\\"
spin[2]="|"
spin[3]="/"

# http://stackoverflow.com/questions/20165057/executing-bash-loop-while-command-is-running

speedtest > .st.txt &           ## & : continue running script
pid=$!                          ## PID of last command

# If this script is killed, kill 'speedtest':
trap "kill $pid 2> /dev/null" EXIT

# While 'speedtest' is running:
while kill -0 $pid 2> /dev/null; do
for i in "${spin[@]}"
do
    echo -ne "\b$i"
    sleep 0.1
done
done

# Disable the trap on a normal exit:
trap - EXIT

printf "\n\t           "
grep Download: .st.txt
printf "\t             "
grep Upload: .st.txt
echo ''
rm -f st.txt

Update - example:

animated GIF

Victoria Stuart
  • 4,610
  • 2
  • 44
  • 37
6

Aside from the classical spinner, you can use this progress bar

It achieves subcharacter precision by using half block characters

enter image description here

Code included on the link.

Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
nachoparker
  • 1,678
  • 18
  • 14
2

https://github.com/extensionsapp/progre.sh

Create 82 percent progress: progreSh 82

enter image description here

Hello World
  • 840
  • 10
  • 20
1

I extended the answer of checksum in his answer by displaying a variable info message after the spinner:

#!/usr/bin/env bash 
function spinner() {
    local info="$1"
    local pid=$!
    local delay=0.75
    local spinstr='|/-\'
    while kill -0 $pid 2> /dev/null; do
        local temp=${spinstr#?}
        printf " [%c]  $info" "$spinstr"
        local spinstr=$temp${spinstr%"$temp"}
        sleep $delay
        local reset="\b\b\b\b\b\b"
        for ((i=1; i<=$(echo $info | wc -c); i++)); do
            reset+="\b"
        done
        printf $reset
    done
    printf "    \b\b\b\b"
}

# usage:
(a_long_running_task) &
spinner "performing long running task..."

I don't like that if the stdout output with a spinner is redirected to a file, less shows ^H for each backspace instead of avoiding them in a file output at all. Is that possible with an easy spinner like this one?

jan
  • 2,741
  • 4
  • 35
  • 56
0

@DavidD's comments on Pez Cuckows answer, this is an example of how you can capture the output of the progress bar in a script and still see the spinner on the screen:

#!/usr/bin/env bash 

#############################################################################
###########################################################################
###
### Modified/Rewritten by A.M.Danischewski (c) 2015 v1.1
### Issues: If you find any issues emai1 me at my <first name> dot 
###         <my last name> at gmail dot com.  
###
### Based on scripts posted by Pez Cuckow, William Pursell at:  
### http://stackoverflow.com/questions/12498304/using-bash-to-display-\
###      a-progress-working-indicator
###
### This program runs a program passed in and outputs a timing of the 
### command and it exec's a new fd for stdout so you can assign a 
### variable the output of what was being run. 
### 
### This is a very new rough draft but could be expanded. 
### 
### This program is free software: you can redistribute it and/or modify
### it under the terms of the GNU General Public License as published by
### the Free Software Foundation, either version 3 of the License, or
### (at your option) any later version.
###
### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
### GNU General Public License for more details.
###
### You should have received a copy of the GNU General Public License
### along with this program.  If not, see <http://www.gnu.org/licenses/>.
###########################################################################
#############################################################################

declare    CMD="${1}"
shift      ## Clip the first value of the $@, the rest are the options. 
declare    CMD_OPTIONS="$@"
declare    CMD_OUTPUT=""
declare    TMP_OUTPUT="/tmp/_${0##*/}_$$_$(date +%Y%m%d%H%M%S%N)" 
declare -r SPIN_DELAY="0.1"
declare -i PID=

function usage() {
cat <<EOF

Description: ${0##*/}

This program runs a program passed in and outputs a timing of the 
command and it exec's a new fd for stdout so you can assign a variable 
the output of what was being run. 

Usage: ${0##*/} <command> [command options]

 E.g.  
    >$ ${0##*/} sleep 5 \&\& echo "hello" \| figlet
     Running: sleep 5 && echo hello | figlet, PID 2587:/

     real   0m5.003s
     user   0m0.000s
     sys    0m0.002s
      _          _ _       
     | |__   ___| | | ___  
     | '_ \ / _ \ | |/ _ \ 
     | | | |  __/ | | (_) |
     |_| |_|\___|_|_|\___/ 

     Done..
    >$ var=\$(${0##*/} sleep 5 \&\& echo hi)
     Running: sleep 5 && echo hi, PID 32229:-
     real   0m5.003s
     user   0m0.000s
     sys    0m0.001s
     Done..
     >$ echo \$var
     hi

EOF
} 

function spin_wait() { 
 local -a spin 
 spin[0]="-"
 spin[1]="\\"
 spin[2]="|"
 spin[3]="/"
 echo -en "Running: ${CMD} ${CMD_OPTIONS}, PID ${PID}: " >&3
 while kill -0 ${PID} 2>/dev/random; do
   for i in "${spin[@]}"; do
     echo -ne "\b$i" >&3
     sleep ${SPIN_DELAY}
   done
 done
} 

function run_cmd() { 
 exec 3>$(tty)
 eval "time ${CMD} ${CMD_OPTIONS}" 2>>"${TMP_OUTPUT}" | tee "${TMP_OUTPUT}" & 
 PID=$! # Set global PID to process id of the command we just ran. 
 spin_wait
 echo -en "\n$(< "${TMP_OUTPUT}")\n" >&3 
 echo -en "Done..\n" >&3
 rm "${TMP_OUTPUT}"
 exec 3>&-
} 

if [[ -z "${CMD}" || "${CMD}" =~ ^-. ]]; then 
 usage | more && exit 0 
else 
 run_cmd  
fi 

exit 0 
0

Psychedelic progress bar for bash scripting. Call by command line as './progressbar x y' where 'x' is a time in seconds and 'y' is a message to display. The inner function progressbar() works standalone as well and takes 'x' as a percentage and 'y' as a message.

#!/bin/bash

if [ "$#" -eq 0 ]; then echo "x is \"time in seconds\" and z is \"message\""; echo "Usage: progressbar x z"; exit; fi
progressbar() {
        local loca=$1; local loca2=$2;
        declare -a bgcolors; declare -a fgcolors;
        for i in {40..46} {100..106}; do
                bgcolors+=("$i")
        done
        for i in {30..36} {90..96}; do
                fgcolors+=("$i")
        done
        local u=$(( 50 - loca ));
        local y; local t;
        local z; z=$(printf '%*s' "$u");
        local w=$(( loca * 2 ));
        local bouncer=".oO°Oo.";
        for ((i=0;i<loca;i++)); do
                t="${bouncer:((i%${#bouncer})):1}"
                bgcolor="\\E[${bgcolors[RANDOM % 14]}m \\033[m"
                y+="$bgcolor";
        done
        fgcolor="\\E[${fgcolors[RANDOM % 14]}m"
        echo -ne " $fgcolor$t$y$z$fgcolor$t \\E[96m(\\E[36m$w%\\E[96m)\\E[92m $fgcolor$loca2\\033[m\r"
};
timeprogress() {
        local loca="$1"; local loca2="$2";
        loca=$(bc -l <<< scale=2\;"$loca/50")
        for i in {1..50}; do
                progressbar "$i" "$loca2";
                sleep "$loca";
        done
        echo -e "\n"
};
timeprogress "$1" "$2"
nexace
  • 1
  • 3
  • Please don't hard-code terminal-specific control codes - make friends with `tput` instead. – Toby Speight May 19 '16 at 21:28
  • I redid the script using tput as you suggest. The speed was terribly reduced. Not a good suggestion. – nexace May 22 '16 at 17:23
  • Sorry, I still see control codes, and no `tput` usage. I'd be very surprised if it was noticeably slower to call it a handful of times (to initialise suitable variables), but it would possibly be noticeable if one were to invoke it anew every time the control code were needed. – Toby Speight May 29 '16 at 23:02