当在bash或*NIX中的任何其他shell中编写脚本时,在运行需要超过几秒钟时间的命令时,需要一个进度条。
例如,复制一个大文件,打开一个大tar文件。
你建议用什么方法向shell脚本添加进度条?
当在bash或*NIX中的任何其他shell中编写脚本时,在运行需要超过几秒钟时间的命令时,需要一个进度条。
例如,复制一个大文件,打开一个大tar文件。
你建议用什么方法向shell脚本添加进度条?
当前回答
APT风格的进度条(不中断正常输出)
编辑:更新版本检查我的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}"
}
其他回答
我今天有同样的事情要做,根据Diomidis的答案,下面是我是如何做到的(linux debian 6.0.7)。 也许,这可以帮助你:
#!/bin/bash
echo "getting script inode"
inode=`ls -i ./script.sh | cut -d" " -f1`
echo $inode
echo "getting the script size"
size=`cat script.sh | wc -c`
echo $size
echo "executing script"
./script.sh &
pid=$!
echo "child pid = $pid"
while true; do
let offset=`lsof -o0 -o -p $pid | grep $inode | awk -F" " '{print $7}' | cut -d"t" -f 2`
let percent=100*$offset/$size
echo -ne " $percent %\r"
done
要制作一个tar进度条
tar xzvf pippo.tgz |xargs -L 19 |xargs -I@ echo -n "."
其中“19”是tar中的文件数除以预期进度条的长度。 例如:.tgz包含140个文件,你想要一个76 "."的进度条,你可以放-L 2。
你什么都不需要了。
没有看到任何类似的东西,这里所有的自定义函数似乎都只关注渲染,所以……下面是我非常简单的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
对我来说,到目前为止最容易使用和最好看的是命令pv或bar,就像某人已经写的那样
例如:需要用dd备份整个驱动器
通常你使用dd if="$input_drive_path" of="$output_file_path"
对于pv,你可以这样做:
如果dd = input_drive_path美元“硒| | dd =“output_file_path美元”
进程直接进入STDOUT,如下所示:
7.46GB 0:33:40 [3.78MB/s] [ <=> ]
做完之后,总结就出来了
15654912+0 records in
15654912+0 records out
8015314944 bytes (8.0 GB) copied, 2020.49 s, 4.0 MB/s
有一次,我也有一个繁忙的脚本,它被占用了几个小时,没有任何进展。所以我实现了一个函数,主要包括前面的回答技巧:
#!/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