文章目錄
- 三、撰寫ls命令
- 1. 閱讀聯機幫助
- 2. ls是如何作業的
- 3. 如何撰寫ls
- 4. 改進ls命令
- 5. ls -l 是如何作業的
- 6. 如何撰寫 ls -l
三、撰寫ls命令
1. 閱讀聯機幫助

列出有關檔案的資訊(默認為當前目錄),如果未指定 -cftuvSUX 或 --sort,則按字母順序對條目進行排序,
可以看到,ls命令 能夠找出當前目錄中所有檔案的檔案名,按字典序排序后輸出,
ls命令 還能顯示其他資訊,如果加上 -l 選項,ls 會列出每個檔案的詳細資訊,也叫 ls的長格式,在 man手冊 中可以看到:

使用長串列格式
現在在我們的終端鍵入命令:

通過實驗和聯機幫助可以知道 ls 做了以下兩件事(ls 能判定引數指定的是檔案還是目錄):
- 列出目錄的內容
- 顯示檔案的資訊
在正式開始之前,來看一下 Unix 是如何組織磁盤上的檔案的,

大方框表示目錄,大方框內的小方框表示檔案,目錄之間的連線表示目錄之間的組織關系,
2. ls是如何作業的
通過聯機幫助(程序省略)可以知道,從目錄讀資料與從檔案讀資料是類似的, opendir 打開一個目錄,readdir 回傳目錄中的當前項,closedir 關閉一個目錄,seekdir、telldir、rewinddir與 lseek 的功能類似,
接下來用 man手冊 查詢一下 readdir(3) ,可以看到:

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);
}
}
運行結果:
4. 改進ls命令
加入以下功能:
-
排序
解決辦法:把所有的檔案名讀入一個陣列,用qsort函式排序 -
分欄:標準的 ls 輸出是分欄排列的,有些以行排列,有些以列排列
解決辦法:把檔案名讀入陣列,然后計算出列的寬度和行數 -
“.”檔案:ls 列出了“.”檔案,而標準的 ls只有在給出 -a 選項時才會列出
解決辦法:使 ls1 能夠接收選項 -a,并在沒有 -a 的時候不顯示隱藏檔案 -
選項 -l:如果選項中有 -l,標準的 ls會列出檔案的詳細資訊,而 ls1不會
解決辦法: 下面討論
5. ls -l 是如何作業的
下面我們將把 ls -l 拆分成幾個小組件逐一分析并實作:
1. 用 stat 得到檔案資訊

根據 man手冊 所提供的資訊,再去查詢 fstatat,可以看到如下的資訊:

下面寫一個程式將以上我們需要的屬性顯示出來:
#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);
}
運行結果:

對比可以看到,鏈接數、檔案大小 的顯示都沒問題,最后修改時間是 time_t 型別,用 ctime 將其轉換為字串也可以解決,
為了進一步完善 ls -l ,我們需要進一步處理 模式、用戶名和組 的顯示
2. 將模式欄位轉換成字符
st_mode 是一個16位的二進制數,檔案型別和權限被編碼在這個數中,如圖所示:

- 其中前 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 可以看到:

繼續往下翻,

結構體 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 可以看到:

繼續往下翻,

實作代碼:
#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;
}
}
運行結果:

我們自己撰寫的 ls2 對比 系統提供的 ls -l 效果已經很不錯了,模式欄位、用戶名和組名的處理均已完成,剩下的 隱藏 “.”,顯示記錄總數等功能暫時不再討論,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/337854.html
標籤:其他
