首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【Linux】进程控制(4)自主shell命令行解释器

【Linux】进程控制(4)自主shell命令行解释器

作者头像
mosheng
发布2026-01-14 19:01:58
发布2026-01-14 19:01:58
610
举报
文章被收录于专栏:c++c++

hello~ 很高兴见到大家! 这次带来的是Linux系统中关于进程控制这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢? 个 人 主 页: 默|笙

一、搭建基本框架

  1. 首先bash是一个进程,而且是一个需要一直运行的进程,所以主函数一定是一个死循环。while(1){}。
代码语言:javascript
复制
156 int main()
157 { 
158   Loadenv();
159   char command_line[MAXSIZE] = {0};
160   while(1)
161   {
162     //打印命令行字符串
163     PrintCommandLine();
164     //获取用户输入的字符串
165     if (0 ==GetCommand(command_line, sizeof(command_line)))
166     {
167       continue;
168     }
169 
170     //解析字符串
171     ParseCommand(command_line);
172     //检查一个命令是要由bash执行的内建命令,还是要让子进程执行的普通命令
173     if(CheckBuiltinExecute() > 0)
174       continue;
175     //执行解析出来的字符串
176     ExecuteCommand(); 
177   }
178   return 0;
179 }        
  1. 首先就是要导入环境变量表,之后进入while循环,不断执行输出命令行字符串,获取用户输入字符串,解析字符串,和执行命令这几步。

1.0 导入环境变量表

代码语言:javascript
复制
 16 //环境变量表
 17 int genvc = 0;
 18 char* genv[MAXARGS];
 23 void Loadenv()
 24 {
 25   //正常情况下这个Shell它是从配置文件里面读取环境变量                                                                                    
 26   //但这里我们没办法实现,因为非常复杂
 27   //所以就直接从bash里面读取了
 28   extern char** environ;
 29   for(; environ[genvc]; genvc++)
 30   {
 31     genv[genvc] = (char*)malloc(sizeof(char) * 4096);
 32     strcpy(genv[genvc], environ[genvc]);
 33   }
 34   genv[genvc] = NULL;
 35 
 36   for (int i = 0; genv[i]; i++)
 37   {
 38     printf("genv[%d]: %s\n", i, genv[i]);
 39   }
 40 }

1.1 输出shell命令行函数PrintCommandLine()

在这里插入图片描述
在这里插入图片描述
  1. 在我们什么都不输入的时候,bash会输出这样一串字符串,这个字符串可以拆分为[用户名@主机名 当前文件的名字]/#。其中代表普通用户,#代表root用户。
  2. 我们就可以根据这个格式来写出这个shell命令行的函数PrintCommandLine()。其中用户名、主机名以及当前文件的名字都可以用从环境变量表里面进行获取,需要用到的函数是getenv()。
代码语言:javascript
复制
//后面添加的函数,用于提取路径最后的那个文件名
 43 std::string rfindDir(const std::string &p)
 44 {
 45     if(p == "/")
 46         return p;
 47     const std::string psep = "/";
 48     auto pos = p.rfind(psep);
 49     if(pos == std::string::npos)
 50         return std::string();
 51     return p.substr(pos+1); // /home/whb
 52 }


  9 //获取用户名
 10 const char* GetUsername()
 11 {
 12   char* name = getenv("USER");
 13   if (name == NULL)
 14     return "None";
 15   return name;
 16 }
 17 //获取主机名
 18 const char* GetHostname()
 19 {
 20   char* hostname = getenv("HOSTNAME");
 21   if (hostname == NULL)
 22     return "None";                                                                                                                       
 23   return hostname;
 24 }
 25 //获取pwd当前路径
 26 const char* GetPwd()
 27 {
 28   char* pwd = getenv("PWD");
 29   if (pwd == NULL)
 30     return "None";
 31   return pwd;
 32 }
 33 //打印命令行字符串函数
 34 void PrintCommandLine()
 35 {
 36   printf("[%s@%s %s]#", GetUsername(), GetHostname(), rfindDir(GetPwd()).c_str()); 
 37   fflush(stdout);
 38 }
  1. 打印出这样一串shell命令行,由于后面需要用户来输入命令,所以不能加换行符\n,但是不加\n的话这串字符就只能一直呆在缓冲区里面,所以需要fflush函数来刷新一下缓冲区。

