第四章

4-1 tee命令是从标准输入中读取数据,直至文件结尾,随后将数据写入标准输出和命令行参数所指定的文件。请使用I/O系统调用实现tee命令。默认情况下,若已存在与命令行参数指定文件名同名的文件,tee命令将其覆盖。或者,当使用-a命令时,则在同名文件后面追加数据。

我们可以使用tee --help命令来查看帮助。可以看到deepin中所显示的是中文帮助。我们要实现的是 -a指令。

$ tee --help
用法:tee [选项]... [文件]...
将标准输入复制到每个指定文件,并显示到标准输出。

  -a, --append          内容追加到给定的文件而非覆盖
  -i, --ignore-interrupts       忽略中断信号
  -p                        diagnose errors writing to non pipes
      --output-error[=MODE]   set behavior on write error.  See MODE below
      --help            显示此帮助信息并退出
      --version         显示版本信息并退出

这是本书第一次编程,我们先写一个程序来读取一下参数吧,下面这个程序可以将所有参数打印出来。

#include <unistd.h>
#include <stdio.h>

int main(int argc, char *argv[]){
    for(int i=0;i<argc;i++){
        printf("%s", argv[i]);
    }
}

题目中提到了可以使用getopt来获取参数。我们可以使用getopt来获取第一个参数,来判断是不是-a,如果是的话,我们将-a这个标记记到存储信息的结构体中。

#include <unistd.h>
#include <stdio.h>

struct teeConfigStruct
{
    int a;
    char *outputPath;
    int outputFd;
} teeConfig;

int main(int argc, char * const argv[]){
    int opt;
    if ((opt = getopt(argc, argv, "a")) != -1)
    {
        teeConfig.a = 0;
        switch (opt)
        {
        case 'a':
            printf("追加模式\n");
            teeConfig.a = 1;
            break;
        default:
            printf("参数错误\n");
            _exit(0);
            break;
        }
    }
    if (argc - optind < 1)
    {
        printf("参数太少\n");
        _exit(0);
    }
    teeConfig.outputPath = argv[optind];
    printf("输出路径 %s \n", teeConfig.outputPath);
}

运行这段程序,我们发现程序如预期运行。下面演示了四种情况,分别是:正常模式的正常输入,追加模式的正常输入,参数错误,参数太少。

gcc 4-1.c
$ ./a.out def
输出路径 def
$ ./a.out -a def
追加模式
输出路径 def
$ ./a.out -b
./a.out: invalid option -- 'b'
参数错误
$ ./a.out
参数太少

重构代码。

#include <unistd.h>
#include <stdio.h>

struct teeConfigStruct
{
    int a;
    char *outputPath;
    int outputFd;
} teeConfig;

void getArgs(int argc, char *const argv[])
{
    int opt;
    if ((opt = getopt(argc, argv, "a")) != -1)
    {
        teeConfig.a = 0;
        switch (opt)
        {
        case 'a':
            printf("追加模式\n");
            teeConfig.a = 1;
            break;
        default:
            printf("参数错误\n");
            _exit(0);
            break;
        }
    }
    if (argc - optind < 1)
    {
        printf("参数太少\n");
        _exit(0);
    }
    teeConfig.outputPath = argv[optind];
    printf("输出路径 %s \n", teeConfig.outputPath);
}


int main(int argc, char * const argv[]){
    getArgs(argc, argv);
}

然后,我们完成tee的程序的最终功能。程序最重要的部分就是拷贝数据了,首先我们打开标准输入和输出文件,然后,我们使用write和read分别对标准输入和目标文件进行操作。为此,我们需要判断判断-a的标志,如果设置为1,我们在读取输出文件的时候,则加上O_APPEND标签,否则,只是打开它。

另外,如果文件不存在,我们需要自动创建文件,所以我们需要O_CREATE来打开文件。

此外,我们需要截断文件的结尾。在覆盖模式下,文件原来的内容比本次的要长,如果直接结束的话,原来剩余的内容仍然存在。我们需要加上O_TRUNC来帮助结束。

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>

struct teeConfigStruct
{
    int a;
    char *outputPath;
    int outputFd;
} teeConfig;

