三、编写ls命令

1. 阅读联机帮助

image-20211025161948853

列出有关文件的信息(默认为当前目录)。如果未指定 -cftuvSUX 或 --sort,则按字母顺序对条目进行排序。

可以看到,ls命令 能够找出当前目录中所有文件的文件名,按字典序排序后输出。

ls命令 还能显示其他信息,如果加上 -l 选项,ls 会列出每个文件的详细信息,也叫 ls的长格式,在 man手册 中可以看到:

image-20211025162605960

使用长列表格式

现在在我们的终端键入命令:

image-20211025162512511

通过实验和联机帮助可以知道 ls 做了以下两件事(ls 能判定参数指定的是文件还是目录):

  • 列出目录的内容
  • 显示文件的信息

在正式开始之前,来看一下 Unix 是如何组织磁盘上的文件的。

image-20211026090955799

大方框表示目录,大方框内的小方框表示文件,目录之间的连线表示目录之间的组织关系。

2. ls是如何工作的

通过联机帮助(过程省略)可以知道,从目录读数据与从文件读数据是类似的, opendir 打开一个目录,readdir 返回目录中的当前项,closedir 关闭一个目录,seekdir、telldir、rewinddir与 lseek 的功能类似。

接下来用 man手册 查询一下 readdir(3) ,可以看到:

image-20211025164542255

readdir() 函数返回一个指向 dirent 结构的指针,该结构表示 dirp 指向的目录流中的下一个目录条目。它在到达目录流末尾或发生错误时返回 NULL。

也就是说, readdir() 来读取 struct dirent获得目录中的记录。

3. 如何编写ls

最初级的ls命令

下面实现了一个最初级的 ls命令

#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>     // opendir() readdir() closedir()

void do_ls(char*);

int main(int argc, char* argv[]) {
    if (argc == 1) {
        do_ls(".");
    }
    else {
        while (--argc) {
            printf("%s:\n", *(++argv));
            do_ls(*argv);
        }
    }
    
    return 0;
}

void do_ls(char dirname[]) {
    DIR* dir_ptr;       // 记录opendir()后的返回值
    struct dirent* direntp; // 记录readdir()后的返回值

    if ((dir_ptr = opendir(dirname)) == NULL) {
        fprintf(stderr, "ls1: cannot open %s\n", dirname);
    }
    else {
        while ((direntp = readdir(dir_ptr)) != NULL) {
            printf("%s\n", direntp->d_name);
        }
        closedir(dir_ptr);
    }
}

运行结果:image-20211025172557513

4. 改进ls命令

加入以下功能:

  1. 排序
    解决办法:把所有的文件名读入一个数组,用qsort函数排序

  2. 分栏:标准的 ls 输出是分栏排列的,有些以行排列,有些以列排列
    解决办法:把文件名读入数组,然后计算出列的宽度和行数

  3. “.”文件:ls 列出了 “.”文件,而标准的 ls只有在给出 -a 选项时才会列出
    解决办法:使 ls1 能够接收选项 -a,并在没有 -a 的时候不显示隐藏文件

  4. 选项 -l:如果选项中有 -l,标准的 ls会列出文件的详细信息,而 ls1不会
    解决办法: 下面讨论

5. ls -l 是如何工作的

下面我们将把 ls -l 拆分成几个小组件逐一分析并实现:

1. 用 stat 得到文件信息

image-20211025175703042

根据 man手册 所提供的信息,再去查询 fstatat,可以看到如下的信息:

image-20211025175602492

下面写一个程序将以上我们需要的属性显示出来:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>

void show_stat_info(char* fname, struct stat* buf);

int main(int argc, char* argv[]) {
    struct stat info;

    if (argc > 1) {
        if (stat(argv[1], &info) != -1) {
            show_stat_info(argv[1], &info);
            return 0;
        }
        else {
            perror(argv[1]);
        }     
    }
    
    return 0;
}

void show_stat_info(char* fname, struct stat* buf) {
    printf("   mode: %o\n", buf->st_mode);
    printf("  links: %d\n", buf->st_nlink);
    printf("   user: %d\n", buf->st_uid);
    printf("  group: %d\n", buf->st_gid);
    printf("   size: %d\n", buf->st_size);
    printf("modtime: %d\n", buf->st_mtim.tv_sec);
    printf("   name: %s\n", fname);
}

