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 + ]

參考#

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