【期末复习笔记】在Linux C环境下实现一些简单的Shell命令

288 阅读10分钟

本部分内容的考试形式:对手撕完整代码基本不作要求,主要通过代码填空或补全代码的题型进行考察。

1. cp命令的简单实现

这里我们仅实现一个简单的cp命令,用于将文件file1复制成文件file2(其中file1和file2可包含文件相对于当前目录的相对路径): cp file1 file2

1.1 利用C标准库实现

在《高级语言程序设计》这门课中,我们已经接触过C语言标准库自带的文件操作函数。我们可以先尝试使用它们来编写代码。

代码如下:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {
    
    // 如果输入参数不合法,则直接输出报错信息并退出程序
    if (argc != 3) {
        printf("Usage: %s file_source file_target\n", argv[0]);
        return 1;
    }
    
    const char* fileName_src = argv[1];
    const char* fileName_dest = argv[2]; 
    
    // 调用fopen函数访问源文件
    // 参数"rb+"表示以读写方式访问(二进制)文件
    // 如果要访问的文件不存在,则fopen函数会返回一个NULL指针
    FILE* file_src = fopen(fileName_src, "rb+");
    if (!file_src) {
        printf("Error: Fail to open file %s\n", fileName_src);
        return 1;
    }
    
    // 访问目标文件
    // 参数"wb+"表示以读写方式方式访问(二进制)文件
    // 与参数"rb+"不同,当要访问的文件不存在时,此时fopen函数会创建一个新文件
    // 当要访问的文件存在时,fopen函数会清空其原有内容,重新从文件开头开始写入
    // 当创建或清空文件失败时,fopen函数会返回NULL指针
    FILE* file_dest = fopen(fileName_dest,  "wb+");
    if (!file_dest) {
        printf("Error: Fail to open file %s\n", fileName_dest);
        return 1;
    }
    
    int buffer_size = 4096;
    unsigned char buffer[buffer_size];  // 定义一个4096字节(4KB)大小的临时缓存区
    int bytes_read;
    
    // 使用fread函数从源文件中读取数据,该函数的用法如下:
    // fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    //      ptr表示读取所得数据的储存地址,此处为我们刚才定义的缓存区buffer
    //      size表示读取的每个数据元素的大小,这里我们取1个字节
    //      nmemb表示要读取多少个数据元素,这里我们一次读取4096个字节将缓存区填满
    //      stream表示要读取文件对应的指针
    // fread返回实际读取到的数据元素数量,这里用其来判断成功读取到了数据
    while ((bytes_read = fread(buffer, 1, buffer_size, file_src)) > 0) {
        // 使用fwrite函数将所得数据写入目标文件,其用法与fread类似:
        // fwrite(const void *buffer, size_t  size,  size_t count , FILE *stream)
        fwrite(buffer, 1, bytes_read, file_dest);
    }
    
    // fclose函数的功能:
    // 刷新I/O缓冲区,以将文件操作过程中尚未写入硬盘的数据刷新到硬盘上
    // 并释放FILE指针指向的内存
    fclose(file_src);
    fclose(file_dest);
    
    return 0;
}

代码执行效果如下:

实现cp命令

1.2 利用Linux平台的接口实现

我们也可以使用使用Linux系统自带的unistd库中的相关函数来完成实现。

代码如下:

#include <fcntl.h>   // 文件控制头文件
#include <unistd.h>  // POSIX API头文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc, char* argv[]) {

    if (argc != 3) {
        printf("Usage: %s file_source file_target\n", argv[0]);
        return 1;
    }
    
    const char* fileName_src = argv[1];
    const char* fileName_dest = argv[2]; 
    
    // open函数执行成功时,返回当前访问文件的一个id号(文件描述符)
    // 执行失败时,返回一个负数
    int file_src = open(fileName_src, O_RDONLY);
    if (file_src < 0) {
        printf("Error: Fail to open file %s\n", fileName_src);
        return 1;
    }
    
    mode_t file_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
    int file_dest = open(fileName_dest, O_WRONLY | O_CREAT | O_TRUNC, file_mode);
    if (file_dest < 0) {
        printf("Error: Fail to open file %s\n", fileName_dest);
        return 1;
    }
    
    int buffer_size = 4096;
    unsigned char buffer[buffer_size];
    int bytes_read;

    while ((bytes_read = read(file_src, buffer, buffer_size)) > 0) {
        write(file_dest, buffer, bytes_read);
    }
    
    close(file_src);
    close(file_dest);
    
    return 0;
}

我们注意到这段程序大体上与前面我们用C标准库进行实现的程序是相似的。这里我们主要分析一下这部分代码:

    mode_t file_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
    int file_dest = open(fileName_dest, O_WRONLY | O_CREAT | O_TRUNC, file_mode);

