使用 Bash 显示进度指示器

只使用 bash 脚本,如何提供 bash 进度指示器?

例如,当我从 bash 运行一个命令时——当该命令正在执行时——让用户知道仍在发生某些事情。

122765 次浏览

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

If you have a way to estimate percentage done, such as the current number of files processed and total number, you can make a simple linear progress meter with a little math and assumptions about screen width.

count=0
total=34
pstr="[=======================================================================]"


while [ $count -lt $total ]; do
sleep 0.5 # this is work
count=$(( $count + 1 ))
pd=$(( $count * 73 / $total ))
printf "\r%3d.%1d%% %.${pd}s" $(( $count * 100 / $total )) $(( ($count * 1000 / $total) % 10 )) $pstr
done

Or instead of a linear meter you could estimate time remaining. It's about as accurate as other similar things.

count=0
total=34
start=`date +%s`


while [ $count -lt $total ]; do
sleep 0.5 # this is work
cur=`date +%s`
count=$(( $count + 1 ))
pd=$(( $count * 73 / $total ))
runtime=$(( $cur-$start ))
estremain=$(( ($runtime * $total / $count)-$runtime ))
printf "\r%d.%d%% complete ($count of $total) - est %d:%0.2d remaining\e[K" $(( $count*100/$total )) $(( ($count*1000/$total)%10)) $(( $estremain/60 )) $(( $estremain%60 ))
done
printf "\ndone\n"

Referred from here is a nice spinner function (with slight modification), will help your cursor to stay in original position also.

