16

How can I create a progress bar with bash?

This is my script:

 #!/bin/bash
 pass='number1 number12 number13 number14 number15 number16'
 chk='number14'
 for i in $pass ; do
 if [ "$i" == "$chk" ]; then
 echo ' Found ^_^'
 else
 echo 'loading 50%'
 fi
 done

I want to replace echo 'loading 50%' with anything to create a progress bar.

Zanna
  • 70,465
Ghadeer R. Majeed
  • 193
  • 2
  • 2
  • 8

5 Answers5

15

whiptail comes preinstalled on Ubuntu and many other distros, and will show full-screen (but still terminal-based) progress elements.

dialog is a superset of whiptail, so this example will work equally well with both. It does provide more advanced UI elements, so it may come in handy if you're looking for user interaction such as file pickers and forms, but it has the disadvantage of not coming preinstalled on many systems.

whiptail

dialog

for i in $(seq 1 100)
do
    sleep 0.1 
    echo $i
done | whiptail --title 'Test script' --gauge 'Running...' 6 60 0

Note that the script output is interpreted as a percentage, so you may have to adjust your output accordingly.

Whiptail and Dialog also allow you to modify the text at run time via a rather cryptic syntax:

phases=( 
    'Locating Jebediah Kerman...'
    'Motivating Kerbals...'
    'Treating Kessler Syndrome...'
    'Recruiting Kerbals...'
)   

for i in $(seq 1 100); do  
    sleep 0.1

    if [ $i -eq 100 ]; then
        echo -e "XXX\n100\nDone!\nXXX"
    elif [ $(($i % 25)) -eq 0 ]; then
        let "phase = $i / 25"
        echo -e "XXX\n$i\n${phases[phase]}\nXXX"
    else
        echo $i
    fi 
done | whiptail --title 'Kerbal Space Program' --gauge "${phases[0]}" 6 60 0

pv shows the progress of a file or stream being piped through it. It cannot however be (easily?) used to show progress of a custom operation such as a loop. It's designed specifically for streams.

$ head -c 1G < /dev/urandom | pv -s 1G > /dev/null
 277MB 0:00:16 [17.4MB/s] [========>                           ] 27% ETA 0:00:43

Some real-world examples where pv comes in handy:

# progress while importing a DB dump
pv mybigfile.sql | mysql -uroot -p dbname

# importing straight from a remote server
ssh user@server 'cat mybigfile.sql.gz' | pv | gzip -cd | mysql -uroot -p dbname

# taking a snapshot of a btrfs partition
btrfs send /snapshots/$date | pv | btrfs receive /mnt/backup/root

I don't know of any commands that give one-line progress bars in the style of pv or wget, but there are plenty of simple Bash/Perl/sed scripts that will add that functionality, as others have shared here.

Mikkel
  • 343
  • To show the process of a loop with pv you can either make it look for the loop's output or create some fake output, e.g. an echo in every iteration, pipe it to pv and give it the iteration count with -s. If it's not wanted, remember to redirect the loop's stdout to /dev/null. Here's an example showing this approach. – dessert Apr 24 '18 at 06:58
7

You can use zenity to create simple GTK dialog windows. One of the available options is a progress bar dialog.

You create such a window using zenity --progress. To make it useful, you should specify more information by adding some of the options below (excerpt from man zenity):

   Progress options
   --text=STRING
          Set the dialog text
   --percentage=INT
          Set initial percentage
   --auto-close
          Close dialog when 100% has been reached
   --auto-kill
          Kill parent process if cancel button is pressed
   --pulsate
          Pulsate progress bar
   --no-cancel
          Hides the cancel button

There are two modes:

  • pulsating: The progress bar is pulsating, it just indicates that something is running, but does not tell anything about the progress. You do this by setting the --pulsating option.

  • manual: You have to pipe the current progress percentage to the zenity command's standard input to update the progress bar.
    An example for this could look like that below. Note that the previous commands are grouped to a subshell so that all the output is redirected to the zenity dialog and not just that of the last command:

    (echo 10; sleep 2; echo 20; sleep 2; echo 50; sleep 2) | zenity --progress
    
