Tom Feiner
Tom Feiner

Reputation: 21386

How to add a progress bar to a shell script?

When scripting in bash or any other shell in *NIX, while running a command that will take more than a few seconds, a progress bar is needed.

For example, copying a big file, opening a big tar file.

What ways do you recommend to add progress bars to shell scripts?

Upvotes: 559

Views: 553924

Answers (30)

F. Hauri  - Give Up GitHub
F. Hauri - Give Up GitHub

Reputation: 70957

Hires (floating point) progress bar

Preamble

Sorry for this not so short answer. In this answer I will use integer to render floating point, UTF-8 fonts for rendering progress bar more finely, and parallelise another task (sha1sum) in order to follow his progression, all of this with minimal resource footprint using pure and no forks.

For impatiens: Please test code (copy/paste in a new terminal window) at Now do it! (in the middle), with

  • either: Last animated demo (near end of this.),
  • either Practical sample (at end).

All demos here use read -t <float seconds> && break instead of sleep. So all loop could be nicely stopped by hitting Return key.

Introduction

Yet Another Bash Progress Bar...

As there is already a lot of answer here, I want to add some hints about performances and precision.

1. Avoid forks!

Because a progress bar are intented to run while other process are working, this must be a nice process...

So avoid using forks when not needed. Sample: instead of

mysmiley=$(printf '%b' \\U1F60E)

Use

printf -v mysmiley '%b' \\U1F60E

Explanation: When you run var=$(command), you initiate a new process to execute command and send his output to variable $var once terminated. This is very resource expensive. Please compare:

TIMEFORMAT="%R"
time for ((i=2500;i--;)){ mysmiley=$(printf '%b' \\U1F60E);}
2.292
time for ((i=2500;i--;)){ printf -v mysmiley '%b' \\U1F60E;}
0.017
bc -l <<<'2.292/.017'
134.82352941176470588235

On my host, same work of assigning $mysmiley (just 2500 time), seem ~135x slower / more expensive by using fork than by using built-in printf -v.

Then

echo $mysmiley 
๐Ÿ˜Ž

So your function have to not print (or output) anything. Your function have to attribute his answer to a variable.

2. Use integer as pseudo floating point

Here is a very small and quick function to compute percents from integers, with integer and answer a pseudo floating point number:

percent(){
    local p=00$(($1*100000/$2))
    printf -v "$3" %.2f ${p::-3}.${p: -3}
}

Usage:

# percent <integer to compare> <reference integer> <variable name>
percent 33333 50000 testvar
printf '%8s%%\n' "$testvar"
   66.67%

3. Hires console graphic using UTF-8: โ– โ–Ž โ– โ–Œ โ–‹ โ–Š โ–‰ โ–ˆ

To render this characters using bash, you could:

printf -v chars '\\U258%X ' {15..8}
printf '%b\n' "$chars"
โ– โ–Ž โ– โ–Œ โ–‹ โ–Š โ–‰ โ–ˆ 

or

printf %b\  \\U258{{f..a},9,8}
โ– โ–Ž โ– โ–Œ โ–‹ โ–Š โ–‰ โ–ˆ

Then we have to use 8x string width as graphic width.

Now do it!

This function is named percentBar because it render a bar from argument submited in percents (floating):

