功能要求#
- 实现与 Linux 原生命令【ls -al】类似的效果
- 需要的信息有:文件信息、连接数、用户名、组名、文件大小、修改时间、文件名
- 附加实现:文件排序、颜色美化、软连接显示
最终效果#
- 已实现 ls -al 的基本模板
实现过程#
思路流程图#
获取命令行参数#
捕捉 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 获取文件基本信息#
- 使用【lstat】获取文件基本信息【文件权限、硬连接数、uid、gid、文件大小、修改时间】
- 需要为创建的 stat 结构体分配好内存,而不是创建结构体指针,参考C - linux stat 函数中 struct stat * buffer 和&buffer 有什么区别
- 对于不是当前路径下的目录或文件,需要使用路径拼接后的绝对路径访问 [或者 chdir 直接切换到指定目录下]
- 否则 lstat 无法定位文件路径
// 路径拼接,获得绝对路径
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) {
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 ;
}
- 思路,参考Printing file permissions like 'ls -l' using stat(2) in C——StackOverflow
- 本文用的是第 2 个回答的解法,开辟了 rwx 数组,巧用位运算
- 细节,参考stat 结构体 st_mode 字段、Linux 中用 st_mode 判断文件类型——CSDN
- 每一位的含义很清晰
- 宏定义,可参考 man 手册:man inode,搜索 st_mode
修改时间的友好显示#
- 时间戳 👉 友好可读的时间
// 获取友好的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 ;
}
- 时间戳→时间结构体→指定格式
- 通过 localtime—cppreference完成第一步转换,得到 tm 结构体
- 通过 strftime—cppreference完成第二步转换,得到字符串
- 参考C 语言中的时间戳和时间格式—— 简书
- [亦或] 参考 man 手册的例子,使用 ctime—cppreference,再提取子字符串
获取所属用户名、组名#
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 + ]