Linux 进程控制(四)自主Shell命令行解释器.

目录

自主Shell命令行解释器

[第1步 : 打印命令行字符串](#第1步 : 打印命令行字符串)

[第2步 : 从键盘中获取用户的字符串输入](#第2步 : 从键盘中获取用户的字符串输入)

[第3步 : 解析命令行字符串](#第3步 : 解析命令行字符串)

[第4步 : 利用程序替换函数执行解析完的命令](#第4步 : 利用程序替换函数执行解析完的命令)

[第 5 步 : 内建命令的特殊处理](#第 5 步 : 内建命令的特殊处理)

[第6步 : 解析重定向命令](#第6步 : 解析重定向命令)


自主Shell命令行解释器

在前面学习完进程的创建,进程的等待以及进程替换等函数之后,我们可以自己设计一个shell命令行解释器

shell的本质就是一个死循环

因为当我们输入完一条命令后, 它执行完后不能直接退出,还要等你下一条命令, 所以必须while(1) 死循环顶着。

第1步 : 打印命令行字符串

cpp 复制代码
void PrintCommandLine()
{
    printf("[%s@%s:%s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); 
          // 用户名 @ 主机名 : 当前路径
    fflush(stdout);
}

用GetUserNane GetHostName GetPwd这三个函数分别来获取用户名 主机名 当前路径

怎么获取用户名 主机名 当前路径这三个变量呢?

我们可以通过环境变量来获取 其实真正的shell是通过专门的系统调用来获取的, 但是我们在自己设计的时候就通过getenv从环境变量中获取, 这样更方便

cpp 复制代码
const char *GetUserName()
{
    char *name = getenv("USER");
    if(name == NULL)
        return "None";
    return name;
}

const char *GetHostName()
{
    char *hostname = getenv("HOSTNAME");
    if(hostname == NULL)
        return "None";
    return hostname;
}
const char *GetPwd()
{
    char *pwd = getenv("PWD");
    //char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd == NULL)
        return "None";
    return pwd;
}
static std::string rfindDir(const std::string &p)
{
    if(p == "/")
        return p;
    const std::string psep = "/";
    auto pos = p.rfind(psep);
    if(pos == std::string::npos)
        return std::string();
    return p.substr(pos+1); // /home/whb
}
void PrintCommandLine()
{
    printf("[%s@%s:%s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); 
          // 用户名 @ 主机名 : 当前路径
    fflush(stdout);
}

最后一行 fflush(stdout); 是为了强制刷新标准输出缓冲区,把提示符立刻显示在屏幕上。因为标准输出(stdout)默认是行缓冲,只有遇到换行符 \n 时才会把缓冲区里的内容真正输出到屏幕。但是我们这里的提示符[%s@%s:%s]# 结尾没有换行符,所以 printf 只是把它写到了缓冲区里,并没有立刻显示。如果不调用 fflush(stdout) ,程序会一直卡在那里,用户看不到提示符,以为程序没反应。

第2步 : 从键盘中获取用户的字符串输入

从键盘读取用户输入的整行字符串,只有shell获取了用户的指令需求(指令就是字符串)shell才能进一步进行解析和执行相关指令操作,这是 Shell "读取-解析-执行"循环中读取阶段的关键操作。

这一步通常用 fgets() 实现读取字符串操作:

cpp 复制代码
#define MAXSIZE 128
char command_line[MAXSIZE] = {0};    //缓冲区
int GetCommand(char commandline[], int size)
{
    if(NULL == fgets(commandline, size, stdin))
        return 0;
    // 2.1 用户输入的时候,至少会摁一下回车\n 即abcd\n ,我们将\n换成'\0'
    commandline[strlen(commandline)-1] = '\0';
    return strlen(commandline);
}

// 2. 获取用户输入
if(0 == GetCommand(command_line, sizeof(command_line)))
    continue;

因为 fgets 会一直读取,直到遇到换行符 \n 或缓冲区满,能完整保留命令中的空格和参数,比如 ls -a -l 会被完整读入。而 scanf("%s", ...) 遇到空格就会停止,只能拿到命令名,拿不到后面的参数。还有就是 fgets 要求传入缓冲区及缓冲区大小(如 sizeof(cmd) ),能自动截断过长输入,避免缓冲区溢出。还需要注意的是 fgets 会把用户输入完指令后按下回车产生的 \n 也读进字符串,此时我们需要手动把它替换成 \0 即可。

第3步 : 解析命令行字符串

解析字符串 -> "ls -a -l" -> "ls" "-a" "-l" 命令行解释器,就要对用户输入的命令字符串首先进行解析!此时就要用到命令参数表 argv[] 了, 因为我们输入的 ls -a -l 切割后就是:

cpp 复制代码
argv[0] = "ls"
argv[1] = "-a"
argv[2] = "-l"
argv[3] = NULL

所以我们在自己设计时还要在代码中自己维护一张 argv[] 命令行参数表, 我们取名为 gargv[]

cpp 复制代码
#define MAXARGS 32
// shell自己内部维护的第一张表: 命令行参数表
// 故意设计成为全局的
// 命令行参数表
char *gargv[MAXARGS];  //全局参数表,存储切割后的命令与参数。
int gargc = 0;    //全局参数个数,记录gargv中有效元素的数量。
const char *gsep = " ";   //分隔符,定义为空格,用于按空格切割命令行字符串。

int ParseCommand(char commandline[])
{
    gargc = 0;
    memset(gargv, 0, sizeof(gargv));
    // ls -a -l
    // 故意 commandline : ls
    gargv[0] = strtok(commandline, gsep);
    while((gargv[++gargc] = strtok(NULL, gsep)));

    return gargc;
}

ParseCommand(command_line);    //传缓冲区

这段代码是命令解析的核心实现,它的核心任务是把用户输入的一行文本(如 ls -a -l ),转换成后续 execvp 函数能直接执行的参数表结构,是 Shell 从"读入文本"到"执行命令"的关键桥梁。

代码通过定义全局数组 gargv 和全局变量 gargc ,在 Shell 内部维护了一张命令行参数表:

  • gargv :对应标准 C 程序的 argv ,是一个指针数组,用来存放命令名和所有参数。
  • gargc :对应标准 C 程序的 argc ,记录参数表中有效元素的个数。
  • MAXARGS :限制参数最大数量,防止数组越界,保证程序稳定性。
  • gsep :定义分隔符为空格,明确按空格来切割命令行字符串。

这种全局设计的好处是,解析结果可以在 Shell 的各个模块(如执行、重定向处理)中直接复用,无需反复传递参数。

ParseCommand函数的执行过程可以分为三步:1. 初始化重置 memset(gargv, 0, sizeof(gargv));

的作用是每次解析新命令前,必须重置参数表和计数,避免上一次解析的残留数据干扰本次结果。

  1. 第一次切割(提取命令名)gargv[0] = strtok(commandline, gsep); 会调用 strtok 函数,以空格为分隔符,从原始命令行中切割出第一个子串,也就是命令名(如 ls),并存入 gargv[0] 。strtok 会把原字符串中第一个空格替换成 \0,并记录下切割位置。3. 循环切割(提取所有参数)while((gargv[++gargc] = strtok(NULL, gsep))); 这是最关键的循环:再次调用 strtok 时传入NULL,表示从上次切割结束的位置继续切割。++gargc 先自增再赋值,保证 gargv[1] 存第一个参数,gargv[2] 存第二个参数,以此类推。当 strtok 返回 NULL 时,说明没有更多参数,循环终止,此时 gargc 的值就是命令名加上所有参数的总个数。

第4步 : 利用程序替换函数执行解析完的命令

让子进程去执行解析出来的命令,而父进程(Shell 本身)继续等待和管理。如果在父进程(bash)里直接调用 execvp,那么 bash 进程的代码和数据段会被完全替换成要执行的命令(比如 ls)。执行完 ls 后,整个进程就结束了,你的 Shell 也就直接退出了,无法继续等待下一条命令。

所以我们的思路就是 : 1. 让父进程(Shell)调用 fork() 创建一个和自己一模一样的子进程。自己进入 waitpid() 等待,直到子进程执行完毕。子进程结束后,父进程回到循环开头,打印提示符,等待下一条命令。2. 子进程调用 execvpe 函数, 把自己的代码和数据段完全替换成要执行的命令程序(比如 ls)。执行完毕后,子进程就结束了,不会再回到原来的 Shell 代码。

cpp 复制代码
int ExecuteCommand()
{
    // 能不能让你的bash自己执行命令:ls -a -l
    pid_t id = fork();
    if(id < 0)
        return -1;
    else if(id == 0)
    {
        //printf("我是子进程,我是exec启动前: %dp\n", getpid());
        // 子进程: 如何执行, gargv, gargc
        // ls -a -l
        int fd = -1;
        if(redir_type == NoneRedir)
        {
            // Do Nothing
        }
        else if(redir_type == OutputRedir)
        {
            // 子进程要进行输出重定向
            fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == AppRedir)
        {
            // 子进程要进行输出追加重定向
            fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == InputRedir)
        {
            // 子进程要进行输入重定向
            fd = open(filename, O_RDONLY);
            dup2(fd, 0);
        }
        else{
            //bug??
        }
        execvpe(gargv[0], gargv, genv);
        exit(1);   //子进程exit退出后一定要被父进程wait等待回收,不然就会形成僵尸进程
    }
    else
    {
        // 父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
            //printf("wait child process success!\n");
        }
    }
    return 0;
}

为什么选择execvpe函数而不选其他的程序替换函数?

使用 execvpe,因为它最适配:命令数组 + 自动找 PATH + 自定义环境变量。1. v:参数是数组 gargv ,正好对应解析完的命令,必须用 v。2. p:自动按系统 PATH 找命令(不用写 /bin/ls)。3. e:可以自己传环境变量表 genv ,方便后续实现功能。

子进程进行的操作后面讲

第 5 步 : 内建命令的特殊处理

第4步代码只适合外部命令 ls, cat, mkdir, rm等指令, 凡是磁盘上有对应可执行文件,都能跑。

因为它们是独立程序,子进程 exec 替换就能跑。 但是代码不能执行cd, exit, export 这些内建命令, 因为这些命令是修改 Shell 自己的,我们让子进程去改,父进程 Shell 纹丝不动。

外部命令为什么是独立程序?

ls, cat, cp, mv, mkdir, ps, grep它们本来就是磁盘上真实存在的二进制文件。在命令行输 ls,本质是:运行 /usr/bin/ls 这个程序, 它们的特点:1.不修改 Shell 本身 2.只是读取、打印、创建文件...

  1. 跑完就结束,不影响 Shell 的状态

内建命令为什么不能是独立程序?

cd、exit、export、pwd 这些命令,做的事情是修改 Shell 自己本身

父子进程的目录、环境变量、文件描述符......全都存在各自的 PCB 里,互相独立、互不干扰。

因为每个进程的 PCB(task_struct) 里面都存着:当前工作目录 pwd , 环境变量表 , 文件描述符表 , 进程ID、状态、优先级...这些都是每个进程自己私有的。

而父子进程的 PCB 是两份完全独立的 , 父进程 Shell 有一个 PCB , 子进程 fork 出来,复制一份一模一样的 PCB , 但从此以后:父的PCB ≠ 子的PCB,改谁的就是谁的,互不影响。

这也就解释了:为什么 cd 不能让子进程执行?执行 cd .. :1. 子进程的 PCB 里的 当前目录 被改掉 2. 但父进程 Shell 的 PCB 里的目录纹丝不动 3. 子进程退出,它的 PCB 被销毁 4. Shell 还是原来的路径 → cd 白改!再看外部命令 ls 为什么没问题?因为 ls 不修改 PCB 里的目录、环境变量。

它只是:读磁盘 , 打印内容 , 跑完退出 , 不碰父进程 PCB,也不碰自己 PCB 里的"进程状态"

所以子进程 exec 替换跑完全没问题。

所以本质上可以说 : 内建命令 = 直接修改/读取当前进程 PCB 里的内容 , 和 PCB 是强相关的

PCB 里存这些东西:当前工作目录 pwd , 环境变量 , 文件描述符 , 进程身份、状态

内建命令干的事:

  • cd → 改 PCB 里的 当前目录
  • export → 改 PCB 里的 环境变量
  • pwd → 读 PCB 里的 当前目录
  • exit → 让进程自己 PCB 标记退出

全都是在动 PCB!而外部命令(ls、cat)不碰你 PCB 里的目录、环境变量 , 只是读文件、打印、计算 , 所以它们不需要在父进程执行

所以这里我们需要对这种内建命令进行特殊处理:

cpp 复制代码
// 我们shell自己所处的工作路径
char cwd[MAXSIZE];
// 最近一个命令执行完毕,退出码
int lastcode = 0;

// retunr val:
// 0 : 不是内建命令
// 1 : 内建命令&&执行完毕
int CheckBuiltinExecute()
{
    if(strcmp(gargv[0], "cd") == 0)  //
    {
        // 内建命令
        if(gargc == 2)  //cd 路径
        {
            // 新的目标路径: gargv[1]
            // 1. 更改进程内核中的路径
            chdir(gargv[1]);
            // 2. 更改环境变量的用户路径
            char pwd[1024];
            getcwd(pwd, sizeof(pwd)); // /home/whb
            snprintf(cwd, sizeof(cwd), "PWD=%s", pwd); // cwd: PWD=/home/home
            putenv(cwd);
            lastcode = 0;
        }
        return 1;
    }
    else if(strcmp(gargv[0], "echo") == 0) // cd , echo , env , export 内建命令
    {
        if(gargc == 2)
        {
            if(gargv[1][0] == '$')
            {
                // $? ? : 看做一个变量名字
                if(strcmp(gargv[1]+1, "?") == 0)
                {
                    printf("lastcode: %d\n", lastcode);
                }
                else if(strcmp(gargv[1]+1, "PATH") == 0)
                {
                    // 不准你用getenv和putenv
                    printf("%s\n", getenv("PATH")); // putenv 和 getenv 究竟是什么, 访问环境变量表!
                }
                lastcode = 0;
            }
            return 1;
            // echo helloworld
            // echo $?
        }
    }

    return 0;
}

// 5. 这个命令,到底是让父进程bash自己执行(内建命令)?还是让子进程执行
if(CheckBuiltinExecute()) // > 0
{
   continue;
}

这段代码就是在创建子进程执行外部命令之前,先由父进程自己判断当前解析出来的命令是不是内建命令,函数 CheckBuiltinExecute 会先检查命令是否为 cd 或特殊的 echo ? 、echo PATH,如果是 cd 命令,就直接在父进程 Shell 内部调用 chdir 函数修改当前进程 PCB 里的工作目录,同时更新 PWD 环境变量,保证后续获取路径时是最新的,执行完直接返回 1,表示这是内建命令且已经在父进程执行完毕;如果是 echo 后面跟着 ? 或 PATH,也直接在父进程里打印上一条命令的退出码或者系统环境变量,同样返回 1;如果都不是内建命令,函数就返回 0。而在主流程里,一旦 CheckBuiltinExecute 返回 1(也就是内建命令),就直接 continue 跳过后续创建子进程、程序替换的逻辑,回到命令读取循环继续等待下一条指令,只有当返回 0 时,才会走 fork 创建子进程,再通过 execvpe 进行程序替换去执行 ls 、cat 这类外部命令,这样既保证了 cd 这种修改 Shell 自身状态的内建命令能真正生效,又让外部命令不影响父进程 Shell 的运行,完整实现了标准 Shell 区分内建命令与外部命令、父子进程分工执行的核心逻辑。

第6步 : 解析重定向命令

我们再输入命令时有可能会输入像 ... > XX.txt 这样的重定向命令 , 所以我们还要对这样的重定向命令进行处理:

cpp 复制代码
// ls -a -l > XX.txt -> "ls -a -l" && "XX.txt" && 重定向的方式
// 表明重定向的信息
#define NoneRedir   0  //无重定向
#define InputRedir  1  //输入重定向 <
#define AppRedir    2  //追加重定向 >> 
#define OutputRedir 3  //输出重定向 >

int redir_type = NoneRedir; // 重定向类型 记录正在执行的执行,重定向方式
char *filename = NULL;      // 保存重定向的目标文件 重定向到哪个文件

// 空格空格空格filename.txt
#define TrimSpace(start) do{\
    while(isspace(*start)) start++;\
}while(0) 

// ls -a -l >> filenamel.txt -> ls -a -l \0\0 filename.txt
// ls -a -l > XX.txt || ls -a -l >> XX.txt || cat < log.txt || ls -a -l
void ParseRedir(char commandline[])
{
    redir_type = NoneRedir;
    filename = NULL;
    char *start = commandline;    //每次解析前先清空,防止上次结果干扰。
    char *end = commandline+strlen(commandline);  //定义头尾指针
    while(start < end)    //从左到右扫描,找  < 、 > 、 >> 
    {
        if(*start == '>')
        {
            if(*(start+1) == '>')
            {
                // 追加重定向
                *start = '\0'; // 把第一个 > 变成字符串结束
                start++;
                *start = '\0'; // 把第二个 > 也变成结束
                start++;
                TrimSpace(start); // 去掉文件名前面的空格
                redir_type = AppRedir; 
                filename = start;
                break;
            }
            // 输出重定向 >
            *start = '\0';
            start++;
            TrimSpace(start);
            redir_type = OutputRedir;
            filename = start;
            break;
        }
        else if(*start == '<')
        {
            // 输入重定向 <
            *start = '\0';
            start++;
            TrimSpace(start);
            redir_type = InputRedir;
            filename = start;
            break;
        }
        else
        {
            // 没有重定向
            start++;
        }
    }
}

//printf("%s\n", command_line);
// ls -a -l > XX.txt || ls -a -l >> XX.txt || cat < log.txt || ls -a -l
// ls -a -l > XX.txt -> "ls -a -l" && "XX.txt" && 重定向的方式
ParseRedir(command_line);

ParseRedir这个函数首先要遍历输入的命令行字符串并识别三种重定向符号 , 若未发现任何重定向符号,则标记为 NoneRedir(无重定向)。找到重定向符号后,将符号位置替换为 \0,把原命令行字符串切割为两部分:前半部分:纯命令(如 ls -a -l )后半部分:重定向目标文件名(如 log.txt ), 调用 TrimSpace 宏,跳过文件名前的多余空格(如 > log.txt → 提取出 log.txt),确保拿到干净的文件名。将识别到的重定向类型存 redir_type ,目标文件名存入 filename ,供后续执行 open / dup2 完成实际重定向使用。ParseRedir 函数负责从命令行中提取重定向信息,将命令与目标文件分离,并记录重定向类型,为后续执行 I/O 重定向做好准备。

cpp 复制代码
// 空格空格空格filename.txt
#define TrimSpace(start) do{\
    while(isspace(*start)) start++;\
}while(0)

TrimSpace(start) 这个宏函数的作用是提取的是后面的文件名!把指针 start 前面所有的空格全部跳过,让 start 直接指向第一个不是空格的字符。去掉字符串左边的所有空格,只保留后面的有效内容。因为 TrimSpace 是在找到 > / >> / < 之后才调用的,这时候 start 指针已经指向符号后面的内容了,所以它清理的是文件名前面的空格。

但是总的来说ParseRedir 是只解析命令,并不执行命令 , 它只负责从命令行里提取出 2 个关键信息,存到全局变量:redir_type :哪种重定向( > / >> / < /无), filename :重定向到哪个文件 , 但是它不打开文件,不重定向,不执行命令。所以下一步还是得回到第四步的 ExecuteCommand 解析命令函数中

cpp 复制代码
redir_type = OutputRedir;  // 或 AppRedir / InputRedir
filename = start;          // 记录文件名
 
 
ExecuteCommand 一进来就用:

int ExecuteCommand()   //第4步
{
    // 能不能让你的bash自己执行命令:ls -a -l
    pid_t id = fork();
    if(id < 0)
        return -1;
    else if(id == 0)
    {
        //printf("我是子进程,我是exec启动前: %dp\n", getpid());
        // 子进程: 如何执行, gargv, gargc
        // ls -a -l
        int fd = -1;
        if(redir_type == NoneRedir)
        {
            // Do Nothing
        }
        else if(redir_type == OutputRedir)
        {
            // 子进程要进行输出重定向
            fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == AppRedir)
        {
            // 子进程要进行输出追加重定向
            fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == InputRedir)
        {
            // 子进程要进行输入重定向
            fd = open(filename, O_RDONLY);
            dup2(fd, 0);
        }
        else{
            //bug??
        }
        execvpe(gargv[0], gargv, genv);
        exit(1);   //子进程exit退出后一定要被父进程wait等待回收,不然就会形成僵尸进程
    }
    else
    {
        // 父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
            //printf("wait child process success!\n");
        }
    }
    return 0;
}

父进程在创建子进程之后 , 就会让子进程进行重定向的操作 , 为什么是子进程做?

因为重定向会改变文件描述符(0、1、2),不能污染父进程,只能在子进程里做。

举个例子:

cpp 复制代码
fd = open("file.txt", ...);
dup2(fd, 1);

执行前:1 号描述符 → 屏幕 , fd → file.txt

执行 dup2(fd, 1) 后:1 号描述符 → 指向 file.txt , 不再指向屏幕!

后果就是以后这个进程里所有往 1 写(printf、write、ls 输出)全部 → 写进 file.txt而不会再显示在屏幕上。

最后在子进程执行完重定向操作之后也就是执行完这几个if-else语句后 , 就会执行到execvpe进程替换函数 , 也就是说到这一步就只剩下正常的指令了 , 此时就调用进程替换执行

我们再说一下这个进程替换 , execvpe 就是让子进程变成另一个程序(比如 ls),三个参数就是告诉它:你是谁、你带什么参数、你用什么环境。

cpp 复制代码
execvpe(gargv[0], gargv, genv);

第一个参数: gargv[0] 意思就是你要执行谁?(程序名字)比如: "ls" 就会告诉 execvpe:我要运行 ls 这个程序

第二个参数: gargv 意思就是 命令 + 参数列表 , 是一个字符串数组,格式固定:{"ls", "-a", "-l", NULL} , 告诉 ls 你运行的时候,要带上 -a -l 这些参数。

bash 复制代码
-  gargv[0]  = 命令本身  "ls" 
-  gargv[1]  = 参数  "-a" 
-  gargv[2]  = 参数  "-l" 
- 最后必须以 NULL 结尾

第三个参数: genv 意思是环境变量 , 就是系统里的 PATH、HOME 这些。 , 一般直接传父进程的环境变量就行。作用是让系统能找到 ls 在哪里,不然它不知道 ls 在哪个目录。

好了到这里 , 自主shell的基本内容也就完了

完整代码如下:

cpp 复制代码
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <iostream>
#include <string>

#define MAXSIZE 128
#define MAXARGS 32
// shell自己内部维护的第一张表: 命令行参数表
// 故意设计成为全局的
// 命令行参数表
char *gargv[MAXARGS];
int gargc = 0;
const char *gsep = " ";

// 环境变量表
char *genv[MAXARGS];
int genvc = 0;

// 我们shell自己所处的工作路径
char cwd[MAXSIZE];

// 最近一个命令执行完毕,退出码
int lastcode = 0;

// vector<std::string> cmds; // 1000


// ls -a -l > XX.txt -> "ls -a -l" && "XX.txt" && 重定向的方式
// 表明重定向的信息
#define NoneRedir   0
#define InputRedir  1
#define AppRedir    2
#define OutputRedir 3

int redir_type = NoneRedir; // 记录正在执行的执行,重定向方式
char *filename = NULL;      // 保存重定向的目标文件

// 空格空格空格filename.txt
#define TrimSpace(start) do{\
    while(isspace(*start)) start++;\
}while(0)

void LoadEnv()
{
    // 正常情况,环境变量表内部是从配置文件来的
    // 今天我们从父进程拷贝
    extern char **environ;
    for(; environ[genvc]; genvc++)
    {
        genv[genvc] = (char*)malloc(sizeof(char)*4096);
        strcpy(genv[genvc], environ[genvc]);
    }
    genv[genvc] = NULL;

    printf("Load env: \n");
    for(int i = 0; genv[i]; i++)
        printf("genv[%d]: %s\n", i, genv[i]);
}
static std::string rfindDir(const std::string &p)
{
    if(p == "/")
        return p;
    const std::string psep = "/";
    auto pos = p.rfind(psep);
    if(pos == std::string::npos)
        return std::string();
    return p.substr(pos+1); // /home/whb
}

const char *GetUserName()
{
    char *name = getenv("USER");
    if(name == NULL)
        return "None";
    return name;
}

const char *GetHostName()
{
    char *hostname = getenv("HOSTNAME");
    if(hostname == NULL)
        return "None";
    return hostname;
}
const char *GetPwd()
{
    char *pwd = getenv("PWD");
    //char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd == NULL)
        return "None";
    return pwd;
}

void PrintCommandLine()
{
    printf("[%s@%s %s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); // 用户名 @ 主机名 当前路径
    fflush(stdout);
}

int GetCommand(char commandline[], int size)
{
    if(NULL == fgets(commandline, size, stdin))
        return 0;
    // 2.1 用户输入的时候,至少会摁一下回车\n abcd\n ,\n '\0'
    commandline[strlen(commandline)-1] = '\0';
    return strlen(commandline);
}

// ls -a -l >> filenamel.txt -> ls -a -l \0\0 filename.txt
// ls -a -l > XX.txt || ls -a -l >> XX.txt || cat < log.txt || ls -a -l
void ParseRedir(char commandline[])
{
    redir_type = NoneRedir;
    filename = NULL;
    char *start = commandline;
    char *end = commandline+strlen(commandline);
    while(start < end)
    {
        if(*start == '>')
        {
            if(*(start+1) == '>')
            {
                // 追加重定向
                *start = '\0';
                start++;
                *start = '\0';
                start++;
                TrimSpace(start); // 去掉左半部分的空格
                redir_type = AppRedir;
                filename = start;
                break;
            }
            // 输出重定向
            *start = '\0';
            start++;
            TrimSpace(start);
            redir_type = OutputRedir;
            filename = start;
            break;
        }
        else if(*start == '<')
        {
            // 输入重定向
            *start = '\0';
            start++;
            TrimSpace(start);
            redir_type = InputRedir;
            filename = start;
            break;
        }
        else
        {
            // 没有重定向
            start++;
        }
    }
}

int ParseCommand(char commandline[])
{
    gargc = 0;
    memset(gargv, 0, sizeof(gargv));
    // ls -a -l
    // 故意 commandline : ls
    gargv[0] = strtok(commandline, gsep);
    while((gargv[++gargc] = strtok(NULL, gsep)));

//    printf("gargc: %d\n", gargc); // ?
//    int i = 0;
//    for(; gargv[i]; i++)
//        printf("gargv[%d]: %s\n", i, gargv[i]);
    return gargc;
}

// retunr val:
// 0 : 不是内建命令
// 1 : 内建命令&&执行完毕
int CheckBuiltinExecute()
{
    if(strcmp(gargv[0], "cd") == 0)
    {
        // 内建命令
        if(gargc == 2)
        {
            // 新的目标路径: gargv[1]
            // 1. 更改进程内核中的路径
            chdir(gargv[1]);
            // 2. 更改环境变量
            char pwd[1024];
            getcwd(pwd, sizeof(pwd)); // /home/whb
            snprintf(cwd, sizeof(cwd), "PWD=%s", pwd); // cwd: PWD=/home/home
            putenv(cwd);
            lastcode = 0;
        }
        return 1;
    }
    else if(strcmp(gargv[0], "echo") == 0) // cd , echo , env , export 内建命令
    {
        if(gargc == 2)
        {
            if(gargv[1][0] == '$')
            {
                // $? ? : 看做一个变量名字
                if(strcmp(gargv[1]+1, "?") == 0)
                {
                    printf("lastcode: %d\n", lastcode);
                }
                else if(strcmp(gargv[1]+1, "PATH") == 0)
                {
                    // 不准你用getenv和putenv
                    printf("%s\n", getenv("PATH")); // putenv 和 getenv 究竟是什么, 访问环境变量表!
                }
                lastcode = 0;
            }
            return 1;
            // echo helloworld
            // echo $?
        }
    }

    return 0;
}

int ExecuteCommand()
{
    // 能不能让你的bash自己执行命令:ls -a -l
    pid_t id = fork();
    if(id < 0)
        return -1;
    else if(id == 0)
    {
        //printf("我是子进程,我是exec启动前: %dp\n", getpid());
        // 子进程: 如何执行, gargv, gargc
        // ls -a -l
        int fd = -1;
        if(redir_type == NoneRedir)
        {
            // Do Nothing
        }
        else if(redir_type == OutputRedir)
        {
            // 子进程要进行输出重定向
            fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == AppRedir)
        {
            fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == InputRedir)
        {
            fd = open(filename, O_RDONLY);
            dup2(fd, 0);
        }
        else{
            //bug??
        }
        execvpe(gargv[0], gargv, genv);
        exit(1);
    }
    else
    {
        // 父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
            //printf("wait child process success!\n");
        }
    }
    return 0;
}

int main()
{
    // 0. 从配置文件中获取环境变量填充环境变量表的
    //LoadEnv();
    char command_line[MAXSIZE] = {0};
    while(1)
    {
        // 1. 打印命令行字符串
        PrintCommandLine();
        // 2. 获取用户输入
        if(0 == GetCommand(command_line, sizeof(command_line)))
            continue;

        //printf("%s\n", command_line);
        // ls -a -l > XX.txt || ls -a -l >> XX.txt || cat < log.txt || ls -a -l
        // ls -a -l > XX.txt -> "ls -a -l" && "XX.txt" && 重定向的方式
        ParseRedir(command_line);
        //printf("command: %s\n", command_line);
        //printf("redir type: %d\n", redir_type);
        //printf("filename: %s\n", filename);

        // 4. 解析字符串 -> "ls -a -l" -> "ls" "-a" "-l" 命令行解释器,就要对用户输入的命令字符串首先进行解析!
        ParseCommand(command_line);

        // 5. 这个命令,到底是让父进程bash自己执行(内建命令)?还是让子进程执行
        if(CheckBuiltinExecute()) // > 0
        {
            continue;
        }
    
        // 6. 让子进程执行这个命令
        ExecuteCommand();
    }

    return 0;
}

本文介绍了如何实现一个简单的Shell命令行解释器。Shell本质上是一个持续运行的循环,主要包含以下功能:1. 打印命令行提示符,通过环境变量获取用户名、主机名和当前路径;2. 使用fgets读取用户输入命令,处理换行符;3. 解析命令字符串,使用strtok分割命令和参数;4. 处理特殊命令:区分内建命令(如cd、echo)和外部命令,内建命令由父进程直接执行;5. 处理重定向命令(>、>>、<),子进程通过dup2实现I/O重定向;6. 使用fork创建子进程,通过execvpe执行外部命令。该实现涵盖了Shell的基本功能,包括命令解析、进程管理和I/O重定向等核心机制。

相关推荐
历程里程碑2 小时前
Linux 38 网络协议:从独立主机到全球互通
java·linux·运维·服务器·网络·c++·职场和发展
ISU(考研版)2 小时前
从零开始复现 ThinkPHP RCE:Docker + Burp Suite 实战
运维·docker·容器
Franciz小测测2 小时前
基于FastAPI的自动化随机初始密码方案
运维·自动化·fastapi
yuanmenghao2 小时前
Linux 性能实战系列 - 附录 Valgrind介绍
linux·运维·服务器
主角1 72 小时前
Nginx安全
linux·运维·nginx
wanhengidc2 小时前
服务器被攻击该怎么办
运维·服务器·网络·安全·游戏·智能手机
qq_404265832 小时前
C++中的代理模式实战
开发语言·c++·算法
玛卡巴卡ldf2 小时前
【LeetCode 手撕算法】(滑动窗口) 3-无重复字符的最长子串、438-找到字符串中所有字母异位词
数据结构·算法·leetcode·哈希算法
liuyao_xianhui2 小时前
动态规划_最大子数组和_C++
java·开发语言·数据结构·c++·算法·链表·动态规划