Bo2SS

Bo2SS

Linux中ls -al的手动实现

功能要求#

  • 实现与 Linux 原生命令【ls -al】类似的效果
  • 图片
  • 需要的信息有:文件信息、连接数、用户名、组名、文件大小、修改时间、文件名
  • 附加实现:文件排序、颜色美化、软连接显示

最终效果#

  • 图片
  • 已实现 ls -al 的基本模板

实现过程#

思路流程图#

  • image-20210117100933677

获取命令行参数#

捕捉 ls 的选项 - al 和可选的选项参数 path

  • 识别 ls -al、ls -l、ls -al path、ls -l path、其他报错
#include "head.h"
void myls(char **argv, char *path, int a_flag) {
    if (a_flag) printf("run %s -al %s\n", argv[0], path);
    else printf("run %s -l %s\n", argv[0], path);
    return ;
}
void show_tips(char **argv) {
    fprintf(stderr, "Usage: %s -[a]l [path]\n", argv[0]);
    return ;
}
int main(int argc, char **argv) {
    // 不带路径参数 [默认为当前目录]
    if (argc == 2){
        if (!strcmp(argv[1], "-al")) myls(argv, ".", 1);
        else if (!strcmp(argv[1], "-l")) myls(argv, ".", 0);
        else show_tips(argv), exit(1);
        return 0;
    }
    int opt;
    int a_flag = 0;  // 判断-a选项
    // 带路径参数
    while ((opt = getopt(argc, argv, "al:")) != -1) {
        switch (opt) {
            case 'a': {
                a_flag = 1;
            } break;
            case 'l': {
                myls(argv, optarg, a_flag);
            } break;
            default: {
                show_tips(argv);
                exit(1);
            }
        }
    }
    return 0;
}
  • 因为 getopt 中对于可选选项参数的选项【选项后跟 "::"】,其参数必须跟在其后【如 ls -al123】,而不能像原生 ls -al path 那样将选项和选项参数用空格隔开【如 ls -al 123】,所以对于选项的参数设置为必选【选项后跟 ":"】,再在前面做一个无选项参数时的判断
  • 头文件 head.h 见末尾
  • 使用测试脚本 test.sh [见末尾] 测试,效果如下:
    • 图片
    • 已经可以捕捉识别 ls -al、ls -l、ls -al path、ls -l path,以及其他错误输入

读取目录#

读取出目录的文件名列表

  • 使用 opendir、readdir [后面为了排序方便换用了 scandir,见排序显示]
  • 只需修改 myls () 函数,代码如下
void myls(char *path, int a_flag) {
    DIR *dirp;
    // 0. 打开并读取目录
    if ((dirp = opendir(path)) != NULL) {
        struct dirent *dent;
        while (dent = readdir(dirp)) {
            if (dent->d_name[0] == '.' && !a_flag) continue;
            printf("%s\n", dent->d_name);
        }
    } else {
        perror("opendir");
        exit(1);
    }
    return ;
}
  • 此时 myls 已经不需要 argv 参数
  • 根据是否有 a 选项,判断是否显示隐藏文件
    • 只要文件名以 "." 开头,则为隐藏文件
  • 部分测试效果如下
    • 图片
    • 已经可以读取出目录里所有文件的名称,并区分出隐藏文件

读取并处理文件信息⭐#

根据每个文件的名称,获取文件的信息

lstat 获取文件基本信息#

  1. 使用【lstat】获取文件基本信息【文件权限、硬连接数、uid、gid、文件大小、修改时间】
// 路径拼接,获得绝对路径
void absolute_path(char *ab_path, char* path, char *filename) {
    strcpy(ab_path, path);                            // 保留原path值
    strcat(ab_path, "/");                             // 注意添加路径分隔符
    strcat(ab_path, filename);                        // 拼接
    return ;
}

文件信息的友好显示#

  1. 文件信息 <文件类型、文件 [特殊] 权限 > 👉 字符形式
