如何在Shell脚本中添加进度条?

543

在bash或其他* NIX中运行脚本时,如果要运行的命令需要花费几秒钟以上的时间,则需要使用进度条。

例如,拷贝大文件,打开大的tar文件等。

您推荐使用哪些方法来为shell脚本添加进度条?


请参考https://dev59.com/7Wcs5IYBdhLWcg3w5H5a,了解控制逻辑的示例(将作业放入后台并执行某些操作,直到其完成)。 - tripleee
4
在脚本编写时,我们经常需要一些要求:记录日志、显示进度、着色、特殊输出等等。我一直觉得应该有一种简单的脚本框架,但找不到,所以我决定自己实现一个。你可能会觉得这很有用。它纯粹使用 Bash 编写,就是说只用 Bash。https://github.com/SumuduLansakara/JustBash - Anubis
这个问题不应该移至 unix.stackexchange.com 吗? - Ethan
我喜欢在任何可以使用管道的情况下使用 pv。例如:ssh remote "cd /home/user/ && tar czf - accounts" | pv -s 23091k | tar xz - bitsoflogic
42个回答

796

你可以通过覆盖一行来实现这一点。使用\r回到行首而不将\n写入终端。

完成后写入\n以推进下一行。

使用echo -ne

  1. 不打印\n
  2. 识别转义序列,如\r

以下是演示:

echo -ne '#####                     (33%)\r'
sleep 1
echo -ne '#############             (66%)\r'
sleep 1
echo -ne '#######################   (100%)\r'
echo -ne '\n'

puk 在下面的评论中提到,如果您从一行长文开始,然后想写一行短文,则会 "失败":在这种情况下,您需要覆盖长文的长度(例如,使用空格)。


25
根据“echo”命令的手册(至少在MacOS X上),sh/bash使用它们自己内置的“echo”命令,不接受“-n”参数... 因此,为了实现相同的功能,您需要在字符串末尾放置“\r\c”,而不仅仅是“\r”。 - Justin Jenkins
63
输出这个的可移植方法是使用 printf 而不是 echo - Jens
15
对于printf,我们需要使用这种格式:printf "#### (50%%)\r",不能使用单引号,并且百分号需要转义。 - nurettin
10
这个“我猜测在未知的硬件上进行这个操作需要多长时间”的技巧,为什么会被接受并赞成得如此之多,我不太理解。在我看来,pv 是正确的答案(但bar也可以)。 - Stephen
27
问题是“如何制作进度条”,并提供了复制文件的示例。我关注的是“图形”问题,而不是计算文件复制操作进展的程度。 - Mitch Haile
显示剩余8条评论

120

您可能对如何在Bash中制作旋转器也感兴趣:链接

我能用Bash做一个旋转器吗?

当然可以!

i=1
sp="/-\|"
echo -n ' '
while true
do
    printf "\b${sp:i++%${#sp}:1}"
