[Linux C/C++] 实现ls -R的命令

文章目录

  • 一、读取目录
  • 二、分析选项
  • 三、递归实现-R
  • 四、输出颜色

linux编程 实现ls -R的命令

关于C++语言的基础知识可以点开笔者主页专栏【跟我一起从C到C++】
使用到的知识点:
3.1.1 bool类型
4.1 string
6.2 异常处理

ls命令,想必大家在使用Linux时,都感觉十分亲切,日常必用命令之一:

它可以罗列文件夹和文件,让人们更加清晰地管理自己的文件。

ls -R更是夸张,可以将每个文件夹下的文件(以及文件夹下的文件夹,无限套娃)都列出来:
[Linux C/C++] 实现ls -R的命令_第1张图片

一、读取目录

实现ls的第一步就是读取目录

读取目录之前,肯定要先打开目录:

DIR * opendir(const char * name);

opendir用来打开参数name指定的目录, 并返回DIR*形态的目录流, 和open(老朋友了)类似, 接下来对目录的读取和搜索都要使用此返回值。

参阅资料,我们知道了readdir函数可以实现读取目录的功能,于是我们man readdir
[Linux C/C++] 实现ls -R的命令_第2张图片
readdir返回值:成功则返回下个目录进入点;有错误发生或读取到目录文件尾则返回NULL
dirent定义如下:

struct dirent
{
    ino_t d_ino; //d_ino 此目录进入点的inode
    ff_t d_off; //d_off 目录文件开头至此目录进入点的位移
    signed short int d_reclen; //d_reclen _name 的长度, 不包含NULL 字符
    unsigned char d_type; //d_type d_name 所指的文件类型 d_name 文件名
    har d_name[256];
};

dirent的成员d_name即是文件/文件夹名称,我们即将用到。

我们先引入头文件

#include 

我们编写一个读取目录:

	std::string file_name;
	cin >> file_name;
	DIR   *dir;
	dir = opendir(file_name.c_str());
	if(dir == nullptr)
	{
		cout << "opendir is NULL" << endl;
		return -1;
	}
	
	dirent *dp;
    while ((dp = readdir(dir)) != nullptr)
    {
        cout << dp->d_name << endl;
    }
    closedir(dir);
    return 0;

我们再进行一次封装,并用上错误检测和处理:

#include 
using std::runtime_error;
DIR *My_OpenDir(const char *path)
{
    DIR *dir;
    try
    {
        dir = opendir(path);
        if (dir == nullptr)
        {
            throw runtime_error("opendir error: DIR* is NULL");
        }
        
    }
    catch(const std::exception& e)
    {
        std::cerr << e.what() << '\n';
        return nullptr;
    }
    return dir;
}

我们再引入头文件

#include 

这样就能用return EXIT_SUCCESS;表示成功结束
return EXIT_FAILURE;表示错误结束
于是:

	DIR *dir;
    dir = My_OpenDir(file_name.c_str());
    if (dir == nullptr) goto fail_opendir;

    dirent *dp;
    while ((dp = readdir(dir)) != NULL)
    {
        cout << dp->d_name << endl;
    }
    closedir(dir);

    return EXIT_SUCCESS;

fail_opendir:
    return EXIT_FAILURE;
}

二、分析选项

getopt 方法是用来分析命令行参数的,该方法由Linux标准库提供,包含在头文件中。

int getopt(int argc, char * const argv[], const char *optstring);
 
extern char *optarg;
extern int optind, opterr, optopt;

getopt参数说明:
argc:通常由 main 函数直接传入,表示参数的数量
argv:通常也由 main 函数直接传入,表示参数的字符串变量数组
optstring:一个包含正确的参数选项字符串,用于参数的解析。例如 “abc:”,其中 -a-b 就表示两个普通选项,-c 表示一个必须有参数的选项,因为它后面有一个冒号

外部变量说明:
optarg:如果某个选项有参数,这包含当前选项的参数字符串
optindargv 的当前索引值
opterr:正常运行状态下为 0。非零时表示存在无效选项或者缺少选项参数,并输出其错误信息
optopt:当发现无效选项字符时,即 getopt方法返回 ? 字符,optopt 中包含的就是发现的无效选项字符。

故,我们引入头文件:

