从零开始实现一个自己的 Shell:mybash 项目实战

在学习了 Linux 系统编程和 C 语言之后,最好的巩固方式就是动手实践。今天,我们将从零开始,逐步实现一个简化版的 Shell ------ mybash。本系列将从最基础的命令行提示符开始,逐步深入,最终实现一个功能完整的命令行解释器。

整体项目的代码可以分为三大部分:输出命令行提示符、获取用户命令和参数、执行用户命令

在主函数中利用一个死循环实现重复响应

cpp 复制代码
#include <stdio.h>
int main()
{
    while (1)
    {
        // 1.命令行提示符输出

        // 2.获取用户命令

        // 3.执行用户命令
    }
}

一、命令行提示符

1、系统提示符分析

当我们打开 Linux 终端时,会看到类似这样的提示符:

它的结构非常清晰:登录用户名@主机名: 当前工作目录/路径$

  • wuya:我当前登录的用户名
  • wuya-virtual-machine:我的主机名
  • ~:当前工作目录(家目录的简写)
  • $:普通用户权限标识(如果是 # 则为 root 用户)

我们的第一个目标就是用 C 语言复现这个提示符。

2、核心函数

要实现这个提示符,我们需要获取三部分信息:登录用户名、主机名和当前工作目录。下面我们逐一解析所需的系统函数。

(1) 获取登录用户名:getlogin()

getlogin() 函数用于获取当前终端的登录用户名。

下图是在终端用 man getlogin 命令获取到的信息

  • 函数原型char *getlogin(void);
  • 返回值 :成功时返回一个指向以 \0 结尾的用户名字符串的指针;失败时返回 NULL,并设置 errno
  • 原理 :该函数通过读取系统记录登录信息的文件(如 /var/run/utmp)来获取用户名。
  • 头文件:<unistd.h>

使用示例:

cpp 复制代码
char *loginname = getlogin();
if (loginname == NULL) {
    perror("getlogin failed");
    exit(EXIT_FAILURE);
}

(2) 获取主机名:uname()

uname() 是一个系统调用,用于获取内核和系统的详细信息,其中就包括主机名。

下图为通过 man 2 uname 命令获取到的详细信息

  • 函数原型int uname(struct utsname *buf);
  • 返回值 :成功时返回非负整数;失败时返回 -1,并设置 errno
  • 头文件:<sys/utsname.h>
  • 结构体 struct utsname
cpp 复制代码
struct utsname {
    char sysname[];    // 操作系统名称,如 "Linux"
    char nodename[];   // 主机名,即我们需要的 hostname
    char release[];    // 内核发行版号
    char version[];    // 内核版本
    char machine[];    // 硬件架构,如 "x86_64"
};

使用示例:

cpp 复制代码
struct utsname sysinfo;
if (uname(&sysinfo) == -1) {
    perror("uname failed");
    exit(EXIT_FAILURE);
}
// 主机名存储在 sysinfo.nodename 中

(3) 获取路径

getcwd() 函数用于获取当前进程的工作目录的绝对路径。

通过 man 2 getcwd查看详细信息:

  • 函数原型char *getcwd(char *buf, size_t size);
  • 参数
    • buf:指向用于存储路径的缓冲区。
    • size:缓冲区的大小(字节数)。
  • 返回值 :成功时返回指向路径字符串的指针;失败时返回 NULL,并设置 errno
  • 错误处理 :如果路径长度(包括 \0)超过缓冲区大小 size,函数会返回 NULL 并设置 errnoERANGE,表示缓冲区不足。
  • 头文件:<unistd.h>

使用示例

cpp 复制代码
char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) == NULL) {
    perror("getcwd failed");
    exit(EXIT_FAILURE);
}

3、代码实现与优化

利用上面的三个函数,就可以实现基础的系统提示符的输出,代码如下:

为了让函数不一直输出,我们先加上简单的获取用户命令的fgets语句

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/utsname.h>

// 输出命令行提示符
void PrintPrompt()
{
    // 获取主机名
    struct utsname sysinfo; // 创建结构体
    // 调用函数,获取内核和系统的详细信息
    if (uname(&sysinfo) == -1)
    {
        // 获取失败
        perror("get host info failed:");
        printf("mybash> "); // 在提示符最前面加上mybash>,和系统终端区分开
        fflush(stdout);     // 刷新缓冲区
        return;
    }
    // 获取目录/路径
    char buf[128] = {0};          // 存储路径的缓冲区
    char *ptr = getcwd(buf, 127); // 获取当前进程的工作目录的绝对路径
    if (ptr == NULL)              // 获取失败
    {
        perror("get current working directory failed:");
        printf("mybash> ");
        fflush(stdout);
        return;
    }
    // 输出
    printf("%s@%s:%s$ ", getlogin(), sysinfo.nodename, buf);
}
int main()
{
    while (1)
    {
        // 1.系统提示符输出
        PrintPrompt();
        // 2.获取用户命令
        char cmd[128] = {0};
        char *s = fgets(cmd, 127, stdin); // 获取用户命令
        if (s == NULL)
        {
            continue; // 获取失败,直接进行下一次获取
        }
        printf("cmd=%s\n", cmd);
        // 3.执行用户命令
    }
}

编译链接后的运行结果

优化改进

参照系统终端,我们可以做出以下优化,使得我们自己的终端更完善

  • 直接显示目录,而非绝对路径
  • root用户显示 # 而非 $
  • 用户名不用 getlogin() 获取
  • 家目录输出~
(1) 直接显示当前目录

不做特殊设置,系统终端的冒号后显示的是当前路径,若想要只显示当前目录,需要设置,设置的方法可以自行百度。

直接显示的目录(当前目录)就是当前路径的最后一个字符串,而路径是由 / 分割开的,所以直接获取最后一个 / 后的字符串即可。

cpp 复制代码
 // 获取目录
    char *s = NULL;
    s = buf + sizeof(buf); // 将s移动到buf末尾
    // 将s向前移动至最后一个'/'位置
    while (*s != '/')
    {
        s--;
    }
    // 输出
    // s+1开始的字符串就是当前目录
    printf("%s@%s:%s$ ", getlogin(), sysinfo.nodename, s + 1);
(2) 用户名不用 getlogin() 获取

如果是root用户,用getlogin()函数获取到的登录用户名还是原本的用户名,不会输出root,所以,需要改变获取用户名的方式------使用函数 getpwuid()

getpwuid() 会++根据你传入的用户 ID++ (uid),去系统的密码数据库(通常是 /etc/passwd)中++查找对应的用户记录++ ,并返回一个指向 struct passwd 结构体的指针,里面包含了该用户的详细信息。

函数 getuid() 可以获取到当前进程的用户ID

