Simulating a Spinner for Progress in Bash

Simulating a spinner for progress in Bash

While I admire the DIY spirit of Ed and Jakuje I also like to reuse other folks code. If you'd rather recycle than recreate consider Louis Marascio's spinner. I put his spinner() function into my shell library and it is easy to use:

#!/bin/bash

. lib.sh

run_10s &

echo -n wait
spinner $!

echo -e "\rdone"

displays

$ ./test_lib
wait [/]

for 10 seconds with the spinner spinning and then it clears that line left containing wait with the \r and you are left with just

$ ./test_lib
done
$

on the screen.

Using Bash to display a progress indicator

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

How to add a progress bar to a shell script?

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).

How to code a spinner for waiting processes in a Batch file?

This can actually be done quite easily with pure native commands, you just have to know how to use the more tricky of them. No use of external tools like VBScript or nasty side effects like clearing the screen are necessary.

What you're looking for is the equivalent of the bash "echo -n" command which outputs a line without the newline. In XP batch, this is achieved by using "set /p" (ask user for response with a prompt) with empty input as follows:

<nul (set /p junk=Hello)
echo. again.

will output the string "Hello again." with no intervening newline.

That trick (and the use of CTRL-H, the backspace character can be seen in the following test script which starts (one after the other) a 10-second sub-task with a 20-second timeout and a 15-second sub-task with a 10-second timeout.

The payload script is created by the actual running script and its only requirement is that it do the work it has to do then delete a flag file when finished, so that the monitor function will be able to detect it.

Keep in mind that the ^H strings in this script are actually CTRL-H characters, the ^| is two separate characters used to escape the pipe symbol.

@echo off

:: Localise environment.
setlocal enableextensions enabledelayedexpansion

:: Specify directories. Your current working directory is used
:: to create temporary files tmp_*.*
set wkdir=%~dp0%
set wkdir=%wkdir:~0,-1%

:: First pass, 10-second task with 20-second timeout.
del "%wkdir%\tmp_*.*" 2>nul
echo >>"%wkdir%\tmp_payload.cmd" ping 127.0.0.1 -n 11 ^>nul
echo >>"%wkdir%\tmp_payload.cmd" del "%wkdir%\tmp_payload.flg"
call :monitor "%wkdir%\tmp_payload.cmd" "%wkdir%\tmp_payload.flg" 20

:: Second pass, 15-second task with 10-second timeout.
del "%wkdir%\tmp_*.*" 2>nul:
echo >>"%wkdir%\tmp_payload.cmd" ping 127.0.0.1 -n 16 ^>nul
echo >>"%wkdir%\tmp_payload.cmd" del "%wkdir%\tmp_payload.flg"
call :monitor "%wkdir%\tmp_payload.cmd" "%wkdir%\tmp_payload.flg" 10

goto :final

:monitor
:: Create flag file and start the payload minimized.
echo >>%2 dummy
start /min cmd.exe /c "%1"

:: Start monitoring.
:: i is the indicator (0=|,1=/,2=-,3=\).
:: m is the number of seconds left before timeout.
set i=0
set m=%3
<nul (set /p z=Waiting for child to finish: ^|)

:: Loop here awaiting completion.
:loop
:: Wait one second.
ping 127.0.0.1 -n 2 >nul

:: Update counters and output progress indicator.
set /a "i = i + 1"
set /a "m = m - 1"
if %i% equ 4 set i=0
if %i% equ 0 <nul (set /p z=^H^|)
if %i% equ 1 <nul (set /p z=^H/)
if %i% equ 2 <nul (set /p z=^H-)
if %i% equ 3 <nul (set /p z=^H\)

:: End conditions, complete or timeout.
if not exist %2 (
echo.
echo. Complete.
goto :final
)
if %m% leq 0 (
echo.
echo. *** ERROR: Timed-out waiting for child.
goto :final
)
goto :loop
:final
endlocal

How to create a spinning command line cursor?

Something like this, assuming your terminal handles \b

import sys
import time

def spinning_cursor():
while True:
for cursor in '|/-\\':
yield cursor

spinner = spinning_cursor()
for _ in range(50):
sys.stdout.write(next(spinner))
sys.stdout.flush()
time.sleep(0.1)
sys.stdout.write('\b')

bash: echo phrases with the delays on the same line

You asked for the waiting animation? There ya go:)

#Add some color
. ~/SCR/color

#Animation sprites
sprite=(
"$YLW($RED* $YLW) ( $RED*$YLW)"
"$YLW ($RED* $YLW)( $RED*$YLW) "
"$YLW ( $RED*$YLW)($RED* $YLW) "
"$YLW( $RED*$YLW) ($RED* $YLW)"
"$YLW($RED* $YLW) ( $RED*$YLW)")

#get the pid of your process
pid=$(...)