运行结果:

image-20211025184253301

对比可以看到,链接数、文件大小 的显示都没问题,最后修改时间是 time_t 类型,用 ctime 将其转换为字符串也可以解决。

为了进一步完善 ls -l ,我们需要进一步处理 模式、用户名和组 的显示

2. 将模式字段转换成字符

st_mode 是一个16位的二进制数,文件类型和权限被编码在这个数中,如图所示:

image-20211025210218202

  • 其中前 4 位用作文件类型,最多可以标识 16 种类型,1 代表具有某个属性,0 代表没有,目前已经使用了其中的 7 个
  • 接下来的 3 位是文件的特殊属性,1 代表具有某个属性,0 表示没有,这 3 位分别是 set-user-ID位、set-group-ID位 和 sticky位
  • 最后的 9 位是许可权限,分为 3 组,对应 3 种用户,它们是文件所有者、同组用户和其他用户。
    每组 3 位,分别是读、写和执行的权限(相应的地方如果是 1,就说明该用户拥有对应的权限,0 代表没有)

如何读取被编码的值?

利用 子域编码掩码 的技术。

对 2 进制进行位与操作,即我们所说的解码。判断目录代码:

if((info.st_mode & 0170000) == 0040000) {
    printf("this is a directory");
}

通过掩码把其他无关的部分置为 0,再与表示目录的代码比较,从而判断这是否是一个目录。

更简单的方法是用 #include <sys/stat.h> 中的宏代替上述代码:

#define S_ISFIFO(m)  (((m)&(0170000)) == (0010000))
#define S_ISDIR(m) (((m)&(0170000)) == (0040000))
#define S_ISCHR(m) (((m)&(0170000)) == (0020000))
#define S_ISBLK(m) (((m)&(0170000)) == (0060000))
#define S_ISREG(m) (((m)&(0170000)) == (0100000))

使用宏后就这样写代码:

if(S_ISDIR(info.st_mode)) {
    printf("this is a directory");
}

下面实现将模式字段转换为字符

#include <sys/stat.h>

void mode_to_letters(int mode, char str[]) {
    strcpy(str,"----------");

    if(S_ISDIR(mode)) str[0]='d';
    if(S_ISCHR(mode)) str[0]='c';
    if(S_ISBLK(mode)) str[0]='b';

    if(mode & S_IRUSR) str[1]='r';
    if(mode & S_IWUSR) str[2]='w';
    if(mode & S_IXUSR) str[3]='x';

    if(mode & S_IRGRP) str[4]='r';
    if(mode & S_IWGRP) str[5]='w';
    if(mode & S_IXGRP) str[6]='x';


    if(mode & S_IROTH) str[7]='r';
    if(mode & S_IWOTH) str[8]='w';
    if(mode & S_IXOTH) str[9]='X';
}

现在还剩下最后一个要解决的问题,文件所有者(user) 和 组(group) 的表示

3. 将用户/组 ID转换成字符串

用户

这里需要用到库函数 getpwuid() 来访问用户列表,getpwuid 需要 UID(user ID)作为参数,返回一个指向 struct passwd 的指针,这个结构定义在 /usr/include/pwd.h 中,通过 man手册查询 getpwuid 可以看到:

image-20211025224514839

继续往下翻,

image-20211025223128187

结构体 struct passwd 正是 ls -l 所需要的信息,实现代码:

#include <pwd.h>

char* uid_to_name(uid_t uid) {
    return getpwuid(uid)->pw_name;
}

这段代码很简单,但不够健壮,如果 uid 不是一个合法的用户 ID,那 getpwuid 返回空指针 NULL,这时 getpwuid(uid)->pw_name 失去了意义。

常用的 ls命令 有一种处理这种情况的办法。(这里不做讨论)

文件 /etc/group 是一个保存所有的组信息的文本文件。在网络计算系统中,组信息也被保存在 NIS 中。(另外,前面讨论的 用户的信息 保存在 NIS 中)

Unix 系统提供 getgrgid() 函数来访问组列表,通过 man手册查询 getgrgid 可以看到:

image-20211025225640055

继续往下翻,

image-20211025225754611

实现代码:

#include <grp.h>

char* gid_to_name(gid_t gid) {
    return getgrgid(gid)->gr_name;
}