spinner()
{
local pid=$!
local delay=0.75
local spinstr='|/-\'
while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do
local temp=${spinstr#?}
printf " [%c]  " "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b\b\b\b\b\b"
done
printf "    \b\b\b\b"
}

with usage:

(a_long_running_task) &
spinner

@DavidD's comments on Pez Cuckows answer, this is an example of how you can capture the output of the progress bar in a script and still see the spinner on the screen:

#!/usr/bin/env bash


#############################################################################
###########################################################################
###
### Modified/Rewritten by A.M.Danischewski (c) 2015 v1.1
### Issues: If you find any issues emai1 me at my <first name> dot
###         <my last name> at gmail dot com.
###
### Based on scripts posted by Pez Cuckow, William Pursell at:
### http://stackoverflow.com/questions/12498304/using-bash-to-display-\
###      a-progress-working-indicator
###
### This program runs a program passed in and outputs a timing of the
### command and it exec's a new fd for stdout so you can assign a
### variable the output of what was being run.
###
### This is a very new rough draft but could be expanded.
###
### This program is free software: you can redistribute it and/or modify
### it under the terms of the GNU General Public License as published by
### the Free Software Foundation, either version 3 of the License, or
### (at your option) any later version.
###
### This program is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
### GNU General Public License for more details.
###
### You should have received a copy of the GNU General Public License
### along with this program.  If not, see <http://www.gnu.org/licenses/>.
###########################################################################
#############################################################################


declare    CMD="${1}"
shift      ## Clip the first value of the $@, the rest are the options.
declare    CMD_OPTIONS="$@"
declare    CMD_OUTPUT=""
declare    TMP_OUTPUT="/tmp/_${0##*/}_$$_$(date +%Y%m%d%H%M%S%N)"
declare -r SPIN_DELAY="0.1"
declare -i PID=


function usage() {
cat <<EOF


Description: ${0##*/}


This program runs a program passed in and outputs a timing of the
command and it exec's a new fd for stdout so you can assign a variable
the output of what was being run.


Usage: ${0##*/} <command> [command options]


E.g.
>$ ${0##*/} sleep 5 \&\& echo "hello" \| figlet
Running: sleep 5 && echo hello | figlet, PID 2587:/


real   0m5.003s
user   0m0.000s
sys    0m0.002s
_          _ _
| |__   ___| | | ___
| '_ \ / _ \ | |/ _ \
| | | |  __/ | | (_) |
|_| |_|\___|_|_|\___/


Done..
>$ var=\$(${0##*/} sleep 5 \&\& echo hi)
Running: sleep 5 && echo hi, PID 32229:-
real   0m5.003s
user   0m0.000s
sys    0m0.001s
Done..
>$ echo \$var
hi


EOF
}


function spin_wait() {
local -a spin
spin[0]="-"
spin[1]="\\"
spin[2]="|"
spin[3]="/"
echo -en "Running: ${CMD} ${CMD_OPTIONS}, PID ${PID}: " >&3
while kill -0 ${PID} 2>/dev/random; do
for i in "${spin[@]}"; do
echo -ne "\b$i" >&3
sleep ${SPIN_DELAY}
done
done
}


function run_cmd() {
exec 3>$(tty)
eval "time ${CMD} ${CMD_OPTIONS}" 2>>"${TMP_OUTPUT}" | tee "${TMP_OUTPUT}" &
PID=$! # Set global PID to process id of the command we just ran.
spin_wait
echo -en "\n$(< "${TMP_OUTPUT}")\n" >&3
echo -en "Done..\n" >&3
rm "${TMP_OUTPUT}"
exec 3>&-
}


if [[ -z "${CMD}" || "${CMD}" =~ ^-. ]]; then
usage | more && exit 0
else
run_cmd
fi


exit 0

Here a simple one-liner, that I use:

while true; do for X in '-' '/' '|' '\'; do echo -en "\b$X"; sleep 0.1; done; done

Here's my attempt. I'm new to bash scripts so some of this code may be terrible :)

Example Output:

In Progress Done

The Code:

progressBarWidth=20


# Function to draw progress bar
progressBar () {


# Calculate number of fill/empty slots in the bar
progress=$(echo "$progressBarWidth/$taskCount*$tasksDone" | bc -l)
fill=$(printf "%.0f\n" $progress)
if [ $fill -gt $progressBarWidth ]; then
fill=$progressBarWidth
fi
empty=$(($fill-$progressBarWidth))


# Percentage Calculation
percent=$(echo "100/$taskCount*$tasksDone" | bc -l)
percent=$(printf "%0.2f\n" $percent)
if [ $(echo "$percent>100" | bc) -gt 0 ]; then
percent="100.00"
fi


# Output to screen
printf "\r["
printf "%${fill}s" '' | tr ' ' ▉
printf "%${empty}s" '' | tr ' ' ░
printf "] $percent%% - $text "
}






## Collect task count
taskCount=33
tasksDone=0


while [ $tasksDone -le $taskCount ]; do


# Do your task
(( tasksDone += 1 ))


# Add some friendly output
text=$(echo "somefile-$tasksDone.dat")


# Draw the progress bar
progressBar $taskCount $taskDone $text


sleep 0.01
done


echo

You can see the source here: https://gist.github.com/F1LT3R/fa7f102b08a514f2c535

This is a pretty easy technique:
(just replace sleep 20 with whatever command you want to indicate is running)

#!/bin/bash


sleep 20 & PID=$! #simulate a long process


echo "THIS MAY TAKE A WHILE, PLEASE BE PATIENT WHILE ______ IS RUNNING..."
printf "["
# While process is running...
while kill -0 $PID 2> /dev/null; do
printf  "▓"
sleep 1
done
printf "] done!"

The output looks like this:

> THIS MAY TAKE A WHILE, PLEASE BE PATIENT WHILE ______ IS RUNNING...
> [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] done!

It adds a (high density dotted) every second until the process is complete.

Psychedelic progress bar for bash scripting. Call by command line as './progressbar x y' where 'x' is a time in seconds and 'y' is a message to display. The inner function progressbar() works standalone as well and takes 'x' as a percentage and 'y' as a message.

#!/bin/bash


if [ "$#" -eq 0 ]; then echo "x is \"time in seconds\" and z is \"message\""; echo "Usage: progressbar x z"; exit; fi
progressbar() {
local loca=$1; local loca2=$2;
declare -a bgcolors; declare -a fgcolors;
for i in {40..46} {100..106}; do
bgcolors+=("$i")
done
for i in {30..36} {90..96}; do
fgcolors+=("$i")
done
local u=$(( 50 - loca ));
local y; local t;
local z; z=$(printf '%*s' "$u");
local w=$(( loca * 2 ));
local bouncer=".oO°Oo.";
for ((i=0;i<loca;i++)); do
t="${bouncer:((i%${#bouncer})):1}"
bgcolor="\\E[${bgcolors[RANDOM % 14]}m \\033[m"
y+="$bgcolor";
done
fgcolor="\\E[${fgcolors[RANDOM % 14]}m"
echo -ne " $fgcolor$t$y$z$fgcolor$t \\E[96m(\\E[36m$w%\\E[96m)\\E[92m $fgcolor$loca2\\033[m\r"
};
timeprogress() {
local loca="$1"; local loca2="$2";
loca=$(bc -l <<< scale=2\;"$loca/50")
for i in {1..50}; do
progressbar "$i" "$loca2";
sleep "$loca";
done
echo -e "\n"
};
timeprogress "$1" "$2"

Here is an example of an 'activity indicator,' for an internet connection speed test via the linux 'speedtest-cli' command:

printf '\n\tInternet speed test:  '


# http://stackoverflow.com/questions/12498304/using-bash-to-display-a-progress-working-indicator


spin[0]="-"
spin[1]="\\"
spin[2]="|"
spin[3]="/"


# http://stackoverflow.com/questions/20165057/executing-bash-loop-while-command-is-running


speedtest > .st.txt &           ## & : continue running script
pid=$!                          ## PID of last command


# If this script is killed, kill 'speedtest':
trap "kill $pid 2> /dev/null" EXIT


# While 'speedtest' is running:
while kill -0 $pid 2> /dev/null; do
for i in "${spin[@]}"
do
echo -ne "\b$i"
sleep 0.1
done
done


# Disable the trap on a normal exit:
trap - EXIT


printf "\n\t           "
grep Download: .st.txt
printf "\t             "
grep Upload: .st.txt
echo ''
rm -f st.txt

Update - example:

animated GIF

Aside from the classical spinner, you can use this progress bar

It achieves subcharacter precision by using half block characters

enter image description here

Code included on the link.

I extended the answer of checksum in his answer by displaying a variable info message after the spinner:

#!/usr/bin/env bash
function spinner() {
local info="$1"
local pid=$!
local delay=0.75
local spinstr='|/-\'
while kill -0 $pid 2> /dev/null; do
local temp=${spinstr#?}
printf " [%c]  $info" "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
local reset="\b\b\b\b\b\b"
for ((i=1; i<=$(echo $info | wc -c); i++)); do
reset+="\b"
done
printf $reset
done
printf "    \b\b\b\b"
}


# usage:
(a_long_running_task) &
spinner "performing long running task..."

I don't like that if the stdout output with a spinner is redirected to a file, less shows ^H for each backspace instead of avoiding them in a file output at all. Is that possible with an easy spinner like this one?

https://github.com/extensionsapp/progre.sh

Create 82 percent progress: progreSh 82

enter image description here