当在bash或*NIX中的任何其他shell中编写脚本时,在运行需要超过几秒钟时间的命令时,需要一个进度条。

例如,复制一个大文件,打开一个大tar文件。

你建议用什么方法向shell脚本添加进度条?


当前回答

这可以通过一种相当简单的方式来实现:

使用for循环从0迭代到100 每一步睡眠25毫秒(0.25秒) 在$bar变量后面附加另一个=号,使进度条变宽 返回进度条和百分比(\r清除行并返回行首;-ne使echo在结尾不添加换行符,并解析\r特殊字符)

function progress {
    bar=''
    for (( x=0; x <= 100; x++ )); do
        sleep 0.25
        bar="${bar}="
        echo -ne "$bar ${x}%\r"
    done
    echo -e "\n"
}
$ progress
> ========== 10% # here: after 2.5 seconds
$ progress
> ============================== 30% # here: after 7.5 seconds

彩色进度条

function progress {
    bar=''
    for (( x=0; x <= 100; x++ )); do
        sleep 0.05
        bar="${bar} "

        echo -ne "\r"
        echo -ne "\e[43m$bar\e[0m"

        local left="$(( 100 - $x ))"
        printf " %${left}s"
        echo -n "${x}%"
    done
    echo -e "\n"
}

要使进度条变得彩色,你可以使用格式化转义序列-这里进度条是黄色的:\e[43m,然后我们用\e[0m重置自定义设置,否则即使进度条完成了,它也会影响进一步的输入。

其他回答

你可能也对如何做一个旋转器感兴趣:

我能在Bash中做旋转吗?

Sure! i=1 sp="/-\|" echo -n ' ' while true do printf "\b${sp:i++%${#sp}:1}" done Each time the loop iterates, it displays the next character in the sp string, wrapping around as it reaches the end. (i is the position of the current character to display and ${#sp} is the length of the sp string). The \b string is replaced by a 'backspace' character. Alternatively, you could play with \r to go back to the beginning of the line. If you want it to slow down, put a sleep command inside the loop (after the printf). A POSIX equivalent would be: sp='/-\|' printf ' ' while true; do printf '\b%.1s' "$sp" sp=${sp#?}${sp%???} done If you already have a loop which does a lot of work, you can call the following function at the beginning of each iteration to update the spinner: sp="/-\|" sc=0 spin() { printf "\b${sp:sc++:1}" ((sc==${#sp})) && sc=0 } endspin() { printf "\r%s\n" "$@" } until work_done; do spin some_work ... done endspin

没有看到任何类似的东西,这里所有的自定义函数似乎都只关注渲染,所以……下面是我非常简单的POSIX兼容解决方案,并逐级解释,因为这个问题并不简单。

博士TL;

渲染进度条非常简单。估计它应该渲染多少是另一回事。这是如何渲染(动画)进度条-你可以复制粘贴这个例子到一个文件并运行它:

#!/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}—取值为1到20 回显-打印到终端(即输出到标准输出) Echo -n -打印结尾没有新行 Echo -e -在打印时解释特殊字符 "\r" -回车,返回行首的特殊字符

你可以让它以任何速度渲染任何内容,所以这种方法是非常通用的,例如,经常用于愚蠢的电影中的可视化“黑客”,没有开玩笑。

完整答案(从0到工作示例)

问题的核心是如何确定$i值,即显示多少进度条。在上面的例子中,我只是让它在for循环中递增以说明原理,但实际应用程序将使用无限循环并在每次迭代中计算$ I变量。要进行上述计算,需要以下成分:

有多少工作要做 到目前为止已经做了多少工作

对于cp,它需要源文件的大小和目标文件的大小:

#!/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

Foo =$(bar) -在子进程中运行bar并将其标准输出保存到$ Foo 打印文件统计到标准输出 打印格式化的值 %s -总大小的格式

对于像文件解包这样的操作,计算源文件大小稍微困难一些,但仍然像获得未压缩文件的大小一样简单:

#!/bin/sh
src_size=$(gzip -l "$src" | tail -n1 | tr -s ' ' | cut -d' ' -f3)

Gzip -l打印关于zip存档的信息 尾部-n1 -从底部开始画一条线 Tr -s ' ' -将多个空格转换为一个(“挤压”它们) Cut -d' ' -f3 -切割第三个以空格分隔的字段(列)

Here's the meat of the problem I mentioned before. 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/bash

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 -用于打印给定格式的东西的内置程序 打印%50s -只打印50个空格 Tr ' ' '#' -将每个空格转换为散列号

你可以这样使用它:

#!/bin/bash

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

显然,你可以把它包装在一个函数中,重写以使用管道流,用$!然后把它传递给progress_bar。sh这样它就能猜出如何计算要做的功和已经完成的功,不管你想要什么。

一边笔记

我经常被问到这两件事:

${}: in above examples I use ${foo:A:B}. The technical term for this syntax is Parameter Expansion, a built-in shell functionality that allows to manipulate a variable (parameter), e.g. to trim a string with : but also to do other things - it does not spawn a subshell. The most prominent description of parameter expansion I can think of (that isn't fully POSIX compatible but lets the reader understand the concept well) is in the man bash page. $(): in above examples I use foo=$(bar). It spawns a separate shell in a subprocess (a.k.a. a Subshell), runs the bar command in it and assigns its standard output to a $foo variable. It's not the same as Process Substitution and it's something entirely different than pipe (|). Most importantly, it works. Some say this should be avoided because it's slow. I argue this is "a okay" here because whatever this code is trying to visualise lasts long enough to require a progress bar. In other words, subshells are not the bottleneck. Calling a subshell also saves me the effort of explaining why return isn't what most people think it is, what is an Exit Status and why passing values from functions in shells is not what shell functions are good at in general. To find out more about all of it I, again, highly recommend the man bash page.

故障排除

如果你的shell实际上运行的是sh而不是bash,或者非常旧的bash,比如默认的osx,它可能会在echo -ne "\r${BAR:0:$i}"时阻塞。准确的误差是Bad代入。如果这种情况发生在你身上,根据评论部分,你可以使用echo -ne "\r$(expr "x$name": "x.\{0,$num_skip\}\(.\{0,$num_keep\}\)")"来做一个更可移植的posix兼容/可读性较低的子字符串匹配。

一个完整的/bin/sh示例:

#!/bin/sh

src=100
tgt=0

get_work_todo() {
    echo $src
}

do_work() {
    echo "$(( $1 + 1 ))"
}

BAR_length=50
BAR_character='#'
BAR=$(printf %${BAR_length}s | tr ' ' $BAR_character)
work_todo=$(get_work_todo)             # how much there is to do
work_done=0
while true; do
    work_done="$(do_work $work_done)"
    i=$(( $work_done * $BAR_length / $work_todo ))
    n=$(( $BAR_length - $i ))
    printf "\r$(expr "x$BAR" : "x.\{0,$n\}\(.\{0,$i\}\)")"
    if [ $work_done = $work_todo ]; then
        echo "\n"
        break;
    fi
    sleep .1
done

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

创造40%的进度:progress 40

我以“恐惧之边”提供的答案为基础

它连接到Oracle数据库以检索RMAN恢复的进度。

#!/bin/bash

 # 1. Create ProgressBar function
 # 1.1 Input is currentState($1) and totalState($2)
 function ProgressBar {
 # Process data
let _progress=(${1}*100/${2}*100)/100
let _done=(${_progress}*4)/10
let _left=40-$_done
# Build progressbar string lengths
_fill=$(printf "%${_done}s")
_empty=$(printf "%${_left}s")

# 1.2 Build progressbar strings and print the ProgressBar line
# 1.2.1 Output example:
# 1.2.1.1 Progress : [########################################] 100%
printf "\rProgress : [${_fill// /#}${_empty// /-}] ${_progress}%%"

}

function rman_check {
sqlplus -s / as sysdba <<EOF
set heading off
set feedback off
select
round((sofar/totalwork) * 100,0) pct_done
from
v\$session_longops
where
totalwork > sofar
AND
opname NOT LIKE '%aggregate%'
AND
opname like 'RMAN%';
exit
EOF
}

# Variables
_start=1

# This accounts as the "totalState" variable for the ProgressBar function
_end=100

_rman_progress=$(rman_check)
#echo ${_rman_progress}

# Proof of concept
#for number in $(seq ${_start} ${_end})

while [ ${_rman_progress} -lt 100 ]
do

for number in _rman_progress
do
sleep 10
ProgressBar ${number} ${_end}
done

_rman_progress=$(rman_check)

done
printf '\nFinished!\n'

使用Linux命令pv。

如果它在管道中间,它不知道大小,但它给出了速度和总数,从那里你可以计算出它需要多长时间,并得到反馈,这样你就知道它没有挂起。