Linux指令 - tee的实现

近日学习《Linux/UNIX 系统编程手册》一书,巩固了C语言中一些常用库函数的使用,主要涉及基本IO操作相关的库函数。为了加深理解,手动实现了Linux系统中的tee指令的功能。借此文记述实现过程。

tee功能简述

tee类似于一个单输入双输出的三通管道,将标准输入的数据输出到指定文件和标准输出中。为实现这个指令,主要考虑以下几点:

  1. 解析tee包含的命令行参数
  2. 读取标准输入数据,并将数据写入标准输出和指定文件
  3. 若未指定文件,则仅将数据输出到标准输出

下面逐步分析每个要点的实现方法。

参数解析

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

为解析命令行的可选项,需要用到库函数getoptgetopt_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: 存储可选项的参数值,如果不带参数则为NULL
  • optind: 存储下一个可选参数的索引,每执行一次getopt就加1
  • opterr: 错误提示标志,默认为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;
}

以上代码结合whileswitch,使用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);
        //...

说明:

  1. 如果不带-a,程序默认会以O_TRUNC方式打开文件,表示截断,会以重写的方式覆盖原文件;
  2. 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分别返回ahv三个字符。

但注意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);
}

小结

关于参数解析,说到底就是getoptgetopt_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

参考文档

  1. tee命令
  2. 解析命令行参数--getopt和getopt_long