Linux指令 - tee的实现
近日学习《Linux/UNIX 系统编程手册》一书,巩固了C语言中一些常用库函数的使用,主要涉及基本IO操作相关的库函数。为了加深理解,手动实现了Linux系统中的tee
指令的功能。借此文记述实现过程。
tee功能简述
tee
类似于一个单输入双输出的三通管道,将标准输入的数据输出到指定文件和标准输出中。为实现这个指令,主要考虑以下几点:
- 解析
tee
包含的命令行参数 - 读取标准输入数据,并将数据写入标准输出和指定文件
- 若未指定文件,则仅将数据输出到标准输出
下面逐步分析每个要点的实现方法。
参数解析
tee
包含以下可选项,本文实现仅考虑-a
,--version
,--help
Usage: tee [OPTION]... [FILE]...
Copy standard input to each FILE, and also to standard output.
-a, --append append to the given FILEs, do not overwrite
-i, --ignore-interrupts ignore interrupt signals
-p diagnose errors writing to non pipes
--output-error[=MODE] set behavior on write error. See MODE below
--help display this help and exit
--version output version information and exit
为解析命令行的可选项,需要用到库函数getopt
或getopt_long
,前者仅支持短格式,后者支持长短格式。下面对使用这两个函数解析参数的方法进行对比。
getopt
getopt
函数声明及相关参数如下:
#include <unistd.h>
int getopt(int argc, char * const argv[],
const char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;
argc
: 与main函数的argc一致,代表参数个数argv
: 与main函数的argv一致,代表参数值optstring
: 可选项字符串,如a:bc:d:
,参数后带冒号代表该选项需要给定参数值optarg
: 存储可选项的参数值,如果不带参数则为NULLoptind
: 存储下一个可选参数的索引,每执行一次getopt
就加1opterr
: 错误提示标志,默认为1,当输入参数无效时,会给出提示optopt
: 是对可选参数字符的一个备份,当输入的参数无效时可用
下面使用getopt
实现对选项-a
的解析:
int main(int argc, char *argv[])
{
int opt, fd = -1;
int flag_append = 0;
int flags = O_WRONLY | O_CREAT;
while((opt = getopt(argc, argv, "a"))!=-1) {
switch(opt){
case 'a':
flag_append = 1;
break;
case '?':
default:
exit(EXIT_FAILURE);
break;
}
}
if(optind < argc){
flags += flag_append ? O_APPEND:O_TRUNC;
fd = open(argv[optind], flags, S_IRUSR | S_IWUSR
| S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if(fd == -1){
printf("invaild file -- \"%s\"\n", argv[optind]);
exit(EXIT_FAILURE);
}
}
output(fd);
if(fd > 0)
close(fd);
return 0;
}
以上代码结合while
、 switch
,使用getopt
循环获取和分析可选项。当前只对一个可选项a
进行了解析,当包含-a
可选项时,flag_append设为1,之后根据该标志为文件的打开方式添加O_APPEND
标志,后面读写数据时就会以附加的方式在文件尾部开始写入。
if(optind < argc){
flags += flag_append ? O_APPEND:O_TRUNC;
fd = open(argv[optind], flags, S_IRUSR | S_IWUSR
| S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
//...
说明:
- 如果不带
-a
,程序默认会以O_TRUNC
方式打开文件,表示截断,会以重写的方式覆盖原文件; optind
小于argc
时,说明除了可选参数之外,用户还输入了其它参数,这里对应的是tee
指令所需的文件名称。此时argv[optind]刚好对应第一个非可选参数。
getopt_long
getopt_long
定义如下:
#include <getopt.h>
int getopt_long(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);
与getopt
相比,多了两个参数longopts
, longindex
longopts
: 长格式可选项,option
结构体指针
struct option {
const char *name; // 名称,如"help"
int has_arg; // 带参标志,0 或 1
int *flag; // 常设为NULL,若非NULL,将会把val值存入flag
int val; // getopt_long的返回值或将存入flag的值
};
当flag
为NULL时,getopt_long
返回val
,否则返回0,并将val
值存入flag
longindex
: 用于存储当前解析的长选项在longopts
中的索引值(0,1,...),通常设为NULL
下面使用getopt_long
实现对选项--help
,--version
, -a
的解析:
int main(int argc, char *argv[])
{
int opt, fd = -1;
int flag_append = 0;
int flags = O_WRONLY | O_CREAT;
struct option opts[] = {
{"append", 0, NULL, 'a'},
{"help", 0, NULL, 'h'},
{"version", 0, NULL, 'v'}
};
while((opt = getopt_long(argc, argv, ":av",opts, NULL))!=-1) {
switch(opt){
case 'a':
flag_append = 1;
break;
case 'h':
usage();
break;
case 'v':
printf(VERSION"\n");
exit(EXIT_FAILURE);
break;
case '?':
printf("tee: invaild option -- '%c'\n"
"Try 'tee --help' for more infomation.\n", optopt);
default:
exit(EXIT_FAILURE);
break;
}
}
if(optind < argc){
flags += flag_append ? O_APPEND:O_TRUNC;
fd = open(argv[optind], flags, S_IRUSR | S_IWUSR
| S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if(fd == -1){
printf("invaild file -- \"%s\"\n", argv[optind]);
exit(EXIT_FAILURE);
}
}
output(fd);
if(fd > 0)
close(fd);
return 0;
}
以上代码中,长格式选项数组如下:
struct option opts[] = {
{"append", 0, NULL, 'a'},
{"help", 0, NULL, 'h'},
{"version", 0, NULL, 'v'}
};
可以看到,当用户分别输入--append
,--help
,--version
时,getopt_long
分别返回a
,h
,v
三个字符。
但注意while((opt = getopt_long(argc, argv, ":av",opts, NULL))!=-1)
只包含了av
两个参数,所以当用户输入-h
时,程序会认为是无效参数,也就是说--help
在这里只支持长格式,而其它两个支持长短两种方式。
这也是指令的某些选项仅支持长格式的实现方法之一了,其它方法可参考函数getopt_long_only
说明:
:av
最前面的冒号可以起到opterr=0
的效果,就是在参数无效时不给出默认提示
usage
getopt_long提到的--help
选项是绝大多数指令都会实现的,用于提供帮助信息,下面是tee
的--help
输出。
void usage(){
printf("Usage: tee [OPTION]... [FILE]\n"
"Copy standard input to each FILE, and also to standard output.\n\n"
" -a, --append append to the given FILEs, do not overwrite\n"
" -v, --version output version information and exit\n"
" --help display this help and exit\n"
);
exit(EXIT_FAILURE);
}
小结
关于参数解析,说到底就是getopt
或getopt_long
的应用。以上提到的可选参数中,其实也就-a
会影响后续写入文件的方式,其它两个长格式选项均用于打印信息,之后便直接退出了。
而非可选项也只考虑了一个待写入文件的文件名,暂不考虑同时多文件写入。
数据读写
tee
数据读写很简单,仅需不断读取标准输入(stdin)数据,然后写入标准输出(stdout)和文件中,直到无数据可读或遇到中断信号为止。
#define BUF_SIZE 512
void output(int fd)
{
int i = 0;
char buffer[BUF_SIZE] = {0};
char ch;
fflush(stdin);
fflush(stdout);
while(read(STDIN_FILENO, &ch, 1) > 0){ // read from stdin
buffer[i++] = ch;
if(ch = '\n' || i == BUF_SIZE){
write(STDOUT_FILENO, buffer, i); // output to stdout
if(fd > 0) write(fd, buffer, i);
memset(buffer, 0, sizeof(buffer));
i = 0;
}
}
}
STDIN_FILENO
, STDOUT_FILENO
分别对应标准输入和标准输出的文件描述符0
, 1
。这两个加上标准错误输出STDERR_FILENO
是所有应用程序默认打开的,所以无需手动open
。
此外,输出函数output
会判断传入的文件描述符是否有效,如果无效则不会写入文件,仅将数据输出至标准输出。
完整代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <getopt.h>
#define BUF_SIZE 512
#define VERSION "litreily 1.0.0"
void output(int fd)
{
int i = 0;
char buffer[BUF_SIZE] = {0};
char ch;
fflush(stdin);
fflush(stdout);
while(read(STDIN_FILENO, &ch, 1) > 0){
buffer[i++] = ch;
if(ch = '\n' || i == BUF_SIZE){
write(STDOUT_FILENO, buffer, i); // output to stdout
if(fd > 0) write(fd, buffer, i);
memset(buffer, 0, sizeof(buffer));
i = 0;
}
}
}
void usage(){
printf("Usage: tee [OPTION]... [FILE]\n"
"Copy standard input to each FILE, and also to standard output.\n\n"
" -a, --append append to the given FILEs, do not overwrite\n"
" -v, --version output version information and exit\n"
" --help display this help and exit\n"
);
exit(EXIT_FAILURE);
}
int main(int argc, char *argv[])
{
int opt, fd = -1;
int flag_append = 0;
int flags = O_WRONLY | O_CREAT;
struct option opts[] = {
{"append", 0, NULL, 'a'},
{"help", 0, NULL, 'h'},
{"version", 0, NULL, 'v'}
};
while((opt = getopt_long(argc, argv, ":av",opts, NULL))!=-1) {
switch(opt){
case 'a':
flag_append = 1;
break;
case 'h':
usage();
break;
case 'v':
printf(VERSION"\n");
exit(EXIT_FAILURE);
break;
case '?':
printf("tee: invaild option -- '%c'\n"
"Try 'tee --help' for more infomation.\n", optopt);
default:
exit(EXIT_FAILURE);
break;
}
}
if(optind < argc){
flags += flag_append ? O_APPEND:O_TRUNC;
fd = open(argv[optind], flags, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if(fd == -1){
printf("invaild file -- \"%s\"\n", argv[optind]);
exit(EXIT_FAILURE);
}
}
output(fd);
if(fd > 0)
close(fd);
return 0;
}
指令测试
使用gcc
完成编译,得到tee
可执行文件
gcc tee.c -o tee
下面对指令进行测试:
$ ./tee --help
Usage: tee [OPTION]... [FILE]
Copy standard input to each FILE, and also to standard output.
-a, --append append to the given FILEs, do not overwrite
-v, --version output version information and exit
--help display this help and exit
$ ./tee --version
litreily 1.0.0
$ ./tee -h
tee: invaild option -- 'h'
Try 'tee --help' for more infomation.
$ ./tee -v
litreily 1.0.0
$ ./tee -a -d
tee: invaild option -- 'd'
Try 'tee --help' for more infomation.
$ ./tee test.txt
1
1
22
22
$ cat test.txt
1
22
$ ./tee -a test.txt
4444
4444
55555
55555
$ cat test.txt
1
22
4444
55555
$ ls | ./tee test.txt
main.c
Makefile
README.md
tee
tee.c
test.txt
$ cat test.txt
main.c
Makefile
README.md
tee
tee.c
test.txt
参考文档
版权声明:本博客所有文章除特殊声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明出处 litreily的博客!