所以我们可以先用 getuid() 函数获取到用户id,再使用 getpwuid()函数获取到用户的详细信息,其中的pw_name就是对应的用户名

(getpwuid函数所需的头文件:<sys/type.h> 和 <pwd.h> )

cpp 复制代码
#include <sys/types.h>
#include <pwd.h>

......
// 获取uid
    uid_t uid = getuid();
    // 获取用户名
    struct passwd *p = getpwuid(uid); // p->pw_name为用户名
    if (p == NULL)
    {
        perror("get passwd info faild:");
        printf("mybash> ");
        fflush(stdout);
        return;
    }
......
(3) root用户显示 # 而非 $

使用 sudo -i 命令输入密码后切换到 root的登录环境,可以看出,最后的符合是'#',而不是'$',所以我们可以针对这一点进行优化

基于获取用户名方式的改变,有两种方法可以实现 # 和 $ 的选择

**法一:**比较用户名和"root"

cpp 复制代码
//$和#的选择
    char ch = '$'; // 默认为'$'
//  如果登录用户名等于root,就输出'#'
    if (strcmp(p->pw_name, "root") == 0)
    {
        ch = '#';
    }

**法二:**root的uid为0

cpp 复制代码
//$和#的选择
    char ch = '$'; // 默认为'$'
    // root的uid为0
    if (uid == 0)
    {
        ch = '#';
    }

最后的输出语句

cpp 复制代码
printf("%s@%s:%s%c ", p->pw_name, sysinfo.nodename, s + 1, ch);
(4) 处理特殊情况:~和/

如图,当路径为家目录时,系统会用"~"代替,所以我们也可以对家目录的输出进行优化

使用 getpwuid() 函数获取到的passwd结构体里面有一个字段就是家目录

所以可以直接使用pw_dir获取即可,然后和获取到的路径比较即可

cpp 复制代码
// 显示当前目录
    char *s = NULL;
    if (strcmp(buf, p->pw_dir) == 0)//当前路径为家目录
    {
        s = "~";//输出~
    }
    else //不是家目录,正常输出
    {
        s = buf + sizeof(buf); // 将s移动到buf末尾
        // 将s向前移动至最后一个'/'位置
        while (*s != '/')
        {
            s--;
        }
        // s+1开始的字符串就是当前目录
        s++;
    }
    // 输出
    printf("mybash> %s@%s:%s%c ", p->pw_name, sysinfo.nodename, s, ch);
    fflush(stdout);

另外,如果所处的绝对路径为" / ",通过上面的代码 s++会出现越界的问题,所以这种情况也需要特殊处理:当当前路径为" / "时,直接输出s即可。

cpp 复制代码
// 显示当前目录
    char *s = NULL;
    if (strcmp(buf, p->pw_dir) == 0)//当前路径为家目录
    {
        s = "~";//输出~
    }
    else //不是家目录,正常输出
    {
        s = buf + sizeof(buf); // 将s移动到buf末尾
        // 将s向前移动至最后一个'/'位置
        while (*s != '/')
        {
            s--;
        }
        //当前绝对路径为"/"时,直接输出s
        // s+1开始的字符串就是当前目录
        if(strlen(s)!=1){
            s+=1;
        }
    }
    // 输出
    printf("mybash> %s@%s:%s%c ", p->pw_name, sysinfo.nodename, s, ch);
(5) 提示符高亮显示

系统终端的提示符有些部分是彩色高亮显示的,要实现这个功能,使用printf本身就可以实现。参考:https://www.cnblogs.com/GrootStudy/p/17025354.html

cpp 复制代码
printf("mybash> \033[1;32m%s@%s\033[0m\033[1;34m:%s\033[0m%c ", p->pw_name, sysinfo.nodename, s, ch);

4、代码

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/utsname.h>
#include <string.h>
#include <sys/types.h>
#include <pwd.h>

// 输出命令行提示符
void PrintPrompt()
{
    //$和#的选择
    char ch = '$'; // 默认为'$'
    // 获取uid
    uid_t uid = getuid();
    // 获取用户名
    struct passwd *p = getpwuid(uid); // p->pw_name为用户名
    if (p == NULL)
    {
        perror("get passwd info faild:");
        printf("mybash> ");
        fflush(stdout);
        return;
    }
    // 法一
    //  如果登录用户名等于root,就输出'#'
    /*if (strcmp(p->pw_name, "root") == 0)
    {
        ch = '#';
    }*/
    // 法二
    // root的uid为0
    if (uid == 0)
    {
        ch = '#';
    }

    // 获取主机名
    struct utsname sysinfo; // 创建结构体
    // 调用函数,获取内核和系统的详细信息
    if (uname(&sysinfo) == -1)
    {
        // 获取失败
        perror("get host info failed:");
        printf("mybash> "); // 在提示符最前面加上mybash>,和系统终端区分开
        fflush(stdout);     // 刷新缓冲区
        return;
    }
    // 获取路径
    char buf[128] = {0};          // 存储路径的缓冲区
    char *ptr = getcwd(buf, 127); // 获取当前进程的工作目录的绝对路径
    if (ptr == NULL)              // 获取失败
    {
        perror("get current working directory failed:");
        printf("mybash> ");
        fflush(stdout);
        return;
    }
    // 显示当前目录
    char *s = NULL;
    if (strcmp(buf, p->pw_dir) == 0)//当前路径为家目录
    {
        s = "~";//输出~
    }
    else //不是家目录,正常输出
    {
        s = buf + sizeof(buf); // 将s移动到buf末尾
        // 将s向前移动至最后一个'/'位置
        while (*s != '/')
        {
            s--;
        }
        //当前绝对路径为"/"时,直接输出s
        // s+1开始的字符串就是当前目录
        if(strlen(s)!=1){
            s+=1;
        }
    }
    // 输出
    printf("mybash> \033[1;32m%s@%s\033[0m\033[1;34m:%s\033[0m%c ", p->pw_name, sysinfo.nodename, s, ch);
    fflush(stdout);
}
int main()
{
    while (1)
    {
        // 1.系统提示符输出
        PrintPrompt();
        // 2.获取用户命令
        char cmd[128] = {0};
        char *s = fgets(cmd, 127, stdin); // 获取用户命令
        if (s == NULL)
        {
            continue; // 获取失败,直接进行下一次获取
        }
        printf("cmd=%s\n", cmd);
        // 3.执行用户命令
    }
}

二、分离用户命令和参数

在第一部分我们初步通过fgets()函数获取到用户输入的字符串cmd,实现了获取用户输入并输出的功能,接下来对该功能进行完善。

系统终端通过获取用户输入,针对输入的命令以及参数来实现功能,所以我们也需要将命令和参数分离开来,命令用来对应需要实现的功能,参数作为命令的补充实现。

1、核心函数 strtok()