// 获取友好的文件信息
void mode2str(mode_t smode, char *mode) {
    int i = 0;
    // rwx数组,分别对应000、001、...、110、111
    char *rwx[] = {
        "---", "--x", "-w-",
        "-wx", "r--", "r-x",
        "rw-", "rwx"
    };
    // 获取文件类型
    if (S_ISREG(smode)) mode[0] = '-';
    else if (S_ISDIR(smode)) mode[0] = 'd';
    else if (S_ISBLK(smode)) mode[0] = 'b';
    else if (S_ISCHR(smode)) mode[0] = 'c';
#ifdef S_ISFIFO
    else if (S_ISFIFO(smode)) mode[0] = 'p';
#endif
#ifdef S_ISLNK
    else if (S_ISLNK(smode)) mode[0] = 'l';
#endif
#ifdef S_ISSOCK
    else if (S_ISSOCK(smode)) mode[0] = 's';
#endif
    else mode[i++] = '?';
    // 获取文件权限
    strcpy(&mode[1], rwx[(smode >> 6) & 7]);
    strcpy(&mode[4], rwx[(smode >> 3) & 7]);
    strcpy(&mode[7], rwx[smode & 7]);
    // 获取文件特殊权限
    if (smode & S_ISUID) mode[3] = (smode & S_IXUSR) ? 's' : 'S';
    if (smode & S_ISGID) mode[6] = (smode & S_IXGRP) ? 's' : 'S';
    if (smode & S_ISVTX) mode[9] = (smode & S_IXOTH) ? 't' : 'T';
    return ;
}

修改时间的友好显示#

  1. 时间戳 👉 友好可读的时间
// 获取友好的mtime
void mtim2str(struct timespec *mtim, char *str, size_t max) {
    struct tm *tmp_time = localtime(&mtim->tv_sec);           // 转换为当地日历时间
    strftime(str, max, "%b %e %H:%M", tmp_time);              // 转换为指定格式
    return ;
}

获取所属用户名、组名#

4、5. 通过【getpwuid、getgrgid】与 uid、gid 获得文件所属的【用户名、组名】

strcpy(uname, getpwuid(st.st_uid)->pw_name);  // 4. 获取文件所属者名称
strcpy(gname, getgrgid(st.st_gid)->gr_name);  // 5. 获取文件所属组名称
  • 效果如下:
    • 图片
    • 只有想不到的工具,当然也可以尝试自己实现这个通过 uid、gid 获取名称的过程

排序显示#

  • 详见文末完整代码
  • readdir 读取目录文件的时候是乱序了,而scandir可以按字典序读取出文件列表
  • 参考 man scandir 的 EXAMPLE
    • 图片
    • 创建文件列表,按字典序读入,逆序输出
  • scandir 完成了 opendir 和 readdir 两步工作,所以将 opendir 与 readdir 替换为 sancdir 即可,后面同样是将 d_name 给 lstat 使用
  • 效果如下
    • 图片
    • 排序规则稍有区别,接下来控制颜色和显示软连接指向

颜色控制#

  • 详见文末完整代码 [颜色宏见 head.h]
  • 主要对目录、软连接、可执行文件、部分特殊权限文件的颜色进行了美化
    • 其他颜色大同小异
  • 效果如下
    • 图片
    • 再实现软连接指向的显示

软连接指向的显示#

  • 详见文末完整代码
  • 使用 readlink 读取软连接指向的源文件名,参考 man readlink
  • 效果如下
    • 图片
    • 🔚

完整代码#

myls.c#