#include  

使用bool类型的真假表示某个选项是否被输入:

struct option_t
{
    bool R = false;
    bool help = false;
};

option_t opt;

编写函数:

int analyseOpt(int argc, char* argv[])
{
    
    try
    {
        char c;
        while((c = getopt(argc, argv, "Rh")) != -1)
        {
            switch (c) 
            {
                case 'R':
                    opt.R = true;
                    break;
                case 'h':
                    opt.help = true;
                    break;
                default:
                    throw runtime_error("Unknown options:" + std::string(1, optopt));
            }
        }
    }
    catch(const std::exception& e)
    {
        std::cerr << "Error " << e.what() << '\n';
        goto fail_opt;
    }
    
    return EXIT_SUCCESS;
fail_opt:
    return EXIT_FAILURE;
}

于是我们编写我们的main函数,提前用ls_help()(一般不会出错,故不添加错误检测和处理,返回值为void)和ls_R()占好位置,搭好框架:

#include 
#include 
#include 
#include 
#include  

using std::runtime_error;
using std::cout;    using std::endl;

struct option_t
{
    bool R = false;
    bool help = false;
};

option_t opt;

int analyseOpt(int argc, char* argv[]);
void ls_help();//还没想好,未定义
int ls_R();//还没想好,未定义,参数也没有添加
DIR *My_OpenDir(const char *path);

int main(int argc, char **argv)
{
    std::string file_name;
    bool exist_dir = false;//用户是否提供目录
    if(analyseOpt(argc, argv) != EXIT_SUCCESS)
        goto fail_analyseOpt;

    for (size_t i = 1; i < argc; ++i)
    {
        if(argv[i][0] == '-')
            continue;//假如是选项就下一个
        exist_dir = true;//发现了目录
        file_name = argv[i];//把目录给file_name
    }

    if (opt.help)
    {
        ls_help();
        return EXIT_SUCCESS;
    }
    
    if (!exist_dir)
    {
        file_name = ".";//没有提供目录,就当前目录
    }

    if (opt.R)
    {
        if(ls_R() == EXIT_SUCCESS)
            return EXIT_SUCCESS;
        else 
            return EXIT_FAILURE;
    }

    DIR *dir;
    dir = My_OpenDir(file_name.c_str());
    if (dir == nullptr) goto fail_opendir;


    dirent *dp;
    while ((dp = readdir(dir)) != NULL)
    {
        cout << dp->d_name << endl;
    }
    closedir(dir);

    return EXIT_SUCCESS;

fail_opendir:
fail_analyseOpt:
    return EXIT_FAILURE;
}

DIR *My_OpenDir(const char *path)
{
    DIR *dir;
    try
    {
        dir = opendir(path);
        if (dir == nullptr)
        {
            throw runtime_error("opendir error: DIR* is NULL");
        }
        
    }
    catch(const std::exception& e)
    {
        std::cerr << e.what() << '\n';
        return nullptr;
    }
    return dir;
}


int analyseOpt(int argc, char* argv[])
{
    
    try
    {
        char c;
        while((c = getopt(argc, argv, "Rh")) != -1)
        {
            switch (c) 
            {
                case 'R':
                    opt.R = true;
                    break;
                case 'h':
                    opt.help = true;
                    break;
                default:
                    throw runtime_error("Unknown options:" + std::string(1, optopt));
            }
        }
    }
    catch(const std::exception& e)
    {
        std::cerr << "Error " << e.what() << '\n';
        goto fail_opt;
    }
    
    return EXIT_SUCCESS;
fail_opt:
    return EXIT_FAILURE;
}

因为-h提供帮助信息这里比较简单,故可以里面完成:

void ls_help()
{
    cout << "help:\n" 
             << "-h         help" << "\n"
             << "-r         list dir" << endl;
}

三、递归实现-R

我们可以编写一个一直检测当前目录path的函数,它第一次先遍历整个目录流并展示名字,第二次则开始判断目录流的类型,如果是文件就跳过,如果是文件夹name就修改路径为path/name,并递归调用函数。

查阅资料得知stat能获取与文件系统及文件相关的许多信息,位于头文件的它拥有结构:

