[GNU/Linux] 自己实现ls

12 篇文章 1 订阅
本文讲述了作者在GNU/Linux环境下自己实现ls命令的过程,从拒绝到接受,通过编写Makefile、头文件和多个cpp源文件,期间遇到了参数处理、文件路径构建、列宽计算、颜色设置、错误处理等问题,并分享了调试和优化的经验。
摘要由CSDN通过智能技术生成

随便扯点什么

实际上刚开始让我实现ls(1)我是拒绝的,因为,你不能让我写,我就马上去写,第一我要看一下,因为我不愿意写完了以后再加一些特技上去,比如ls忽然“Segmentation Fault”了,很懵逼、很低能,这样用过ls的人出来一定会骂我,根本没有这样辣鸡的ls,就证明这ls一点也不好用,就是在骗人。后来组长告诉我只用实现-a -l -R就好,我写了两天左右,感觉还不错,后来我在使用的时候就没有加特技,就很舒服。

源代码

src/Makefile

SOURCES = $(wildcard *.cpp)
OBJECTS = $(patsubst %.cpp, %.o, $(SOURCES))
CXXFLAG = -std=c++14 -I ../include -Wall -O2

pls: $(OBJECTS)
    g++ $(CXXFLAG) -o pls $(OBJECTS)

$(OBJECTS): $(SOURCES)
    g++ $(CXXFLAG) -c *.cpp

.PHONY: clean
clean :
    -rm $(OBJECTS)

include/pangda/ls.h

#pragma once
#include<string>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/ioctl.h>
#include<unistd.h>
#include<dirent.h>
#include<algorithm>
#include<vector>
#include<cmath>
#include<map>

using std::string;

//表示参数常量
const int PARAM_a = 0x01; //-a --all        显示所有文件
const int PARAM_A = 0x02; //-A --almost-all 显示除过.和..之外的所有文件
const int PARAM_f = 0x04; //-f              不进行排序
const int PARAM_r = 0x08; //-r              逆序排序显示
const int PARAM_R = 0x10; //-R              递归地显示目录下所有目录的文件
const int PARAM_l = 0x20; //-l              以列表的形式显示

struct list_type {
    int rows, cols, judge;          //几行几列
    std::vector<int> widths_list;   //宽度表,指每一列应该是多少宽度
};

struct param_t {
    std::string path;   //ls哪一个路径
    int param_flag;     //都有哪些参数
};

struct files_t {
    std::string fst_name;           //文件名
    unsigned short fst_namelen;     //文件名长度,用来计算列宽
    mode_t fst_mode;                //文件权限
    char fst_type;                  //文件类型,l表示链接,d表示目录,-表示其他
    long fst_creatime;              //文件的更改时间
    off_t fst_size;                 //文件大小
    nlink_t fst_linknum;            //文件被硬链接的次数
    std::string fst_uid;            //文件所属的用户的uid
    std::string fst_gid;            //文件所属的用户组的gid
};

struct filelist_t {
    static const bool FLT_LIST = true;
    static const bool FLT_MAP = false;
    bool type = FLT_MAP;                                //文件列表的类型,true表示nlist可用,false代表nmap可用
    std::vector<files_t> nlist;
    std::map<std::string,  std::vector<files_t> > nmap;
};

typedef std::vector<std::string> vstr_t;
typedef std::vector<int> vint_t;
typedef std::vector<std::vector<int> > vvint_t;
typedef std::vector<files_t> vft_t;

//函数定义
int output(param_t param, const filelist_t flt);
param_t set_param(const int argc, char *argv[]);
bool check_param(const param_t msg, const int param);
filelist_t build_filelist(const param_t param);
string get_username(uid_t uid);
string get_groupname(gid_t gid);

src/ls.cpp

#include<pangda/ls.h>

int main(int argc, char *argv[]) {
    param_t param = set_param(argc, argv);         //分析带有的参数
    if (param.path[param.path.size() - 1] != '/')
        param.path += '/';  //若要显示的目录不以/结尾,那么手动在之后加上'/'

    auto fli = build_filelist(param);   //根据参数构建文件列表
    output(param, fli);     //按指定参数形式输出文件列表
    return 0;
}

