Hires (floating point) progress bar
Yet Another Bash Progress Bar...
Preamble
Sorry for this not so short answer. In this answer I will address
- granularity and look, by using
integer to render floating point, UTF-8 fonts for rendering progress bar more finely,
- interaction between your program and the bar rendering jobs, by using
parallelism
- some samples, running another task (
sha1sum) in order to follow his progression, with minimal resource footprint using pure bash and no forks.
- creating separated background task to show a spinner while your main script continue, with the two function:
startSpinner and stopSpinner.
For impatiens: Please test code (copy/paste in a new terminal window) at Now do it! (in the middle), with
- either: Last animated demo,
- either any of Practical samples.
All demos here use read -t <float seconds> && break instead of sleep. So all loop could be nicely stopped by hitting Return key.
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

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

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:

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 read timeout: Display some prompt by running:
TIMEFORMAT=%R
time displaySleep 1.5
time displaySleep 1.5 userKey && echo ${userKey@Q} || echo Done
printf 'Hit any key in \e[1m3 secs\e[0m to stop ("A" for "accept"): ';\
time displaySleep 3 userKey && echo User hit ${userKey@Q}. || echo Done.
Should show something like:

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.

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
Variants using width x2 instead of x8
Inspired by [python]'s pip._internal.cli.progress_bars, noticed that UTF-8 do offer a lot of different characters filling HALF or FULL character width:
HEAVY HORIZONTAL: '╺ ━ ╸'
LIGHT HORIZONTAL: '╶ ─ ╴'
LOWER HALF/QUADRANT: '▗ ▄ ▖'
UPPER HALF/QUADRANT: '▝ ▀ ▘'
BRAILLE PATTERN: '⠰ ⠶ ⠆'
and lot more...
I've built this variant:
percentBar2 () {
local prct totlen=$(( 2 * $2 )) lastchar lhs rhs \
lmr=${4:-'\U257a\U2501\U2578'} sep="${5:-\\e[1m:\\e[0;2m}"
printf -v lmr %b "$lmr"
printf -v prct %.2f "$1"
((prct=10#${prct/.}*totlen/10000, prct%2)) && lastchar="${lmr:2}"
printf -v lhs '%*s' $((prct/2)) '';
printf -v rhs '%*s' $(((totlen-prct-1)/2)) '';
[[ -z $lastchar ]] && (( totlen > prct )) && rhs="${lmr::1}$rhs";
printf -v "$3" '%b%b%b%b%b\e[0m' "${sep%:*}" "${lhs// /${lmr:1:1}}" \
"$lastchar" "${sep#*:}" "${rhs// /${lmr:1:1}}"
}
I've called them percentBar2 because of x2 instead of x8. So less precision on small width, but permit nice designs!
Chars: ''
Width 1 char ▕╺▏, 3 chars ▕╸━━▏, 12 chars ▕━━━╺━━━━━━━━▏, or full width: 28.93%
━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Chars: '▗ ▄ ▖'
Width 1 char ▕▗▏, 3 chars ▕▖▄▄▏, 12 chars ▕▄▄▄▗▄▄▄▄▄▄▄▄▏, or full width: 28.93%
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▗▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
Chars: '╺ ━ ╸'
Width 1 char ▕╺▏, 3 chars ▕╸━━▏, 12 chars ▕━━━╺━━━━━━━━▏, or full width: 28.93%
━━━━━━━━━━━━━━━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Chars: '╶ ─ ╴'
Width 1 char ▕╶▏, 3 chars ▕╴──▏, 12 chars ▕───╶────────▏, or full width: 28.93%
───────────────────────╶────────────────────────────────────────────────────────
Chars: '▝ ▀ ▘'
Width 1 char ▕▝▏, 3 chars ▕▘▀▀▏, 12 chars ▕▀▀▀▝▀▀▀▀▀▀▀▀▏, or full width: 28.93%
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▝▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
Chars: '⠰ ⠶ ⠆'
Width 1 char ▕⠰▏, 3 chars ▕⠆⠶⠶▏, 12 chars ▕⠶⠶⠶⠰⠶⠶⠶⠶⠶⠶⠶⠶▏, or full width: 28.93%
⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠰⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶⠶
Full demo, showing both percentBar and percentBar2:
#!/bin/bash
# shellcheck disable=SC2059
COLUMNS=$(tput cols) bar=()
doDelay() {
local delay=.0125 userKey
case ${percnt%?} in
0.0|33.[36]|49.8|50.1|66.[69]|99.9|100.0) delay=.5 ;;
esac
IFS= read -rst $delay -n 1 userKey && case $userKey in
n ) return 1 ;;
q ) return 2 ;;
esac
return 0
}
chars=( '\U2588\U2589\U258A\U258B\U258C\U258D\U258E\U258F' ''
'\U2597\U2584\U2596' '\U257A\U2501\U2578' '\U2576\U2500\U2574' \
'\U259D\U2580\U2598' '\U2830\U2836\U2806')
mapfile -t printchars < <(printf '%b\n' "${chars[@]:---default--}")
mapfile -t printchars < <(printf '%s\n' "${printchars[@]//?/ &}")
mapfile -t printchars < <(printf '%s\n' "${printchars[@]/# }")
Put here all three functions: percent(){ ..., percentBar(){ ... and percentBar2(){ ..., and
printf '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\e[20A\e7' &&
for thm in '' '\e[32;1m:\e[0;30m' '\e[38;5;125m:\e[38;5;95m'; do
thmFg="${thm%:*}" thmBg="${thm#*:}"
thmBg="${thmBg/\[0;/[}"
tthm="${thmFg}${thmBg/\[3/[4}"
tthm="${tthm/m\\e[/;}"
printThm="${thm:---default--}"
str="\\e8\e[AColors: \47%s\47 \47%b\U259A\U259A\U259A\U259A\e[0m\47 \47%s"
str+="\47 \47%b\U259A\U259A\e[0;1m\47:\e[0m\47%b\U259A\U259A\e[0m\47\n"
printf "$str" "${tthm:---default--}" "$tthm" "${printThm/:/\':\'}" \
"${thm%:*}" "${thm#*:}"
for i in {0..9999..11} 10000; do # {0..9999..33}
o=0 i=0$i
printf -v percnt %0.2f "${i::-2}.${i: -2}"
for l in 1 3 12 $((COLUMNS)); do
percentBar "$percnt" $l "bar[$((o++))]"
done
for char in "${chars[@]:1}"; do
for l in 1 3 12 $((COLUMNS)); do
percentBar2 "$percnt" $l "bar[$((o++))]" "$char" "$thm"
done
done
str="Chars: \47%%%%b\47\nWidth 1 char \U2595%%%%%%%%b%s\e[0m\U258F, 3 c"
str+="hars \U2595%%%%%%%%b%s\e[0m\U258F, 12 chars \U2595%%%%%%%%b%s\e[0"
str+="m\U258F, or full width:%%7.2f%%%%%%%%%%%%%%%%\n%%%%%%%%b%s\e[0m\n"
printf -v str "$str" "${bar[@]}"
printf -v str "$str" "$percnt"{,,,,,,}
printf -v str "$str" "${printchars[@]}"
printf "\e8${str%$'\n'}" "$tthm"{,,,}
doDelay || break $?
done
done
Should produce something like:

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
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;read -rsn 1 -t 4 _;stopSpinner

Then, for sample short stat of $HOME directory:
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, %d files and %d others entries.\n' \
${homeStats[entries]} "$HOME" ${homeStats[files]} ${homeStats[dirs]} \
$(( homeStats[entries] - 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;read -rsn 1 -t 4 _;stopSpinner

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;read -rsn 1 -t 4 _;stopSpinner

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;read -rsn 1 -t 12 _;stopSpinner

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 old view of output:

You may found my parallel grep script on my website
Here is a screen shot:

pvfor anything that can be piped. Example:ssh remote "cd /home/user/ && tar czf - accounts" | pv -s 23091k | tar xz