struct stat
{
    dev_t     st_dev;     /* ID of device containing file */文件使用的设备号
    ino_t     st_ino;     /* inode number */    索引节点号 
    mode_t    st_mode;    /* protection */  文件对应的模式,文件,目录等
    nlink_t   st_nlink;   /* number of hard links */    文件的硬连接数  
    uid_t     st_uid;     /* user ID of owner */    所有者用户识别号
    gid_t     st_gid;     /* group ID of owner */   组识别号  
    dev_t     st_rdev;    /* device ID (if special file) */ 设备文件的设备号
    off_t     st_size;    /* total size, in bytes */ 以字节为单位的文件容量   
    blksize_t st_blksize; /* blocksize for file system I/O */ 包含该文件的磁盘块的大小   
    blkcnt_t  st_blocks;  /* number of 512B blocks allocated */ 该文件所占的磁盘块  
    time_t    st_atime;   /* time of last access */ 最后一次访问该文件的时间   
    time_t    st_mtime;   /* time of last modification */ /最后一次修改该文件的时间   
    time_t    st_ctime;   /* time of last status change */ 最后一次改变该文件状态的时间   
};

我们用stat函数或者lstat函数来获取linux操作系统下文件的属性。
stat函数和lstat函数的区别:

  • 对于普通文件, 这两个函数没有区别, 是一样的.
  • 对于连接文件,调用lstat函数获取的是链接文件本身的属性信息;而stat函数获取的是链接文件指向的文件的属性信息

这里就主要介绍一下stat
函数原型: int stat(const char *pathname,struct stat *buf);

参数:第一个参数为传入参数,pathname为文件的绝对路径或相对路径。第二参数为传出参数,一个struct stat类型的结构体指针。传出参数可以采用下边两种方法,定义结构体变量struct stat st,或定义结构体指针变量strut stat *st = &st(注意这里一定要进行初始化,说明其为一块有效的内存空间),相对而言,使用结构体变量更为方便。

参考:
linux系统调用函数 lstat–获取文件属性


stat结构中的st_mode 则定义了下列情况:(可以使用类似if (s.st_mode & S_IXUSR)这样的方式)

	st_mode的情况:
 	S_IFMT   0170000    文件类型的位遮罩
    S_IFSOCK 0140000    套接字
    S_IFLNK 0120000     符号连接
    S_IFREG 0100000     一般文件
    S_IFBLK 0060000     区块装置
    S_IFDIR 0040000     目录
    S_IFCHR 0020000     字符装置
    S_IFIFO 0010000     先进先出
​
    S_ISUID 04000     文件的(set user-id on execution)位
    S_ISGID 02000     文件的(set group-id on execution)位
    S_ISVTX 01000     文件的sticky位
​
    S_IRUSR(S_IREAD) 00400     文件所有者具可读取权限
    S_IWUSR(S_IWRITE)00200     文件所有者具可写入权限
    S_IXUSR(S_IEXEC) 00100     文件所有者具可执行权限
​
    S_IRGRP 00040             用户组具可读取权限
    S_IWGRP 00020             用户组具可写入权限
    S_IXGRP 00010             用户组具可执行权限
​
    S_IROTH 00004             其他用户具可读取权限
    S_IWOTH 00002             其他用户具可写入权限
    S_IXOTH 00001             其他用户具可执行权限
​
    上述的文件类型在POSIX中定义了检查这些类型的宏定义:
    S_ISLNK (st_mode)    判断是否为符号连接
    S_ISREG (st_mode)    是否为一般文件
    S_ISDIR (st_mode)    是否为目录
    S_ISCHR (st_mode)    是否为字符装置文件
    S_ISBLK (s3e)        是否为先进先出
    S_ISSOCK (st_mode)   是否为socket
    若一目录具有sticky位(S_ISVTX),则表示在此目录下的文件只能被该文件所有者、此目录所有者或root来删除或改名,在linux中,最典型的就是这个/tmp目录啦。

注意,在linux操作系统下,一切皆文件。文件共有七种类型,分别是普通文件、目录文件、管道文件、可执行文件、压缩文件、设备文件(字符、管道和块)和其他文件。

参见:
Linux系统函数之文件系统管理(二)

而我们主要使用S_ISDIR(),判断其是否为目录文件:

