简介
在上一篇博文中,介绍了 Unix/Linux编程实践教程 第一章中的两个标程,这两个标程实现了很简陋(根本没办法用)的more
命令,这一篇博文则是Rivers在自己实现一个勉强可以使用的more
命令的途中的一些感悟。具体的源码在Github上都有了。可以通过查看不同的commit
来看各种功能具体是怎么实现的。也欢迎来找bug。
输入字符,立刻交互
在之前实现的的more
命令中,如果想要输入命令,必须要回车,比如说想要退出,就要先键入q
,再键入回车,真实的more
命令却不是这样的,键入了q
之后会立刻退出,而且可以发现,当键入了q
或空格之后,终端的屏幕上也出现了q
或空格字符,真实的more
命令则不会回显我们的字符。
事实上,这是因为终端默认是处在canonical
和echoing
的模式,只能一行行地读入,并且会自动回显输入,那么,怎么修改这一属性呢?这需要两个函数tcgetattr
和tcsetattr
,从函数名上就可以看出,这两个函数一个用来获取终端属性,另一个用来设置终端属性,接下来要做的就是查看手册,找到合适的设置项了。
经过一番挣扎,有了如下的结果
1 |
|
其中old
用于保存原始的终端状态,以便在程序结束时恢复,&= ~(ICANON | ECHO)
是同时关掉 canonical模式和回显模式,下面设置的c_cc[VMIN|VTIME]
分别是读入字符的个数和时延,时延当然是设置为0,因为需要及时反馈,字符个数设置为1或0都可以,如果大于1,就会导致要输入固定数量个字符才会读入,不是Rivers想要的。
可能还有人会注意到Rivers第一次设置属性的时候使用的是TCSADRAIN
行为模式,恢复的时候却是TCSANOW
模式,其实这两个在这里并没有区别,只不过测试的时候,最终没有统一,虽然Rivers不敢说搞懂了这两个之间的区别,但是Rivers觉得,TCSANOW
就是立刻设置,TCSADRAIN
就是等待之前所有被缓冲的输出成功后再生效,如果有人确切知道,欢迎与Rivers交流。
之前错误的实现
在上面的正确实现之前,Rivers一直被一个bug困扰着,甚至于前往了stackoverflow上提问。
这个bug
的表现是,当我使用命令行参数传入文件名的时候(如more test_file
),一切都没有问题,但是当我使用管道时(如ls /bin | more
),就再也做不到键盘输入,立刻交互和没有回显。
啊呀,这个bug怎么有点熟悉呢?似乎在上一篇博文中,第一个参考实现就是参数文件名可以键盘读入,而管道连接不可以键盘读入,解决的方法是不从stdin
读入,而从/dev/tty
读入,事实上也确实如此,傻傻的Rivers在这里犯了同样的错误,导致bug的代码如下:1
2
3
4
5
6tcgetattr(STDIN_FILENO, &old);
tm = old;
tm.c_lflag &= ~(ICANON | ECHO);
tm.c_cc[VMIN] = 1;
tm.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSADRAIN, &tm);
只设置stdin
的属性,当参数传递文件名时,/dev/tty
就是stdin
,所以没有问题,但是使用管道之后,/dev/tty
和stdin
不同,就出现了设置失效的问题。(果然Rivers就是不能举一反三,太气了呜呜呜)
获得终端的高度
由于more
命令需要一次显示一页内容,所以这一页内容有多少,就是一个必要的信息,也就是终端的高度,终端的高度有多种方式获得
首先是可以使用环境变量LINES
获得,在终端中直接使用echo $LINES
就可以查看终端高度,在C中,结合库函数getenv
和atoi
就可以获得。
但是当Rivers使用这一方法时,却失败了,查阅了网上资料之后,发现LINES
环境变量是默认不传入程序的(当然有时也会传入程序,终端命令env
会显示所有传入程序的环境变量),终端中使用export LINES
就可以传入了,但是这显然不行,因为平时使用more
的时候根本不需要传入LINES
。
于是,又可以使用笨拙一点的办法1
2
3
4
5
6
7
8
9
10// get number of lines
char * lines_str = getenv("LINES");
struct winsize ws;
if (lines_str != NULL) {
num_of_lines = atoi(lines_str) - 1;
} else if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0){
num_of_lines = ws.ws_row - 1;
} else {
num_of_lines = 24;
}
使用ioctl
来获取终端的大小信息。其中-1
是因为最后一行输出反白的More
而不是文件内容。
藏起来的光标和不在上滚的反白More?
在真实使用的more
命令中,终端上是没有光标的,按回车,More?
提示也不会上滚,Rivers实现这两个功能时,使用了Escape Code。
1 | void |
其中实现掩藏光标是直接通过\033[?25l
和\033[?25h
两个Escape Code 实现的,而More?
不在上滚则是通过在see_more()
返回前将More?
一行清空实现的,这并不会有问题,因为只有在等待读入的时候才有必要显示More?
提示符。
文件百分比与多文件支持
文件百分比和多文件支持都要在More?
提示符后加入一条额外的消息,或是百分比,或是Next File FILE_NAME
,那么只需要给see_more
多设置几个参数,来指示下一次输出哪一种消息以及消息的具体内容。
多文件还要在每个文件之前输出其文件名,比如1
2
3
4
5::::::::::::
./src/more.c
::::::::::::
......
这也可以通过给do_more
加参数实现,具体可参考Rivers的Github源码。
后记
在实现more
的时候,Rivers也去翻了翻more
的官方源码,有2000+行,其中考虑了各种各样的情况,包括如果/dev/tty
打不开应该如何等等,有兴趣的朋友可以下载来看,more
在util-linux
包里。