引言
在 linux 上最常用的命令之一莫过于 cat 命令,用它来快速查看文件内容再合适不过了。
cat 命令也是 coreutils 包中的一个工具,coreutils 提供了一系列工具来操作文件,cat 是其中最基本的工具,相关的还有 tac, head, tail, nl, less, more。
如果想学习文件操作的相关知识,通过这几个工具的深入学习,完全可以学到想要的知识点。接下来会有一系列的文章来逐个分析上述工具,本文作为本系列的一个开篇,讲述 cat 的原理和源代码中的一些亮点。
本文源代码使用 cat (GNU coreutils) 8.21 版本。
原理
cat 命令的基本功能并不复杂,常用的场景是用 cat 将一个文件的内容输出到屏幕上,比如:cat /etc/hosts
会在屏幕上得到该文件的内容。
基本原理是从标准输入读入要 cat 的文件列表,然后逐个打开,读入文件内容,再将内容输出到标准输出上。整个功能并不复杂,甚至C语言的初学者都可以实现出一个可用的版本。
基本功能
命令 cat --help
会得到它具体的命令行参数格式,通过不同的参数组合影响 cat 输出的格式,也可以说所有的命令行参数都是针对输出格式的,可能只有20%的情况下会使用这些参数,但针对这些参数的实现缺占据了几乎 80% 的代码量,二八定律在这里有惊人的准确。
这些参数里,常用的或者会影响到实现复杂度的参数有:
- -E 在输出的每行行尾显示$
- -n 在输出的每行开头显示行号
- -b 只对非空白行计算行号
- -s 忽略连续的空行
- -v 显示不可打印的字符
这里比较复杂的是 -s 参数,因为它涉及到上下文的状态。后面可以看到针对这个参数有大量的代码实现。-v 参数的复杂度略低,它需要实现一个针对非打印字符到可打印字符的关系映射,但这个映射并没有上下文状态,所以还比较简单。
另外为了正确性和运行效率,程序还做了一些考虑,后面逐一会提到。
基本功能实现
如果只是一个最简单的 cat 程序,不考虑任何对输出格式的影响,那么程序实现出来应该是这样的:
- 打开输入文件 infile.
- 从 infile 里读一段数据存在 buffer 里.
- 将 buffer 里的数据写到 STDOUT_FILENO 里
- 重复上述过程直到 infile 的结尾
整个执行模式符合 unix 程序的功能单一的精神,从文件或者 STDIN_FILENO读入数据,向 STDOUT_FILENO 输出数据。
避免输入输出都指向同一个文件
STDIN_FILENO 和 STOUT_FILENO 都是 unix 平台的数据接口,他们通过重定向可以是任何文件。
因此在 cat 程序开头也会查看检查输入和输出文件的 inode 标号,看是否是同一个文件。cat 不允许输入和输出是同一个文件。但是对 STDIN_FILENO 留了例外,所以 cat >&1
竟然是合法的,我并没有想明白这样做的原因。
模拟实现
根据上面的思想,我们模拟实现一段核心代码:
// 课堂教学用的代码,不能用在生产环境。
int too_simple_cat(int infd)
{
char buf[1024 * 128];
size_t buf_size = sizeof(buf);
int n_read = 0;
do {
n_read = read(infd, buf, buf_size);
if (n_read == 0)
return 0;
else if (n_read == -1) {
perror("too_simple_cat() read error");
return -1;
}
else {
if (write(STDOUT_FILENO, buf, n_read) < 0) {
perror