int ls_R(std::string path)
{
	//普通的罗列目录
    cout << path + ":"<<endl;
    DIR *dir;
    dir = My_OpenDir(path.c_str());
    if (dir == nullptr) goto fail_Myopendir1;
    dirent *dp;
    while ((dp = readdir(dir)) != NULL)
    {
        if (dp->d_name[0] == '.')
            continue;
        cout << dp->d_name << endl;
    }
    closedir(dir);
	//检查是否为目录,是就更改路径,递归调用
    dir = My_OpenDir(path.c_str());
    if (dir == nullptr) goto fail_Myopendir2;
    while ((dp = readdir(dir)) != NULL)
    {
        if (dp->d_name[0] == '.')
            continue;
        struct stat file_stat;
        std::string temp;
        temp = path + "/" + dp->d_name;//更改路径
        try
        {
            if (stat(temp.c_str(), &file_stat) == -1)
                throw runtime_error(temp + ":get file stat error");
        }
        catch(const std::exception& e)
        {
            std::cerr << e.what() << '\n';
            goto fail_getstat;
        }
        if(S_ISDIR(file_stat.st_mode))//判断是否为目录,如果是目录就进入递归
        {
            ls_R(temp);
        }
        
    }
    closedir(dir);//不要忘记关闭dir
    return EXIT_SUCCESS;
fail_getstat:
fail_Myopendir2:
    closedir(dir);   
fail_Myopendir1:
    return EXIT_FAILURE;
}

四、输出颜色

我们仔细观察ls命令,发现不同文件类型有不同的颜色,笔者这里就举两个例子来实现:

  • 当文件为可执行文件时,它是亮绿色的
  • 当文件为目录时,它是亮蓝色的

具体颜色可以参考:
超级终端的字体背景和颜色显示

调配颜色的方法就是:
输出一段咒语:
C语言

printf("\033[32m");//绿色

或C++

cout << "\033[1m\033[32m";

于是我们提前定义好宏:

#define LightGreen {cout << "\033[1m\033[32m" << std::flush;}
#define LightBlue {cout << "\033[1m\033[34m" << std::flush;}
#define CLOSE {cout << "\033[0m" << std::flush;}

接着判断文件类型:
回顾 判断文件类型的方法

	if(S_ISDIR(file_stat.st_mode))//判断是否为目录
	{
		LightBlue;
        cout << dp->d_name << " ";
        CLOSE;
    }
    else if (file_stat.st_mode & S_IXUSR)//如果是可执行文件
    {
        LightGreen;
        cout << dp->d_name << " ";
        CLOSE;
    }
    else  
        cout << dp->d_name << " ";

结尾附上全文以供参考:

#include 
#include 
#include 
#include 
#include  
#include 

#define LightGreen {cout << "\033[1m\033[32m" << std::flush;}
#define LightBlue {cout << "\033[1m\033[34m" << std::flush;}
#define CLOSE {cout << "\033[0m" << std::flush;}


using std::runtime_error;
using std::cout;    using std::endl;

struct option_t
{
    bool R = false;
    bool help = false;
};

option_t opt;

int analyseOpt(int argc, char* argv[]);
void ls_help();
int ls_R(std::string path);
DIR *My_OpenDir(const char *path);

int main(int argc, char **argv)
{
    std::string file_name;
    bool exist_dir = false;
    if(analyseOpt(argc, argv) != EXIT_SUCCESS)
        goto fail_analyseOpt;

    for (size_t i = 1; i < argc; ++i)
    {
        if(argv[i][0] == '-')
            continue;
        exist_dir = true;
        file_name = argv[i];
    }

    if (opt.help)
    {
        ls_help();
        return EXIT_SUCCESS;
    }
    
    if (!exist_dir)
    {
        file_name = ".";
    }

    if (opt.R)
    {
        if(ls_R(file_name) == EXIT_SUCCESS)
            return EXIT_SUCCESS;
        else 
            return EXIT_FAILURE;
    }

    DIR *dir;
    dir = My_OpenDir(file_name.c_str());
    if (dir == nullptr) goto fail_opendir;


    dirent *dp;
    while ((dp = readdir(dir)) != NULL)
    {
         if (dp->d_name[0] == '.')
            continue;
        struct stat file_stat;
        std::string temp;
        temp = file_name + "/" + dp->d_name;
        try
        {
            if (stat(temp.c_str(), &file_stat) == -1)
                throw runtime_error(temp + ":get file stat error");
        }
        catch(const std::exception& e)
        {
            std::cerr << e.what() << '\n';
            goto fail_getstat;
        }
        if(S_ISDIR(file_stat.st_mode))//判断是否为目录,如果是目录就进入递归
        {
            LightBlue;
            cout << dp->d_name << " ";
            CLOSE;
        }
        else if (file_stat.st_mode & S_IXUSR)//如果是可执行文件
        {
            LightGreen;
            cout << dp->d_name << " ";
            CLOSE;
        }
        else  
            cout << dp->d_name << " ";
    }
    cout << endl;
    closedir(dir);

    return EXIT_SUCCESS;

fail_getstat:
    closedir(dir);
fail_opendir:
fail_analyseOpt:
    return EXIT_FAILURE;
}