#include "head.h"
// 路径拼接,获得绝对路径
void absolute_path(char *ab_path, char* path, char *filename) {
    strcpy(ab_path, path);                            // 保留原path值
    strcat(ab_path, "/");                             // 注意添加路径分隔符
    strcat(ab_path, filename);                        // 拼接
    return ;
}
// 获取友好的文件信息
void mode2str(mode_t smode, char *mode, char *color) {
    int i = 0;
    // rwx数组,分别对应000、001、...、110、111
    char *rwx[] = {
        "---", "--x", "-w-",
        "-wx", "r--", "r-x",
        "rw-", "rwx"
    };
    // 获取文件类型
    if (S_ISREG(smode)) mode[0] = '-';
    else if (S_ISDIR(smode)) mode[0] = 'd', strcpy(color, "BLUE_HL");
    else if (S_ISBLK(smode)) mode[0] = 'b';
    else if (S_ISCHR(smode)) mode[0] = 'c';
#ifdef S_ISFIFO
    else if (S_ISFIFO(smode)) mode[0] = 'p';
#endif
#ifdef S_ISLNK
    else if (S_ISLNK(smode)) mode[0] = 'l', strcpy(color, "BLUE");
#endif
#ifdef S_ISSOCK
    else if (S_ISSOCK(smode)) mode[0] = 's';
#endif
    else mode[i++] = '?';
    // 获取文件权限
    strcpy(&mode[1], rwx[(smode >> 6) & 7]);
    strcpy(&mode[4], rwx[(smode >> 3) & 7]);
    strcpy(&mode[7], rwx[smode & 7]);
    // 获取文件特殊权限
    if (smode & S_ISUID) mode[3] = (smode & S_IXUSR) ? 's' : 'S';
    if (smode & S_ISGID) mode[6] = (smode & S_IXGRP) ? 's' : 'S';
    if (smode & S_ISVTX) mode[9] = (smode & S_IXOTH) ? 't' : 'T';
    // + 配置颜色 [上面也有]
    if (mode[0] == '-') {
        if (strstr(mode, "x")) strcpy(color, "GREEN_HL");
        if (strstr(mode, "s") || strstr(mode, "S")) strcpy(color, "YELLOW_BG");
    }
    return ;
}
// 获取友好的mtime
void mtim2str(struct timespec *mtim, char *str, size_t max) {
    struct tm *tmp_time = localtime(&mtim->tv_sec);       // 转换为当地日历时间
    strftime(str, max, "%b %e %H:%M", tmp_time);          // 转换为指定格式
    return ;
}
void myls(char *path, int a_flag) {
    // 0. 按 [字典序] 读取出目录的文件列表
    struct dirent **namelist;
    int n, i = -1;
    n = scandir(path, &namelist, NULL, alphasort);        // 按字典序读
    if (n == -1) {
        perror("scandir");
        exit(1);
    }
    // 正序遍历文件列表
    while (i < n - 1) {
        i++;
        struct dirent *dent = namelist[i];
        if (dent->d_name[0] == '.' && !a_flag) continue;  // 是否显示隐藏文件
        struct stat st;                                   // 需要为该结构体开辟内存
        char ab_path[128];                                // 记录绝对路径
        absolute_path(ab_path, path, dent->d_name);       // +. 路径拼接
        // 1. 读取文件信息
        if (!lstat(ab_path, &st)) {
            char mode[16], mtime[32], uname[16], gname[16], filename[512], color[16] = {0};
            mode2str(st.st_mode, mode, color);            // 2. 处理文件信息 [并设置颜色]
            mtim2str(&st.st_mtim, mtime, sizeof(mtime));  // 3. 处理修改时间
            strcpy(uname, getpwuid(st.st_uid)->pw_name);  // 4. 获取文件所属者名称
            strcpy(gname, getgrgid(st.st_gid)->gr_name);  // 5. 获取文件所属组名称
            // 打印除文件名外的基本信息
            printf("%s %lu %s %s %*lu %s ",
                    mode, st.st_nlink, uname, gname,
                    5, st.st_size, mtime
                  );
            // 6. 根据颜色包装文件名
            if (!strcmp(color, "BLUE")) sprintf(filename, BLUE("%s"), dent->d_name);
            else if (!strcmp(color, "BLUE_HL")) sprintf(filename, BLUE_HL("%s"), dent->d_name);
            else if (!strcmp(color, "GREEN_HL")) sprintf(filename, GREEN_HL("%s"), dent->d_name);
            else if (!strcmp(color, "YELLOW_BG")) sprintf(filename, YELLOW_BG("%s"), dent->d_name);
            else strcpy(filename, dent->d_name);
            // 7. 对软连接特殊处理
            if (mode[0] == 'l') {
                char linkfile[32];
                readlink(ab_path, linkfile, 32);
                strcat(filename, " -> ");
                strcat(filename, linkfile);
            }
            // 单独打印文件名称 [含颜色]
            printf("%s\n", filename);
        } else {
            perror("lstat");
            exit(1);
        }
        free(namelist[i]);
    }
    free(namelist);
    return ;
}
void show_tips(char **argv) {
    fprintf(stderr, "Usage: %s -[a]l [path]\n", argv[0]);
    return ;
}
int main(int argc, char **argv) {
    // 不带路径参数 [默认为当前目录]
    if (argc == 2){
        if (!strcmp(argv[1], "-al")) myls(".", 1);
        else if (!strcmp(argv[1], "-l")) myls(".", 0);
        else show_tips(argv), exit(1);
        return 0;
    }
    int opt, a_flag = 0;  // a_flag:判断-a选项
    // 带路径参数
    while ((opt = getopt(argc, argv, "al:")) != -1) {
        switch (opt) {
            case 'a': {
                a_flag = 1;
            } break;
            case 'l': {
                myls(optarg, a_flag);
            } break;
            default: {
                show_tips(argv);
                exit(1);
            }
        }
    }
    return 0;
}
  • 详见注释
  • 列间距可通过 printf ("%*s", len, "xyz") 形式控制,用 len 控制 * 的值,len 根据字符串长度调整 [未考虑]
  • 排序显示是将 opendir + readdir 的方式改为了 scandir,后者可以将目录下的文件按顺序读入到一个字符串数组
  • 颜色控制
    • ❗ 在 mode2str 里设置颜色时,color 字符数组的赋值用 strcpy 的方式,如果直接用 "=" 赋值的话,color 在函数里赋的值出不了函数,即使 color 是字符指针也如此。所以用 color 字符数组与 strcpy 函数 [这里 color 采用的是传出参数的方式]
    • 两个字符串判等使用 strcmp,而不是 "=="
    • 为了方便文件名颜色和文件名的绑定,创建一个512字节大小的文件名字符数组 filename 封装文件名和对应的颜色
      • 使用 512 是因为 sprintf 中的 % s,编译器认为最高可达 256 字节
      • 🆒 有更优美的解决方案 —— 使用 snprintf,并根据数据动态开辟空间,参考Detecting String Truncation with GCC 8
        • 图片
        • malloc 是精髓
  • 软连接指向的显示
    • filename 的产生源于此,指向的箭头和指向的原文件的显示为默认颜色