src/ls_param.cpp

#include<pangda/ls.h>
#include<cstring>

static const char *PARAMSTR_a = "all";
static const char *PARAMSTR_A = "almost-all";

//分析并返回是否具有参数,返回值是参数做|运算后的数值
static int analyse_param(const char *args) {
    if (args[0] != '-')     //若参数不以-开头,说明他不是一个参数
        return -1;
    if (args[1] == '-') {   //若以--开头,说明需要以字符串来判断
        if (strcmp(args + 2, PARAMSTR_a) == 0)
            return PARAM_a;
        if (strcmp(args + 2, PARAMSTR_A) == 0)
            return PARAM_A;
        return 0;
    }

    int ret = 0;
    while (*args++) {   //依次判断,因为可能出现-alRf的形式
        switch (*args) {
        case 'a':
            ret |= PARAM_a;
            break;
        case 'A':
            ret |= PARAM_A;
            break;
        case 'f':
            ret |= PARAM_f;
            break;
        case 'r':
            ret |= PARAM_r;
            break;
        case 'R':
            ret |= PARAM_R;
            break;
        case 'l':
            ret |= PARAM_l;
        }
    }
    return ret;
}

//检查msg是否具有param所指定的参数
bool check_param(const param_t msg, const int param) {
    return (msg.param_flag & param);
}

//设置参数
param_t set_param(int argc, char *argv[]) {
    param_t ret = { "./", 0 };  //若没有默认路径,则默认以./作为路径

    for (int i = 1; i < argc; i++) {
        int t = analyse_param(argv[i]);
        if (t == -1)
            ret.path = argv[i];     //若返回值是-1,说明这个参数是要显示的路径
        else
            ret.param_flag |= t;    //反之则说明应当设定参数
    }
    return ret;
}

src/ls_common.cpp

#include<pangda/ls.h>
#include<pwd.h>
#include<grp.h>
#include<errno.h>
#include<queue>
using std::pair;
using std::make_pair;
using std::queue;

//打开一个目录并返回DIR流
static DIR *open_folder(const param_t param) {
    DIR *ret;
    if ((ret = opendir(param.path.c_str())) == NULL) {  //尝试打开
        return NULL;    //出错则返回NULL
    }
    return ret;
}

//返回一个文件的属性字,即目录返回d,链接返回l等等
static char check_type(const mode_t mode) {
    if (S_ISLNK(mode))
        return 'l';
    if (S_ISDIR(mode))
        return 'd';
    if (S_ISCHR(mode))
        return 'c';
    if (S_ISBLK(mode))
        return 'b';
    return '-';
}