done
每次循环迭代时,它会显示sp字符串中的下一个字符,并在到达末尾时进行换行 (i是要显示的当前字符的位置,${#sp}是sp字符串的长度)。
\b字符串将被替换为 'backspace' 字符。或者,你可以使用\r回到行的开头。
如果您想让它减慢速度,请在循环内部(printf之后)放置一个sleep命令。
POSIX等效的命令如下:
sp='/-\|'
printf ' '
while true; do
    printf '\b%.1s' "$sp"
    sp=${sp#?}${sp%???}
done
如果您已经有一个耗费大量时间的循环,您可以在每次迭代的开头调用以下函数来更新旋转器:
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

28
更加简洁的可移植版本: while :;do for s in / - \\ \|; do printf "\r$s";sleep 1;done;done(*注意:sleep函数可能需要整数而不是小数作为参数)。 - Adam Katz
1
@Daenyth。谢谢。请问我们应该在哪里调用命令以便使用先前的代码来监视其进度? - goro
我给你的是一个旋转器,而不是进度条。就像这个答案一样。除非你愿意包装,否则进度条不容易被塞入本地单行代码中。 - Adam Katz
@AdamKatz 很好的提示。顺便问一下,如何停止这个旋转器。我的意思是,我正在等待某些长脚本/命令完成并显示此旋转器。当长命令完成时如何停止它? - kaushal
1
@kaushal - Ctrl+C 可以手动停止它。如果您有一个后台作业,可以存储其 PID(job=$!),然后运行 while kill -0 $job 2>/dev/null;do …,例如:sleep 15 & job=$!; while kill -0 $job 2>/dev/null; do for s in / - \\ \|; do printf "\r$s"; sleep .1; done; done - Adam Katz
显示剩余7条评论

79

我前几天写了一个简单的进度条函数:

#!/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}%%"

}

# Variables
_start=1

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

# Proof of concept
for number in $(seq ${_start} ${_end})
do
    sleep 0.1
    ProgressBar ${number} ${_end}
done
printf '\nFinished!\n'

或者从以下地址下载:
https://github.com/fearside/ProgressBar/


请问您能解释一下1.2.1.1下面的那行代码吗?您是在使用sed替换_fill和_empty变量吗?我有点困惑。 - Chirag
不使用sed,我使用bash内部的“子字符串替换”,因为这是一项简单的工作,我更喜欢使用bash的内部函数来完成这种工作。代码看起来也更美观。 :-) 在这里查看http://www.tldp.org/LDP/abs/html/string-manipulation.html并搜索子字符串替换。 - fearside
并且 ${_fill} 被分配为 ${_done} 个空格。这太棒了,干得好!我肯定会在我的所有 bash 脚本中使用它哈哈。 - Chirag
不错。将短横线改为矩形会使其看起来更专业:printf "\r进度:[${_fill// /▇}${_empty// / }] ${_progress}%%" - Mehdi LAMRANI
你可以避免整个“用填充字符替换空格”的过程,而只需创建一个字符字符串: printf -v _fill“#。0s”$(seq 1 $ done) - Jason Kohles
显示剩余4条评论

60
使用Linux命令pv。如果它在管道的中间,它不知道大小,但它会显示速度和总量,从而您可以计算出所需时间并获得反馈,以确保它没有挂起。

51

我想找一个比选定答案更性感的东西,所以写了自己的脚本。

预览

progress-bar.sh in action

源代码

我把它放在github progress-bar.sh

progress-bar() {
  local duration=${1}


    already_done() { for ((done=0; done<$elapsed; done++)); do printf "▇"; done }
    remaining() { for ((remain=$elapsed; remain<$duration; remain++)); do printf " "; done }
    percentage() { printf "| %s%%" $(( (($elapsed)*100)/($duration)*100/100 )); }
    clean_line() { printf "\r"; }

  for (( elapsed=1; elapsed<=$duration; elapsed++ )); do
      already_done; remaining; percentage
      sleep 1
      clean_line
  done
  clean_line
}

使用方法

 progress-bar 100

5
我不明白这是如何集成到一些处理中的,其中处理的长度未知。如果我的进程提前完成(例如解压文件),应该如何停止进度条? - jan
我认为应该使用 progress-bar 100 - jirarium
2
@faceless 这不在你提供的代码范围内,你需要提供时间并进行倒计时。 - Édouard Lopez
1
@Fusion 是一个 Unicode 字符(U+2587 LOWER SEVEN EIGHTHS BLOCK),在现代 shell 中应该是安全的。在您的环境中尝试一下吧。 - Édouard Lopez
4
@RajeshHatwar,你不能这样做,除非你进行特殊处理。这只是一个简单的计时器,而不是进度条。 - cprn
显示剩余3条评论

51
一些帖子展示了如何显示命令的进度。为了计算它,您需要看到您已经取得了多少进展。在BSD系统中,一些命令(例如dd(1))接受SIGINFO信号,并会报告其进度。在Linux系统中,一些命令将类似地响应SIGUSR1信号。如果有此功能可用,您可以通过_dd将输入导入管道以监视处理的字节数。
或者,您可以使用lsof获取文件读取指针的偏移量,从而计算进度。下面是使用lsof(1)查看wc(1)读取名为blob的大文件的进度的示例。
$ wc -l blob &
[1] 3405769

$ lsof -w -o0 -o -c wc
COMMAND     PID USER   FD   TYPE  DEVICE       OFFSET     NODE NAME
[...]
wc      3405769  dds    3r   REG   254,7 0t2656059392  7733716 blob

我写了一个命令,名为pmonitor,它显示正在处理指定进程或文件的进度。你可以使用它来执行以下操作。
$ pmonitor -c gzip
/home/dds/data/mysql-2015-04-01.sql.gz 58.06%

我的博客上有Linux和FreeBSD shell脚本的早期版本(“在Unix上监控进程进展”)。

这太棒了,我总是忘记通过pv管道传输数据 :-) 我认为我的“stat”命令的工作方式有点不同,我的(Linux)版本的脚本在这里:https://gist.github.com/unhammer/b0ab6a6aa8e1eeaf236b - unhammer
请在您的回答中引用代码的相关部分,如此帮助页面所述:https://stackoverflow.com/help/how-to-answer - cprn
@cpm 我引用了链接的标题。如果您认为需要其他内容,请更具体地说明。 - Diomidis Spinellis
链接到其他网站应该始终是有帮助的,但尽量避免让点击它们成为必要。这个答案虽然在某种程度上有帮助,但除非打开链接,否则并没有提供解决方案。我建议添加lsof -w -o0 -o -c command命令行以及可能对其功能或如何阅读输出进行简要解释。 - cprn