O_WRONLY | O_CREAT | O_TRUNC利用按位或运算符将三种访问目标文件的方式组合起来,表示当文件存在时,以可写模式打开它(O_WRONLY),将其清空(O_TRUNC);当文件不存在时,创建它(O_CREAT)。

当利用open函数创建新文件时,需要根据第三个参数file_mode对新建的文件的权限进行设置。在这段代码中,file_mode表示的是文件拥有者具有读写权限(S_IRUSR、S_IWUSR),当前组的用户及其他用户具有读的权限(S_IRGRP、S_IROTH)。

2. cat命令的简单实现

我们同样仅实现一个简单的cat命令,用于将文本文件的内容输出到终端:cat filename

利用Linux平台接口实现的代码如下:

#include <fcntl.h>   // 文件控制头文件
#include <unistd.h>  // POSIX API头文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc, char* argv[]) {

    if (argc != 2) {
        printf("Usage: %s filename\n", argv[0]);
        return 1;
    }
    
    const char* fileName = argv[1];
    
    // 访问目标文件
    int file = open(fileName, O_RDONLY);
    if (file < 0) {
        printf("Error: Fail to open file %s", fileName);
        return 1;
    }
    
    int buffer_size = 4096;
    unsigned char buffer[buffer_size];
    int bytes_read;

    while ((bytes_read = read(file, buffer, buffer_size)) > 0) {
        // 将读取到的内容通过标准输出打印到终端上
        // 这里不要混淆stdout和STDOUT_FILENO:
        // 前者为标准输出对应的FILE*指针,后者为标准输出对应文件描述符
        write(STDOUT_FILENO, buffer, bytes_read);
    }
    putchar('\n');

    return 0;
}

执行效果如下:

mycat

这里再补充一点东西。在调用fopen/open函数出错的时候(例如文件不存在、没有访问权限等等),如果我们引入了errno.h库,open函数将会将相应的错误代码赋值给一个叫errno的全局变量,我们再通过string.h库中的strerror函数就可以很方便地查询到当前错误代码对应的文本错误信息。

下面是我在网上找的Windows平台下的演示截图,在Linux平台中这是类似的。

b3cf2ae3029f42ed8a8e4e7d0b41dcb5.png

905e3c5c1c5240c789d95620a330bd43.png

3. pwd命令的简单实现

该命令的实现需要借助对类Unix平台文件系统inode机制的理解。

具体如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <stdlib.h>

// getInode函数用于查询文件(目录)的inode编号
// 该函数对Linux平台的stat系统函数进行封装,实现对文件信息的查询
ino_t getInode(char fname[]) {
    struct stat info;
    if (stat(fname, &info) < 0) {
        printf("Error: Fail to stat\n");
        exit(1);
    }
    return info.st_ino;    
}

// getNameByInode函数根据目录的inode编号查询目录的名称
void getNameByInode(ino_t inode, char* name) {
    
    // 调用chdir函数将程序工作目录切换到父级目录
    chdir("..");
    
    // 调用opendir系统函数访问当前目录(即要查询目录的父目录)
    // 该函数返回一个指向访问目录信息的结构体指针dir_ptr
    DIR* dir_ptr = opendir(".");
    if (dir_ptr == NULL) {
        printf("Error: Fail to opendir\n");
        exit(1);
    }
    
    // 调用readdir函数对当前目录下所有条目(包括文件和子目录)进行遍历
    // 每次调用该函数,都会返回指向当前条目结构体的指针,结构体中包含inode和文件名等信息
    // 当所有条目遍历完成后,readdir函数会返回NULL,表示遍历完成
    struct dirent* direntp;
    while ((direntp = readdir(dir_ptr)) != NULL) {
        // 找到目标目录,则记录下其名称并退出函数
        if (direntp->d_ino == inode) {
            strcpy(name, direntp->d_name);
            // 调用chdir将程序工作目录重新切回目标目录
            chdir(name);
            // 关闭当前目录,释放存储相关信息的空间
            closedir(dir_ptr);
            return;
        }        
    }
    
    // 遍历完成后如仍为查询成功。则输出报错
    printf("Error: Fail to look for file by inode %d\n", (int)inode);
    exit(1);        
}


int main() {
    
    char its_name[256] = {0};
    char ans[256] = {0};
    
    // 获取当前目录的inode编号
    ino_t this_inode = getInode(".");
    
    // 回溯查询当前目录的父目录,直至根目录
    // 这里通过根目录与其父目录inode相等的特点来判断是否已回溯到根目录
    while (getInode("..") != this_inode) {
        // 查询当前目录的名称,保存到its_name数组
        getNameByInode(this_inode, its_name);
        // 拼接字符串,记录结果
        if (strlen(ans) == 0) {
            strcpy(ans, its_name);
        } else {
            strcat(strcat(its_name, "/"), ans);
            strcpy(ans, its_name);
        }
        // 调用chdir系统函数,将程序的工作目录移动到父目录
        chdir("..");
        // 获取父目录的inode编号
        this_inode = getInode(".");
    }
    
    // 输出结果
    printf("/%s\n", ans);
    
    return 0;
}