6. 如何编写 ls -l

通过上面的分析,下面实现最终的代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>   // mode_to_letters() show_stat_info()
#include <dirent.h>     // opendir() readdir() closedir()
#include <pwd.h>        // getpwuid()
#include <grp.h>        // getgrgid()
#include <string.h>     // strcpy()
#include <time.h>		// ctime()

void do_ls(char dirname[]);
void dostat(char* filename);
void show_file_info(char* fname, struct stat* buf);
// void mode_to_letters(int mode, char str[]);
char* uid_to_name(uid_t uid);
char* gid_to_name(gid_t gid);

int main(int argc, char* argv[]) {
    if (argc == 1) {
        do_ls(".");
    }
    else {
        while (--argc) {
            printf("%s:\n", *(++argv));
            do_ls(*argv);
        }
    }
    
    return 0;
}


void do_ls(char dirname[]) {
    DIR* dir_ptr;       // 记录opendir()后的返回值
    struct dirent* direntp; // 记录readdir()后的返回值

    if ((dir_ptr = opendir(dirname)) == NULL) {
        fprintf(stderr, "ls1: cannot open %s\n", dirname);
    }
    else {
        while ((direntp = readdir(dir_ptr)) != NULL) {
            // printf("%s\n", direntp->d_name);
            dostat(direntp->d_name);
        }
        closedir(dir_ptr);
    }
}

void dostat(char* filename) {
    struct stat info;       // 存储filename的信息
    if (stat(filename, &info) == -1) {
        perror(filename);
    }
    else {
        show_file_info(filename, &info);
    }
}

void show_file_info(char* filename, struct stat* info_p) {
    // char* uid_t_name(), *ctime(), *gid_to_name();
    
    void mode_to_letters();
    char modestr[11];

    mode_to_letters(info_p->st_mode, modestr);  // 将模式字段转换成字符

    printf("%s", modestr);
    printf("%4d", (int)info_p->st_nlink);
    printf(" %-10s", uid_to_name(info_p->st_uid));   // 将用户ID转换成字符串
    printf("%-10s", gid_to_name(info_p->st_gid));   // 将组ID转换成字符串
    printf("%8ld", (long)info_p->st_size);
    printf("%.12s", 4 + ctime(&info_p->st_mtim.tv_sec));    // 通过ctime()函数转换时间,之前的who命令有用到
    printf(" %s\n", filename);
}

// 将模式字段转换成字符
void mode_to_letters(int mode, char str[]) {
    strcpy(str,"----------");
    // 用到了 子域编码 与 掩码 的技术
    if(S_ISDIR(mode)) str[0]='d';
    if(S_ISCHR(mode)) str[0]='c';
    if(S_ISBLK(mode)) str[0]='b';

    if(mode & S_IRUSR) str[1]='r';
    if(mode & S_IWUSR) str[2]='w';
    if(mode & S_IXUSR) str[3]='x';

    if(mode & S_IRGRP) str[4]='r';
    if(mode & S_IWGRP) str[5]='w';
    if(mode & S_IXGRP) str[6]='x';

    if(mode & S_IROTH) str[7]='r';
    if(mode & S_IWOTH) str[8]='w';
    if(mode & S_IXOTH) str[9]='X';
}

// 将用户ID转换成字符串
char* uid_to_name(uid_t uid) {
    // struct passwd* getpwuid(), *pw_ptr;
    struct passwd* pw_ptr;
    static char numstr[10];

    if ((pw_ptr = getpwuid(uid)) == NULL) {
        sprintf(numstr, "%d", uid);
        return numstr;
    }
    else {
        return pw_ptr->pw_name;
    }
}

// 将组ID转换成字符串
char* gid_to_name(gid_t gid) {
    // struct group* getgrgid(), *grp_str;
    struct group* grp_str;
    static char numstr[10];

    if ((grp_str = getgrgid(gid)) == NULL) {
        sprintf(numstr, "%d", gid);
        return numstr;
    }
    else {
        return grp_str->gr_name;
    }
}

运行结果:
image-20211026160428886

我们自己编写的 ls2 对比 系统提供的 ls -l 效果已经很不错了,模式字段、用户名和组名的处理均已完成。剩下的 隐藏 “.”,显示记录总数等功能暂时不再讨论。

Logo

更多推荐