#Waiting animation
i=0; while [ -e /proc/$pid ]; do sleep 0.1
printf "\r${GRN}Please wait... ${sprite[$i]}$DEF"
((i++)); [[ $i = ${#sprite[@]} ]] && i=0
done

This animation will be running while the pid of your process exists.

animation

PS: here is the llink to that color script

Printing an ASCII spinning cursor in the console

Yes, this works on Windows, OS X, and Linux. Improving on Niklas' suggestion, you can make this more general like so:

def show_wait_cursor(seconds,fps=10)
chars = %w[| / - \\]
delay = 1.0/fps
(seconds*fps).round.times{ |i|
print chars[i % chars.length]
sleep delay
print "\b"
}
end

show_wait_cursor(3)

If you don't know how long the process will take, you can do this in another thread:

def show_wait_spinner(fps=10)
chars = %w[| / - \\]
delay = 1.0/fps
iter = 0
spinner = Thread.new do
while iter do # Keep spinning until told otherwise
print chars[(iter+=1) % chars.length]
sleep delay
print "\b"
end
end
yield.tap{ # After yielding to the block, save the return value
iter = false # Tell the thread to exit, cleaning up after itself…
spinner.join # …and wait for it to do so.
} # Use the block's return value as the method's
end

print "Doing something tricky..."
show_wait_spinner{
sleep rand(4)+2 # Simulate a task taking an unknown amount of time
}
puts "Done!"

This one outputs:

Doing something tricky...|
Doing something tricky.../
Doing something tricky...-
Doing something tricky...\
(et cetera)
Doing something tricky...done!

How to set up progress bar and also print output from subprocess.Popen

I wasn't happy with the tqdm or alive_progress capability. I found yaspin and created a custom "title" formatter since setting the percent of packages was impossible to characterize based on the steps I use to determine the progress. Instead, I add a counter to a yaspin formatter along with some methods to update the text manually. Not exactly what I wanted, but it definitely streamlined stdout:

class BarFormat:
def __init__(self, title: str, total=None, width=None):
self.title = title
self._total = total
self._cnt = 0
self._start = datetime.datetime.now()
self._status = ""
self._width = width if width else os.get_terminal_size(0)[0]

def __str__(self):
delta = datetime.datetime.now() - self._start
if self._total:
text = f"{self.title} ({self._cnt}/{self._total} elapsed {delta}) {self._status}"
else:
text = f"{self.title} (elapsed {delta}) {self._status}"
# resize if needed
return text[:self._width-10]

def current(self) -> int:
return self._cnt

def incr(self) -> None:
self._cnt += 1

def update(self, status: str) -> None:
self._status = status.strip()

Then in my code, I have something like this:

        fmt = BarFormat(bar_title, total=len(bar_items))
with yaspin(text=fmt).green.arc as bar:
for line in p.stdout:
l = line.decode('utf-8').strip()
if l:
# update status of bar only if there's output
fmt.update(l)
if bar_items:
if l == bar_items[fmt.current()]:
bar.write("> "+l)
fmt.incr()
rc = p.wait()

# final spinner status
if rc in ignore_rcs:
bar.green.ok()
else:
bar.red.fail()

This produces something like:

...
> compile code
> <other steps...>
<spinner> Running Some Tool (125/640 elapsed 0:12:34.56789) @I: some info

I noticed that yaspin doesn't handle lines in the text field that overflow the terminal width. I made it so that the width is determined when the class is instanced. It would be super easy, though, to update this to be dynamic.

EDIT: I actually ditched yaspin. I started using rich instead because I decided to update some of my logs to use RichHandler and found out it supports spinners and progress bars. Here's information about that https://rich.readthedocs.io/en/latest/progress.html

How to display a progress indicator in pure C/C++ (cout/printf)?

With a fixed width of your output, use something like the following:

float progress = 0.0;
while (progress < 1.0) {
int barWidth = 70;

std::cout << "[";
int pos = barWidth * progress;
for (int i = 0; i < barWidth; ++i) {
if (i < pos) std::cout << "=";
else if (i == pos) std::cout << ">";
else std::cout << " ";
}
std::cout << "] " << int(progress * 100.0) << " %\r";
std::cout.flush();

progress += 0.16; // for demonstration only
}
std::cout << std::endl;

http://ideone.com/Yg8NKj

[>                                                                     ] 0 %
[===========> ] 15 %
[======================> ] 31 %
[=================================> ] 47 %
[============================================> ] 63 %
[========================================================> ] 80 %
[===================================================================> ] 96 %

Note that this output is shown one line below each other, but in a terminal emulator (I think also in Windows command line) it will be printed on the same line.

At the very end, don't forget to print a newline before printing more stuff.

If you want to remove the bar at the end, you have to overwrite it with spaces, to print something shorter like for example "Done.".

Also, the same can of course be done using printf in C; adapting the code above should be straight-forward.



Related Topics



Leave a reply



Submit