如何在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个回答

14

使用管道查看(pv)工具,在我的系统上可以使用更简单的方法。

srcdir=$1
outfile=$2


tar -Ocf - $srcdir | pv -i 1 -w 50 -berps `du -bs $srcdir | awk '{print $1}'` | 7za a -si $outfile

12

可能的外观

上传文件

[##################################################] 100% (137921 / 137921 bytes)

等待作业完成

[#########################                         ] 50% (15 / 30 seconds)

实现它的简单函数

您只需将其复制粘贴到您的脚本中即可。它不需要任何其他内容来工作。

PROGRESS_BAR_WIDTH=50  # progress bar length in characters

draw_progress_bar() {
  # Arguments: current value, max value, unit of measurement (optional)
  local __value=$1
  local __max=$2
  local __unit=${3:-""}  # if unit is not supplied, do not display it

  # Calculate percentage
  if (( $__max < 1 )); then __max=1; fi  # anti zero division protection
  local __percentage=$(( 100 - ($__max*100 - $__value*100) / $__max ))

  # Rescale the bar according to the progress bar width
  local __num_bar=$(( $__percentage * $PROGRESS_BAR_WIDTH / 100 ))

  # Draw progress bar
  printf "["
  for b in $(seq 1 $__num_bar); do printf "#"; done
  for s in $(seq 1 $(( $PROGRESS_BAR_WIDTH - $__num_bar ))); do printf " "; done
  printf "] $__percentage%% ($__value / $__max $__unit)\r"
}

使用示例

在这里,我们上传文件并在每次迭代时重新绘制进度条。无论实际执行的任务是什么,只要我们能够获得2个值:最大值和当前值即可。

在下面的示例中,最大值为file_size,当前值由某些函数提供,并称为uploaded_bytes

# Uploading a file
file_size=137921

while true; do
  # Get current value of uploaded bytes
  uploaded_bytes=$(some_function_that_reports_progress)

  # Draw a progress bar
  draw_progress_bar $uploaded_bytes $file_size "bytes"

  # Check if we reached 100%
  if [ $uploaded_bytes == $file_size ]; then break; fi
  sleep 1  # Wait before redrawing
done
# Go to the newline at the end of upload
printf "\n"

简洁明了的函数。非常感谢! - Andreas Kraft
这就是我正在寻找的!非常感谢 :) - wajdi_jurry

11

这样可以让您直观地看到一个命令仍在执行中:

while :;do echo -n .;sleep 1;done &
trap "kill $!" EXIT  #Die with parent if we die prematurely
tar zxf packages.tar.gz; # or any other command here
kill $! && trap " " EXIT #Kill the loop and unset the trap or else the pid might get reassigned and we might end up killing a completely different process

这将创建一个无限循环,后台执行并每秒回显一个“.”。这将在shell中显示.运行tar命令或任何您想要的命令。当该命令完成执行时,杀死在后台运行的最后一个作业 - 即无限循环


在执行期间,是否可能有另一个作业在后台启动并潜在地被杀死而不是进度循环? - Centimane
1
我认为这个想法是你会把它放在一个脚本中,所以这只会捕获该脚本的退出。 - Iguananaut
1
我喜欢这个命令,我在我的文件中使用它。但是我有点不安,因为我真的不明白它是如何工作的。第一行和第三行比较容易理解,但我仍然不确定。我知道这是一个旧答案,但是否有一种方法可以得到针对编程新手的不同解释呢? - Felipe
2
这是唯一正确的答案,其他只是Scripting 101玩具进度条,毫无意义,对于真正的、一次性的、不可追踪的(几乎全部)程序没有任何用处。谢谢。 - bekce
1
@Centimane,你可以在while循环后将PID存储在一个变量中(如 pid=$!),并在陷阱声明中稍后使用该变量。 - jarno
显示剩余4条评论

6

我需要一条进度栏来迭代csv文件中的行。我能够将cprn的代码改编为对我有用的东西:

BAR='##############################'
FILL='------------------------------'
totalLines=$(wc -l $file | awk '{print $1}')  # num. lines in file
barLen=30

# --- iterate over lines in csv file ---
count=0
while IFS=, read -r _ col1 col2 col3; do
    # update progress bar
    count=$(($count + 1))
    percent=$((($count * 100 / $totalLines * 100) / 100))
    i=$(($percent * $barLen / 100))
    echo -ne "\r[${BAR:0:$i}${FILL:$i:barLen}] $count/$totalLines ($percent%)"

    # other stuff
    (...)
done <$file

看起来像这样:

[##----------------------------] 17128/218210 (7%)

谢谢你提供的解决方案!它完全符合我的需求。 - storenth

5
大多数Unix命令不会直接提供您可以使用的反馈。有些命令将在stdout或stderr上输出,您可以使用它们。
例如,对于tar,您可以使用-v开关并将输出导出到一个程序,该程序为读取的每一行更新一个小动画。当tar写出文件列表时,程序可以更新动画。要完成百分比,您必须知道文件数量并计算行数。
据我所知,cp不提供此类输出。要监视cp的进度,您必须监视源和目标文件,并观察目标文件的大小。您可以编写一个使用stat (2)系统调用获取文件大小的小型C程序。这将读取源的大小,然后轮询目标文件并根据迄今为止写入的文件大小更新完成百分比栏。

5

我需要一个进度条,可适用于弹出的气泡消息(notify-send),以代表电视音量水平。最近,我在使用Python编写音乐播放器,电视屏幕大部分时间都是关闭状态。

终端输出示例

test_progress_bar3.gif


Bash脚本

#!/bin/bash

# Show a progress bar at step number $1 (from 0 to 100)


function is_int() { test "$@" -eq "$@" 2> /dev/null; } 

# Parameter 1 must be integer
if ! is_int "$1" ; then
   echo "Not an integer: ${1}"
   exit 1
fi

# Parameter 1 must be >= 0 and <= 100
if [ "$1" -ge 0 ] && [ "$1" -le 100 ]  2>/dev/null
then
    :
else
    echo bad volume: ${1}
    exit 1
fi

# Main function designed for quickly copying to another program 
Main () {

    Bar=""                      # Progress Bar / Volume level
    Len=25                      # Length of Progress Bar / Volume level
    Div=4                       # Divisor into Volume for # of blocks
    Fill="▒"                    # Fill up to $Len
    Arr=( "▉" "▎" "▌" "▊" )     # UTF-8 left blocks: 7/8, 1/4, 1/2, 3/4

    FullBlock=$((${1} / Div))   # Number of full blocks
    PartBlock=$((${1} % Div))   # Size of partial block (array index)

    while [[ $FullBlock -gt 0 ]]; do
        Bar="$Bar${Arr[0]}"     # Add 1 full block into Progress Bar
        (( FullBlock-- ))       # Decrement full blocks counter
    done

    # If remainder zero no partial block, else append character from array
    if [[ $PartBlock -gt 0 ]]; then
        Bar="$Bar${Arr[$PartBlock]}"
    fi

    while [[ "${#Bar}" -lt "$Len" ]]; do
        Bar="$Bar$Fill"         # Pad Progress Bar with fill character
    done

    echo Volume: "$1 $Bar"
    exit 0                      # Remove this line when copying into program
} # Main

Main "$@"

测试bash脚本

使用此脚本可在终端中测试进度条。

#!/bin/bash

# test_progress_bar3

Main () {

    tput civis                              # Turn off cursor
    for ((i=0; i<=100; i++)); do
        CurrLevel=$(./progress_bar3 "$i")   # Generate progress bar 0 to 100
        echo -ne "$CurrLevel"\\r            # Reprint overtop same line
        sleep .04
    done
    echo -e \\n                             # Advance line to keep last progress
    echo "$0 Done"
    tput cnorm                              # Turn cursor back on
} # Main

Main "$@"

简述

本节详细介绍如何使用notify-send快速向桌面发送弹出气泡消息。这是必需的,因为音量级别可能每秒钟变化多次,默认的气泡消息行为是让消息在桌面停留多秒钟。

示例弹出气泡消息

tvpowered.gif

弹出气泡消息bash代码

从上面的脚本中,将main函数复制到名为VolumeBar的现有bash脚本中。复制后的main函数中的exit 0命令被删除。

以下是如何调用它并让Ubuntu的notify-send命令知道我们将会发送弹出气泡消息:

VolumeBar $CurrVolume
# Ask Ubuntu: https://askubuntu.com/a/871207/307523
notify-send --urgency=critical "tvpowered" \
    -h string:x-canonical-private-synchronous:volume \
    --icon=/usr/share/icons/gnome/48x48/devices/audio-speakers.png \
    "Volume: $CurrVolume $Bar"

这是一行新的代码,它告诉 notify-send 立即替换最后一个弹出气泡:

-h string:x-canonical-private-synchronous:volume \

volume 组合了弹出式气泡消息,并将该组中的新消息立即替换掉先前的消息。您可以使用 anything 代替 volume


5

基于Edouard Lopez的工作,我创建了一个进度条,适合任何屏幕大小。快来看看吧。

enter image description here

它也发布在Git Hub上。

#!/bin/bash
#
# Progress bar by Adriano Pinaffo
# Available at https://github.com/adriano-pinaffo/progressbar.sh
# Inspired on work by Edouard Lopez (https://github.com/edouard-lopez/progress-bar.sh)
# Version 1.0
# Date April, 28th 2017

function error {
  echo "Usage: $0 [SECONDS]"
  case $1 in
    1) echo "Pass one argument only"
    exit 1
    ;;
    2) echo "Parameter must be a number"
    exit 2
    ;;
    *) echo "Unknown error"
    exit 999
  esac
}