Byte Commander
  • 107,489
  • Just in case this would also be an option. – Byte Commander Mar 17 '16 at 15:59
  • 1
    Sorry my dear , this is GUI progress bar windows , I want to create a progress bar in the terminal , for example i want to see it while the script is checking ==> [ ###########--------------] 52% – Ghadeer R. Majeed Mar 17 '16 at 16:39
  • 1
    Yes, I understand. It's just that I already had written half of my answer when you said that, so I just decided to post it anyway in case somebody else might need it in the future. I hope you don't mind, as there are quite a few terminal based solutions as well. – Byte Commander Mar 18 '16 at 07:06
5

This code will do it, and doesn't require anything (other than bash, of course). It prints # signs, like you asked in your comment:

pass='number1 number12 number13 number14 number15 number16'
chk='number14'
passarr=($pass)
lenProgressBar=${#passarr[@]}

echo -n '['
i=0

while [ $i -lt $lenProgressBar ]; do
    echo -n '-'
    ((i++))
done

echo -n ']'
i=0

while [ $i -lt $lenProgressBar ]; do
    echo -e -n '\b'
    ((i++))
done

echo -e -n '\b'
for i in $pass ; do
    if [ "$i" = "$chk" ]; then
        echo -e '#\nFound ^_^'
        break
    else
        echo -n '#'
    fi
done

However, if you have a lot to check, this will just fill your screen with # signs. To fix that issue, try this code:

lenProgressBar=5
pass='number1 number12 number13 number14 number15 number16'
chk='number14'
passarr=($pass)
lenPass=${#passarr[@]}

if [ $lenProgressBar -gt $lenPass ]; then
    lenProgressBar=lenPass
elif [ $lenProgressBar -lt 1 ]; then
    lenProgressBar=1
fi

let "chksForEqualsPrint = $lenPass / $lenProgressBar"
echo -n '['
i=0

while [ $i -lt $lenProgressBar ]; do
    echo -n '-'
    ((i++))
done

echo -n ']'
i=0

while [ $i -lt $lenProgressBar ]; do
    echo -e -n '\b'
    ((i++))
done

echo -e -n '\b'
n=1

for i in $pass ; do
    if [ "$i" = "$chk" ]; then
        echo -e '\nFound ^_^'
        break
    else
        if [ $n -eq $chksForEqualsPrint ]; then
            echo -n '#'
            n=1
        else
            ((n++))
        fi
    fi
done

Change the 5 in the first line (lenProgressBar=5) to the length that you want your progress bar to be. It'll take longer to print a # sign with lower length progress bars than with higher length ones, but don't let the length of the progress bar exceed your screen size; it won't work well if you do. (It won't let you use a progress bar higher than the number of items you're checking or lower than 1)

muru
  • 197,895
  • 55
  • 485
  • 740
  • 1
    You could use tput cols to detect the width of the terminal window and scale the progressbar accordingly. – Mikkel Jul 18 '16 at 21:03
1

From Stack Overflow

I needed a progress bar that would fit in popup bubble message (notify-send) to represent TV volume level. Recently I've been writing a music player in python and the TV picture is turned off most of the time.

Sample output from terminal

test_progress_bar3.gif


Bash script

#!/bin/bash

Show a progress bar at step number $1 (from 0 to 100)

function is_int() { test "$@" -eq "$@" 2> /dev/null; }

Parameter 1 must be integer

if ! is_int "$1" ; then echo "Not an integer: ${1}" exit 1 fi

Parameter 1 must be >= 0 and <= 100

if [ "$1" -ge 0 ] && [ "$1" -le 100 ] 2>/dev/null then : else echo bad volume: ${1} exit 1 fi

Main function designed for quickly copying to another program

Main () {

Bar=&quot;&quot;                      # Progress Bar / Volume level
Len=25                      # Length of Progress Bar / Volume level
Div=4                       # Divisor into Volume for # of blocks
Fill=&quot;▒&quot;                    # Fill up to $Len
Arr=( &quot;▉&quot; &quot;▎&quot; &quot;▌&quot; &quot;▊&quot; )     # UTF-8 left blocks: 7/8, 1/4, 1/2, 3/4

FullBlock=$((${1} / Div))   # Number of full blocks
PartBlock=$((${1} % Div))   # Size of partial block (array index)

while [[ $FullBlock -gt 0 ]]; do
    Bar=&quot;$Bar${Arr[0]}&quot;     # Add 1 full block into Progress Bar
    (( FullBlock-- ))       # Decrement full blocks counter
done

# If remainder zero no partial block, else append character from array
if [[ $PartBlock -gt 0 ]]; then
    Bar=&quot;$Bar${Arr[$PartBlock]}&quot;
fi

while [[ &quot;${#Bar}&quot; -lt &quot;$Len&quot; ]]; do
    Bar=&quot;$Bar$Fill&quot;         # Pad Progress Bar with fill character
done

echo Volume: &quot;$1 $Bar&quot;
exit 0                      # Remove this line when copying into program

} # Main

Main "$@"


Test bash script

Use this script to test the progress bar in the terminal.

#!/bin/bash

test_progress_bar3

Main () {

tput civis                              # Turn off cursor
for ((i=0; i&lt;=100; i++)); do
    CurrLevel=$(./progress_bar3 &quot;$i&quot;)   # Generate progress bar 0 to 100
    echo -ne &quot;$CurrLevel&quot;\\r            # Reprint overtop same line
    sleep .04
done
echo -e \\n                             # Advance line to keep last progress
echo &quot;$0 Done&quot;
tput cnorm                              # Turn cursor back on

} # Main

Main "$@"


TL;DR

This section details how notify-send is used to quickly spam popup bubble messages to the desktop. This is required because volume level can change many times a second and the default bubble message behavior is for a message to stay on the desktop for many seconds.

Sample popup bubble message

tvpowered.gif

Popup bubble message bash code

From the script above the main function was copied to a new functioned called VolumeBar in an existing bash script called tvpowered. The exit 0 command in the copied main function was removed.

Here's how to call it and let Ubuntu's notify-send command know we will be spamming popup bubble message:

VolumeBar $CurrVolume
# Ask Ubuntu: https://askubuntu.com/a/871207/307523
notify-send --urgency=critical "tvpowered" \
    -h string:x-canonical-private-synchronous:volume \
    --icon=/usr/share/icons/gnome/48x48/devices/audio-speakers.png \
    "Volume: $CurrVolume $Bar"

This is the new line which tells notify-send to immediately replace last popup bubble:

-h string:x-canonical-private-synchronous:volume \

volume groups the popup bubble messages together and new messages in this group immediately replaces the previous. You can use anything instead of volume.

1

Here's another approach using ansi escape codes:

#!/bin/bash

pass='number1 number2 number 3 number4 number12 number13 number14 number15 number16'
chk='number15'
result="Not Found!"

echo
echo -n "Working... "
echo -ne "\033[1;32m\033[7m\033[?25l"

for i in $pass ; do
   sleep .4s
   if [ "$i" == "$chk" ]; then
      result="  Found ^_^"
      break
   else
      echo -n " "
   fi
done

echo -ne "\r\033[0m\033[K\033[?25h"
echo $result
echo
bashBedlam
  • 1,022