1.2 获取用户输入字符串函数GetCommand()

  1. 获取字符串的时候,就会卡住,也就是卡在打印出的命令行后面。
  2. 用来获取用户输入的字符串函数有很多,比如scanf,fgets,这里我们用fgets读取一整行,因为scanf遇到空格就不会读取了,这不符合我们的要求,我们要把空格一起读取到。
代码语言:javascript
复制
 41 #define MAXSIZE 128

 49 int GetCommand(char* commandline, int size)
 50 { 
 51     if(NULL == fgets(commandline, size, stdin))
 52       return 0;
 53     //用户输入的时候至少会摁一下回车键,把回车键所在位置置为'\0'
 54     commandline[strlen(commandline) - 1] = '\0';
 55     //printf("%s\n", commandline);
 56     return strlen(commandline);
 57 }

//while循环外
 92   char command_line[MAXSIZE] = {0};


//main函数while循环里
 97     //获取用户输入的字符串
 98     if (0 ==GetCommand(command_line, sizeof(command_line)))
 99     {
100       continue;
101     }
  1. 首先我在while循环外定义了一个command_line命令行数组用来存储用户输入的命令行,它的大小定为128字节。
在这里插入图片描述
在这里插入图片描述
  1. 之后是Getcommand函数,这个函数通过fgets函数读取用户所输入的命令行。由于用户在每次命令行输入之后都会摁一下回车键,这个回车键也会被当成字符读取到commandline数组里面,所以这里需要做一些处理,让commandline[strlen(commandline) - 1] = ‘\0’,手动将最后一个回车字符置为结束符’\0’。
  2. 如果用户没有输入,那么fgets读取失败Getcommand函数的返回值就是0,会执行continue语句,不会执行接下来的代码,重新进入循环。

1.3 解析字符串函数

  1. 首先是解析之后的字符串要放在哪里,根据空格进行分割后的字符串就是一个个的命令行参数,当然是要放在命令行参数表里面,我设置一个char*类型的数组gargv来进行存储。同时还有记录命令行参数个数的gargc变量。
strtok函数
在这里插入图片描述
在这里插入图片描述
  1. 再就是用来解析字符串的strtok函数,它的第一个参数是需要进行切割的字符串,第二个参数是分隔符字符串。它的返回值是切割后的目标字符串首字母地址。
  2. 切割下第一个目标字符串第一个参数传入需要进行切割的字符串str,如果要接着进行切割的话第一个参数就不能也传入str了,而是需要传入NULL。这样strtok函数才会知道是需要接着进行上一次的切割,否则它仍旧会切割下str的第一个目标字符串。
  3. strtok函数支持传入多个分隔符,比如传入" #!"它就会按照空格、#、! 这三个字符作为分隔符来对传入的字符串进行切割。
  4. 至于为什么strtok跟其他函数不同,可以继续进行上一次函数的切割,这是因为它的实现运用了static变量。
代码语言:javascript
复制
//全局
 42 #define MAXARGS 32
 43 
 44 //命令行参数表
 45 int gargc = 0;
 46 char* gargv[MAXARGS];
 47 const char* gsep = " ";

 59 void ParseCommand(char* commandline)
 60 {                                                                                                                                        
 61    gargc = 0;
 62    memset(gargv, 0, sizeof(gargv));
 63   
 64    gargv[0] = strtok(commandline, gsep);
 //strtok先切割返回一个值,再把这个值存入gargv[++gargc]里面,再检测这个值
 65    while ((gargv[++gargc] = strtok(NULL, gsep)));
 69 }
  1. while ((gargv[++gargc] = strtok(NULL, gsep)));代码逻辑是strtok先切割返回一个值,再把这个值存入gargv[++gargc]里面,再检测这个值
  2. 代码还有一点,那就是strtok函数在没有可以切割的字符串了之后会返回NULL,这个NULL会存入gargv[++gargc]里面,gargc自增1,所以gargc的个数不会少,而是刚刚好。
  3. 每次进行切割的时候都要把原来的gargv也就是命令行参数列表清空,gargc命令行参数个数清零。

