Whiptail Progress Gauges
Creating Progress Gauges with Whiptail
Whiptail is a dandy TUI (terminal user interface) toolkit that helps you make bash scripts
for non-technical users. Or for people who just don’t like terminals. The terminal is
a huge source of information. Sometimes it’s too much information. When it is, a toolkit
like Whiptail can make your scripts seem more friendly. In particular, though, progress
gauges can seem a little problematic, because you must provide the indication of progress
yourself. The --gauge
switch doesn’t possess any magical qualities that help it to know
how much more a process must operate before it completes. In fact, it has absolutely no way
to know the progress of a process toward completion. You must program that progress
yourself. I’m going to talk about that in this article.
The Basic Syntax
It’s worth reading the man page for all the switches for Whiptail. But the switches you’ll use
a lot are --title
, --infobox
, --msgbox
, --yesno
, --radiolist
, --inputbox
, --textbox
,
--passwordbox
, --menu
, --checklist
, and --gauge
. Followed by the dimensions of the
box itself in lines and columns. Example:
name=$(whiptail --title "Sample input" --inputbox "What is your name?" 8 60 3&>1 1>&2 2&>3)
Some of the switches need to redirect STDIN and STDOUT, so we need to redirect it back so that
everything works. Here we capture user input into a variable that we can use later. You’ll have
to read up more on that redirection elsewhere, because I’m going to focus on the --gauge
switch.
The Problem with Progress Gauges
As I mentioned, you need to solve the problem of displaying a stream of numbers between 1 and 100
that meaningfully show the progress a UNIX process is making. The --gauge
switch has no built-in
way of doing that. That means that whatever process you’re measuring, you need to find a way of
providing --gauge
a way of knowing its progress.
In most tutorials for whiptail I see something like the following:
{
for ((i=0; i<=100; i+=1)); do
sleep 0.1
echo $i
done
} | whiptail --backtitle "PROGRESS GAUGE" --title "Calculating Result" --gauge "Please wait for calculation" 8 50 0
This will work. It will display a progress bar while some process has been executed, but there will probably be no correlation between the process’s progress and the gauge. What are some other possibilities?
Let’s assume for now that our process is abstract enough that we cannot measure the physical size of a target file and a destination file. That would be easy enough to show in a progress bar. And if your gauge just needs to measure a shrinking differential between two physical objects like that, then your calculation is simple. But what if all you have is a process that doesn’t easily lend itself to being measured? Where all you have is a PID?
First, we’ll have to launch the process in the background which means spawning a subshell. Otherwise, we couldn’t measure the process in the same script. So, remember that! We need to launch the process to the background and capture its PID, then watch for the process to disappear from the PID table. How on Earth do we make that happen?
Well, I would recommend passing the name of the function to execute in the background as a parameter to
the processgauge
function. We need three workers here. So that would mean something like this:
The Calculator, The Caller and the Gauge
calculate(){ ## The Calculator! 'How many angels on a pidhead?'
echo `Highly technical, time-consuming process here...`
sleep 600 # sleep for 10 minutes
}
showprogress(){ ## The Gauge: Produce the number stream
start=$1; end=$2; shortest=$3; longest=$4
for n in $(seq $start $end); do
echo $n
pause=$(shuf -i ${shortest:=1}-${longest:=3} -n 1) # random wait between 1 and 3 seconds
sleep $pause
done
}
processgauge(){ ## The Caller: Start the gauge and watch the PID
process_to_measure=$1
message=$2
backmessage=$3
eval $process_to_measure &
thepid=$!
num=25
while true; do
showprogress 1 $num 1 3
sleep 2
while $(ps aux | grep -v 'grep' | grep "$thepid" &>/dev/null); do
if [[ $num -gt 97 ]] ; then num=$(( num-1 )); fi
showprogress $num $((num+1))
num=$((num+1))
done
showprogress 99 100 3 3
done | whiptail --backtitle "$backmessage" --title "Progress Gauge" --gauge "$message" 6 70 0
}
This idea relies on capturing the PID of the process to measure. Then producing numbers up until the process drops out
of the PID table. We get a guaranteed number stream up to 25, but you can set num
to whatever you want. You can
also make the sleep period longer by offering larger numbers for the shortest and longest sleep values. So one
advantage of this approach is that it is flexible.
This type of approach works for short processes and long processes. You can set longer sleep times in
process gauge (The Caller) for a longer process or shorter sleep times. You can make consistent
sleep times if you like. Unfortunately, shuf
will not accept decimal arguments. They must be
integers, even though sleep
will accept decimal arguments. You could probably find another way
of managing that problem if you like. For now, this works for me. Perhaps in the future I’ll
generate shorter sleep times than one second.
So, from a menu to run calculate
, you could call
processgauge calculate "Calculating Maximum Beer Intake" "Calculating MAXBEERS..."
You’ll have to experiment with different calculations to see how you like the progress bar with
different scenarios. On shorter processes, the progress bar may jump from 25 to 100 percent
really quickly. On longer processes, the bar may go back and forth between 97 and 98 percent
for a long time. I chose this vacilation back and forth because at least you know the process hasn’t
gotten hung up. At least it’s still running. You could put something else in calculate
that
outputs to a logfile and then tail -f
that logfile to show any errors encountered or other issues.
For example:
calculate(){
logfile="./logfile"
length=600
[[ -f $logfile ]] && rm $logfile
echo "=== START ===" &>$logfile
for ((i=0; i<=$length; i+=1)); do
echo "$i:" &>>$logfile
date +"%D::%N" &>>$logfile
done
echo "=== END ===" &>>$logfile
}
Now for longer processes in the background you can watch a logfile to see how it’s doing.
Conclusion
This is only one beginning. I’m sure there are lots more approaches to creating a progress gauge with Whiptail. I like the flexibility. I like how easily I can adapt it. But this approach got me going for today. I’m back in business with Whiptail!