[[ $# -ne 1 ]] && error 1
[[ $1 =~ ^[0-9]+$ ]] || error 2

duration=${1}
barsize=$((`tput cols` - 7))
unity=$(($barsize / $duration))
increment=$(($barsize%$duration))
skip=$(($duration/($duration-$increment)))
curr_bar=0
prev_bar=
for (( elapsed=1; elapsed<=$duration; elapsed++ ))
do
  # Elapsed
prev_bar=$curr_bar
  let curr_bar+=$unity
  [[ $increment -eq 0 ]] || {  
    [[ $skip -eq 1 ]] &&
      { [[ $(($elapsed%($duration/$increment))) -eq 0 ]] && let curr_bar++; } ||
    { [[ $(($elapsed%$skip)) -ne 0 ]] && let curr_bar++; }
  }
  [[ $elapsed -eq 1 && $increment -eq 1 && $skip -ne 1 ]] && let curr_bar++
  [[ $(($barsize-$curr_bar)) -eq 1 ]] && let curr_bar++
  [[ $curr_bar -lt $barsize ]] || curr_bar=$barsize
  for (( filled=0; filled<=$curr_bar; filled++ )); do
    printf "▇"
  done

  # Remaining
  for (( remain=$curr_bar; remain<$barsize; remain++ )); do
    printf " "
  done

  # Percentage
  printf "| %s%%" $(( ($elapsed*100)/$duration))

  # Return
  sleep 1
  printf "\r"
done
printf "\n"
exit 0

享受


4
许多答案都描述了编写自己的打印命令'\r' + $some_sort_of_progress_msg。 问题在于每秒打印数百个更新会减慢进程速度。
然而,如果您的任何进程产生输出(例如7z a -r newZipFile myFolder将在压缩时输出每个文件名),则存在一种更简单,更快,无痛和可定制的解决方案。
安装Python模块tqdm
$ sudo pip install tqdm
$ # now have fun
$ 7z a -r -bd newZipFile myFolder | tqdm >> /dev/null
$ # if we know the expected total, we can have a bar!
$ 7z a -r -bd newZipFile myFolder | grep -o Compressing | tqdm --total $(find myFolder -type f | wc -l) >> /dev/null

帮助:tqdm -h。一个使用更多选项的示例:

$ find / -name '*.py' -exec cat \{} \; | tqdm --unit loc --unit_scale True | wc -l

作为额外的福利,您还可以使用tqdm在Python代码中包装可迭代对象。 https://github.com/tqdm/tqdm/blob/master/README.rst#module

我认为你的带有“更多选项”的示例不起作用。它似乎通过管道将tqdm STDOUT传递给wc -l。你可能想要转义它。 - cprn
1
@cprn tqdm 会在将其输入从 STDIN 管道传输到 STDOUT 的同时,在 STDERR 上显示进度。 在这种情况下,wc -l 将接收与未包含 tqdm 时相同的输入。 - casper.dcl
啊,现在明白了。谢谢你的解释。 - cprn

4
我的解决方案会显示当前正在解压和写入的tarball的百分比。我在编写2GB根文件系统映像时使用它。这些东西真的需要进度条。我的做法是使用gzip --list获取tarball的总未压缩大小。然后,我计算出需要分成100个部分的阻塞因子。最后,我为每个块打印检查点消息。对于一个2GB的文件,这大约给出了10MB一个块。如果太大了,那么您可以将BLOCKING_FACTOR除以10或100,但是这样更难以以百分比的形式打印漂亮的输出。
假设您正在使用Bash,则可以使用以下shell函数:
untar_progress () 
{ 
  TARBALL=$1
  BLOCKING_FACTOR=$(gzip --list ${TARBALL} |
    perl -MPOSIX -ane '$.==2 && print ceil $F[1]/50688')
  tar --blocking-factor=${BLOCKING_FACTOR} --checkpoint=1 \
    --checkpoint-action='ttyout=Wrote %u%  \r' -zxf ${TARBALL}
}

不错的解决方案,但是当你想压缩一个目录时该怎么做呢? - Samir Sadek

4

首先,bar不是唯一的管道进度条。另一个(可能更为人所知)是pv(pipe viewer)。

其次,可以像下面这样使用bar和pv:

$ bar file1 | wc -l 
$ pv file1 | wc -l

甚至更多:
$ tail -n 100 file1 | bar | wc -l
$ tail -n 100 file1 | pv | wc -l

如果你想在处理作为参数给定的文件时使用 bar 和 pv 命令进行操作,比如复制 file1 到 file2,那么一个有用的技巧是使用 进程替换

$ copy <(bar file1) file2
$ copy <(pv file1) file2

进程替换是一种bash魔法技巧,它创建临时的fifo管道文件/dev/fd/,并通过此管道连接运行进程(在括号内)的标准输出,并将其复制,就像一个普通文件一样(唯一的例外是它只能向前读取)。
更新:
bar命令本身也允许复制。根据bar命令手册:
bar --in-file /dev/rmt/1cbn --out-file \
     tape-restore.tar --size 2.4g --buffer-size 64k

但是在我看来,进程替代是更通用的方法。它使用cp程序本身。


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