34

我没有看到类似的东西,这里的所有自定义函数似乎都只关注渲染,因此......以下是我非常简单、符合 POSIX 标准的解决方案,包含逐步说明,因为这个问题并不简单。

TL;DR

渲染进度条非常容易,但确定应该渲染多少则不同。以下是如何渲染(动画)进度条的方法 - 您可以将此示例复制并粘贴到文件中运行:

#!/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 - 输出到终端(即stdout
  • echo -n - 输出时不换行
  • echo -e - 在输出时解释特殊字符
  • "\r" - 回车符,用于回到行首

你可以以任何速度渲染任何内容,因此这种方法非常通用,在愚蠢的电影中常用于“黑客”可视化,不开玩笑。

完整答案(从零到实际示例)

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

  1. 还有多少工作要完成
  2. 目前已完成了多少工作

对于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子进程并将其stdout保存到$foo
  • stat - 将文件统计信息打印到stdout
  • stat -c - 打印格式化的值
  • %s - 总大小的格式化方式

对于像文件解压缩等操作,计算源大小略微困难,但仍然与获取未压缩文件的大小一样容易:

#!/bin/sh
src_size=$(gzip -l "$src" | tail -n1 | tr -s ' ' | cut -d' ' -f3)
  • gzip -l - 打印有关zip存档的信息
  • tail -n1 - 从底部处理1行
  • tr -s ' ' - 将多个空格转换为一个(“挤压”它们)
  • cut -d' ' -f3 - 剪切第3个以空格分隔的字段(列)

这是我之前提到的问题的核心。该解决方案越来越不通用。所有有关实际进度的计算都与您试图可视化的域紧密绑定,是单个文件操作、计时器倒计时、目录中文件数量逐渐增加、对多个文件的操作等,因此,它不能被重复使用。唯一可重复使用的部分是进度条呈现。要重用它,您需要将其抽象化并保存在文件中(例如/ usr / lib / progress_bar.sh ),然后定义计算特定于您领域的输入值的函数。这是通用代码的样子(我还使$BAR动态,因为人们正在询问它,其余部分现在应该很清楚):

#!/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 - 以给定的格式打印输出内容的内置命令。
  • 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

显然,你可以将其封装在一个函数中,重写以使用管道流工作,使用 $! 获取分叉的进程 ID,并将其传递给 progress_bar.sh,以便它可以“猜测”如何计算要做的工作和已完成的工作,任何你想要的方式都可以。

附注

我最常被问到的两件事:

  1. ${}:在上面的示例中,我使用了 ${foo:A:B}。这种语法的技术术语是“参数扩展”,它是内置的 shell 功能,允许操作变量(参数),例如使用 : 去掉字符串的空格,但也可以做其他事情 - 它不会生成子 Shell。我能想到的最著名的参数扩展描述(并非完全符合 POSIX 标准,但有助于读者理解概念)在 man bash 页面中。
  2. $():在上面的示例中,我使用了 foo=$(bar)。它会在子进程(即子外壳)中生成一个单独的 Shell,运行其中的 bar 命令,并将其标准输出分配给 $foo 变量。它与 Process Substitution 不同,也与 pipe (|) 完全不同。最重要的是,它可行。有些人认为应该避免使用它,因为它很慢。我认为这在这里是“可以的”,因为无论这段代码试图可视化什么都需要花费足够长的时间才需要一个进度条。换句话说,子 Shell 并不是瓶颈。调用子 Shell 还省去了我解释为什么 return 不是大多数人想象中那样,什么是 Exit Status,以及为什么总体而言从 shell 函数传递值不是 shell 函数擅长的事情。要了解更多信息,我再次强烈推荐阅读 man bash 页面。