通过strtok() 函数可以将获取到的cmd字符串分割。

(1) 函数作用

strtok() 会把一个字符串按照指定的分隔符,切割成一系列非空的 token(子串)

(2) 核心用法规则

  • 首次调用:必须传入要分割的字符串和分隔符
  • 后续调用:要继续分割同一个字符串,字符串内容必须传NULL,函数会自动从上次结束的位置继续
  • 分隔符:可以是一个包含多个字符的字符串,函数会把其中任意一个字符都当作分隔符。在后续调用中,可以更换不同的分隔符

(3) 返回值

  • 每次调用返回一个以\0结尾的token字符串的指针,这个token不包含分隔符
  • 当没有更多token时,返回NULL

(4) 内部机制

  • 函数内部维护了一个静态指针,用来记录下一次分割的起始位置
  • 首次调用时,这个指针指向字符串的开头
  • 后续调用时,函数会从这个指针开始扫描,跳过所有分隔符,找到下一个非分隔符字节作为新token的起点,直到再次遇到分隔符
  • 如果扫描到字符串末尾都没有找到新token,返回NULL

2、代码实现

cpp 复制代码
// 命令和参数总数
#define CMD_NUM 10

// 分割用户命令和参数
int SplitCmd(char *cmd, char *cmdArr[])
{
    // 首次分割:传递字符串
    // 以空格为标准进行分割
    char *token = strtok(cmd, " ");
    int count = 0; // 数组索引
    while (token != NULL)
    {
        // 将分割得到的字符串保存在cmdArr数字中
        cmdArr[count++] = token;
        // 继续分割,传递的字符串为NULL
        token = strtok(NULL, " ");
    }
    // 返回字符串个数
    return
}
cpp 复制代码
int main()
{
    while (1)
    {
        // 1.系统提示符输出
        PrintPrompt();
        // 2.获取用户命令
        char cmd[128] = {0};
        char *s = fgets(cmd, 127, stdin); // 获取用户命令
        if (s == NULL)
        {
            continue; // 获取失败,直接进行下一次获取
        }
        cmd[strlen(cmd) - 1] = 0;
        char *cmdArr[CMD_NUM] = {0}; // 记录分割后的输入
        // 进行分割
        int count = SplitCmd(cmd, cmdArr);
        // 没有输入,直接进行下一次获取
        if (count == 0)
        {
            continue;
        }
        // 输出命令和参数------验证
        for (int i = 0; i < count; i++)
        {
            printf("cmd:%s\n", cmdArr[i]);
        }
        // 3.执行用户命令
    }
}

3、运行结果

三、执行用户命令

内置命令 VS 外置命令

首先需要了解一下系统命令分为两种:内置bash的命令和外置命令。外置命令可以通过 which + 命令查到命令所在的文件/目录,如 ls 命令,而内置bash命令不行,如cd。

外置命令一般放在bin目录内,所以我们自己实现时写可以创建一个 mybin 目录装外置命令实现函数。内置命令直接放在 mybash.c 中即可。

具体区别

