wget
是 GNU 开发的实用下载工具,最近它刚刚发布了 v1.16。进度条样式
先介绍一下背景,
wget
向来就有两种进度条,“点形”和“条形”,其中,“条形”还有可选的跑马灯效果。“刷屏点形”是指这样的效果
0K .......... .......... .......... .......... .......... 0% 107K 8m30s 50K .......... .......... .......... .......... .......... 0% 52.0K 13m0s 100K .......... .......... .......... .......... .......... 0% 31.3K 18m21s 150K .......... .......... .......... .......... .......... 0% 22.0K 24m4s
如果你的终端设备功能有限,不能做到即时更新屏幕内容,或者是重定向到了文件,“点形”进度条就很实用。
“条形”就是指这样的效果
filename 0%[ ] 134.05K 59.7KB/s
很适合可以即时更新的终端。
这两种类型可以使用
wget --progress=dot wget --progress=bar
切换。
跑马灯
现在问题来了。如果文件名称很长,条形进度条左侧那一点点空间显示不下该怎么办呢?
wget
的开发者想出了一个跑马灯效果,很像大街上的 LED 横幅,不停的让文字滚动,这样用户就可以“管窥”整个文件名了。然而,盖子发现一个特别郁闷,而且能逼死强迫症患者的问题。
wget
的滚动有 Bug,文件名的最后一个字符始终不显示!就象这样:this_is_a_ his_is_a_f is_is_a_fi s_is_a_fil _is_a_file is_a_file_ s_a_file_n _a_file_na a_file_nam <-- WTF! this_is_a_
实现
在
progress.c
中,不难发现这段代码if (((orig_filename_cols > MAX_FILENAME_COLS) && !opt.noscroll) && !done) offset_cols = ((int) bp->tick) % (orig_filename_cols - MAX_FILENAME_COLS); else offset_cols = 0; offset_bytes = cols_to_bytes (bp->f_download, offset_cols, cols_ret); bytes_in_filename = cols_to_bytes (bp->f_download + offset_bytes, MAX_FILENAME_COLS, cols_ret); memcpy (p, bp->f_download + offset_bytes, bytes_in_filename); p += bytes_in_filename;
由于有一些字符占用 1 字节,有些占用 2 字节,因此下面部分的代码全都在处理把字符转换成字节数的问题,其实这段代码做的事情很简单
if (orig_filename_cols > MAX_FILENAME_COLS && !opt.noscroll // 没有禁用跑马灯滚动效果 && !done) // 下载仍未结束 offset_cols = bp->tick % (orig_filename_cols - MAX_FILENAME_COLS);
bp->tick
是int
,每刷新一次进度条,它会就自增 1,可以把它理解成进度条刷新的次数,(orig_filename_cols - MAX_FILENAME_COLS)
就不用多说了,显然是计算文件名超出最大允许长度的字符数。最后,从 offset_cols 开始截取 orig_filename_cols,一直截取 MAX_FILENAME_COLS 个字符。
用取余数运算来实现不断截取字符串的特性,看起来还是挺巧妙的。不过,正是这里的代码存在着问题。
范围差 1
写程序的时候,经常因为该
+ 1
/- 1
而忘记了(len()
vs. 下标),不该加减 1 的时候乱加减,导致典型的越界往往和正确范围只差 1 位。再比如这些令人困惑的表述
def range(begin, end) /* 11 日到 21 日间断电 */
到底包不包括这个 end,或者包不包括 21 日?
而
wget
的“最后一个字符始终不显示”的 Bug,具有典型的“范围差 1”的特征,那真正的问题到底出不出在这里呢?实验
为了看看
wget
的这个算法到底有没有 Bug,写个程序检验一下。FILENAME = "this_is_a_file_name" MAX = 10 def cut(string, _min, _max): assert _max <= len(string) - 1 return string[_min:_max] for i in range(0, 20): offset = i % (len(FILENAME) - MAX) print(offset, offset + MAX, cut(FILENAME, offset, offset + MAX))
这就是 Python 版本的简单算法实现了,运行之后:
0 10 this_is_a_ 1 11 his_is_a_f 2 12 is_is_a_fi 3 13 s_is_a_fil 4 14 _is_a_file 5 15 is_a_file_ 6 16 s_a_file_n 7 17 _a_file_na 8 18 a_file_nam <-- WTF! 0 10 this_is_a_
果然出错?那么问题究竟出在哪里呢?拿这个例子分析一下,"this_is_a_file_name" 有 19 个字符,而最大的允许字符是 10 个。那么,那么,相差 9,看来编写者认为 9 就是需要的滚动次数。然而,滚动 9 次,就是有 10 种组合啊!
果然忘记 `+1`,而接下来就不用多说了。就等开发者接受补丁了。
文章来源:https://biergaizi.info/archives/2014/11/1919.html