1.4 执行命令函数

1. 普通命令vs内建命令
  1. 像是ls这样的二进制文件,一般需要通过创建子进程然后让子进程进行程序切换来完成调用。这种就叫做普通命令。
  2. 而cd和echo这种,前者所要改变的是当前bash的路径而不是子进程的路径所以不能通过子进程进行程序切换来完成调用,不然改变的只是子进程的路径,而不会影响到它的父进程bash的路径。而echo有一个作用$?可以打印出上一个所执行的二进制文件的退出码。这个文件是由一般子进程执行的,谁能获得子进程的退出码?只能是父进程。像是这种不能够由子进程通过程序替换来执行的命令,需要由Shell自行执行的命令就叫做内建命令
  • 内建命令是 Shell 自身实现的功能(无独立二进制文件),无需通过「创建子进程 + 程序替换」执行,直接在 Shell 进程内运行,这是与普通命令的核心区别
2. 父进程执行内建命令函数CheckBuiltinExecute()
代码语言:javascript
复制
151     //检查一个命令是要由bash执行的内建命令,还是要让子进程执行的普通命令
152     if(CheckBuiltinExecute() > 0)
153       continue;
  1. 在让子进程执行命令之前,首先需要判断该命令是否为内建命令,如果是内建命令就让自主Shell运行,不是则通过创建子进程+程序替换执行。
  2. 内建命令有很多,这里只实现cd和echo命令。如果是需要父进程执行的命令则返回1,执行后续的continue语句,如果是要子进程执行的命令则返回0,会继续执行接下来的代码。
cd
  1. 需要切换Shell的路径,需要用到函数chdir。
chdir函数和getcwd函数
在这里插入图片描述
在这里插入图片描述
  1. 使用chdir函数需要包含头文件unistd.h头文件。
  2. 这个函数的作用是更改当前所执行这个函数的进程的路径。会将当前进程的工作路径修改为传入的path
  3. 成功则返回0,失败则返回-1。
在这里插入图片描述
在这里插入图片描述
  1. chdir用于修改进程的工作目录,而getcwd用于获取进程当前的工作目录,执行成功则返回指向buf的指针,buf存储的是以’\0’为结尾的绝对路径字符串
  2. 用户使用cd时传入的第二个命令行参数就是我们的目标路径,所以只需要给chdir函数传入gargv[1]就好。
  3. 但是会存在一个问题,那就是通过chdir修改Shell的路径,系统不会自动给我们修改环境变量里面的PWD,这会导致我们的Shell打印的命令行字符串后面的路径不会改变。所以这里我们需要手动的更改一下环境变量PWD。