维度 内置命令(Built-in) 外置命令(External Command)
本质 Shell 程序本身的一部分(函数 / 指令) 独立的可执行文件(二进制 / 脚本)
执行方式 由 Shell 直接执行,不创建子进程 Shell 会 fork 子进程,再 exec 加载执行
速度 极快(无需磁盘 IO、无进程创建开销) 较慢(需要加载文件、创建子进程)
作用域 影响当前 Shell 进程(如 cd 仅影响子进程(无法改变父 Shell 状态)
查找方式 无需 PATH,Shell 内部直接识别 依赖 PATH 环境变量查找可执行文件

我们先实现cd和exit两个内置命令。

1、运行命令函数+exit命令的实现

编写一个函数,在函数中根据用户输入的命令进行不同的操作

在主函数中已经获取到了用户输入,且对输入进行了分割,cmdArr[0]就是命令,将cmdArr[0]和exit等命令对比,就能针对不同情况进行处理。

可以先得出一个大致框架如下:exit命令直接调用系统exit函数即可。

cpp 复制代码
#include<stdlib.h> //exit需要的头文件

// 执行系统命令
void RunCmd(char *cmdArr[], int count)
{
    char* cmd = cmdArr[0];
    if (strcmp(cmd, "cd") == 0)
    {
        printf("cd->%s\n", cmd);
    }
    else if (strcmp(cmd, "exit") == 0)
    {
        exit(0);
    }
    else
    {
        printf("other cmd:%s\n", cmd);
    }
}

主函数调用该函数

cpp 复制代码
int main(){
    while(1){
        ......
        //3.执行用户命令
        RunCmd(cmdArr,count);
    }
    return 0;
}

2、cd 命令的实现

我们先实现基础的cd命令,后续在完善。

假设用户输入"cd 参数1",该命令实现的功能:从当前路径切换到参数1路径下,所以我们的主要工作是切换路径。

(1) 核心函数 chdir()

通过函数 chdir() 可以实现路径的切换,切换成功,函数返回0;失败,返回-1。

(2) 代码实现

情况分析

首先来分析cd命令的几种常见情况:

1.没有参数

如果没有参数,cd命令会将路径切换到家目录

所以目的路径为家目录,家目录的获取在命令行提示符实现部分已经说过:结构体的pw_dir字段对应的就是家目录,所以切换到 pw_dir即可。

2.参数为 ~

~代表家目录,和没有参数的情况一样,将路径切换为 pw_dir字段即可。

若 ~ 后还有参数,目录路径为:家目录/后面的参数。

3.一般情况

参数为一般的路径,直接切换即可

代码
cpp 复制代码
// cd命令的实现
void RunCd(char *path)//参数为传递的参数
{
    char buf[128]={0};
    struct passwd* p=getpwuid(getuid());
    if(p==NULL){
        printf("get pwuid info failed\n");
        return;
    }
    //没有参数
    if(path==NULL){
        path=p->pw_dir;//目的路径为家目录
    }
    //参数为~ + ~后面还有参数
    else if(strncmp(path,"~",1)==0){
        strcpy(buf,p->pw_dir);//令buf为家目录
        strcat(buf,path+1);//把剩余参数连接到buf上
        path=buf;//令path保存目的路径
    }
    //一般情况,直接切换路径
    //切换路径
    if (chdir(path) == -1)
    {
        perror("cd failed\n");
        return;
    }
}
......
if (strcmp(cmd, "cd") == 0)
    {
        RunCd(cmdArr[1]);//将参数传递给函数
    }
......
运行结果

3、退格键功能的实现

在运行mybash可执行文件时,会存在无法删除以前输入的命令的情况,这时需要自己实现删除功能。

终端默认有两种模式:

模式 特点 能否删除字符
规范模式(Canonical Mode) 终端会先缓存输入,直到按回车才把整行传给程序;同时终端内核会处理删除、退格、光标移动等编辑操作 可以删除(这是终端内核帮你做的,不是 fgets() 做的)
非规范模式(Non-Canonical Mode) 输入不缓存,字符直接传给程序;终端内核不处理任何编辑操作 无法删除(按删除键只会输入乱码 / 特殊字符)

无法删除的原因就是系统处于非标准模式。

(1) 核心结构体 termios

termios 函数族提供了一套通用的终端接口,用于控制异步通信端口(比如终端)的各种行为,包括输入模式、输出模式、控制字符等。

书籍《Linux程序设计》中写到:

(标准模式是规范模式的口语化描述)

可以通过 tcgetattr() 获取当前终端属性,修改 c_lflag 字段,开启 ICANON (规范模式)和 ECHO (回显),然后用 **tcsetattr()**应用修改,从而恢复终端的正常编辑功能。

(2) 核心函数

tcgetattr()

作用:读取终端属性

int tcgetattr(int fd, struct termios *termios_p);

参数 含义 你的 mybash 场景
fd 终端文件描述符(必须是终端设备,否则报错) STDIN_FILENO(值为 0),表示 "标准输入对应的终端"
termios_p 指向 struct termios 结构体的指针 用来存储读取到的终端属性(比如当前是否是规范模式、是否开启回显)

返回值:成功返回0;失败返回-1,并设置error

tcsetattr()

作用:设置终端属性

int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

参数 含义 你的 mybash 场景
fd tcgetattr(),终端文件描述符 同样用 STDIN_FILENO
optional_actions 生效时机 TCSANOW(立即生效)
termios_p 指向修改后的 struct termios 结构体 传入你修改过的属性(比如开启 ICANONECHO

返回值:成功返回0;失败返回-1,并设置error

(3) 代码实现

代码除了使用termios结构和相关函数,还使用到了printf函数

cpp 复制代码
#include<termios.h>

// 手动获取用户输入
void GetCmd(char *cmd)
{
    struct termios tcAttr, newtcAttr; // 创建两个结构体变量
    if (tcgetattr(0, &tcAttr) == -1)  // 获取当前终端属性
    {
        perror("get tcAttr failed\n");
        return;
    }
    newtcAttr = tcAttr; // 将属性复制给newtcAttr
    // 改变newtcAttr的信息
    newtcAttr.c_lflag &= ~ICANON; // 关闭规范模式
    newtcAttr.c_lflag &= ~ECHO;   // 取消回显功能
    // 应用newtcAttr的属性
    if (tcsetattr(0, TCSANOW, &newtcAttr) == -1)
    {
        perror("set tcAttr failed\n");
        return;
    }

    int index = 0;
    char ch = 0;
    while ((ch = getchar()) != '\n') // 注意符号的优先级
    {
        if (ch == 8) // 对应Backspace(退格)
        {
            if (index >= 1) // 边界处理
            {
                // 如果用户按下Backspace,则舍弃命令的最后一个字符
                cmd[--index] = 0;
                // 实现删除操作的显示
                printf("\033[1D"); // 光标左移1列
                printf("\033[K");  // 删除光标到行尾的内容
                fflush(stdout);
            }
        }
        else
        {
            // 反之,显示按下的字符
            // 这里属于手动实现回显功能
            cmd[index++] = ch; // 将输入的字符记录到cmd中
            printf("%c", ch);
            fflush(stdout);
        }
    }

    // 改为终端的设置
    if (tcsetattr(0, TCSANOW, &tcAttr) == -1)
    {
        perror("set tcAttr failed\n");
        return;
    }

    // 输出换行符,让提示符在下一行显示
    printf("\n");
}

int main()
{
    while (1)
    {
        ......
        char cmd[128] = {0};
        GetCmd(cmd);                 // 获取用户输入
        char *cmdArr[CMD_NUM] = {0}; // 记录分割后的输入
        ......
    }
}

4、pwd 命令的实现

下面的命令都是外置命令,为了和系统终端类似,我们需要自建一个 mybin 目录,用来存储外置命令函数及可执行文件(一般把自定义命令和 mybash 主程序放在同一项目目录下)

(1) 准备工作

在上文提到过外置命令的实现是:先 fork 子进程,再 exec 加载执行,所以需要在 RunCmd() 函数中创建子进程来执行 pwd 的命令。

execv() 函数

int execv(const char *pathname, char *const argv[]);

参数 含义
pathname 指向要执行的可执行文件的完整路径 (如 /bin/ls/home/wuya/mybin/pwd)。
argv 一个以 NULL 结尾的字符串数组,作为新程序的命令行参数。格式要求:{程序名, 参数1, 参数2, ..., NULL}。这个数组会直接传递给新程序的 main 函数,作为 argv 参数。

这里我们传递的pathname需要提前定义一个 binPath,再连接上cmdArr[0],就组成了指定命令的可执行文件的路径;argv传递获取到的命令cmdArr即可,既包含命令又包含参数

代码
cpp 复制代码
#include<sys/wait.h>// wait()函数需要的头文件

// 路径:设置为自己的mybin文件的路径
// 注意最后有一个 /
char *binPath = "/mnt/hgfs/LinuxShare/MyBash/mybin/"; // 需要保存不变

// 执行系统命令
void RunCmd(char *cmdArr[], int count)
{
    char *cmd = cmdArr[0];
    if (strcmp(cmd, "cd") == 0) // cd命令
    {
        RunCd(cmdArr[1]); // 将参数传递给函数
    }
    else if (strcmp(cmd, "exit") == 0) // exit命令
    {
        exit(0);
    }
    else // 外置命令
    {
        pid_t pid = fork(); // 创建子进程
        if (pid == -1)      // 创建失败
        {
            perror("fork failed\n");
            return;
        }
        if (pid == 0) // 子进程执行
        {
            char cmdPath[128] = {0};
            // 拼接自定义路径+命令名(如 /mybin/pwd
            strcpy(cmdPath, binPath);
            strcat(cmdPath, cmdArr[0]);
            // 替换子进程代码,执行自定义路径下的 pwd
            execv(cmdPath, cmdArr);
            perror("exec failed\n"); // 子进程执行失败
            // 如果execv失败,需要解释子进程,防止回到主函数的while循环,再次生成子进程
            exit(0);
        }
        else
        {
            wait(NULL); // 等待子进程结束
        }
    }
}

(2) 命令分析+核心函数 getcwd()

pwd命令的作用是:打印当前工作目录的绝对路径

其功能实现也比较简单,直接使用到 **getcwd()**函数即可。

需要头文件 <unistd.h>

(3) 代码实现

pwd.c
cpp 复制代码
#include <stdio.h>
#include <unistd.h>
int main()
{
    char buf[128] = {0};
    if (getcwd(buf, 127) == NULL)
    {
        perror("get cwd failed\n");
        return;
    }
    printf("%s\n", buf);
    return 0;
}

(4) 执行命令

注意:执行前,需要先将 pwd.c 编译链接为可执行文件 pwd

5、ls 命令的实现

ls命令的核心功能是:列出指定目录下的文件 / 目录信息

(1) 核心函数与结构

opendir()

目录流(directory stream):这是一个抽象的概念,本质上是一个 DIR* 类型的指针,代表了对某个目录的 "读取会话"。它内部维护了读取位置、文件信息等状态。

DIR *opendir(const char *name); //name 是要打开的目录路径(可以是相对路径或绝对路径)

函数返回值 :成功时返回一个非空的 DIR* 指针;失败时返回 NULL,并设置 errno(如目录不存在、权限不足等)。

readdir()

readdir() 会从目录流中读取下一个条目,直到没有更多条目为止。

closedir()
dirent 结构

(2) ls.c 基础功能实现

首先实现 ls 不带参数(如-l、-a等)的功能

ShowPathFile()函数
cpp 复制代码
void ShowPathFile(char *path)
{
    DIR *dirp = opendir(path); // 打开目录并建立目录流dirp
    // 只有多个文件,才显示路径
    if (pathNum > 1)
    {
        printf("%s:\n", path);
    }
    struct dirent *pfile = NULL;            // 创建dirent结构体
    while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
    {
        if (strncmp(pfile->d_name, ".", 1) == 0)// 过滤隐藏文件(. 和 ..)
        {
            continue;
        }
        // 打印文件名称
        printf("%s    ", pfile->d_name);
    }
    printf("\n");
    // 关闭目录流并释放资源
    closedir(dirp);
}
  • 若指定多个路径,先打印路径名,再列出对应文件
cpp 复制代码
void ShowPathFile(char *path)
{
    ......
    // 只有多个文件,才显示路径
    if (pathNum > 1)
    {
        printf("%s:\n", path);
    }
    .......
    closedir(dirp);
}
主函数
  • 统计用户输入的非参数路径个数:过滤掉以 - 开头的参数
cpp 复制代码
#include <stdio.h>
#include <string.h>

static int pathNum = 0; // ls命令后的非参数路径个数

//argv[0]:ls
//argv[1]:-i 选项
//argv[2]:路径
int main(int argc, char *argv[])
{
    // 统计输入的路径个数,注意需要跳过 argv[0]
    for (int i = 1; i < argc; i++)
    {
        if (strncmp(argv[i], "-", 1) == 0)//略过-开头的参数
        {
            continue;
        }
        pathNum++;
    }
    return 0;
}
  • 遍历每个路径,调用 ShowPathFile() 函数,列出该目录下的非隐藏文件
cpp 复制代码
int main(...)
{   
    ...... 
    int count = 0;// 记录非-开头的参数个数
    for (int i = 1; i < argc; i++)
    {
        if (strncmp(argv[i], "-", 1) == 0)
        {
            continue;
        }
        ShowPathFile(argv[i]);
        count++;
    }
    return 0;
}
  • 如果用户未指定任何路径,默认列出当前目录下的文件
cpp 复制代码
int main(int argc, char *argv[])
{
    ......
    if (count == 0) // 无指定路径时,默认列出当前目录
    {
        ShowPathFile("."); // 显示当前路径下的文件
    }
    return 0;
}
完整代码
cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>

static int pathNum = 0; // ls命令后的非参数路径个数

void ShowPathFile(char *path)
{
    DIR *dirp = opendir(path); // 打开目录并建立目录流dirp
    // 只有多个文件,才显示路径
    if (pathNum > 1)
    {
        printf("%s:\n", path);
    }
    struct dirent *pfile = NULL;            // 创建dirent结构体
    while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
    {
        if (strncmp(pfile->d_name, ".", 1) == 0)// 过滤隐藏文件(. 和 ..)
        {
            continue;
        }
        // 打印文件名称
        printf("%s    ", pfile->d_name);
    }
    printf("\n");
    // 关闭目录流并释放资源
    closedir(dirp);
}
//argv[0]:ls
//argv[1]:-i 选项
//argv[2]:路径
int main(int argc, char *argv[])
{
    // 统计输入的路径个数
    for (int i = 1; i < argc; i++)// 跳过argv[0]
    {
        if (strncmp(argv[i], "-", 1) == 0) // 略过-开头的参数
        {
            continue;
        }
        pathNum++;
    }
    int count = 0; // 记录非-开头的参数个数
    for (int i = 1; i < argc; i++)// 跳过argv[0]
    {
        if (strncmp(argv[i], "-", 1) == 0) // 略过-开头的参数
        {
            continue;
        }
        ShowPathFile(argv[i]);
        count++;
    }
    if (count == 0) // 参数为0
    {
        ShowPathFile("."); // 显示当前路径下的文件
    }
    return 0;
}
运行结果

注意:执行前,需要先将 ls.c 编译链接为可执行文件 ls

(3) 增加对不同参数的处理

整体操作还是只针对与 ls.c 文件,在这里先实现两个常见参数 -a 和 -i

参数 核心作用
-a 显示目录下所有文件 / 目录 (包括以 . 开头的隐藏文件 / 目录,如 .bashrc..
-i 显示文件 / 目录对应的 inode 编号(inode 是文件系统中标识文件的唯一数字 ID)

可以定义全局bool类型变量来表示某个参数是否存在

cpp 复制代码
#include<stdbool.h>

// 参数存在标识:默认不存在
static bool haveA = false; // 参数a
static bool haveI = false; // 参数i
OptionAns()

统计参数个数,并判断以-开头的参数是否存在

cpp 复制代码
//分析选项函数
void OptionAns(int argc,char* argv[])
{
    for(int i=1;i<argc;i++)
    {
        if(strncmp(argv[i],"-",1)==0)
        {
            if(strstr(argv[i],"a")!=NULL)
            {
                haveA=true;
            }
            if(strstr(argv[i],"i")!=NULL)
            {
                haveI=true;
            }
            continue;
        }
        pathNum++;
    }    
}
ShowPathFile()
cpp 复制代码
void ShowPathFile(char *path)
{
    DIR *dirp = opendir(path); // 打开目录并建立目录流dirp
    // 只有多个文件,才显示路径
    if (pathNum > 1)
    {
        printf("%s:\n", path);
    }
    struct dirent *pfile = NULL;            // 创建dirent结构体
    while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
    {
        //没有参数 -a 时过滤隐藏文件(. 和 ..)
        if (!haveA && strncmp(pfile->d_name, ".", 1) == 0)
        {
            continue;
        }
        // 显示结点索引
        if (haveI)
        {
            printf("%d ", pfile->d_ino);
        }

        // 打印文件名称
        printf("%s    ", pfile->d_name);
    }
    printf("\n");
    // 关闭目录流并释放资源
    closedir(dirp);
}

(4) 不同类型文件不同颜色输出

目录:蓝色;可执行文件:绿色

lstat()函数

获取文件属性(包括类型、权限),存入 struct stat 结构体;

int lstat(const char *restrict pathname, struct stat *restrict statbuf);

类型 名称 含义
入参 pathname 要获取属性的文件 / 目录路径(相对路径 / 绝对路径均可)
出参 statbuf 指向 struct stat 结构体的指针,用于存储文件属性(需提前定义)
返回值 0 成功:文件属性已存入 statbuf
返回值 -1 失败:设置 errno(如路径不存在、权限不足、路径过长等)
struct stat 结构体
cpp 复制代码
struct stat {
    ino_t     st_ino;     // inode 编号(对应 ls -i)
    mode_t    st_mode;    // 核心:文件类型 + 权限(判断目录/可执行文件)
    nlink_t   st_nlink;   // 硬链接数(ls -l 显示的数字)
    uid_t     st_uid;     // 文件所有者的 UID
    gid_t     st_gid;     // 文件所属组的 GID
    off_t     st_size;    // 文件大小(字节)
    time_t    st_mtime;   // 最后修改时间
    // 其他次要字段(如块大小、设备号等)
};
相关宏定义
宏名 含义 对应数值(八进制) 作用
S_IXUSR User execute(所有者可执行) 0100 判断文件是否对所有者有可执行权限
S_IXGRP Group execute(组用户可执行) 0010 判断文件是否对组用户有可执行权限
S_IXOTH Other execute(其他用户可执行) 0001 判断文件是否对其他用户有可执行权限
代码
cpp 复制代码
#include <sys/stat.h>

void ShowPathFile(char *path)
{
    ......
    while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
    {
        //没有参数 -a 时过滤隐藏文件(. 和 ..)
        if (!haveA && strncmp(pfile->d_name, ".", 1) == 0)
        {
            continue;
        }
        // 显示结点索引
        if (haveI)
        {
            printf("%ld ", pfile->d_ino);
        }

        //不同类型文件不同颜色输出
        //目录:蓝色;可执行文件:绿色
        struct stat st;
        char fullpath[1028] = {0};
        // 拼接路径:防止ls命令找不到文件
        //如ls /home,pfile->d_name为test.txt,不拼接lstat会会去当前目录找test/txt,而非/home/test.txt
        snprintf(fullpath, sizeof(fullpath), "%s/%s", path, pfile->d_name); 

        if (lstat(fullpath, &st) == -1) { // 判断是否失败
            perror(fullpath); // 打印错误原因
            printf("%s    ", pfile->d_name); // 默认颜色显示
            continue; // 跳过当前文件,处理下一个
        }

        if (S_ISDIR(st.st_mode))
        {
            printf("\033[1;34m%s\033[0m   ", pfile->d_name);
        }
        else if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
        //等同于:if(st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_name & S_IXOTH)
        {
            printf("\033[1;32m%s\033[0m   ", pfile->d_name);
        }
        else
        {
            printf("%s    ", pfile->d_name);
        }
    }
    printf("\n");
    // 关闭目录流并释放资源
    closedir(dirp);
}

(5) 完整代码

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdbool.h>
#include <sys/stat.h>

static int pathNum = 0; // ls命令后的非参数路径个数
// 参数存在标识:默认不存在
static bool haveA = false; // 参数a
static bool haveI = false; // 参数i

void ShowPathFile(char *path)
{
    DIR *dirp = opendir(path); // 打开目录并建立目录流dirp
    // 只有多个文件,才显示路径
    if (pathNum > 1)
    {
        printf("%s:\n", path);
    }
    struct dirent *pfile = NULL;            // 创建dirent结构体
    while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
    {
        //没有参数 -a 时过滤隐藏文件(. 和 ..)
        if (!haveA && strncmp(pfile->d_name, ".", 1) == 0)
        {
            continue;
        }
        // 显示结点索引
        if (haveI)
        {
            printf("%ld ", pfile->d_ino);
        }

        //不同类型文件不同颜色输出
        //目录:蓝色;可执行文件:绿色
        struct stat st;
        char fullpath[1028] = {0};
        // 拼接路径:防止ls命令找不到文件
        //如ls /home,pfile->d_name为test.txt,不拼接lstat会会去当前目录找test/txt,而非/home/test.txt
        snprintf(fullpath, sizeof(fullpath), "%s/%s", path, pfile->d_name); 

        if (lstat(fullpath, &st) == -1) { // 判断是否失败
            perror(fullpath); // 打印错误原因
            printf("%s    ", pfile->d_name); // 默认颜色显示
            continue; // 跳过当前文件,处理下一个
        }

        if (S_ISDIR(st.st_mode))
        {
            printf("\033[1;34m%s\033[0m   ", pfile->d_name);
        }
        else if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
        //等同于:if(st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_name & S_IXOTH)
        {
            printf("\033[1;32m%s\033[0m   ", pfile->d_name);
        }
        else
        {
            printf("%s    ", pfile->d_name);
        }
    }
    printf("\n");
    // 关闭目录流并释放资源
    closedir(dirp);
}

// 分析选项函数
void OptionAns(int argc, char *argv[])
{
    for (int i = 1; i < argc; i++)
    {
        if (strncmp(argv[i], "-", 1) == 0)
        {
            if (strstr(argv[i], "a") != NULL)
            {
                haveA = true;
            }
            if (strstr(argv[i], "i") != NULL)
            {
                haveI = true;
            }
            continue;
        }
        pathNum++;
    }
}
// argv[0]:ls
// argv[1]:-i 选项
// argv[2]:路径
int main(int argc, char *argv[])
{
    // 统计参数个数,并判断以-开头的参数是否存在
    OptionAns(argc, argv);
    int count = 0;                 // 记录非-开头的参数个数
    for (int i = 1; i < argc; i++) // 跳过argv[0]
    {
        if (strncmp(argv[i], "-", 1) == 0) // 略过-开头的参数
        {
            continue;
        }
        ShowPathFile(argv[i]);
        count++;
    }
    if (count == 0) // 参数为0
    {
        ShowPathFile("."); // 显示当前路径下的文件
    }
    return 0;
}

(6)运行结果

四、项目总结

通过从零实现自定义命令行解释器 mybash ,我们完整打通了 Linux 系统编程的核心知识链,从进程管理、文件系统、终端交互字符串处理、错误处理,全方位理解了 Shell 的底层运行机制。

本项目涉及的核心技术点包括:

  • Linux 进程模型 :熟练使用 fork 创建子进程、exec 系列函数加载外部程序、wait/waitpid 回收子进程,理解父子进程、孤儿进程与僵尸进程原理。
  • 文件与目录操作 :掌握 opendir/readdir/closedir 目录遍历,使用 stat/lstat 获取文件属性,通过文件类型与权限位实现文件颜色高亮。
  • 终端交互控制 :使用 termios 结构体修改终端属性,实现规范 / 非规范模式切换,手动处理退格、光标移动与回显逻辑。
  • 命令解析与字符串处理 :使用 strtok 完成命令行分割,实现多参数、组合参数解析,支持 -a/-i 等选项解析。
  • 系统调用与错误处理 :对 getcwd、uname、chdir、lstat 等系统调用做完整返回值判断,使用 perror/strerror 输出友好错误信息,提升程序健壮性。
  • 内置命令与外置命令 :区分并实现 cd/exit 等内置命令,以及 pwd/ls 等外置命令,理解 Shell 执行命令的完整流程。

可扩展方向

在当前 mybash 基础上,可继续扩展以下功能,进一步接近原生 bash:

  1. 实现 ls -l 长格式输出,展示文件权限、硬链接数、用户、大小、时间等信息。
  2. 支持输入输出重定向 ><>>,实现文件读写功能。
  3. 支持管道命令 |,实现多命令间数据传递。
  4. 增加环境变量、PATH 搜索、cd - 返回上一级目录等功能。
  5. 支持命令历史记录、Tab 补全、光标上下左右移动。
  6. 扩展更多内置命令,如 echo、export、umask、jobs、fg/bg 等。
  7. 支持信号处理,如 Ctrl+C 中断、Ctrl+Z 挂起、Ctrl+D 退出。

完整代码

mybash.c

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/utsname.h>
#include <string.h>
#include <sys/types.h>
#include <pwd.h>
#include <stdlib.h>
#include <termios.h>
#include <sys/wait.h>

// 路径:设置为自己的mybin文件的路径
char *binPath = "/mnt/hgfs/LinuxShare/MyBash/mybin/"; // 需要保存不变

// 命令和参数总数
#define CMD_NUM 10

// 输出命令行提示符
void PrintPrompt()
{
    //$和#的选择
    char ch = '$'; // 默认为'$'
    // 获取uid
    uid_t uid = getuid();
    // 获取用户名
    struct passwd *p = getpwuid(uid); // p->pw_name为用户名
    if (p == NULL)
    {
        perror("get passwd info faild:");
        printf("mybash> ");
        fflush(stdout);
        return;
    }
    // 法一
    //  如果登录用户名等于root,就输出'#'
    /*if (strcmp(p->pw_name, "root") == 0)
    {
        ch = '#';
    }*/
    // 法二
    // root的uid为0
    if (uid == 0)
    {
        ch = '#';
    }

    // 获取主机名
    struct utsname sysinfo; // 创建结构体
    // 调用函数,获取内核和系统的详细信息
    if (uname(&sysinfo) == -1)
    {
        // 获取失败
        perror("get host info failed:");
        printf("mybash> "); // 在提示符最前面加上mybash>,和系统终端区分开
        fflush(stdout);     // 刷新缓冲区
        return;
    }
    // 获取路径
    char buf[128] = {0};          // 存储路径的缓冲区
    char *ptr = getcwd(buf, 127); // 获取当前进程的工作目录的绝对路径
    if (ptr == NULL)              // 获取失败
    {
        perror("get current working directory failed:");
        printf("mybash> ");
        fflush(stdout);
        return;
    }
    // 显示当前目录
    char *s = NULL;
    if (strcmp(buf, p->pw_dir) == 0) // 当前路径为家目录
    {
        s = "~"; // 输出~
    }
    else // 不是家目录,正常输出
    {
        s = buf + sizeof(buf); // 将s移动到buf末尾
        // 将s向前移动至最后一个'/'位置
        while (*s != '/')
        {
            s--;
        }
        // 当前绝对路径为"/"时,直接输出s
        //  s+1开始的字符串就是当前目录
        if (strlen(s) != 1)
        {
            s += 1;
        }
    }
    // 输出
    printf("mybash> \033[1;32m%s@%s\033[0m\033[1;34m:%s\033[0m%c ", p->pw_name, sysinfo.nodename, s, ch);
    fflush(stdout);
}

// 分割用户命令和参数
int SplitCmd(char *cmd, char *cmdArr[])
{
    // 首次分割:传递字符串
    // 以空格为标准进行分割
    char *token = strtok(cmd, " ");
    int count = 0; // 数组索引
    while (token != NULL)
    {
        // 将分割得到的字符串保存在cmdArr数字中
        cmdArr[count++] = token;
        // 继续分割,传递的字符串为NULL
        token = strtok(NULL, " ");
    }
    // 返回字符串个数
    return count;
}

// cd命令的实现
void RunCd(char *path) // 参数为传递的参数
{
    char buf[128] = {0};
    struct passwd *p = getpwuid(getuid());
    if (p == NULL)
    {
        printf("get pwuid info failed\n");
        return;
    }
    // 没有参数
    if (path == NULL)
    {
        path = p->pw_dir; // 目的路径为家目录
    }
    // 参数为~ + ~后面还有参数
    else if (strncmp(path, "~", 1) == 0)
    {
        strcpy(buf, p->pw_dir); // 令buf为家目录
        strcat(buf, path + 1);  // 把剩余参数连接到buf上
        path = buf;             // 令path保存目的路径
    }
    // 一般情况,直接切换路径
    // 切换路径
    if (chdir(path) == -1)
    {
        perror("cd failed\n");
        return;
    }
}

// 执行系统命令
void RunCmd(char *cmdArr[], int count)
{
    char *cmd = cmdArr[0];
    if (strcmp(cmd, "cd") == 0) // cd命令
    {
        RunCd(cmdArr[1]); // 将参数传递给函数
    }
    else if (strcmp(cmd, "exit") == 0) // exit命令
    {
        exit(0);
    }
    else // 外置命令
    {
        pid_t pid = fork(); // 创建子进程
        if (pid == -1)      // 创建失败
        {
            perror("fork failed\n");
            return;
        }
        if (pid == 0) // 子进程执行
        {
            char cmdPath[128] = {0};
            // 拼接自定义路径+命令名(如 /mybin/pwd
            strcpy(cmdPath, binPath);
            strcat(cmdPath, cmdArr[0]);
            // 替换子进程代码,执行自定义路径下的 pwd
            execv(cmdPath, cmdArr);
            perror("exec failed\n"); // 子进程执行失败
            // 如果execv失败,需要解释子进程,防止回到主函数的while循环,再次生成子进程
            exit(0);
        }
        else
        {
            wait(NULL); // 等待子进程结束
        }
    }
}

// 手动获取用户输入
void GetCmd(char *cmd)
{
    struct termios tcAttr, newtcAttr; // 创建两个结构体变量
    if (tcgetattr(0, &tcAttr) == -1)  // 获取当前终端属性
    {
        perror("get tcAttr failed\n");
        return;
    }
    newtcAttr = tcAttr; // 将属性复制给newtcAttr
    // 改变newtcAttr的信息
    newtcAttr.c_lflag &= ~ICANON; // 关闭规范模式
    newtcAttr.c_lflag &= ~ECHO;   // 取消回显功能
    // 应用newtcAttr的属性
    if (tcsetattr(0, TCSANOW, &newtcAttr) == -1)
    {
        perror("set tcAttr failed\n");
        return;
    }

    int index = 0;
    char ch = 0;
    while ((ch = getchar()) != '\n') // 注意符号的优先级
    {
        if (ch == 8) // 对应Backspace(退格)
        {
            if (index >= 1) // 边界处理
            {
                // 如果用户按下Backspace,则舍弃命令的最后一个字符
                cmd[--index] = 0;
                // 实现删除操作的显示
                printf("\033[1D"); // 光标左移1列
                printf("\033[K");  // 删除光标到行尾的内容
                fflush(stdout);
            }
        }
        else
        {
            // 反之,显示按下的字符
            // 这里属于手动实现回显功能
            cmd[index++] = ch; // 将输入的字符记录到cmd中
            printf("%c", ch);
            fflush(stdout);
        }
    }

    // 改为终端的设置
    if (tcsetattr(0, TCSANOW, &tcAttr) == -1)
    {
        perror("set tcAttr failed\n");
        return;
    }

    // 输出换行符,让提示符在下一行显示
    printf("\n");
}

int main()
{
    while (1)
    {
        // 1.系统提示符输出
        PrintPrompt();

        // 2.获取用户命令
        char cmd[128] = {0};

        //GetCmd(cmd);                 // 获取用户输入
        char* s = fgets(cmd,127,stdin);
        if(s == NULL){
            continue;
        }
        cmd[strlen(cmd)-1]=0;

        char *cmdArr[CMD_NUM] = {0}; // 记录分割后的输入
        // 进行分割
        int count = SplitCmd(cmd, cmdArr);
        // 没有输入,直接进行下一次获取
        if (count == 0)
        {
            continue;
        }

        // 3.执行用户命令
        RunCmd(cmdArr, count);
    }
}

pwd.c

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
int main()
{
    char buf[128] = {0};
    if (getcwd(buf, 127) == NULL)
    {
        perror("get cwd failed\n");
        return 0;
    }
    printf("%s\n", buf);
    return 0;
}

ls.c

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdbool.h>
#include <sys/stat.h>

static int pathNum = 0; // ls命令后的非参数路径个数
// 参数存在标识:默认不存在
static bool haveA = false; // 参数a
static bool haveI = false; // 参数i

void ShowPathFile(char *path)
{
    DIR *dirp = opendir(path); // 打开目录并建立目录流dirp
    // 只有多个文件,才显示路径
    if (pathNum > 1)
    {
        printf("%s:\n", path);
    }
    struct dirent *pfile = NULL;            // 创建dirent结构体
    while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
    {
        //没有参数 -a 时过滤隐藏文件(. 和 ..)
        if (!haveA && strncmp(pfile->d_name, ".", 1) == 0)
        {
            continue;
        }
        // 显示结点索引
        if (haveI)
        {
            printf("%ld ", pfile->d_ino);
        }

        //不同类型文件不同颜色输出
        //目录:蓝色;可执行文件:绿色
        struct stat st;
        char fullpath[1028] = {0};
        // 拼接路径:防止ls命令找不到文件
        //如ls /home,pfile->d_name为test.txt,不拼接lstat会会去当前目录找test/txt,而非/home/test.txt
        snprintf(fullpath, sizeof(fullpath), "%s/%s", path, pfile->d_name); 

        if (lstat(fullpath, &st) == -1) { // 判断是否失败
            perror(fullpath); // 打印错误原因
            printf("%s    ", pfile->d_name); // 默认颜色显示
            continue; // 跳过当前文件,处理下一个
        }

        if (S_ISDIR(st.st_mode))
        {
            printf("\033[1;34m%s\033[0m   ", pfile->d_name);
        }
        else if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
        //等同于:if(st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_name & S_IXOTH)
        {
            printf("\033[1;32m%s\033[0m   ", pfile->d_name);
        }
        else
        {
            printf("%s    ", pfile->d_name);
        }
    }
    printf("\n");
    // 关闭目录流并释放资源
    closedir(dirp);
}

// 分析选项函数
void OptionAns(int argc, char *argv[])
{
    for (int i = 1; i < argc; i++)
    {
        if (strncmp(argv[i], "-", 1) == 0)
        {
            if (strstr(argv[i], "a") != NULL)
            {
                haveA = true;
            }
            if (strstr(argv[i], "i") != NULL)
            {
                haveI = true;
            }
            continue;
        }
        pathNum++;
    }
}
// argv[0]:ls
// argv[1]:-i 选项
// argv[2]:路径
int main(int argc, char *argv[])
{
    // 统计参数个数,并判断以-开头的参数是否存在
    OptionAns(argc, argv);
    int count = 0;                 // 记录非-开头的参数个数
    for (int i = 1; i < argc; i++) // 跳过argv[0]
    {
        if (strncmp(argv[i], "-", 1) == 0) // 略过-开头的参数
        {
            continue;
        }
        ShowPathFile(argv[i]);
        count++;
    }
    if (count == 0) // 参数为0
    {
        ShowPathFile("."); // 显示当前路径下的文件
    }
    return 0;
}
相关推荐
m0_531237172 小时前
C语言-while循环,continue/break,getchar()/putchar()
java·c语言·算法
kong79069282 小时前
SpringBoot原理
java·spring boot·后端
say_fall2 小时前
二叉树从入门到实践:堆与链式结构全解析
c语言·数据结构·c++
那我掉的头发算什么2 小时前
【图书管理系统】基于Spring全家桶的图书管理系统(下)
java·数据库·spring boot·后端·spring·mybatis
大闲在人10 小时前
C、C++区别还是蛮大的
c语言·开发语言·c++
Codefengfeng11 小时前
CTF工具篇
linux·运维·服务器
上海合宙LuatOS12 小时前
LuatOS核心库API——【i2c】I2C 操作
linux·运维·单片机·嵌入式硬件·物联网·计算机外设·硬件工程
毅炼13 小时前
Java 集合常见问题总结(3)
java·开发语言·后端