程序运行效果:

无标题.png

4. ls命令的简单实现

ls命令在这里我们主要实现两个功能:

  1. 打印当前目录下的所有文件名称: ls
  2. 打印当前目录下某文件的具体信息: ls -l filename

第一个功能是非常简单的,我们可以复用实现pwd命令程序中遍历目录的那部分代码:

第二个功能中,要获取文件的信息并不困难,通过调用stat函数即可。主要的难点在于对求得的struct stat结构体中的信息进行加工处理,其中又以处理表示权限的八进制整型难度最大。

具体代码如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <stdlib.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>

#define N 256

void convertPermission(int octal, char* output) {
    // 二进制数的第10~12位表示文件的类型
    int file_type = (octal >> 9) & 0b111;
    // 二进制数的第7~9位表示文件所属用户的rwx权限
    int user_permission = (octal >> 6) & 0b111;
    // 二进制数的第4~6位表示用户所属组的rwx权限
    int group_permission = (octal >> 3) & 0b111;
    // 二进制数的第1~3位表示其他用户的rwx权限
    int other_permission = octal & 0b111;
    // 判断文件类型
    switch (file_type) {
        case 0:
            output[0] = '-';
            break;
        case 1:
            output[0] = 'd';
            break;
        default:
            output[0] = '?';
    }
    // 判断文件所属用户权限
    output[1] = (user_permission & 0b100) ? 'r' : '-';
    output[2] = (user_permission & 0b010) ? 'w' : '-';
    output[3] = (user_permission & 0b001) ? 'x' : '-';
    // 判断用户所属组的权限
    output[4] = (group_permission & 0b100) ? 'r' : '-';
    output[5] = (group_permission & 0b010) ? 'w' : '-';
    output[6] = (group_permission & 0b001) ? 'x' : '-';
    // 判断其他用户的权限
    output[7] = (other_permission & 0b100) ? 'r' : '-';
    output[8] = (other_permission & 0b010) ? 'w' : '-';
    output[9] = (other_permission & 0b001) ? 'x' : '-';
    output[10] = '\0';
}

void printFileList() {
    DIR* dir_ptr = opendir(".");
    if (dir_ptr == NULL) {
        printf("Error: Fail to opendir\n");
        exit(1);
    }
    struct dirent* direntp;
    while ((direntp = readdir(dir_ptr)) != NULL) {
        char name[N];
        strcpy(name, direntp->d_name);
        // 忽略表示父目录的".."和当前目录的"."
        if (strcmp(name, "..") == 0 || strcmp(name, ".") == 0) continue;
        puts(name);
    }
}

void printFileInfo(char fileName[]) {
    struct stat info;
    if (stat(fileName, &info) < 0) {
        printf("Error: Fail to stat %s\n", fileName);
        exit(1);
    }
    printf("    name   :   %s\n", fileName);
    // 将表示文件权限的16位二进制整型转换为权限字符串
    char permeStr[N];
    convertPermission(info.st_mode, permeStr);
    printf("    mode   :   %s\n", permeStr);
    // 打印指向当前文件的硬链接数量。如果没有为文件创建额外的硬链接,则会输出1。
    printf("    links  :   %d\n", (int)info.st_nlink);
    // 调用pwd.h中的getwuid查询所属用户名称
    uid_t uid = info.st_uid;
    struct passwd* pw = getpwuid(uid);
    printf("    user   :   %s\n", pw->pw_name);
    // 调用grp.h中的getgrgid函数查询组名称
    gid_t gid = info.st_gid;
    struct group *gr = getgrgid(gid);
    printf("    group  :   %s\n", gr->gr_name);
    // 打印文件大小
    printf("    size   :   %d bytes\n", (int)info.st_size);
    // 调用time.h中的ctime函数将时间戳转换为日期文本
    time_t timeStamp = info.st_mtime;
    printf("    modtime:   %s", ctime(&timeStamp));
}

int main(int argc, char* argv[]) {
    if (argc == 1) {
        printFileList();
    }
    else if (argc == 3 && strcmp(argv[1], "-l") == 0) {
        char* fileName = argv[2];
        printFileInfo(fileName);
    }
    else {
        puts("Error: invalid arguments!");
        exit(1);
    }
    return 0;
}

程序运行效果:

微信截图_20230608162746.png

微信截图_20230608162849.png