//以map形式构建输出文件列表
static filelist_t build_filelist_map(param_t param, DIR *where) {
    filelist_t ret;
    ret.type = false;
    queue<pair<string, DIR *> > q;
    where = opendir(param.path.c_str());
    q.push(make_pair(param.path, where));   //将第一个目录放入队列中
    while (!q.empty()) {
        pair<string, DIR *> qt = q.front();
        q.pop();

        files_t temp;
        struct stat stbuf;
        dirent *thisfile;
        bool param_a = check_param(param, PARAM_a);     //检查是否具有a参数
        bool param_A = check_param(param, PARAM_A);     //检查是否具有A参数

        if (qt.second == NULL) {    //若没能打开DIR流,则继续构建下一个
            continue;
        }

        while ((thisfile = readdir(qt.second)) != NULL) {
            temp.fst_name = thisfile->d_name;   //设置名字
            if (!(param_A || param_a)) {        //判断是否存在aA参数,若无这两个参数,直接跳过.开头的文件
                if (temp.fst_name[0] == '.')
                    continue;
            } else if (param_A) {
                if (temp.fst_name == "." || temp.fst_name == "..")
                    continue;
            }
            temp.fst_namelen = thisfile->d_reclen;
            string statp = thisfile->d_name;        //构建绝对路径的文件名
            if (qt.first[qt.first.size() - 1] == '/')
                statp = qt.first + statp;
            else
                statp = qt.first + '/' +statp;

            if (lstat(statp.c_str(), &stbuf) == -1) {   //尝试获得文件属性
                continue;
            }
            temp.fst_creatime = stbuf.st_ctime;
            temp.fst_gid = get_groupname(stbuf.st_gid);
            temp.fst_uid = get_username(stbuf.st_uid);
            temp.fst_linknum = stbuf.st_nlink;
            temp.fst_mode = stbuf.st_mode;
            temp.fst_type = check_type(stbuf.st_mode);
            temp.fst_size = stbuf.st_size;
            ret.nmap[qt.first].push_back(temp);     //将获得的文件输行放入列表中

            //若是一个目录,而且不是.和..目录,将之放入队列中,构建他的列表
            if (temp.fst_type == 'd' && temp.fst_name != "." && temp.fst_name != "..") {
                string newpath = qt.first + temp.fst_name;
                if (newpath[newpath.size() - 1] != '/')
                    newpath += '/';
                DIR *newdir = opendir(newpath.c_str());
                q.push(make_pair(newpath, newdir));
            }
        }
        closedir(qt.second);    //关闭文件流

        if (check_param(param, PARAM_f)) {  //若设定了f参数,那么就不进行排序
            continue;
        }

        //lambda:排序的比较函数 -> bool
        static auto sort_cmp = [](const files_t a, const files_t b) -> bool {
            return a.fst_name < b.fst_name;
        };
        static auto sort_revcmp = [](const files_t a, const files_t b) -> bool {
            return a.fst_name > b.fst_name;
        };

        if (check_param(param, PARAM_r))    //若设定了r参数,则使用逆序排序函数
            sort(ret.nmap[qt.first].begin(), ret.nmap[qt.first].end(), sort_revcmp);
        else    //反之使用正常的排序函数
            sort(ret.nmap[qt.first].begin(), ret.nmap[qt.first].end(), sort_cmp);
    }

    return ret;
}

//以list形式构建输出文件列表
static filelist_t build_filelist_list(param_t param, DIR *where) {
    filelist_t ret = { filelist_t::FLT_LIST };
    files_t temp;
    struct stat stbuf;
    dirent *thisfile;
    //检查是否具有指定参数
    bool param_a = check_param(param, PARAM_a);
    bool param_A = check_param(param, PARAM_A);

    chdir(param.path.c_str());
    if (where == NULL) {    //若没有打开文件流,则报错
        perror("ls");
        exit(-1);
    }

    //以下内容与map形式类似
    while ((thisfile = readdir(where)) != NULL) {
        temp.fst_name = thisfile->d_name;
        if (!(param_A || param_a)) {
            if (temp.fst_name[0] == '.')
                continue;
        } else if (param_A) {
            if (temp.fst_name == "." || temp.fst_name == "..")
                continue;
        }
        temp.fst_namelen = thisfile->d_reclen;

        if (lstat(thisfile->d_name, &stbuf) == -1) {
            return filelist_t();
        }
        temp.fst_creatime = stbuf.st_ctime;
        temp.fst_gid = get_groupname(stbuf.st_gid);
        temp.fst_uid = get_username(stbuf.st_uid);
        temp.fst_linknum = stbuf.st_nlink;
        temp.fst_mode = stbuf.st_mode;
        temp.fst_type = check_type(stbuf.st_mode);
        temp.fst_size = stbuf.st_size;
        ret.nlist.push_back(temp);
    }

    closedir(where);
    if (check_param(param, PARAM_f)) {
        return ret;
    }
    auto sort_cmp = [](const files_t a, const files_t b) -> bool {
        return a.fst_name < b.fst_name;
    };
    auto sort_revcmp = [](const files_t a, const files_t b) -> bool {
        return a.fst_name > b.fst_name;
    };

    if (check_param(param, PARAM_r))
        sort(ret.nlist.begin(), ret.nlist.end(), sort_revcmp);
    else
        sort(ret.nlist.begin(), ret.nlist.end(), sort_cmp);

    return ret;
}