DIR *My_OpenDir(const char *path)
{
    DIR *dir;
    try
    {
        dir = opendir(path);
        if (dir == nullptr)
        {
            throw runtime_error("opendir error: DIR* is NULL");
        }
        
    }
    catch(const std::exception& e)
    {
        std::cerr << e.what() << '\n';
        return nullptr;
    }
    return dir;
}


int analyseOpt(int argc, char* argv[])
{
    
    try
    {
        char c;
        while((c = getopt(argc, argv, "Rh")) != -1)
        {
            switch (c) 
            {
                case 'R':
                    opt.R = true;
                    break;
                case 'h':
                    opt.help = true;
                    break;
                default:
                    throw runtime_error("Unknown options:" + std::string(1, optopt));
            }
        }
    }
    catch(const std::exception& e)
    {
        std::cerr << "Error " << e.what() << '\n';
        goto fail_opt;
    }
    
    return EXIT_SUCCESS;
fail_opt:
    return EXIT_FAILURE;
}



void ls_help()
{

    cout << "help:\n" 
             << "-h         help" << "\n"
             << "-r         list dir" << endl;
}



int ls_R(std::string path)
{
    cout << path + ":"<<endl;
    DIR *dir;
    dir = My_OpenDir(path.c_str());
    if (dir == nullptr) goto fail_Myopendir1;
    dirent *dp;
    while ((dp = readdir(dir)) != NULL)
    {
        if (dp->d_name[0] == '.')
            continue;
        struct stat file_stat;
        std::string temp;
        temp = path + "/" + dp->d_name;
        try
        {
            if (stat(temp.c_str(), &file_stat) == -1)
                throw runtime_error(temp + ":get file stat error");
        }
        catch(const std::exception& e)
        {
            std::cerr << e.what() << '\n';
            goto fail_getstat;
        }
        if(S_ISDIR(file_stat.st_mode))//判断是否为目录,如果是目录就进入递归
        {
            LightBlue;
            cout << dp->d_name << " ";
            CLOSE;
        }
        else if (file_stat.st_mode & S_IXUSR)//如果是可执行文件
        {
            LightGreen;
            cout << dp->d_name << " ";
            CLOSE;
        }
        else  
            cout << dp->d_name << " ";
    }
    cout << endl;
    closedir(dir);

    dir = My_OpenDir(path.c_str());
    if (dir == nullptr) goto fail_Myopendir2;
    while ((dp = readdir(dir)) != NULL)
    {
        if (dp->d_name[0] == '.')
            continue;
        struct stat file_stat;
        std::string temp;
        temp = path + "/" + dp->d_name;
        try
        {
            if (stat(temp.c_str(), &file_stat) == -1)
                throw runtime_error(temp + ":get file stat error");
        }
        catch(const std::exception& e)
        {
            std::cerr << e.what() << '\n';
            goto fail_getstat;
        }
        if(S_ISDIR(file_stat.st_mode))//判断是否为目录,如果是目录就进入递归
        {
            ls_R(temp);
        }
    }
    closedir(dir);
    return EXIT_SUCCESS;
fail_getstat:
fail_Myopendir2:
    closedir(dir);   
fail_Myopendir1:
    return EXIT_FAILURE;

}

你可能感兴趣的:(记录一次编程,linux,c++,c语言)