目录
[第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));
的作用是每次解析新命令前,必须重置参数表和计数,避免上一次解析的残留数据干扰本次结果。
- 第一次切割(提取命令名)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.只是读取、打印、创建文件...
- 跑完就结束,不影响 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重定向等核心机制。