filelist_t build_filelist(const param_t param) {
    DIR *where = open_folder(param);        //打开目录
    if (check_param(param, PARAM_R))        //若设定了R参数,则以map形式构建
        return build_filelist_map(param, where);
    return build_filelist_list(param, where);   //反之以list形式构建
}

//获得用户名
string get_username(uid_t uid) {
    passwd *ret = getpwuid(uid);
    if (ret == NULL)
        return "";
    return string(ret->pw_name);
}

//获得用户组名
string get_groupname(gid_t gid) {
    group *ret = getgrgid(gid);
    if (ret == NULL)
        return "";
    return string(ret->gr_name);
}

src/ls_display.cpp

#include<pangda/ls.h>
#include<cstdio>
#include<ctime>
#include<cstring>

//获得终端窗口的大小,用于计算列宽。
static winsize get_winsize() {
    winsize ret;
    ioctl(STDIN_FILENO, TIOCGWINSZ, (char *)&ret);
    return ret;
}

//根据文件的权限属性设定颜色
static int setc(mode_t mode, string filename) {
    if (S_ISDIR(mode))  //若为目录
        return printf("\033[36m");
    if (S_ISLNK(mode))  //若为符号链接
        return printf("\033[33;35m");
    if ((mode & S_IXUSR) || (mode & S_IXOTH) || (mode & S_IXGRP))   //若为可执行文件
        return printf("\033[32m");
    return printf("\033[0m");   //若为其他文件
}

//取消设定的颜色
static int canc() {
    return printf("\033[0m");
}

//test_tier辅助函数
inline static unsigned int find_pos(const int x, const int y, const int tier) {
    return (x + tier * y);
}

//根据宽度表尝试构建宽度表
static list_type test_tier(const int tier, const vint_t widths) {
    vvint_t build;
    list_type ret { int(ceil(widths.size() / double(tier))),
                    tier, 0, vint_t() };

    for (int i = 0; i < tier; i++) {
        build.push_back(vint_t());
        int col_max = 0;
        for (int j = 0; j < ret.rows; j++) {
            if (find_pos(i, j, tier) >= widths.size()) {
                break;
            }
            int t = widths[find_pos(i, j, tier)];

            build[i].push_back(t);
            if (t > col_max)
                col_max = t;
        }

        ret.widths_list.push_back(col_max);
        ret.judge += col_max + 2;
    }

    ret.judge -= 2;
    return ret;
}

//计算在当前文件下,列宽应该为多少
static list_type calc_cols(const vft_t files) {
    int limit = get_winsize().ws_col;   //获取终端的最大列宽
    vint_t widths;
    for (auto i : files) {
        widths.push_back(i.fst_name.size());    //构建文件长度表
    }

    list_type test;
    int t = 1;
    while (true) {  //测试一行中最多能有多少列
        test = test_tier(t + 1, widths);
        if (test.judge <= limit)
            t++;
        else
            break;
    }
    return test_tier(t, widths);
}

//按普通形式输出一个文件列表
static int outone_normal(const vft_t filist) {
    list_type lt = calc_cols(filist);   //计算该文件列表可以有多少列

    int i = 0;
    bool fir = true;
    for (auto it : filist) {
        if (fir)
            fir = false;
        else
            printf("  ");
        setc(it.fst_mode, it.fst_name); //根据权限来指定颜色
        printf("%-*s", lt.widths_list[i], it.fst_name.c_str());
        canc(); //取消掉颜色
        if (++i == lt.cols)
            putchar('\n'), i = 0, fir = true;
    }
    putchar('\n');
    return 0;
}

//输出是否具有指定的权限,若有输出right,反之输出-
static int put_power(const mode_t mode, const int power, const char right) {
    if (mode & power)
        return putchar(right);
    return putchar('-');
}

//计算数字有多少位
static int calc_numbits(int num) {
    char buf[500];
    sprintf(buf, "%d", num);
    return strlen(buf);
}