percentBar ()  { 
    local prct totlen=$((8*$2)) lastchar barstring blankstring;
    printf -v prct %.2f "$1"
    ((prct=10#${prct/.}*totlen/10000, prct%8)) &&
        printf -v lastchar '\\U258%X' $(( 16 - prct%8 )) ||
            lastchar=''
    printf -v barstring '%*s' $((prct/8)) ''
    printf -v barstring '%b' "${barstring// /\\U2588}$lastchar"
    printf -v blankstring '%*s' $(((totlen-prct)/8)) ''
    printf -v "$3" '%s%s' "$barstring" "$blankstring"
}

Usage:

# percentBar <float percent> <int string width> <variable name>
percentBar 42.42 $COLUMNS bar1
echo "$bar1"
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‰                                              

To show little differences:

percentBar 42.24 $COLUMNS bar2
printf "%s\n" "$bar1" "$bar2"
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‰                                              
โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–Š                                              

With colors

As rendered variable is a fixed widht string, using color is easy:

percentBar 72.1 24 bar
printf 'Show this: \e[44;33;1m%s\e[0m at %s%%\n' "$bar" 72.1

Bar with color

Little animation:

for i in {0..10000..33} 10000;do i=0$i
    printf -v p %0.2f ${i::-2}.${i: -2}
    percentBar $p $((COLUMNS-9)) bar
    printf '\r|%s|%6.2f%%' "$bar" $p
    read -srt .002 _ && break    # console sleep avoiding fork
done

|โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ|100.00%
clear; for i in {0..10000..33} 10000;do i=0$i
     printf -v p %0.2f ${i::-2}.${i: -2}
     percentBar $p $((COLUMNS-7)) bar
     printf '\r\e[47;30m%s\e[0m%6.2f%%' "$bar" $p
     read -srt .002 _ && break
done

PercentBar animation

Last animated demo

Another demo showing different sizes and colored output:

printf '\n\n\n\n\n\n\n\n\e[8A\e7'&&for i in {0..9999..99} 10000;do 
    o=1 i=0$i;printf -v p %0.2f ${i::-2}.${i: -2}
    for l in 1 2 3 5 8 13 20 40 $((COLUMNS-7));do
        percentBar $p $l bar$((o++));done
    [ "$p" = "100.00" ] && read -rst .8 _;printf \\e8
    printf '%s\e[48;5;23;38;5;41m%s\e[0m%6.2f%%%b' 'In 1 char width: ' \
        "$bar1" $p ,\\n 'with 2 chars: ' "$bar2" $p ,\\n 'or 3 chars: ' \
        "$bar3" $p ,\\n 'in 5 characters: ' "$bar4" $p ,\\n 'in 8 chars: ' \
        "$bar5" $p .\\n 'There are 13 chars: ' "$bar6" $p ,\\n '20 chars: '\
        "$bar7" $p ,\\n 'then 40 chars' "$bar8" $p \
        ', or full width:\n' '' "$bar9" $p ''
    ((10#$i)) || read -st .5 _; read -st .1 _ && break
done

Could produce something like this:

Last animation percentBar animation

Practical GNU/Linux sample 1: kind of sleep with progress bar

Rewrite feb 2023: Turn into more usefull displaySleep function suitable to use as displayed timeout read:

This sleep show a progress bar with 50 refresh by seconds (tunnable)

percent(){ local p=00$(($1*100000/$2));printf -v "$3" %.2f ${p::-3}.${p: -3};}
displaySleep() {
    local -i refrBySeconds=50
    local -i _start=${EPOCHREALTIME/.} reqslp target crtslp crtp cols cpos dlen
    local strng percent prctbar tleft
    [[ $COLUMNS ]] && cols=${COLUMNS} || read -r cols < <(tput cols)
    refrBySeconds=' 1000000 / refrBySeconds '
    printf -v strng %.6f $1
    printf '\E[6n' && IFS=\; read -sdR _ cpos
    dlen=${#strng}-1  cols=' cols - dlen - cpos -1 '
    printf \\e7
    reqslp=10#${strng/.} target=reqslp+_start
    for ((;${EPOCHREALTIME/.}<target;)){
        crtp=${EPOCHREALTIME/.}
        crtslp='( target - crtp ) > refrBySeconds? refrBySeconds: target - crtp'
        strng=00000$crtslp  crtp+=-_start
        printf -v strng %.6f ${strng::-6}.${strng: -6}
        percent $crtp $reqslp percent
        percentBar $percent $cols prctbar
        tleft=00000$((reqslp-crtp))
        printf '\e8\e[36;48;5;23m%s\e[0m%*.4fs' \
               "$prctbar" "$dlen" ${tleft::-6}.${tleft: -6}
        IFS= read -rsn1 -t $strng ${2:-_} && { echo; return;}
    }
    percentBar 100 $cols prctbar
    printf '\e8\e[36;48;5;30m%s\e[0m%*.4fs\n' "$prctbar" "$dlen" 0
    false
}

This will keep current cursor position to fill only the rest of line (full line if current cursor position is 1). This could be useful for displaying some kind of prompt:

enter image description here

Practical GNU/Linux sample 2: sha1sum with progress bar

Under linux, you could find a lot of usefull infos under /proc pseudo filesystem, so using previoulsy defined functions percentBar and percent, here is sha1progress:

percent(){ local p=00$(($1*100000/$2));printf -v "$3" %.2f ${p::-3}.${p: -3};}
sha1Progress() { 
    local -i totsize crtpos cols=$(tput cols) sha1in sha1pid
    local sha1res percent prctbar
    exec {sha1in}< <(exec sha1sum -b - <"$1")
    sha1pid=$!
    read -r totsize < <(stat -Lc %s "$1")
    while ! read -ru $sha1in -t .025 sha1res _; do
        read -r _ crtpos < /proc/$sha1pid/fdinfo/0
        percent $crtpos $totsize percent
        percentBar $percent $((cols-8)) prctbar
        printf '\r\e[44;38;5;25m%s\e[0m%6.2f%%' "$prctbar" $percent;

    done
    printf "\r%s  %s\e[K\n" $sha1res "$1"
}

Of course, 25 ms timeout mean approx 40 refresh per second. This could look overkill, but work fine on my host, and anyway, this can be tunned.

sha1Progress sample

Explanation:

  • exec {sha1in}< create a new file descriptor for the output of
  • <( ... ) forked task run in background
  • sha1sum -b - <"$1" ensuring input came from STDIN (fd/0)
  • while ! read -ru $sha1in -t .025 sha1res _ While no input read from subtask, in 25 ms...
  • /proc/$sha1pid/fdinfo/0 kernel variable showing information about file descriptor 0 (STDIN) of task $sha1pid

How to add nice spinner in bash

If you don't have any to create a progress bar, but still want a spinner for ask user to wait, you could simply:

spinner() {
    local shs=( - \\ \| / ) pnt=0
    printf '\e7'
    while ! read -rsn1 -t .2 _; do
        printf '%b\e8' "${shs[pnt++%${#shs[@]}]}"
    done
}
printf '\e[H\e[J Simple spinner: ';spinner

Simple spinner blinking This could be stopped by hitting Any key.

Start / stop spinner as background task

So for using them as a backgroud task, you could use this two wrappers:

startSpinner () { 
    tput civis;
    exec {doSpinner}> >(spinner "$@")
}
stopSpinner () { 
    echo >&"$doSpinner" && exec {doSpinner}>&-;
    tput cnorm;
    echo
}

printf '\e[H\e[J Simple spinner: ';startSpinner;sleep 4;stopSpinner

Simple spinner

Then, for sample short stat of /usr filesystem:

printf 'Scan %s for stats... ' "$HOME"; \
startSpinner; \
declare -A homeStats; \
while read cnt typ; do
    homeStats["$typ"]=$cnt;
done < <(
    find "$HOME" -printf entries\\n -type f -printf files\\n -o -type d \
        -printf dirs\\n 2>/dev/null | sort | uniq -c); \
stopSpinner; \
printf 'There are %d entries in %s, %d directories and %d files.\n' \
     ${homeStats[entries]} "$HOME" ${homeStats[files]} ${homeStats[dirs]}

Using UTF-8 braille characters for drawing a spinner

spinner(){
    local pnt=0 shs=( 01 08 10 20 80 40 04 02 )
    printf '\e7'
    while ! read -sn 1 -t .2 _; do
        printf %b\\e8  \\U28${shs[pnt++%${#shs[@]}]}
    done
}

printf '\e[H\e[J Single dot braille: ';startSpinner;sleep 4;stopSpinner

Single dot braille

Growing snake spinner

More sophisticated shape, drawing a growing snake:

spinner(){
    local pnt=0 shs=( 01 08 10 20 80 40 04 02 09 18 30 a0 c0 44 06 03 19 38
       b0 e0 c4 46 07 0b 39 b8 f0 e4 c6 47 0f 1b b9 f8 f4 e6 c7 4f 1f 3b f9
       fc f6 e7 cf 5f 3f bb fd fe f7 ef df 7f bf fb f9 fc f6 e7 cf 5f 3f bb
       b9 f8 f4 e6 c7 4f 1f 3b 39 b8 f0 e4 c6 47 0f 1b 19 38 b0 e0 c4 46 07
       0b 09 18 30 a0 c0 44 06 03 )
    printf '\e7'
    while ! read -sn 1 -t .2 _; do
        printf %b\\e8  \\U28${shs[pnt++%${#shs[@]}]}
    done
}

printf '\e[H\e[J Growing snake spinner: ';startSpinner;sleep 4;stopSpinner

Growing snake spinner

Two characters braille snake spinner

This is an auto-build function:

 spinner() {
    local shs=( 100 800 1 8 10 20 80  40 8000  4000 400 200 100  900 801 9 18 30
        A0 C0 8040 C000 4400 600 300 900 901 809 19 38 B0 E0 80C0 C040 C400 4600
        700 B00  901 909 819 39 B8 F0 80E0  C0C0 C440 C600  4700 F00 B01 909 919
        839  B9 F8 80F0 C0E0 C4C0  C640 C700  4F00 F01  B09 919 939  8B9 F9 80F8
        C0F0 C4E0  C6C0 C740 CF00 4F01 F09  B19 939 9B9 8F9 80F9  C0F8 C4F0 C6E0
        C7C0  CF40 CF01 4F09  F19 B39 9B9 9F9 88F9 C0F9 C4F8 C6F0 C7E0 CFC0 CF41
        CF09 4F19 F39 BB9 9F9 89F9 C8F9 C4F9 C6F8 C7F0 CFE0 CFC1 CF49 CF19  4F39
        FB9 BF9 89F9 C9F9 CCF9 C6F9 C7F8  CFF0 CFE1 CFC9 CF59 CF39 4FB9 FF9 8BF9
        C9F9 CDF9 CEF9 C7F9  CFF8 CFF1 CFE9 CFD9  CF79 CFB9 4FF9  8FF9 C9F9 CCF9
        C6F9 C7F8  CFF0 CFE1 CFC9 CF59  CF39 4FB9 FF9  89F9 C8F9 C4F9  C6F8 C7F0
        CFE0  CFC1 CF49 CF19  4F39 FB9 9F9 88F9  C0F9 C4F8  C6F0 C7E0  CFC0 CF41
        CF09 4F19 F39 9B9 8F9 80F9  C0F8 C4F0  C6E0 C7C0 CF40 CF01  4F09 F19 939
        8B9 F9 80F8  C0F0 C4E0 C6C0 C740 CF00 4F01 F09  919 839  B9 F8 80F0 C0E0
        C4C0 C640  C700 4F00 F01 909  819 39 B8 F0 80E0 C0C0 C440 C600  4700 F00
        901 809 19 38 B0 E0 80C0  C040 C400  4600 700 900 801 9 18 30 A0 C0 8040
        C000 4400 600 ) chr
    local -i pnt
    for pnt in "${!shs[@]}"; do
        chr="000${shs[pnt]}"
        printf -v shs[pnt] '%b' "\U28${chr: -4:2}\U28${chr: -2}"
    done
    eval "${FUNCNAME[0]}() { local shs=( ${shs[*]@Q}) "'
             local -i pnt;printf '\''\e7'\'';
         while ! read -rsn1 -t "${1:-.02}"; do
              printf '\''%b\e8'\'' "${shs[pnt++%${#shs[@]}]}";
         done;}';
};spinner

Then:

printf 'Snake two braille chars: ';startSpinner ; sleep 12 ; stopSpinner

Two chars braille snake spinner

If you find this fun, you may found a 8 characters: four columns on two lines growing snake spinner at My eight braille pattern progressive snake spinner ( @ Code Golf ;-)

     โกŽโ ‰โ ‰โขน
     โฃ‡โฃ€โฃ€โฃธ 

Another practical sample

I've posted a full grep progress bar script answering grep - how to output progress bar or status

  • using a spinner while building list of files to grep using shopt -s globstar and files=(**)

  • drawing two progress bar:

    • one for file list progression
    • one for each file read (usefull when reading big files)

    Here is a view of output:
    Short view progress grep

  • Promise for a parallel grep script (currently under debugging phase...)

    Here is a screen shot:
    Parallel grep

Upvotes: 48

Diomidis Spinellis
Diomidis Spinellis

Reputation: 19375

Some posts have showed how to display the command's progress. In order to calculate it, you'll need to see how much you've progressed. On BSD systems some commands, such as dd(1), accept a SIGINFO signal, and will report their progress. On Linux systems some commands will respond similarly to SIGUSR1. If this facility is available, you can pipe your input through _dd to monitor the number of bytes processed.

Alternatively, you can use lsof to obtain the offset of the file's read pointer, and thereby calculate the progress. Here is an example of using lsof(1) to see the progress of wc(1) reading a large file named blob.

$ wc -l blob &
[1] 3405769

$ lsof -w -o0 -o -c wc
COMMAND     PID USER   FD   TYPE  DEVICE       OFFSET     NODE NAME
[...]
wc      3405769  dds    3r   REG   254,7 0t2656059392  7733716 blob

I've written a command, named pmonitor, that displays the progress of processing a specified process or file. With it you can do things, such as the following.

$ pmonitor -c gzip
/home/dds/data/mysql-2015-04-01.sql.gz 58.06%

An earlier version of Linux and FreeBSD shell scripts appears on my blog ("Monitor Process Progress on Unix").

Upvotes: 54

Fjor
Fjor

Reputation: 439

SIMPLE COUNTER FOR NOT SO FANCY JOBS

I don't need really a progress bar, just a visual feedback of progress, so I prefer a simple counter after enabling the command monitored to output lines processed:

#!/bin/bash
# pvlines: simple script to count stdin lines read
# copy/chmod a+rx to /usr/local/bin and use: command | pvlines
awk '{N++; printf "\r%d", N} END {print ""}'

Create the script as indicated in the comments, then use like this:

tar zcvf backup.tgz dir-to-backup | pvlines

To process a large amount of files, first count them and then do the process, so we'll have a rough estimate of termination:

find /dir-to-copy | pvlines
rsync -avr /dir-to-copy user@bkserver:destination/ | pvlines

I found this counter have a reduced impact on the process, but it can be modified to skip some lines and report, say, each 200 or 2000 lines read.

First I did that in pure Bash with a while/read/printf loop, but it was too slow, so I migrated pvlines to awk.

Upvotes: 0

cprn
cprn

Reputation: 1710

Haven't seen anything similar and all custom functions here seem to focus on rendering alone so... my very simple POSIX compliant solution below with step by step explanations because this question isn't trivial.

TL;DR

Rendering the progress bar is very easy. Estimating how much of it should render is a different matter. This is how to render (animate) the progress bar - you can copy&paste this example to a file and run it:

#!/bin/sh

BAR='####################'   # this is full bar, e.g. 20 chars

for i in {1..20}; do
    echo -ne "\r${BAR:0:$i}" # print $i chars of $BAR from 0 position
    sleep .1                 # wait 100ms between "frames"
done
  • {1..20} - values from 1 to 20
  • echo - print to terminal (i.e. to stdout)
  • echo -n - print without new line at the end
  • echo -e - interpret special characters while printing
  • "\r" - carriage return, a special char to return to the beginning of the line

You can make it render any content at any speed so this method is very universal, e.g. often used for visualization of "hacking" in silly movies, no kidding.

Full answer (from zero to working example)

The meat of the problem is how to determine the $i value, i.e. how much of the progress bar to display. In the above example I just let it increment in for loop to illustrate the principle but a real life application would use an infinite loop and calculate the $i variable on each iteration. To make said calculation it needs the following ingredients:

  1. how much work there is to be done
  2. how much work has been done so far

In case of cp it needs the size of a source file and the size of the target file:

#!/bin/sh

src="/path/to/source/file"
tgt="/path/to/target/file"

cp "$src" "$tgt" &                     # the & forks the `cp` process so the rest
                                       # of the code runs without waiting (async)

BAR='####################'

src_size=$(stat -c%s "$src")           # how much there is to do

while true; do
    tgt_size=$(stat -c%s "$tgt")       # how much has been done so far
    i=$(( $tgt_size * 20 / $src_size ))
    echo -ne "\r${BAR:0:$i}"
    if [ $tgt_size == $src_size ]; then
        echo ""                        # add a new line at the end
        break;                         # break the loop
    fi
    sleep .1
done
  • foo=$(bar) - run bar in a subprocess and save its stdout to $foo
  • stat - print file stats to stdout
  • stat -c - print a formatted value
  • %s - format for total size

In case of operations like file unpacking, calculating the source size is slightly more difficult but still as easy as getting the size of an uncompressed file:

#!/bin/sh
src_size=$(gzip -l "$src" | tail -n1 | tr -s ' ' | cut -d' ' -f3)
  • gzip -l - print info about zip archive
  • tail -n1 - work with 1 line from the bottom
  • tr -s ' ' - translate multiple spaces into one ("squeeze" them)
  • cut -d' ' -f3 - cut 3rd space-delimited field (column)

Here's the meat of the problem I mentioned before. This solution is less and less general. All calculations of the actual progress are tightly bound to the domain you're trying to visualize, is it a single file operation, a timer countdown, a rising number of files in a directory, operation on multiple files, etc., therefore, it can't be reused. The only reusable part is progress bar rendering. To reuse it you need to abstract it and save in a file (e.g. /usr/lib/progress_bar.sh), then define functions that calculate input values specific to your domain. This is how a generalized code could look like (I also made the $BAR dynamic because people were asking for it, the rest should be clear by now):

#!/bin/bash

BAR_length=50
BAR_character='#'
BAR=$(printf %${BAR_length}s | tr ' ' $BAR_character)

work_todo=$(get_work_todo)             # how much there is to do

while true; do
    work_done=$(get_work_done)         # how much has been done so far
    i=$(( $work_done * $BAR_length / $work_todo ))
    echo -ne "\r${BAR:0:$i}"
    if [ $work_done == $work_todo ]; then
        echo ""
        break;
    fi
    sleep .1
done
  • printf - a builtin for printing stuff in a given format
  • printf %50s - print nothing but pad it with 50 spaces
  • tr ' ' '#' - translate every space to hash sign

And this is how you'd use it:

#!/bin/bash

src="/path/to/source/file"
tgt="/path/to/target/file"

function get_work_todo() {
    echo $(stat -c%s "$src")
}

function get_work_done() {
    [ -e "$tgt" ] &&                   # if target file exists
        echo $(stat -c%s "$tgt") ||    # echo its size, else
        echo 0                         # echo zero
}

cp "$src" "$tgt" &                     # copy in the background

source /usr/lib/progress_bar.sh        # execute the progress bar

Obviously you can wrap this in a function, rewrite to work with piped streams, grab forked process ID with $! and pass it to progress_bar.sh so it could guess how to calculate work to do and work done, whatever's your poison.

Side notes

I get asked about these two things most often:

  1. ${}: in above examples I use ${foo:A:B}. The technical term for this syntax is Parameter Expansion, a built-in shell functionality that allows to manipulate a variable (parameter), e.g. to trim a string with : but also to do other things - it does not spawn a subshell. The most prominent description of parameter expansion I can think of (that isn't fully POSIX compatible but lets the reader understand the concept well) is in the man bash page.
  2. $(): in above examples I use foo=$(bar). It spawns a separate shell in a subprocess (a.k.a. a Subshell), runs the bar command in it and assigns its standard output to a $foo variable. It's not the same as Process Substitution and it's something entirely different than pipe (|). Most importantly, it works. Some say this should be avoided because it's slow. I argue this is "a okay" here because whatever this code is trying to visualise lasts long enough to require a progress bar. In other words, subshells are not the bottleneck. Calling a subshell also saves me the effort of explaining why return isn't what most people think it is, what is an Exit Status and why passing values from functions in shells is not what shell functions are good at in general. To find out more about all of it I, again, highly recommend the man bash page.

Troubleshooting

If your shell is actually running sh instead of bash, or really old bash, like default osx, it may choke on echo -ne "\r${BAR:0:$i}". The exact error is Bad substitution. If this happens to you, per the comment section, you can instead use echo -ne "\r$(expr "x$name" : "x.\{0,$num_skip\}\(.\{0,$num_keep\}\)")" to do a more portable posix-compatible / less readable substring match.

A complete, working /bin/sh example:

#!/bin/sh

src=100
tgt=0

get_work_todo() {
    echo $src
}

do_work() {
    echo "$(( $1 + 1 ))"
}

BAR_length=50
BAR_character='#'
BAR=$(printf %${BAR_length}s | tr ' ' $BAR_character)
work_todo=$(get_work_todo)             # how much there is to do
work_done=0
while true; do
    work_done="$(do_work $work_done)"
    i=$(( $work_done * $BAR_length / $work_todo ))
    n=$(( $BAR_length - $i ))
    printf "\r$(expr "x$BAR" : "x.\{0,$n\}\(.\{0,$i\}\)")"
    if [ $work_done = $work_todo ]; then
        echo "\n"
        break;
    fi
    sleep .1
done

Upvotes: 37

Seth Wegner
Seth Wegner

Reputation: 743

Use the Linux command pv.

It doesn't know the size if it's in the middle of the pipeline, but it gives a speed and total, and from there you can figure out how long it should take and get feedback so you know it hasn't hung.

Upvotes: 71

WinEunuuchs2Unix
WinEunuuchs2Unix

Reputation: 1949

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=""                      # Progress Bar / Volume level
    Len=25                      # Length of Progress Bar / Volume level
    Div=4                       # Divisor into Volume for # of blocks
    Fill="โ–’"                    # Fill up to $Len
    Arr=( "โ–‰" "โ–Ž" "โ–Œ" "โ–Š" )     # 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="$Bar${Arr[0]}"     # 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="$Bar${Arr[$PartBlock]}"
    fi

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

    echo Volume: "$1 $Bar"
    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<=100; i++)); do
        CurrLevel=$(./progress_bar3 "$i")   # Generate progress bar 0 to 100
        echo -ne "$CurrLevel"\\r            # Reprint overtop same line
        sleep .04
    done
    echo -e \\n                             # Advance line to keep last progress
    echo "$0 Done"
    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.

Upvotes: 5

Dustin Michels
Dustin Michels

Reputation: 3236

I needed a progress bar for iterating over the lines in a csv file. Was able to adapt cprn's code into something useful for me:

BAR='##############################'
FILL='------------------------------'
totalLines=$(wc -l $file | awk '{print $1}')  # num. lines in file
barLen=30

# --- iterate over lines in csv file ---
count=0
while IFS=, read -r _ col1 col2 col3; do
    # update progress bar
    count=$(($count + 1))
    percent=$((($count * 100 / $totalLines * 100) / 100))
    i=$(($percent * $barLen / 100))
    echo -ne "\r[${BAR:0:$i}${FILL:$i:barLen}] $count/$totalLines ($percent%)"

    # other stuff
    (...)
done <$file

Looks like this:

[##----------------------------] 17128/218210 (7%)

Upvotes: 6

James Yang
James Yang

Reputation: 636

There are many different answers about this topic, but when calculating percentage for text file operation, using current length / total size way, for example showing percentage of ver_big_file.json progress, and I recommend using awk for this purpose, like below code:

awk '
    function bar(x){s="";i=0;while (i++ < x) s=s "#";return s}
    BEGIN{
        ("ls -l " ARGV[1]) | getline total;
        split(total,array);
        total=array[5];
    }
    {
        cur+=length($0)+1;
        percent=int(cur / total * 100);
        printf "LINE %s:%s %s%%\r", NR, bar(percent*.8), percent 
    }
    END {print}' very_big_file.json | grep "keyword" | ...

This way it's very precise, stream based, but it's only suitable for text files.

Upvotes: 0

Damian Czapiewski
Damian Czapiewski

Reputation: 881

It may be achieved in a pretty simple way:

  • iterate from 0 to 100 with for loop
  • sleep every step for 25ms (0.25 second)
  • append to the $bar variable another = sign to make the progress bar wider
  • echo progress bar and percentage (\r cleans line and returns to the beginning of the line; -ne makes echo doesn't add newline at the end and parses \r special character)
function progress {
    bar=''
    for (( x=0; x <= 100; x++ )); do
        sleep 0.25
        bar="${bar}="
        echo -ne "$bar ${x}%\r"
    done
    echo -e "\n"
}
$ progress
> ========== 10% # here: after 2.5 seconds
$ progress
> ============================== 30% # here: after 7.5 seconds

COLORED PROGRESS BAR

function progress {
    bar=''
    for (( x=0; x <= 100; x++ )); do
        sleep 0.05
        bar="${bar} "

        echo -ne "\r"
        echo -ne "\e[43m$bar\e[0m"

        local left="$(( 100 - $x ))"
        printf " %${left}s"
        echo -n "${x}%"
    done
    echo -e "\n"
}

To make a progress bar colorful, you can use formatting escape sequence - here the progress bar is yellow: \e[43m, then we reset custom settings with \e[0m, otherwise it would affect further input even when the progress bar is done.

custom progress bar

Upvotes: 2

Khalil Gharbaoui
Khalil Gharbaoui

Reputation: 7062

Flexible version with randomized colors, a string to manipulate and date.

function spinner() {
  local PID="$1"
  local str="${2:-Processing!}"
  local delay="0.1"
  # tput civis  # hide cursor
  while ( kill -0 $PID 2>/dev/null )
    do
      printf "\e[38;5;$((RANDOM%257))m%s\r\e[0m" "[$(date '+%d/%m/%Y %H:%M:%S')][ ๐Ÿ™‰  ๐Ÿ™Š  ๐Ÿ™ˆ $str ๐Ÿ™ˆ  ๐Ÿ™Š  ๐Ÿ™‰ ]"; sleep "$delay"
      printf "\e[38;5;$((RANDOM%257))m%s\r\e[0m" "[$(date '+%d/%m/%Y %H:%M:%S')][ ๐Ÿ™ˆ  ๐Ÿ™‰  ๐Ÿ™‰ $str ๐Ÿ™Š  ๐Ÿ™‰  ๐Ÿ™ˆ ]"; sleep "$delay"
      printf "\e[38;5;$((RANDOM%257))m%s\r\e[0m" "[$(date '+%d/%m/%Y %H:%M:%S')][ ๐Ÿ™Š  ๐Ÿ™ˆ  ๐Ÿ™Š $str ๐Ÿ™‰  ๐Ÿ™ˆ  ๐Ÿ™Š ]"; sleep "$delay"
  done
  printf "\e[38;5;$((RANDOM%257))m%s\r\e[0m" "[$(date '+%d/%m/%Y %H:%M:%S')][ โœ…  โœ…  โœ…   Done!   โœ…  โœ…  โœ… ]"; sleep "$delay"
  # tput cnorm  # restore cursor

  return 0
}

Usage:

# your long running proccess pushed to the background
sleep 20 &

# spinner capture-previous-proccess-id string
spinner $! 'Working!'

output example:

[04/06/2020 03:22:24][ ๐Ÿ™Š  ๐Ÿ™ˆ  ๐Ÿ™Š Seeding! ๐Ÿ™‰  ๐Ÿ™ˆ  ๐Ÿ™Š ]

Upvotes: 1

fearside
fearside

Reputation: 837

Got an easy progress bar function that i wrote the other day:

#!/bin/bash
# 1. Create ProgressBar function
# 1.1 Input is currentState($1) and totalState($2)
function ProgressBar {
# Process data
    let _progress=(${1}*100/${2}*100)/100
    let _done=(${_progress}*4)/10
    let _left=40-$_done
# Build progressbar string lengths
    _fill=$(printf "%${_done}s")
    _empty=$(printf "%${_left}s")

# 1.2 Build progressbar strings and print the ProgressBar line
# 1.2.1 Output example:                           
# 1.2.1.1 Progress : [########################################] 100%
printf "\rProgress : [${_fill// /#}${_empty// /-}] ${_progress}%%"

}

# Variables
_start=1

# This accounts as the "totalState" variable for the ProgressBar function
_end=100

# Proof of concept
for number in $(seq ${_start} ${_end})
do
    sleep 0.1
    ProgressBar ${number} ${_end}
done
printf '\nFinished!\n'

Or snag it from,
https://github.com/fearside/ProgressBar/

Upvotes: 82

polle
polle

Reputation: 417

APT style progress bar (Does not break normal output)

enter image description here

EDIT: For an updated version check my github page

I was not satisfied with the responses on this question. What I was personally looking for was a fancy progress bar as is seen by APT.

I had a look at the C source code for APT and decided to write my own equivalent for bash.

This progress bar will stay nicely at the bottom of the terminal and will not interfere with any output sent to the terminal.

Please do note that the bar is currently fixed at 100 characters wide. If you want scale it to the size of the terminal, this is fairly easy to accomplish as well (The updated version on my github page handles this well).

I will post my script here. Usage example:

source ./progress_bar.sh
echo "This is some output"
setup_scroll_area
sleep 1
echo "This is some output 2"
draw_progress_bar 10
sleep 1
echo "This is some output 3"
draw_progress_bar 50
sleep 1
echo "This is some output 4"
draw_progress_bar 90
sleep 1
echo "This is some output 5"
destroy_scroll_area

The script (I strongly recommend the version on my github instead):

#!/bin/bash

# This code was inspired by the open source C code of the APT progress bar
# http://bazaar.launchpad.net/~ubuntu-branches/ubuntu/trusty/apt/trusty/view/head:/apt-pkg/install-progress.cc#L233

#
# Usage:
# Source this script
# setup_scroll_area
# draw_progress_bar 10
# draw_progress_bar 90
# destroy_scroll_area
#


CODE_SAVE_CURSOR="\033[s"
CODE_RESTORE_CURSOR="\033[u"
CODE_CURSOR_IN_SCROLL_AREA="\033[1A"
COLOR_FG="\e[30m"
COLOR_BG="\e[42m"
RESTORE_FG="\e[39m"
RESTORE_BG="\e[49m"

function setup_scroll_area() {
    lines=$(tput lines)
    let lines=$lines-1
    # Scroll down a bit to avoid visual glitch when the screen area shrinks by one row
    echo -en "\n"

    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"
    # Set scroll region (this will place the cursor in the top left)
    echo -en "\033[0;${lines}r"

    # Restore cursor but ensure its inside the scrolling area
    echo -en "$CODE_RESTORE_CURSOR"
    echo -en "$CODE_CURSOR_IN_SCROLL_AREA"

    # Start empty progress bar
    draw_progress_bar 0
}

function destroy_scroll_area() {
    lines=$(tput lines)
    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"
    # Set scroll region (this will place the cursor in the top left)
    echo -en "\033[0;${lines}r"

    # Restore cursor but ensure its inside the scrolling area
    echo -en "$CODE_RESTORE_CURSOR"
    echo -en "$CODE_CURSOR_IN_SCROLL_AREA"

    # We are done so clear the scroll bar
    clear_progress_bar

    # Scroll down a bit to avoid visual glitch when the screen area grows by one row
    echo -en "\n\n"
}

function draw_progress_bar() {
    percentage=$1
    lines=$(tput lines)
    let lines=$lines
    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"

    # Move cursor position to last row
    echo -en "\033[${lines};0f"

    # Clear progress bar
    tput el

    # Draw progress bar
    print_bar_text $percentage

    # Restore cursor position
    echo -en "$CODE_RESTORE_CURSOR"
}

function clear_progress_bar() {
    lines=$(tput lines)
    let lines=$lines
    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"

    # Move cursor position to last row
    echo -en "\033[${lines};0f"

    # clear progress bar
    tput el

    # Restore cursor position
    echo -en "$CODE_RESTORE_CURSOR"
}

function print_bar_text() {
    local percentage=$1

    # Prepare progress bar
    let remainder=100-$percentage
    progress_bar=$(echo -ne "["; echo -en "${COLOR_FG}${COLOR_BG}"; printf_new "#" $percentage; echo -en "${RESTORE_FG}${RESTORE_BG}"; printf_new "." $remainder; echo -ne "]");

    # Print progress bar
    if [ $1 -gt 99 ]
    then
        echo -ne "${progress_bar}"
    else
        echo -ne "${progress_bar}"
    fi
}

printf_new() {
    str=$1
    num=$2
    v=$(printf "%-${num}s" "$str")
    echo -ne "${v// /$str}"
}

Upvotes: 30

Zibri
Zibri

Reputation: 9857

#!/bin/bash
tot=$(wc -c /proc/$$/fd/255 | awk '/ /{print $1}')
now() {
echo $(( 100* ($(awk '/^pos:/{print $2}' < /proc/$$/fdinfo/255)-166) / (tot-166) )) "%"
}
now;
now;
now;
now;
now;
now;
now;
now;
now;

output:

0 %
12 %
25 %
37 %
50 %
62 %
75 %
87 %
100 %

note: if instead of 255 you put 1 you will monitor the standard in...with 2 the standard out (but you must modify the source to set "tot" to the projected output file size)

Upvotes: 0

Qub3r
Qub3r

Reputation: 79

Using suggestions listed above, I decided to implement my own progress bar.

#!/usr/bin/env bash

main() {
  for (( i = 0; i <= 100; i=$i + 1)); do
    progress_bar "$i"
    sleep 0.1;
  done
  progress_bar "done"
  exit 0
}

progress_bar() {
  if [ "$1" == "done" ]; then
    spinner="X"
    percent_done="100"
    progress_message="Done!"
    new_line="\n"
  else
    spinner='/-\|'
    percent_done="${1:-0}"
    progress_message="$percent_done %"
  fi

  percent_none="$(( 100 - $percent_done ))"
  [ "$percent_done" -gt 0 ] && local done_bar="$(printf '#%.0s' $(seq -s ' ' 1 $percent_done))"
  [ "$percent_none" -gt 0 ] && local none_bar="$(printf '~%.0s' $(seq -s ' ' 1 $percent_none))"

  # print the progress bar to the screen
  printf "\r Progress: [%s%s] %s %s${new_line}" \
    "$done_bar" \
    "$none_bar" \
    "${spinner:x++%${#spinner}:1}" \
    "$progress_message"
}

main "$@"

Upvotes: 1

Vagiz Duseev
Vagiz Duseev

Reputation: 1013

Here is how it might look

Uploading a file

[##################################################] 100% (137921 / 137921 bytes)

Waiting for a job to complete

[#########################                         ] 50% (15 / 30 seconds)

Simple function that implements it

You can just copy-paste it to your script. It does not require anything else to work.

PROGRESS_BAR_WIDTH=50  # progress bar length in characters

draw_progress_bar() {
  # Arguments: current value, max value, unit of measurement (optional)
  local __value=$1
  local __max=$2
  local __unit=${3:-""}  # if unit is not supplied, do not display it

  # Calculate percentage
  if (( $__max < 1 )); then __max=1; fi  # anti zero division protection
  local __percentage=$(( 100 - ($__max*100 - $__value*100) / $__max ))

  # Rescale the bar according to the progress bar width
  local __num_bar=$(( $__percentage * $PROGRESS_BAR_WIDTH / 100 ))

  # Draw progress bar
  printf "["
  for b in $(seq 1 $__num_bar); do printf "#"; done
  for s in $(seq 1 $(( $PROGRESS_BAR_WIDTH - $__num_bar ))); do printf " "; done
  printf "] $__percentage%% ($__value / $__max $__unit)\r"
}

Usage example

Here, we upload a file and redraw the progress bar at each iteration. It does not matter what job is actually performed as long as we can get 2 values: max value and current value.

In the example below the max value is file_size and the current value is supplied by some function and is called uploaded_bytes.

# Uploading a file
file_size=137921

while true; do
  # Get current value of uploaded bytes
  uploaded_bytes=$(some_function_that_reports_progress)

  # Draw a progress bar
  draw_progress_bar $uploaded_bytes $file_size "bytes"

  # Check if we reached 100%
  if [ $uploaded_bytes == $file_size ]; then break; fi
  sleep 1  # Wait before redrawing
done
# Go to the newline at the end of upload
printf "\n"

Upvotes: 13

Hello World
Hello World

Reputation: 840

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

Create 40 percent progress: progreSh 40

enter image description here

Upvotes: 2

Mitch Haile
Mitch Haile

Reputation: 11834

You can implement this by overwriting a line. Use \r to go back to the beginning of the line without writing \n to the terminal.

Write \n when you're done to advance the line.

Use echo -ne to:

  1. not print \n and
  2. to recognize escape sequences like \r.

Here's a demo:

echo -ne '#####                     (33%)\r'
sleep 1
echo -ne '#############             (66%)\r'
sleep 1
echo -ne '#######################   (100%)\r'
echo -ne '\n'

In a comment below, puk mentions this "fails" if you start with a long line and then want to write a short line: In this case, you'll need to overwrite the length of the long line (e.g., with spaces).

Upvotes: 800

&#201;douard Lopez
&#201;douard Lopez

Reputation: 43421

I was looking for something more sexy than the selected answer, so did my own script.

Preview

progress-bar.sh in action

Source

I put it on github progress-bar.sh

progress-bar() {
  local duration=${1}


    already_done() { for ((done=0; done<$elapsed; done++)); do printf "โ–‡"; done }
    remaining() { for ((remain=$elapsed; remain<$duration; remain++)); do printf " "; done }
    percentage() { printf "| %s%%" $(( (($elapsed)*100)/($duration)*100/100 )); }
    clean_line() { printf "\r"; }

  for (( elapsed=1; elapsed<=$duration; elapsed++ )); do
      already_done; remaining; percentage
      sleep 1
      clean_line
  done
  clean_line
}

Usage

 progress-bar 100

Upvotes: 55

Mike Q
Mike Q

Reputation: 7337

#!/bin/bash

function progress_bar() {
    bar=""
    total=10
    [[ -z $1 ]] && input=0 || input=${1}
    x="##"
   for i in `seq 1 10`; do
        if [ $i -le $input ] ;then
            bar=$bar$x
        else
            bar="$bar  "
       fi
    done
    #pct=$((200*$input/$total % 2 + 100*$input/$total))
    pct=$(($input*10))
    echo -ne "Progress : [ ${bar} ] (${pct}%) \r"    
    sleep 1
    if [ $input -eq 10 ] ;then
        echo -ne '\n'
    fi

}

could create a function that draws this on a scale say 1-10 for the number of bars :

progress_bar 1
echo "doing something ..."
progress_bar 2
echo "doing something ..."
progress_bar 3
echo "doing something ..."
progress_bar 8
echo "doing something ..."
progress_bar 10

Upvotes: 0

Adriano_Pinaffo
Adriano_Pinaffo

Reputation: 1699

Based on the work of Edouard Lopez, I created a progress bar that fits the size of the screen, whatever it is. Check it out.

enter image description here

It's also posted on Git Hub.

#!/bin/bash
#
# Progress bar by Adriano Pinaffo
# Available at https://github.com/adriano-pinaffo/progressbar.sh
# Inspired on work by Edouard Lopez (https://github.com/edouard-lopez/progress-bar.sh)
# Version 1.0
# Date April, 28th 2017

function error {
  echo "Usage: $0 [SECONDS]"
  case $1 in
    1) echo "Pass one argument only"
    exit 1
    ;;
    2) echo "Parameter must be a number"
    exit 2
    ;;
    *) echo "Unknown error"
    exit 999
  esac
}

[[ $# -ne 1 ]] && error 1
[[ $1 =~ ^[0-9]+$ ]] || error 2

duration=${1}
barsize=$((`tput cols` - 7))
unity=$(($barsize / $duration))
increment=$(($barsize%$duration))
skip=$(($duration/($duration-$increment)))
curr_bar=0
prev_bar=
for (( elapsed=1; elapsed<=$duration; elapsed++ ))
do
  # Elapsed
prev_bar=$curr_bar
  let curr_bar+=$unity
  [[ $increment -eq 0 ]] || {  
    [[ $skip -eq 1 ]] &&
      { [[ $(($elapsed%($duration/$increment))) -eq 0 ]] && let curr_bar++; } ||
    { [[ $(($elapsed%$skip)) -ne 0 ]] && let curr_bar++; }
  }
  [[ $elapsed -eq 1 && $increment -eq 1 && $skip -ne 1 ]] && let curr_bar++
  [[ $(($barsize-$curr_bar)) -eq 1 ]] && let curr_bar++
  [[ $curr_bar -lt $barsize ]] || curr_bar=$barsize
  for (( filled=0; filled<=$curr_bar; filled++ )); do
    printf "โ–‡"
  done

  # Remaining
  for (( remain=$curr_bar; remain<$barsize; remain++ )); do
    printf " "
  done

  # Percentage
  printf "| %s%%" $(( ($elapsed*100)/$duration))

  # Return
  sleep 1
  printf "\r"
done
printf "\n"
exit 0

Enjoy

Upvotes: 5

Daenyth
Daenyth

Reputation: 37461

You may also be interested in how to do a spinner:

Can I do a spinner in Bash?

Sure!

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

Each time the loop iterates, it displays the next character in the sp string, wrapping around as it reaches the end. (i is the position of the current character to display and ${#sp} is the length of the sp string).

The \b string is replaced by a 'backspace' character. Alternatively, you could play with \r to go back to the beginning of the line.

If you want it to slow down, put a sleep command inside the loop (after the printf).

A POSIX equivalent would be:

sp='/-\|'
printf ' '
while true; do
    printf '\b%.1s' "$sp"
    sp=${sp#?}${sp%???}
done

If you already have a loop which does a lot of work, you can call the following function at the beginning of each iteration to update the spinner:

sp="/-\|"
sc=0
spin() {
   printf "\b${sp:sc++:1}"
   ((sc==${#sp})) && sc=0
}
endspin() {
   printf "\r%s\n" "$@"
}

until work_done; do
   spin
   some_work ...
done
endspin

Upvotes: 125

pbatey
pbatey

Reputation: 1441

I wanted to track progress based on the number of lines a command output against a target number of lines from a previous run:

#!/bin/bash
function lines {
  local file=$1
  local default=$2
  if [[ -f $file ]]; then
    wc -l $file | awk '{print $1}';
  else
    echo $default
  fi
}

function bar {
  local items=$1
  local total=$2
  local size=$3
  percent=$(($items*$size/$total % $size))
  left=$(($size-$percent))
  chars=$(local s=$(printf "%${percent}s"); echo "${s// /=}")
  echo -ne "[$chars>";
  printf "%${left}s"
  echo -ne ']\r'
}

function clearbar {
  local size=$1
  printf " %${size}s  "
  echo -ne "\r"
}

function progress {
  local pid=$1
  local total=$2
  local file=$3

  bar 0 100 50
  while [[ "$(ps a | awk '{print $1}' | grep $pid)" ]]; do
    bar $(lines $file 0) $total 50
    sleep 1
  done
  clearbar 50
  wait $pid
  return $?
}

Then use it like this:

target=$(lines build.log 1000)
(mvn clean install > build.log 2>&1) &
progress $! $target build.log

It outputs a progress bar that looks something like this:

[===============================================>   ]

The bar grows as the number of lines output reaches the target. If the number of lines exceeds the target, the bar starts over (hopefully the target is good).

BTW: I'm using bash on Mac OSX. I based this code on a spinner from mariascio.

Upvotes: -1

purushothaman poovai
purushothaman poovai

Reputation: 384

First execute the process to the background, then watch it's running status frequently,that was running print the pattern and again check it status was running or not;

Using while loop to watch the status of the process frequently.

use the pgrep or any other command to watch and getting running status of a process.

if using pgrep redirect the unnecessary output to /dev/null as needed.

Code:

sleep 12&
while pgrep sleep &> /dev/null;do echo -en "#";sleep 0.5;done

This "#" will printed until sleep terminate,this method used to implement the progress bar for progress time of program.

you can also use this method to the commands to shell scripts for analyze it process time as visual.

BUG: this pgrep method doesn't works in all situations,unexpectedly the another process was running with same name, the while loop does not end.

so getting the process running status by specify it's PID, using may the process can available with some commands,

the command ps a will list all the process with id,you need grep to find-out the pid of the specified process

Upvotes: -1

nexace
nexace

Reputation: 1

This is a psychedelic progressbar for bash scripting by nExace. It can be called from command line as './progressbar x y' where 'x' is a time in seconds and 'y' is a message associated with that portion of the progress.

The inner progressbar() function itself is good standalone as well if you want other portions of your script to control the progressbar. For instance, sending 'progressbar 10 "Creating directory tree";' will display:

[#######                                     ] (10%) Creating directory tree

Of course it will be nicely psychedelic though...

#!/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
        printf "\n"
};
timeprogress "$1" "$2"

Upvotes: -1

casper.dcl
casper.dcl

Reputation: 14829

Many answers describe writing your own commands for printing out '\r' + $some_sort_of_progress_msg. The problem sometimes is that printing out hundreds of these updates per second will slow down the process.

However, if any of your processes produce output (eg 7z a -r newZipFile myFolder will output each filename as it compresses it) then a simpler, fast, painless and customisable solution exists.

Install the python module tqdm.

$ sudo pip install tqdm
$ # now have fun
$ 7z a -r -bd newZipFile myFolder | tqdm >> /dev/null
$ # if we know the expected total, we can have a bar!
$ 7z a -r -bd newZipFile myFolder | grep -o Compressing | tqdm --total $(find myFolder -type f | wc -l) >> /dev/null

Help: tqdm -h. An example using more options:

$ find / -name '*.py' -exec cat \{} \; | tqdm --unit loc --unit_scale True | wc -l

As a bonus you can also use tqdm to wrap iterables in python code.

https://github.com/tqdm/tqdm/blob/master/README.rst#module

Upvotes: 4

Sundeep471
Sundeep471

Reputation: 1591

To indicate progress of activity, try the following commands:

while true; do sleep 0.25 && echo -ne "\r\\" && sleep 0.25 && echo -ne "\r|" && sleep 0.25 && echo -ne "\r/" && sleep 0.25 && echo -ne "\r-"; done;

OR

while true; do sleep 0.25 && echo -ne "\rActivity: \\" && sleep 0.25 && echo -ne "\rActivity: |" && sleep 0.25 && echo -ne "\rActivity: /" && sleep 0.25 && echo -ne "\rActivity: -"; done;

OR

while true; do sleep 0.25 && echo -ne "\r" && sleep 0.25 && echo -ne "\r>" && sleep 0.25 && echo -ne "\r>>" && sleep 0.25 && echo -ne "\r>>>"; sleep 0.25 && echo -ne "\r>>>>"; done;

OR

while true; do sleep .25 && echo -ne "\r:Active:" && sleep .25 && echo -ne "\r:aCtive:" && sleep .25 && echo -ne "\r:acTive:" && sleep .25 && echo -ne "\r:actIve:" && sleep .25 && echo -ne "\r:actiVe:" && sleep .25 && echo -ne "\r:activE:"; done;

One can use flags/variables inside the while loop to check and display the value/extent of progress.

Upvotes: 2

CH55
CH55

Reputation: 1

I have built on the answer provided by fearside

This connects to an Oracle database to retrieve the progress of an RMAN restore.

#!/bin/bash

 # 1. Create ProgressBar function
 # 1.1 Input is currentState($1) and totalState($2)
 function ProgressBar {
 # Process data
let _progress=(${1}*100/${2}*100)/100
let _done=(${_progress}*4)/10
let _left=40-$_done
# Build progressbar string lengths
_fill=$(printf "%${_done}s")
_empty=$(printf "%${_left}s")

# 1.2 Build progressbar strings and print the ProgressBar line
# 1.2.1 Output example:
# 1.2.1.1 Progress : [########################################] 100%
printf "\rProgress : [${_fill// /#}${_empty// /-}] ${_progress}%%"

}

function rman_check {
sqlplus -s / as sysdba <<EOF
set heading off
set feedback off
select
round((sofar/totalwork) * 100,0) pct_done
from
v\$session_longops
where
totalwork > sofar
AND
opname NOT LIKE '%aggregate%'
AND
opname like 'RMAN%';
exit
EOF
}

# Variables
_start=1

# This accounts as the "totalState" variable for the ProgressBar function
_end=100

_rman_progress=$(rman_check)
#echo ${_rman_progress}

# Proof of concept
#for number in $(seq ${_start} ${_end})

while [ ${_rman_progress} -lt 100 ]
do

for number in _rman_progress
do
sleep 10
ProgressBar ${number} ${_end}
done

_rman_progress=$(rman_check)

done
printf '\nFinished!\n'

Upvotes: 0

I prefer to use dialog with the --gauge param. Is used very often in .deb package installations and other basic configuration stuff of many distros. So you don't need to reinvent the wheel... again

Just put an int value from 1 to 100 @stdin. One basic and silly example:

for a in {1..100}; do sleep .1s; echo $a| dialog --gauge "waiting" 7 30; done

I have this /bin/Wait file (with chmod u+x perms) for cooking purposes :P

#!/bin/bash
INIT=`/bin/date +%s`
NOW=$INIT
FUTURE=`/bin/date -d "$1" +%s`
[ $FUTURE -a $FUTURE -eq $FUTURE ] || exit
DIFF=`echo "$FUTURE - $INIT"|bc -l`

while [ $INIT -le $FUTURE -a $NOW -lt $FUTURE ]; do
    NOW=`/bin/date +%s`
    STEP=`echo "$NOW - $INIT"|bc -l`
    SLEFT=`echo "$FUTURE - $NOW"|bc -l`
    MLEFT=`echo "scale=2;$SLEFT/60"|bc -l`
    TEXT="$SLEFT seconds left ($MLEFT minutes)";
    TITLE="Waiting $1: $2"
    sleep 1s
    PTG=`echo "scale=0;$STEP * 100 / $DIFF"|bc -l`
    echo $PTG| dialog --title "$TITLE" --gauge "$TEXT" 7 72
done

if [ "$2" == "" ]; then msg="Espera terminada: $1";audio="Listo";
else msg=$2;audio=$2;fi 

/usr/bin/notify-send --icon=stock_appointment-reminder-excl "$msg"
espeak -v spanish "$audio"

So I can put:

Wait "34 min" "warm up the oven"

or

Wait "dec 31" "happy new year"

Upvotes: 3

Zarko Zivanov
Zarko Zivanov

Reputation: 11

I used an answer from Creating string of repeated characters in shell script for char repeating. I have two relatively small bash versions for scripts that need to display progress bar (for example, a loop that goes through many files, but not useful for big tar files or copy operations). The faster one consists of two functions, one to prepare the strings for bar display:

preparebar() {
# $1 - bar length
# $2 - bar char
    barlen=$1
    barspaces=$(printf "%*s" "$1")
    barchars=$(printf "%*s" "$1" | tr ' ' "$2")
}

and one to display a progress bar:

progressbar() {
# $1 - number (-1 for clearing the bar)
# $2 - max number
    if [ $1 -eq -1 ]; then
        printf "\r  $barspaces\r"
    else
        barch=$(($1*barlen/$2))
        barsp=$((barlen-barch))
        printf "\r[%.${barch}s%.${barsp}s]\r" "$barchars" "$barspaces"
    fi
}

It could be used as:

preparebar 50 "#"

which means prepare strings for bar with 50 "#" characters, and after that:

progressbar 35 80

will display the number of "#" characters that corresponds to 35/80 ratio:

[#####################                             ]

Be aware that function displays the bar on the same line over and over until you (or some other program) prints a newline. If you put -1 as first parameter, the bar would be erased:

progressbar -1 80

The slower version is all in one function:

progressbar() {
# $1 - number
# $2 - max number
# $3 - number of '#' characters
    if [ $1 -eq -1 ]; then
        printf "\r  %*s\r" "$3"
    else
        i=$(($1*$3/$2))
        j=$(($3-i))
        printf "\r[%*s" "$i" | tr ' ' '#'
        printf "%*s]\r" "$j"
    fi
}

and it can be used as (the same example as above):

progressbar 35 80 50

If you need progressbar on stderr, just add >&2 at the end of each printf command.

Upvotes: 1

romeror
romeror

Reputation: 137

This lets you visualize that a command is still executing:

while :;do echo -n .;sleep 1;done &
trap "kill $!" EXIT  #Die with parent if we die prematurely
tar zxf packages.tar.gz; # or any other command here
kill $! && trap " " EXIT #Kill the loop and unset the trap or else the pid might get reassigned and we might end up killing a completely different process

This will create an infinite while loop that executes in the background and echoes a "." every second. This will display . in the shell. Run the tar command or any a command you want. When that command finishes executing then kill the last job running in the background - which is the infinite while loop.

Upvotes: 12

Related Questions