Terminal progress indicators are a common sight in command-line applications, often used to show progress of long running tasks and ensuring users don’t get bored. Implementing them in scripts these days is pretty straightforward thanks to various libraries, but I’ve been curious about how they actually work under the hood.
The answer turned out to be very simple; the magic sauce is the character \r, the carriage return character. The carriage return is actually what’s called a control character, it moves the cursor back to the beginning of the line. That in turn allows the next output to overwrite the previous output on the same line. To put it another way, this act of overwriting the previous output is little more than a crude animation technique.
Most modern terminal emulators and environments support this behaviour just fine, and that is how most progress indicators are implemented which I’ll show below. It’ll even work with SSH sessions so you can have progress indicators in remote scripts.
Simple number indicator
Here’s a classic in-place progress number indicator which simply counts to 20. Save it to a Python file and run it.
import
time num_steps =
20
for
step in
range
(
num_steps)
:
print
(
f"Processing {step+1} / {num_steps}"
,
end=
'\r'
)
time.
sleep(
0.3
)
print
(
"\nDone!"
)
Note the use of end='\r' in the print statement in the loop, which is how the in-place update is achieved. Importantly as well, the \n, the newline character on the final print statement is necessary to move the cursor along after the loop is done. Without the newline, the “Done!” message would overwrite the last progress message.
Single character spinner
Single character spinners are a common way to indicate that something is in progress without necessarily showing a percentage. Here, we select from a set of characters in a loop to give the illusion of a spinning animation.
import
time total =
20
chars =
[
"|"
,
"/"
,
"-"
,
"\\"
]
for
step in
range
(
total)
:
current =
step +
1
selected_char =
chars[
step %
len
(
chars)
]
print
(
f"\r{selected_char} Processing..."
,
end=
""
)
time.
sleep(
0.3
)
print
(
"\nDone!"
)
The key is the use of the modulo operator %, to cycle through the characters in the chars list. Each time the loop iterates, it selects the next character based on the current step, creating a spinning effect.
You can play around with the characters in the chars list to create different styles of spinners. Substitute the chars list as shown here:
chars =
[
"⠋"
,
"⠙"
,
"⠹"
,
"⠸"
,
"⠼"
,
"⠴"
,
"⠦"
,
"⠧"
,
"⠇"
,
"⠏"
]
This creates:
See if you can find other interesting characters to use as spinners, here I’ve used the moon phase emojis:
With a ✔ checkmark
You can take it a step further and replace the final progress message with a checkmark to indicate completion, and this is a fairly common pattern and looks nice. The way it works, instead of a newline in the last message, we use another carriage return to overwrite the last progress message.
import
time total =
20
chars =
[
"⠋"
,
"⠙"
,
"⠹"
,
"⠸"
,
"⠼"
,
"⠴"
,
"⠦"
,
"⠧"
,
"⠇"
,
"⠏"
]
for
step in
range
(
total)
:
current =
step +
1
selected_char =
chars[
step %
len
(
chars)
]
print
(
f"\r{selected_char} Processing..."
,
end=
""
)
time.
sleep(
0.3
)
print
(
f"\r✔ Done! "
)
Here it is:
A progress bar
Now that we understand the basics of in place updates, progress bars aren’t that much more complicated. The idea is to create a string that visually represents the progress using a blocky character that fills up a space.
Try this in a file:
import
time total =
20
for
step in
range
(
total)
:
current =
step +
1
percent =
current /
total bar_length =
20
filled =
int
(
bar_length *
percent)
bar =
"█"
*
filled +
"-"
*
(
bar_length -
filled)
print
(
f"\rProcessing: [{bar}] {current}/{total}"
,
end=
""
)
time.
sleep(
0.1
)
print
(
"\nDone!"
)
The bar string is constructed by repeating the “filled” character █ for the completed portion and the - character for the remaining portion.
Bouncing dot progress bar
A variation on the progress bar, when you don’t have a known total, is to create a bouncing dot progress bar. In this example, the dot moves forwards or backwards depending on whether its position is less than or greater than the bar length.
import
time bar_length =
20
for
i in
range
(
70
)
:
pos =
i %
(
bar_length *
2
)
if
pos >=
bar_length:
pos =
(
bar_length *
2
)
-
pos -
1
bar =
[
"-"
]
*
bar_length bar[
pos]
=
"●"
print
(
f"\rProcessing: [{''.join(bar)}]"
,
end=
""
,
flush=
True
)
time.
sleep(
0.05
)
print
(
"\nDone!"
)
Here it is:
Two progress indicators at once
You might even want to have two progress indicators at once, for example a parent task and nested subtasks.
This does get trickier, as we have to make use of two control sequences, \033[A for “cursor up”, and \033[K for “clear line”.
In this example, we print two lines to reserve space for the progress indicators. Then in each loop, move the cursor up two lines to update the overall progress, then move to the next line to update the loop progress.
import
time
import
sys MOVE_UP =
"\033[A"
CLEAR_LINE =
"\033[K"
overall_iterations =
3
loops =
10
print
(
"\n\n"
,
end=
""
)
for
iteration in
range
(
overall_iterations)
:
for
loop in
range
(
loops)
:
sys.
stdout.
write(
f"{MOVE_UP}{MOVE_UP}"
)
print
(
f"{CLEAR_LINE}Overall Progress ({iteration+1}/{overall_iterations})"
)
print
(
f"{CLEAR_LINE}Processing: [{'#' * (loop+1)}{'-' * (loops-loop-1)}] {loop+1}/{loops}"
)
sys.
stdout.
flush(
)
time.
sleep(
0.2
)
print
(
"\nDone!"
)
So to put it another way, we are using the control sequences to move around on the terminal ‘space’ to update relevant lines and make it look like we have two progress indicators at once.
What you should use
The examples here are meant to be educational, or for quick-and-dirty progress indicators without dependencies.
In practice, for production grade scripts, you should consider using a library such as tqdm or rich. They handle a lot of edge cases and have many features and effects that you can easily use.
In Bash
The examples above are all Python for simplicity, but you can do it in Bash too, though it’s a bit more verbose and less readable. Here are the main examples anyway, done in Bash.
The number indicator:
num_steps
=
20
for
((step=1; step<=num_steps; step++))
;
do
printf
"\rProcessing %2d/%2d"
"$step"
"$num_steps"
sleep
0.2
done
echo
-e
"\nDone!"
The single character spinner:
chars
=
(
"|"
, "/"
"-"
"\\"
)
total
=
20
for
((step=0; step<total; step++))
;
do
char
=
"${chars[step % ${#chars[@]}]}"
printf
"\r%s Processing..."
"$char"
sleep
0.2
done
echo
-e
"\nDone!"
And the progress bar:
total
=
20
bar_size
=
20
for
((i=1; i<=total; i++))
;
do
percent
=
$(( i * 100 / total ))
filled
=
$(( i * bar_size / total ))
empty
=
$(( bar_size - filled ))
bar_str
=
$(printf "%${filled}s" | tr ' ' '#')
empty_str
=
$(printf "%${empty}s" | tr ' ' '-')
echo
-ne
"\rProcessing: [${bar_str}${empty_str}] ${percent}%"
sleep
0.1
done
echo
-e
"\nDone!"
