功能要求#
- 實現與 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 + ]