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

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

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


当前回答

没有看到任何类似的东西,这里所有的自定义函数似乎都只关注渲染,所以……下面是我非常简单的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

其他回答

有一次,我也有一个繁忙的脚本,它被占用了几个小时,没有任何进展。所以我实现了一个函数,主要包括前面的回答技巧:

#!/bin/bash
# Updates the progress bar
# Parameters: 1. Percentage value
update_progress_bar()
{
  if [ $# -eq 1 ];
  then
    if [[ $1 == [0-9]* ]];
    then
      if [ $1 -ge 0 ];
      then
        if [ $1 -le 100 ];
        then
          local val=$1
          local max=100

          echo -n "["

          for j in $(seq $max);
          do
            if [ $j -lt $val ];
            then
              echo -n "="
            else
              if [ $j -eq $max ];
              then
                echo -n "]"
              else
                echo -n "."
              fi
            fi
          done

          echo -ne " "$val"%\r"

          if [ $val -eq $max ];
          then
            echo ""
          fi
        fi
      fi
    fi
  fi
}

update_progress_bar 0
# Further (time intensive) actions and progress bar updates
update_progress_bar 100

要制作一个tar进度条

tar xzvf pippo.tgz |xargs -L 19 |xargs -I@ echo -n "."

其中“19”是tar中的文件数除以预期进度条的长度。 例如:.tgz包含140个文件,你想要一个76 "."的进度条,你可以放-L 2。

你什么都不需要了。

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

我能在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

我更喜欢使用dialog和——gauge参数。在许多发行版的.deb包安装和其他基本配置中经常使用。所以你不需要重新发明轮子……再一次

只要输入一个从1到100的int值@stdin。举个简单而愚蠢的例子:

for a in {1..100}; do sleep .1s; echo $a| dialog --gauge "waiting" 7 30; done

我有这个/bin/Wait文件(带有chmod u+x perms)用于烹饪

#!/bin/bash
INIT=`/bin/date +%s`
NOW=$INIT
FUTURE=`/bin/date -d "$1" +%s`
[ $FUTURE -a $FUTURE -eq $FUTURE ] || exit
DIFF=`echo "$FUTURE - $INIT"|bc -l`

while [ $INIT -le $FUTURE -a $NOW -lt $FUTURE ]; do
    NOW=`/bin/date +%s`
    STEP=`echo "$NOW - $INIT"|bc -l`
    SLEFT=`echo "$FUTURE - $NOW"|bc -l`
    MLEFT=`echo "scale=2;$SLEFT/60"|bc -l`
    TEXT="$SLEFT seconds left ($MLEFT minutes)";
    TITLE="Waiting $1: $2"
    sleep 1s
    PTG=`echo "scale=0;$STEP * 100 / $DIFF"|bc -l`
    echo $PTG| dialog --title "$TITLE" --gauge "$TEXT" 7 72
done

if [ "$2" == "" ]; then msg="Espera terminada: $1";audio="Listo";
else msg=$2;audio=$2;fi 

/usr/bin/notify-send --icon=stock_appointment-reminder-excl "$msg"
espeak -v spanish "$audio"

所以我可以写:

等"34分钟" "预热烤箱"

or

等待“12月31日”“新年快乐”

大多数unix命令不会为您提供可以执行此操作的直接反馈。 有些将在您可以使用的stdout或stderr上提供输出。

对于tar之类的东西,您可以使用-v开关并将输出管道到一个程序,该程序为读取的每一行更新一个小动画。当tar写出一个文件列表时,程序可以更新动画。要计算完成百分比,您必须知道文件的数量并计算行数。

据我所知,cp没有给出这种输出。要监视cp的进程,必须监视源文件和目标文件,并监视目标文件的大小。你可以写一个小的c程序,使用stat(2)系统调用来获取文件大小。这将读取源文件的大小,然后轮询目标文件,并根据到目前为止写入的文件的大小更新% complete条。