void getArgs(int argc, char *const argv[])
{
    int opt;
    if ((opt = getopt(argc, argv, "a")) != -1)
    {
        teeConfig.a = 0;
        switch (opt)
        {
        case 'a':
            printf("追加模式\n");
            teeConfig.a = 1;
            break;
        default:
            printf("参数错误\n");
            _exit(0);
            break;
        }
    }
    if (argc - optind < 1)
    {
        printf("参数太少\n");
        _exit(0);
    }
    teeConfig.outputPath = argv[optind];
    printf("输出路径 %s \n", teeConfig.outputPath);
}

int main(int argc, char *const argv[])
{
    getArgs(argc, argv);
    int outputFileFlag;
    if (teeConfig.a == 1)
    {
        outputFileFlag = O_RDWR | O_CREAT | O_APPEND;
    }
    else
    {
        outputFileFlag = O_RDWR | O_CREAT | O_TRUNC;
    }
    teeConfig.outputFd = open(teeConfig.outputPath, outputFileFlag, 0777);
    if (teeConfig.outputFd == -1)
    {
        printf("无法打开输出文件!\n");
        _exit(0);
    }
    char buff;
    while (read(STDIN_FILENO, &buff, 1) > 0)
    {
        printf("%c", buff);
        if (write(teeConfig.outputFd, &buff, 1) == -1)
        {
            printf("无法写入!\n");
            _exit(0);
        }
    }
    if (close(teeConfig.outputFd) == -1)
    {
        printf("无法关闭文件\n");
    }
}

现在我们来测试一下。

$ gcc 4-1.c
$ ./a.out test
在这里输入一些字符,并按下回车换行。按下Ctrl+D来结束程序。
$ cat test
在这里可以看到输出,就是刚刚写入的内容。
$ ./a.out -a test
在这里追加字符。
$ cat test
这里会显示追加后的内容。

经过测试,大体行为和tee表现一致。

4-2 编写一个类似于cp命令的程序,当使用该程序复制一个包含空洞的文件时,能够使得目标文件和源文件内容保持一致,

这个程序思路比较简单,读取源文件的一个字符,如果是0的话,使用lseek右移一个文件偏移量。否则将字符写入目标文件中。

这似乎有一个问题,就是无法真正的区分源文件中的连续0或是真实的连续空洞。但是,系统中的cp命令也是这样实现的,并没有真正的做到复制空洞。

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

struct copyConfigStruct{
    int inputFd;
    int outputFd;
    char * inputPath;
    char * outputPath;
}copyConfig;

int main(int argc, char * const argv[]){
    if(argc <3) {
        printf("参数太少\n");
        _exit(0);
    }
    copyConfig.inputPath = argv[1];
    copyConfig.outputPath = argv[2];
    copyConfig.inputFd = open(copyConfig.inputPath, O_RDONLY);
    if(copyConfig.inputFd == -1){
        printf("输入文件打开失败!\n");
        _exit(0);
    }
    copyConfig.outputFd = open(copyConfig.outputPath, O_RDWR|O_TRUNC|O_CREAT, 0777);
    if(copyConfig.outputFd == -1){
        printf("输出文件打开失败!\n");
        _exit(0);
    }
    char buff;
    while( read(copyConfig.inputFd, &buff, 1) > 0 ){
        if (buff == 0){
            if(lseek(copyConfig.outputFd, 1, SEEK_CUR) == -1){
                printf("跳过空洞失败!\n");
                _exit(0);
            }
        }
        else if(write(copyConfig.outputFd, &buff, 1) == -1){
            printf("写入文件失败!\n");
            _exit(0);
        }
    }
    close(copyConfig.inputFd);
    close(copyConfig.outputFd);
}

接下来进行测试。

$ dd if=/dev/urandom of=testfile bs=4096 seek=999 count=1
这句是创建一个大小为4M,占用空间4K的空洞文件testfile
$ gcc 4-2.c
$ ./a.out testfile output
$ ls -lh
$ du -ah
ls是显示文件大小,而du显示的是占用存储空间大小。可以看到两者文件是一致的。

results matching ""

    No results matching ""