head.h#

#ifndef _HEAD_H
#define _HEAD_H
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
#define COLOR(a, b) "\033[" #b "m" a "\033[0m"
#define COLOR_BG(a, b) "\033[2;" #b "m" a "\033[0m"
#define COLOR_HL(a, b) "\033[1;" #b "m" a "\033[0m"
#define RED(a) COLOR(a, 31)
#define GREEN(a) COLOR(a, 32)
#define YELLOW(a) COLOR(a, 33)
#define BLUE(a) COLOR(a, 34)
#define PURPLE(a) COLOR(a, 35)
#define RED_HL(a) COLOR_HL(a, 31)
#define GREEN_HL(a) COLOR_HL(a, 32)
#define YELLOW_HL(a) COLOR_HL(a, 33)
#define BLUE_HL(a) COLOR_HL(a, 34)
#define PURPLE_HL(a) COLOR_HL(a, 35)
#define RED_BG(a) COLOR_BG(a, 41)
#define GREEN_BG(a) COLOR_BG(a, 42)
#define YELLOW_BG(a) COLOR_BG(a, 43)
#define BLUE_BG(a) COLOR_BG(a, 44)
#define PURPLE_BG(a) COLOR_BG(a, 45)
#endif

test.sh#

#!/bin/bash
path="x.TestDir"
echo "./ls -al:" | lolcat
./ls -al
echo "./ls -l:" | lolcat
./ls -l
echo "./ls -al $path:" | lolcat
./ls -al $path
echo "./ls -l $path:" | lolcat
./ls -l $path
echo "./ls -b:" | lolcat
./ls -b
echo "./ls -b $path:" | lolcat
./ls -b $path
  • ./ls 的 ls 是使用 gcc ... -o ls 生成的可执行文件名

[PS]

  • 如何查看复杂结构体的成员变量对应什么样的格式控制字符串?
    • 如 % s、% lu...
    • ① 先随便写一个,看报错信息的提示
    • ② 或者,使用 ctags 查看源码:ctrl + ]

参考#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。