故障排除

如果你的 shell 实际上运行的是 sh 而不是 bash,或者非常旧的 bash,例如默认的 osx,则可能会出现 echo -ne "\r${BAR:0:$i}" 的问题。确切的错误是“Bad substitution”。如果发生这种情况,根据评论部分,您可以改用 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

2
对于那些想要最简单的东西的人,我只是用cprn的第一个答案制作了我的进度条。这是一个非常简单的进度条函数,它使用一些愚蠢的比例规则来绘制进度条:https://pastebin.com/9imhRLYX - YCN-
1
如果您使用bash而不是sh,则${BAR:0:$i}可能会导致一些人出现“Bad substitution”的错误。 - NoxFly
你可能是对的。现在,在许多发行版中,sh 链接到 bash 或运行 bash --posix 兼容模式的脚本,我怀疑在我编写和测试这个答案的2016年,我的系统也是如此。如果它对你不起作用,你可以将 ${name:n:l} 替换为 $(expr "x$name" : "x.\{0,$n\}\(.\{0,$l\}\)"),这已经被证明在任何 POSIX shell 中都可以工作(源自 ksh93,也存在于 zshmkshbusyboxsh 中)。尽管如此,我仍然保留原始答案,因为它应该在绝大多数情况下都能正常工作,并且易于阅读。 - cprn

28

高分辨率(浮点数)进度条

前言

对于这个不太短的答案,我将使用整数来呈现浮点数,UTF-8字体以更精细地呈现进度条,并并行化另一个任务(sha1sum)以跟踪其进展,所有这些都使用无forks的最小资源占用。

对于快速鸢尾属植物:请在中间的现在就来!处测试代码(复制/粘贴到新的终端窗口),使用以下任一选项:

  • 要么:最后一个动画演示(接近结尾),
  • 要么:实用样例(在结尾处)。

所有演示都使用read -t <float seconds> && break而不是sleep。因此,通过按下回车键可以很好地停止所有循环。

介绍

又一个Bash进度条...

由于这里已经有了很多答案,我想添加一些关于性能精度的提示。

1. 避免派生进程!

因为进度条旨在在其他进程正在工作时运行,所以这必须是一个良好的过程...

所以在不必要的情况下避免使用forks。例如:可以使用...
mysmiley=$(printf '%b' \\U1F60E)

使用

printf -v mysmiley '%b' \\U1F60E
说明:当你运行var=$(command)时,你启动了一个新的进程来执行command并将其输出发送到变量$var一旦终止。这是非常消耗资源的。请比较:
TIMEFORMAT="%R"
time for ((i=2500;i--;)){ mysmiley=$(printf '%b' \\U1F60E);}
2.292
time for ((i=2500;i--;)){ printf -v mysmiley '%b' \\U1F60E;}
0.017
bc -l <<<'2.292/.017'
134.82352941176470588235

在我的主机上,同样分配$mysmiley的工作(只有2500次),使用fork比使用内置的printf -v要慢约135倍/更昂贵。
然后,
echo $mysmiley 

因此,您的函数不得打印(或输出)任何内容。您的函数必须将其答案归因于一个变量

2. 使用整数作为伪浮点数

这是一个非常小而快速的函数,用于从整数计算百分比,并使用整数并返回一个伪浮点数:

percent(){
    local p=00$(($1*100000/$2))
    printf -v "$3" %.2f ${p::-3}.${p: -3}
}

使用方法:

# percent <integer to compare> <reference integer> <variable name>
percent 33333 50000 testvar
printf '%8s%%\n' "$testvar"
   66.67%

3. 使用 UTF-8 实现高清控制台图形:▏ ▎ ▍ ▌ ▋ ▊ ▉ █

要在 bash 中渲染这些字符,您可以:

printf -v chars '\\U258%X ' {15..8}
printf '%b\n' "$chars"
▏ ▎ ▍ ▌ ▋ ▊ ▉ █ 

或者

printf %b\  \\U258{{f..a},9,8}
▏ ▎ ▍ ▌ ▋ ▊ ▉ █