//根据长整型的时间返回字符串形式的时间
static string calc_time(long times) {
    string ret = "";
    char buf[500];
    strcpy(buf, ctime(&times));
    int sz = strlen(buf);
    for (int i = 4; i < sz - 9; i++) {
        ret += buf[i];
    }
    return ret;
}

//按list形式输出一个list
static int outone_list(const vft_t filist) {
    vint_t wcalc[4];
    //简单的计算中间的列宽
    for (auto it : filist) {
        wcalc[0].push_back(it.fst_uid.size());
        wcalc[1].push_back(it.fst_gid.size());
        wcalc[2].push_back(calc_numbits(int(it.fst_size)));
        wcalc[3].push_back(calc_numbits(int(it.fst_linknum)));
    }
    int widths[4] {
        *max_element(wcalc[0].begin(), wcalc[0].end()),
        *max_element(wcalc[1].begin(), wcalc[1].end()),
        *max_element(wcalc[2].begin(), wcalc[2].end()),
        *max_element(wcalc[3].begin(), wcalc[3].end())
    };

    for (auto it : filist) {
        putchar(it.fst_type);                   //输出文件的类型
        put_power(it.fst_mode, S_IRUSR, 'r');   //输出权限
        put_power(it.fst_mode, S_IWUSR, 'w');
        put_power(it.fst_mode, S_IXUSR, 'x');
        put_power(it.fst_mode, S_IRGRP, 'r');
        put_power(it.fst_mode, S_IWGRP, 'w');
        put_power(it.fst_mode, S_IXGRP, 'x');
        put_power(it.fst_mode, S_IROTH, 'r');
        put_power(it.fst_mode, S_IWOTH, 'w');
        put_power(it.fst_mode, S_IXOTH, 'x');
        putchar(' ');
        printf("%*d ", widths[3], (int)it.fst_linknum);        
        printf("%-*s ", widths[0], it.fst_uid.c_str());
        printf("%-*s ", widths[1], it.fst_gid.c_str());
        printf("%*d ", widths[2], (int)it.fst_size);
        printf("%s ", calc_time(it.fst_creatime).c_str());
        setc(it.fst_mode, it.fst_name);
        printf("%s\n", it.fst_name.c_str());
        canc();
    }
    return 0;
}

//输出list
static int output_list(param_t param, const filelist_t filist) {
    if (check_param(param, PARAM_l))
        return outone_list(filist.nlist);
    return outone_normal(filist.nlist);
}

//输出map
static int output_map(param_t param, const filelist_t filist) {
    for (auto it : filist.nmap) {
        printf("%s:\n", it.first.c_str());
        if (check_param(param, PARAM_l))
            outone_list(it.second);
        else
            outone_normal(it.second);
        printf("\n");
    }
    return 0;
}

//通用output函数
int output(param_t param, const filelist_t flt) {
    if (flt.type == filelist_t::FLT_LIST)
        return output_list(param, flt);
    return output_map(param, flt);
}

一些小问题

  1. 参数可能是ls -alRf这样的形式,所以参数处理的时候要注意。
  2. 使用lstat函数的时候要注意文件是不是在当前工作目录下面,如果没有在当前工作目录下面,要记得构建路径,防止lstat函数出现错误。
  3. 输出的时候要计算列宽,不能自信指定多少的列宽够用,不然会出现严重的排版错误。
  4. 设置颜色之后要立即取消掉,不然在个别终端配色会导致空格上有颜色,所有情况下都会使l参数显示时下一个文件的权限等信息染上颜色。
  5. 任何可能出错的地方记得要有合理的错误处理,不然将很难定位到代码出现问题的具体地点。
  6. 关于调试:可以尝试着在printf纠错法找到具体函数之后,依次的在句子后面加上fprintf(stderr, "xxx\n", ...); sleep(50);来定位错误地点,若不sleep错误信息可能就跟其他输出混淆在一起。
  7. 做的还不够好啊QAQ。
  8. 所以说,它是长这样的:
    列表显示
    终端比较宽时
    终端比较窄时
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值