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 b
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.
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 20echo -n
- print without new line at the endecho -e
- interpret special characters while printing"\r"
- carriage return, a special char to return to the beginning of the lineYou 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.
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:
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
stat
- check file stats-c
- return formatted value%s
- total sizeIn 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
- display info about zip archivetail -n1
- work with 1 line from the bottomtr -s ' '
- translate multiple spaces to one (squeeze them)cut -d' ' -f3
- cut 3rd space-delimited columnHere's the meat of the problem, though. 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/sh
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 formatprintf '%50s'
- print nothing, pad it with 50 spacestr ' ' '#'
- translate every space to hash signAnd this is how you'd use it:
#!/bin/sh
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 it can be wrapped in a function, rewritten to work with piped streams, rewritten to other language, whatever's your poison.