那么我们必须将字符串宽度的8倍用作图形宽度

现在开始吧!

这个函数被命名为percentBar,因为它可以根据以百分比(浮点数)提交的参数呈现一个条形图:

percentBar ()  { 
    local prct totlen=$((8*$2)) lastchar barstring blankstring;
    printf -v prct %.2f "$1"
    ((prct=10#${prct/.}*totlen/10000, prct%8)) &&
        printf -v lastchar '\\U258%X' $(( 16 - prct%8 )) ||
            lastchar=''
    printf -v barstring '%*s' $((prct/8)) ''
    printf -v barstring '%b' "${barstring// /\\U2588}$lastchar"
    printf -v blankstring '%*s' $(((totlen-prct)/8)) ''
    printf -v "$3" '%s%s' "$barstring" "$blankstring"
}

使用方法:

# percentBar <float percent> <int string width> <variable name>
percentBar 42.42 $COLUMNS bar1
echo "$bar1"
█████████████████████████████████▉                                              

展示细微差别:
percentBar 42.24 $COLUMNS bar2
printf "%s\n" "$bar1" "$bar2"
█████████████████████████████████▉                                              
█████████████████████████████████▊                                              

带颜色

由于渲染的变量是固定宽度的字符串,使用颜色很容易:

percentBar 72.1 24 bar
printf 'Show this: \e[44;33;1m%s\e[0m at %s%%\n' "$bar" 72.1

Bar with color

小动画:

for i in {0..10000..33} 10000;do i=0$i
    printf -v p %0.2f ${i::-2}.${i: -2}
    percentBar $p $((COLUMNS-9)) bar
    printf '\r|%s|%6.2f%%' "$bar" $p
    read -srt .002 _ && break    # console sleep avoiding fork
done

|███████████████████████████████████████████████████████████████████████|100.00%

clear; for i in {0..10000..33} 10000;do i=0$i
     printf -v p %0.2f ${i::-2}.${i: -2}
     percentBar $p $((COLUMNS-7)) bar
     printf '\r\e[47;30m%s\e[0m%6.2f%%' "$bar" $p
     read -srt .002 _ && break
done

PercentBar animation

最新动画演示

另一个演示展示不同尺寸和颜色输出:

printf '\n\n\n\n\n\n\n\n\e[8A\e7'&&for i in {0..9999..99} 10000;do 
    o=1 i=0$i;printf -v p %0.2f ${i::-2}.${i: -2}
    for l in 1 2 3 5 8 13 20 40 $((COLUMNS-7));do
        percentBar $p $l bar$((o++));done
    [ "$p" = "100.00" ] && read -rst .8 _;printf \\e8
    printf '%s\e[48;5;23;38;5;41m%s\e[0m%6.2f%%%b' 'In 1 char width: ' \
        "$bar1" $p ,\\n 'with 2 chars: ' "$bar2" $p ,\\n 'or 3 chars: ' \
        "$bar3" $p ,\\n 'in 5 characters: ' "$bar4" $p ,\\n 'in 8 chars: ' \
        "$bar5" $p .\\n 'There are 13 chars: ' "$bar6" $p ,\\n '20 chars: '\
        "$bar7" $p ,\\n 'then 40 chars' "$bar8" $p \
        ', or full width:\n' '' "$bar9" $p ''
    ((10#$i)) || read -st .5 _; read -st .1 _ && break
done

可以生成类似以下内容:

Last animation percentBar animation

实用的GNU/Linux示例1:带进度条的sleep

2023年2月重写:转化为更有用的displaySleep函数,适用于作为显示的超时读取

这个sleep会显示一个带有50秒刷新的进度条(可调节)。

percent(){ local p=00$(($1*100000/$2));printf -v "$3" %.2f ${p::-3}.${p: -3};}
displaySleep() {
    local -i refrBySeconds=50
    local -i _start=${EPOCHREALTIME/.} reqslp target crtslp crtp cols cpos dlen
    local strng percent prctbar tleft
    [[ $COLUMNS ]] && cols=${COLUMNS} || read -r cols < <(tput cols)
    refrBySeconds=' 1000000 / refrBySeconds '
    printf -v strng %.6f $1
    printf '\E[6n' && IFS=\; read -sdR _ cpos
    dlen=${#strng}-1  cols=' cols - dlen - cpos -1 '
    printf \\e7
    reqslp=10#${strng/.} target=reqslp+_start
    for ((;${EPOCHREALTIME/.}<target;)){
        crtp=${EPOCHREALTIME/.}
        crtslp='( target - crtp ) > refrBySeconds? refrBySeconds: target - crtp'
        strng=00000$crtslp  crtp+=-_start
        printf -v strng %.6f ${strng::-6}.${strng: -6}
        percent $crtp $reqslp percent
        percentBar $percent $cols prctbar
        tleft=00000$((reqslp-crtp))
        printf '\e8\e[36;48;5;23m%s\e[0m%*.4fs' \
               "$prctbar" "$dlen" ${tleft::-6}.${tleft: -6}
        IFS= read -rsn1 -t $strng ${2:-_} && { echo; return;}
    }
    percentBar 100 $cols prctbar
    printf '\e8\e[36;48;5;30m%s\e[0m%*.4fs\n' "$prctbar" "$dlen" 0
    false
}

这将保留当前光标位置,仅填充行的其余部分(如果当前光标位置为1,则填充整行)。这对于显示某种提示可能很有用:

enter image description here

实用的GNU/Linux示例2: 带有进度条的sha1sum

在Linux下,您可以在/proc伪文件系统下找到许多有用的信息,因此使用先前定义的函数percentBarpercent,这里是sha1progress

percent(){ local p=00$(($1*100000/$2));printf -v "$3" %.2f ${p::-3}.${p: -3};}
sha1Progress() { 
    local -i totsize crtpos cols=$(tput cols) sha1in sha1pid
    local sha1res percent prctbar
    exec {sha1in}< <(exec sha1sum -b - <"$1")
    sha1pid=$!
    read -r totsize < <(stat -Lc %s "$1")
    while ! read -ru $sha1in -t .025 sha1res _; do
        read -r _ crtpos < /proc/$sha1pid/fdinfo/0
        percent $crtpos $totsize percent
        percentBar $percent $((cols-8)) prctbar
        printf '\r\e[44;38;5;25m%s\e[0m%6.2f%%' "$prctbar" $percent;

    done
    printf "\r%s  %s\e[K\n" $sha1res "$1"
}

当然,25毫秒的超时意味着大约每秒刷新40次。这看起来可能过度了一些,但它在我的主机上运行良好,而且无论如何,这可以进行调整。

sha1Progress sample

解释:

  • exec {sha1in}< 为输出创建一个新的文件描述符
  • <( ... ) 后台运行分叉的任务
  • sha1sum -b - <"$1" 确保输入来自STDIN (fd/0)
  • while ! read -ru $sha1in -t .025 sha1res _ 当子任务没有读取到输入时,在25毫秒内执行...
  • /proc/$sha1pid/fdinfo/0 内核变量,显示任务$sha1pid文件描述符0 (STDIN)的信息

很好的回答!在第一个动画演示中,我看到了\r会导致光标重置,因此该条重新绘制自己,但在第二个动画演示中,您是如何实现这一点的? - David
1
@David 第1行打印8行,然后使用 Esc[8A 将光标返回8行。接着使用 Esc7 保存光标位置... 之后使用 Esc8 恢复光标位置 - F. Hauri - Give Up GitHub
1
太酷了,真是太令人惊叹了。 - undefined
那很酷,太惊人了。 - david euler

25

APT风格的进度条(不会破坏正常输出)

enter image description here

编辑:查看更新版本请访问我的github页面

我对这个问题的回答并不满意。我想要的是一个像APT一样花哨的进度条。

我查看了APT的C源代码,并决定为bash编写一个相当的等效版本。

这个进度条会很好地停留在终端的底部,不会干扰发送到终端的任何输出。

请注意,该条长度目前固定为100个字符。如果你想将其缩放到终端的大小,这也是相当容易实现的(我的github页面上的更新版本可以很好地处理这个问题)。

我将在此发布我的脚本。使用示例:

source ./progress_bar.sh
echo "This is some output"
setup_scroll_area
sleep 1
echo "This is some output 2"
draw_progress_bar 10
sleep 1
echo "This is some output 3"
draw_progress_bar 50
sleep 1
echo "This is some output 4"
draw_progress_bar 90
sleep 1
echo "This is some output 5"
destroy_scroll_area

脚本(我强烈建议使用我在github上的版本):

#!/bin/bash

# This code was inspired by the open source C code of the APT progress bar
# http://bazaar.launchpad.net/~ubuntu-branches/ubuntu/trusty/apt/trusty/view/head:/apt-pkg/install-progress.cc#L233

#
# Usage:
# Source this script
# setup_scroll_area
# draw_progress_bar 10
# draw_progress_bar 90
# destroy_scroll_area
#


CODE_SAVE_CURSOR="\033[s"
CODE_RESTORE_CURSOR="\033[u"
CODE_CURSOR_IN_SCROLL_AREA="\033[1A"
COLOR_FG="\e[30m"
COLOR_BG="\e[42m"
RESTORE_FG="\e[39m"
RESTORE_BG="\e[49m"

function setup_scroll_area() {
    lines=$(tput lines)
    let lines=$lines-1
    # Scroll down a bit to avoid visual glitch when the screen area shrinks by one row
    echo -en "\n"

    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"
    # Set scroll region (this will place the cursor in the top left)
    echo -en "\033[0;${lines}r"

    # Restore cursor but ensure its inside the scrolling area
    echo -en "$CODE_RESTORE_CURSOR"
    echo -en "$CODE_CURSOR_IN_SCROLL_AREA"

    # Start empty progress bar
    draw_progress_bar 0
}

function destroy_scroll_area() {
    lines=$(tput lines)
    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"
    # Set scroll region (this will place the cursor in the top left)
    echo -en "\033[0;${lines}r"

    # Restore cursor but ensure its inside the scrolling area
    echo -en "$CODE_RESTORE_CURSOR"
    echo -en "$CODE_CURSOR_IN_SCROLL_AREA"

    # We are done so clear the scroll bar
    clear_progress_bar

    # Scroll down a bit to avoid visual glitch when the screen area grows by one row
    echo -en "\n\n"
}

function draw_progress_bar() {
    percentage=$1
    lines=$(tput lines)
    let lines=$lines
    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"

    # Move cursor position to last row
    echo -en "\033[${lines};0f"

    # Clear progress bar
    tput el

    # Draw progress bar
    print_bar_text $percentage

    # Restore cursor position
    echo -en "$CODE_RESTORE_CURSOR"
}

function clear_progress_bar() {
    lines=$(tput lines)
    let lines=$lines
    # Save cursor
    echo -en "$CODE_SAVE_CURSOR"

    # Move cursor position to last row
    echo -en "\033[${lines};0f"

    # clear progress bar
    tput el

    # Restore cursor position
    echo -en "$CODE_RESTORE_CURSOR"
}

function print_bar_text() {
    local percentage=$1

    # Prepare progress bar
    let remainder=100-$percentage
    progress_bar=$(echo -ne "["; echo -en "${COLOR_FG}${COLOR_BG}"; printf_new "#" $percentage; echo -en "${RESTORE_FG}${RESTORE_BG}"; printf_new "." $remainder; echo -ne "]");

    # Print progress bar
    if [ $1 -gt 99 ]
    then
        echo -ne "${progress_bar}"
    else
        echo -ne "${progress_bar}"
    fi
}

printf_new() {
    str=$1
    num=$2
    v=$(printf "%-${num}s" "$str")
    echo -ne "${v// /$str}"
}

完美!正是我所寻找的。 - Sriram Kannan
1
避免使用forks!不要写var=$(printf...),而是写printf -v var ...,也不要写var=$(echo -n ...;printf),而是写printf -v var ...; var=...${var}... - F. Hauri - Give Up GitHub
1
这就是我一直在寻找的好东西。我不想学习如何使用“\r”来重绘一行,我想看看如何在屏幕上绘制一块区域!太棒了! - Ajax

19

GNU tar具有一个实用的选项,可以提供简单进度条的功能。

(...) 另一个可用的检查点操作是“点”(或“.”)。它指示tar在标准列表流上打印一个点,例如:

$ tar -c --checkpoint=1000 --checkpoint-action=dot /var
...

通过以下方式也可以达到相同的效果:

$ tar -c --checkpoint=.1000 /var

1
如果你看不到任何点被打印,请尝试减少数字,例如 --checkpoint=.10。当使用 tar -xz 进行提取时,它也非常有效。最简单的方法加一! - Noam Manos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接