当在bash或*NIX中的任何其他shell中编写脚本时,在运行需要超过几秒钟时间的命令时,需要一个进度条。
例如,复制一个大文件,打开一个大tar文件。
你建议用什么方法向shell脚本添加进度条?
当在bash或*NIX中的任何其他shell中编写脚本时,在运行需要超过几秒钟时间的命令时,需要一个进度条。
例如,复制一个大文件,打开一个大tar文件。
你建议用什么方法向shell脚本添加进度条?
当前回答
我使用了一个答案,从创建重复字符字符串在shell脚本字符重复。我有两个相对较小的bash版本,用于需要显示进度条的脚本(例如,一个遍历许多文件的循环,但对大的tar文件或复制操作没有用处)。较快的一个由两个函数组成,一个是为条形显示准备字符串:
preparebar() {
# $1 - bar length
# $2 - bar char
barlen=$1
barspaces=$(printf "%*s" "$1")
barchars=$(printf "%*s" "$1" | tr ' ' "$2")
}
一个用来显示进度条:
progressbar() {
# $1 - number (-1 for clearing the bar)
# $2 - max number
if [ $1 -eq -1 ]; then
printf "\r $barspaces\r"
else
barch=$(($1*barlen/$2))
barsp=$((barlen-barch))
printf "\r[%.${barch}s%.${barsp}s]\r" "$barchars" "$barspaces"
fi
}
它可以被用作:
preparebar 50 "#"
这意味着为bar准备50个“#”字符的字符串,在那之后:
progressbar 35 80
将显示“#”字符的数量,对应35/80的比例:
[##################### ]
请注意,该函数在同一行上反复显示条,直到您(或其他程序)打印换行符。如果你把-1作为第一个参数,条形图将被删除:
progressbar -1 80
较慢的版本都在一个函数中:
progressbar() {
# $1 - number
# $2 - max number
# $3 - number of '#' characters
if [ $1 -eq -1 ]; then
printf "\r %*s\r" "$3"
else
i=$(($1*$3/$2))
j=$(($3-i))
printf "\r[%*s" "$i" | tr ' ' '#'
printf "%*s]\r" "$j"
fi
}
它可以被用作(和上面的例子一样):
progressbar 35 80 50
如果你需要stderr上的进度条,只需在每个printf命令的末尾添加>&2。
其他回答
大多数unix命令不会为您提供可以执行此操作的直接反馈。 有些将在您可以使用的stdout或stderr上提供输出。
对于tar之类的东西,您可以使用-v开关并将输出管道到一个程序,该程序为读取的每一行更新一个小动画。当tar写出一个文件列表时,程序可以更新动画。要计算完成百分比,您必须知道文件的数量并计算行数。
据我所知,cp没有给出这种输出。要监视cp的进程,必须监视源文件和目标文件,并监视目标文件的大小。你可以写一个小的c程序,使用stat(2)系统调用来获取文件大小。这将读取源文件的大小,然后轮询目标文件,并根据到目前为止写入的文件的大小更新% complete条。
这只适用于使用地精天顶。Zenity为bash脚本提供了一个很棒的本机界面: https://help.gnome.org/users/zenity/stable/
来自Zenity进度条示例:
#!/bin/sh
(
echo "10" ; sleep 1
echo "# Updating mail logs" ; sleep 1
echo "20" ; sleep 1
echo "# Resetting cron jobs" ; sleep 1
echo "50" ; sleep 1
echo "This line will just be ignored" ; sleep 1
echo "75" ; sleep 1
echo "# Rebooting system" ; sleep 1
echo "100" ; sleep 1
) |
zenity --progress \
--title="Update System Logs" \
--text="Scanning mail logs..." \
--percentage=0
if [ "$?" = -1 ] ; then
zenity --error \
--text="Update canceled."
fi
我为嵌入式系统做了一个纯shell版本,利用了:
/usr/bin/dd的SIGUSR1信号处理特性。 基本上,如果您发送'kill SIGUSR1 $(pid_of_running_dd_process)',它将输出 吞吐量速度和传输量的摘要。 后台dd,然后定期查询它的更新,并生成 像老式的FTP客户端一样。 使用/dev/stdout作为非stdout友好程序(如scp)的目的地
最终的结果允许你进行任何文件传输操作,并获得进度更新,看起来像老式的FTP“哈希”输出,在那里你只需要为每个X字节获得一个哈希标记。
这几乎不是产品质量代码,但您可以理解。我觉得很可爱。
不管怎样,实际的字节计数可能不会正确地反映在哈希数中——根据舍入问题,可能会多一个或少一个。不要将它用作测试脚本的一部分,它只是花瓶。而且,是的,我知道这是非常低效的——这是一个shell脚本,我不为此道歉。
最后提供了使用wget、scp和tftp的示例。它应该与任何发出数据的东西一起工作。确保对标准输出不友好的程序使用/dev/stdout。
#!/bin/sh
#
# Copyright (C) Nathan Ramella (nar+progress-script@remix.net) 2010
# LGPLv2 license
# If you use this, send me an email to say thanks and let me know what your product
# is so I can tell all my friends I'm a big man on the internet!
progress_filter() {
local START=$(date +"%s")
local SIZE=1
local DURATION=1
local BLKSZ=51200
local TMPFILE=/tmp/tmpfile
local PROGRESS=/tmp/tftp.progress
local BYTES_LAST_CYCLE=0
local BYTES_THIS_CYCLE=0
rm -f ${PROGRESS}
dd bs=$BLKSZ of=${TMPFILE} 2>&1 \
| grep --line-buffered -E '[[:digit:]]* bytes' \
| awk '{ print $1 }' >> ${PROGRESS} &
# Loop while the 'dd' exists. It would be 'more better' if we
# actually looked for the specific child ID of the running
# process by identifying which child process it was. If someone
# else is running dd, it will mess things up.
# My PID handling is dumb, it assumes you only have one running dd on
# the system, this should be fixed to just get the PID of the child
# process from the shell.
while [ $(pidof dd) -gt 1 ]; do
# PROTIP: You can sleep partial seconds (at least on linux)
sleep .5
# Force dd to update us on it's progress (which gets
# redirected to $PROGRESS file.
#
# dumb pid handling again
pkill -USR1 dd
local BYTES_THIS_CYCLE=$(tail -1 $PROGRESS)
local XFER_BLKS=$(((BYTES_THIS_CYCLE-BYTES_LAST_CYCLE)/BLKSZ))
# Don't print anything unless we've got 1 block or more.
# This allows for stdin/stderr interactions to occur
# without printing a hash erroneously.
# Also makes it possible for you to background 'scp',
# but still use the /dev/stdout trick _even_ if scp
# (inevitably) asks for a password.
#
# Fancy!
if [ $XFER_BLKS -gt 0 ]; then
printf "#%0.s" $(seq 0 $XFER_BLKS)
BYTES_LAST_CYCLE=$BYTES_THIS_CYCLE
fi
done
local SIZE=$(stat -c"%s" $TMPFILE)
local NOW=$(date +"%s")
if [ $NOW -eq 0 ]; then
NOW=1
fi
local DURATION=$(($NOW-$START))
local BYTES_PER_SECOND=$(( SIZE / DURATION ))
local KBPS=$((SIZE/DURATION/1024))
local MD5=$(md5sum $TMPFILE | awk '{ print $1 }')
# This function prints out ugly stuff suitable for eval()
# rather than a pretty string. This makes it a bit more
# flexible if you have a custom format (or dare I say, locale?)
printf "\nDURATION=%d\nBYTES=%d\nKBPS=%f\nMD5=%s\n" \
$DURATION \
$SIZE \
$KBPS \
$MD5
}
例子:
echo "wget"
wget -q -O /dev/stdout http://www.blah.com/somefile.zip | progress_filter
echo "tftp"
tftp -l /dev/stdout -g -r something/firmware.bin 192.168.1.1 | progress_filter
echo "scp"
scp user@192.168.1.1:~/myfile.tar /dev/stdout | progress_filter
您可以通过重写一行来实现这一点。使用\r返回到行首,而不向终端写入\n。
当你完成时,写\n来推进行。
使用echo -ne:
不打印\n和 识别像\r这样的转义序列。
下面是一个演示:
echo -ne '##### (33%)\r'
sleep 1
echo -ne '############# (66%)\r'
sleep 1
echo -ne '####################### (100%)\r'
echo -ne '\n'
在下面的评论中,puk提到,如果您以长行开始,然后想要编写短行,那么这种方法“失败”:在这种情况下,您将需要覆盖长行的长度(例如,使用空格)。
首先,杆并不是唯一的管道进度仪表。另一个(可能更广为人知)是pv(管道查看器)。
其次,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来处理参数中给出的文件,比如copy file1 file2,一个有用的技巧是使用进程替换:
$ copy <(bar file1) file2
$ copy <(pv file1) file2
进程替换是bash的一个神奇的东西,它创建临时fifo管道文件/dev/fd/,并通过该管道连接运行进程(括号内)的stdout,复制看到它就像一个普通文件一样(只有一个例外,它只能向前读取)。
更新:
Bar命令本身也允许复制。男子酒吧后:
bar --in-file /dev/rmt/1cbn --out-file \
tape-restore.tar --size 2.4g --buffer-size 64k
但是在我看来,过程替换是更通用的方法。它本身使用cp程序。