代码语言:javascript
复制
 49 //我们的Shell自己所处的工作路径
 50 char cwd[MAXSIZE];

 75   if (strcmp(gargv[0], "cd") == 0)
 76   {
 77     if (gargc == 2)
 78     {
 79       chdir(gargv[1]);
 80 
 81       //修改环境变量
 82       char pwd[1024];
 83       //存储当前获取到的工作路径到pwd里面
 84       getcwd(pwd, sizeof(pwd));
 85       //拼接PWD环境变量
 86       snprintf(cwd, sizeof(cwd), "PWD=%s", pwd);
 87       //导入环境变量cwd
 88       putenv(cwd);
 89       return 1; 
 90     }
echo
  1. 需要处理echo $这种内建命令特殊情况和echo 语句这种普通情况,后面的普通情况交给子进程去处理就好,我们处理前面的特殊情况。
代码语言:javascript
复制
 51 //上一次命令执行完毕后的退出码
 52 int lastcode = 0;

 94   else if (strcmp(gargv[0], "echo") == 0)
 95   {
 96     if (gargc == 2)                                                                                                                      
 97     {
 98       if (gargv[1][0] == '$')
 99       {
100           if (strcmp(gargv[1]+1, "?") == 0)
101           {
102             printf("%d\n", lastcode);
103           }
104           else if (strcmp(gargv[1]+1, "PATH") == 0)
105           {
106             printf("%s\n", getenv("PATH"));
107           }
108           lastcode = 0;
109           return 1;
110       }
111     }
112   }
  1. 解释一下gargv[1] + 1,这个gargv[1]它实际上是一个指针,指向这个gargv[1]存储的字符串的首字母,+1这个指针就会往后移动一个字节,就会跳过’$'字符,指向它后面的字符来作为新字符串的首字符。
  2. 然后新增全局变量lastcode,这个变量是用来记录上一个命令执行完之后的退出码。Shell进程执行成功之后也要更新lastcode为0。
3. 子进程执行普通命令函数ExecuteCommand()
代码语言:javascript
复制
 71 int ExecuteCommand()
 72 {
 73   pid_t id = fork();
 74   if (id == 0)
 75   {                                                                                                                                      
 76     execvp(gargv[0], gargv);
 77     exit(0);
 78   }
 79   else if (id < 0)
 80     return -1;
 81   else 
 82   {
 83     int status = 0;
 84     pid_t sid = waitpid(id, &status, 0);
 85     if (sid > 0)
 					lastcode = WEXITSTATUS(status);
 86     //printf("wait childprocess sucess!!!\n");
 87   }
 88   return 0;
 89 }
  1. 也就是创建子进程然后让子进程进行程序切换执行命令,父进程就等待回收子进程,这样父进程就能够得到子进程的退出码,最后如果回收成功则更新退出码。创建子进程成功则返回0,创建失败则返回-1。

二、完整代码

代码语言:javascript
复制
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<string.h>
  4 #include<stdlib.h>
  5 #include<sys/types.h>
  6 #include<sys/wait.h>
  7 #include<iostream>
  8 #include<string>
  9 
 10 #define MAXSIZE 128
 11 #define MAXARGS 32
 12 
 13 //命令行参数表
 14 int gargc = 0;
 15 char* gargv[MAXARGS];
 16 const char* gsep = " ";
 17 //环境变量表
 18 int genvc = 0;
 19 char* genv[MAXARGS];
 20 //我们的Shell自己所处的工作路径
 21 char cwd[MAXSIZE];
 22 //上一次命令执行完毕后的退出码                                                                                                           
 23 int lastcode = 0;
 24 
 25 void Loadenv()
 26 {
 27   //正常情况下这个Shell它是从配置文件里面读取环境变量 
  28   //但这里我们没办法实现,因为非常复杂
 29   //所以就直接从bash里面读取了
 30   extern char** environ;
 31   for(; environ[genvc]; genvc++)
 32   {
 33     genv[genvc] = (char*)malloc(sizeof(char) * 4096);                                                                                    
 34     strcpy(genv[genvc], environ[genvc]);
 35   }
 36   genv[genvc] = NULL;
 37 
 38   for (int i = 0; genv[i]; i++)
 39   {
 40     printf("genv[%d]: %s\n", i, genv[i]);
 41   }
 42 }
 43 std::string rfindDir(const std::string &p)
 44 {
 45     if(p == "/")
 46         return p;
 47     const std::string psep = "/";
 48     auto pos = p.rfind(psep);
 49     if(pos == std::string::npos)
 50         return std::string();
 51     return p.substr(pos+1); // /home/whb
 52 }
 53 //获取用户名
 54 const char* GetUsername()
 55 {
 56   char* name = getenv("USER");
 57   if (name == NULL)
 58     return "None";
 59   return name;
 60 }                                                                                                                                        
 61 //获取主机名
 62 const char* GetHostname()
 63 {
 64   char* hostname = getenv("HOSTNAME");
 65   if (hostname == NULL)
 66     return "None";
 67   return hostname;
 68 }
 69 //获取pwd当前路径
 70 const char* GetPwd()
 71 {
 72   char* pwd = getenv("PWD");
 73   if (pwd == NULL)
 74     return "None";
 75   return pwd;
 76 }
 77 //打印命令行字符串函数
 78 void PrintCommandLine()
 79 {
 80   printf("[%s@%s %s]#", GetUsername(), GetHostname(), rfindDir(GetPwd()).c_str());
 81   fflush(stdout);
 82 }
 83 
 84 
 85 int GetCommand(char* commandline, int size)
 86 { 
 87     if(NULL == fgets(commandline, size, stdin))                                                                                          
 88       return 0;
 89     //用户输入的时候至少会摁一下回车键,把回车键所在位置置为'\0'
 90     commandline[strlen(commandline) - 1] = '\0';
 91     //printf("%s\n", commandline);
 92     return strlen(commandline);
 93 }
 94 
 95 void ParseCommand(char* commandline)
 96 {
 97    gargc = 0;
 98    memset(gargv, 0, sizeof(gargv));
 99   
100    gargv[0] = strtok(commandline, gsep);
101    while ((gargv[++gargc] = strtok(NULL, gsep)));
102    //int i = 0;
103    //for (i = 0; i < gargc; i++)
104    //  printf("%s\n", gargv[i]);
105 }
106 
107 int CheckBuiltinExecute()
108 {
109   if (strcmp(gargv[0], "cd") == 0)
110   {
111     if (gargc == 2)
112     {
113       chdir(gargv[1]);
114                                                                                                                                          
115       //修改环境变量
116       char pwd[1024];
117       //存储当前获取到的工作路径到pwd里面
118       getcwd(pwd, sizeof(pwd));
119       //拼接PWD环境变量
120       snprintf(cwd, sizeof(cwd), "PWD=%s", pwd);
121       //导入环境变量cwd
122       putenv(cwd);
123     }
124     return 1; 
125   }
126   else if (strcmp(gargv[0], "echo") == 0)
127   {
128     if (gargc == 2)
129     {
130       if (gargv[1][0] == '$')
131       {
132           if (strcmp(gargv[1]+1, "?") == 0)
133           {
134             printf("%d\n", lastcode);
135           }
136           else if (strcmp(gargv[1]+1, "PATH") == 0)
137           {
138             printf("%s\n", getenv("PATH"));
139           }
140           lastcode = 0;
141           return 1;                                                                                                                      
142       }
143     }
144   }
145 
146   return 0;
147 }
148 int ExecuteCommand()
149 {
150   pid_t id = fork();
151   if (id == 0)
152   {
153     execvp(gargv[0], gargv);
154     exit(0);
155   }
156   else if (id < 0)
157     return -1;
158   else 
159   {
160     int status = 0;
161     pid_t sid = waitpid(id, &status, 0);
162     if (sid > 0)
163       lastcode = WEXITSTATUS(status);
164     //printf("wait childprocess sucess!!!\n");
165   }
166   return 0;
167 }
168 int main()                                                                                                                               
169 { 
170   Loadenv();
171   char command_line[MAXSIZE] = {0};
172   while(1)
173   {
174     //打印命令行字符串
175     PrintCommandLine();
176     //获取用户输入的字符串
177     if (0 ==GetCommand(command_line, sizeof(command_line)))
178     {
179       continue;
180     }
181 
182     //解析字符串
183     ParseCommand(command_line);
184     //检查一个命令是要由bash执行的内建命令,还是要让子进程执行的普通命令
185     if(CheckBuiltinExecute() > 0)
186       continue;
187     //执行解析出来的字符串
188     ExecuteCommand(); 
189   }
190   return 0;
191 }                                                    

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~ 让我们共同努力, 一起走下去!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、搭建基本框架
    • 1.0 导入环境变量表
    • 1.1 输出shell命令行函数PrintCommandLine()
    • 1.2 获取用户输入字符串函数GetCommand()
    • 1.3 解析字符串函数
    • 1.4 执行命令函数
      • 1. 普通命令vs内建命令
      • 2. 父进程执行内建命令函数CheckBuiltinExecute()
      • 3. 子进程执行普通命令函数ExecuteCommand()
  • 二、完整代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档