[Linux系统编程——Lesson13.自定义shell(讲解原理)]
前言
经过之前的学习,我们已经掌握了一定的Linux基础与系统编程,接下我们将自主实现简易的自定义Shell来加深理解📖
一、打印命令提示符和获取命令行命令字符串🎈
1.1 设计✍️
- 我们首先使用操作系统的bash看到了命令行提示符的组成为[用户名@主机名 当前工作目录]$,获取用户名、主机名和当前工作目录的函数在系统调用中都有,这里我们自己设计一个,这三个数据都是环境变量,我们可以通过getenv来获取到他们,获取后将他们按照操作系统bash的格式输出出来即可,通过下图我们可以发现,我们的当前工作目录与操作系统的有所区别,我们的当前工作目录是一条路径,可以通过裁剪得到与操作系统一样的效果,我这里为了区分与操作系统的区别,这里就不做裁剪了。
- 输出完命令行提示符后,就需要向bash中输入命令了,这里我们就需要一个输入函数来读取命令字符串,需要注意⚠️的是这里不能使用scanf函数,因为scanf函数不能读取空格之后的内容,可以选择gets/fgets函数来读取,当我们输入完命令字符串后需要按回车,那么获取到的字符串中也会获取到这个’\n’,所以我们还需要将这个’\n’处理掉。
#include <stdio.h> // 引入标准输入输出库,提供printf、fgets等函数 #include <string.h> // 引入字符串处理库,提供strlen等字符串操作函数 #include <stdlib.h> // 引入标准库,提供getenv等系统环境相关函数#define NUM 1024 // 定义宏NUM为1024,作为命令输入缓冲区的大小// 获取当前用户名 const char* getUsername() {// 调用getenv函数从环境变量中获取"USER"的值,即用户名char* username = getenv("USER");if(username) // 如果成功获取到用户名(不为空)return username; // 返回获取到的用户名else // 如果获取失败(环境变量不存在或为空)return "none"; // 返回"none"表示未获取到 }// 获取主机名 const char* getHostname() {// 调用getenv函数从环境变量中获取"HOSTNAME"的值,即主机名char* hostname = getenv("HOSTNAME");if(hostname) // 如果成功获取到主机名return hostname; // 返回获取到的主机名else // 如果获取失败return "none"; // 返回"none"表示未获取到 }// 获取当前工作目录 const char* getCwd() {// 调用getenv函数从环境变量中获取"PWD"的值,即当前工作目录char* cwd = getenv("PWD"); if(cwd) // 如果成功获取到当前工作目录 return cwd; // 返回获取到的当前工作目录 else // 如果获取失败 return "none"; // 返回"none"表示未获取到 }int main() {char usercommand[NUM]; // 定义字符数组,用于存储用户输入的命令,大小为NUM(1024)// 打印命令行提示符,格式为"[用户名@主机名 工作目录]# "printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());// 读取用户输入的命令:// 使用fgets而不是scanf,因为scanf遇到空格会停止读取,而fgets可以读取整行输入(包括空格)// 参数说明:存储输入的缓冲区、缓冲区大小、输入流(stdin表示标准输入,即键盘)fgets(usercommand, sizeof(usercommand), stdin);// 处理输入的换行符:// fgets会将用户输入的回车符('\n')也读入缓冲区,这里将其替换为字符串结束符('\0')// strlen(usercommand)获取输入字符串的长度,减1得到换行符的位置usercommand[strlen(usercommand) - 1] = '\0';// 测试输出:打印用户输入的命令,验证是否正确读取和处理printf("%s", usercommand);return 0; // 程序正常结束 }
1.2 封装🔎
这里将打印命令行提示符与获取命令行字符串的工作统一封装到getUserCommand这个函数中。
// 函数功能:获取用户输入的命令并处理 // 参数: // command - 用于存储命令的字符数组指针 // num - 数组最大容量(防止缓冲区溢出) // 返回值:1表示成功,-1表示失败 int getUserCommand(char* command , int num) { // 打印命令行提示符(类似Linux终端样式)// 依赖之前定义的getUsername()、getHostname()、getCwd()获取环境信息printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd()); // 读取用户输入:// 使用fgets而非scanf,因为fgets可以读取含空格的完整命令// 最多读取num-1个字符(预留1个位置给字符串结束符'\0')char* r = fgets(command,num,stdin); // 错误处理:若读取失败(如遇EOF),返回-1if(r == NULL) return -1; // 关键处理:移除输入末尾的换行符// fgets会将用户输入的回车('\n')也存入数组,需替换为字符串结束符'\0'command[strlen(command)-1] = '\0'; // 测试输出:验证输入处理是否正确printf("%s",command);// 成功执行返回1return 1; }int main() {// 定义命令缓冲区,大小为宏NUM(1024)char usercommand[NUM]; // 调用函数获取用户命令getUserCommand(usercommand,NUM);return 0; }
核心要点说明:
- 函数封装:将命令读取逻辑独立为
getUserCommand
,提高代码复用性- 输入处理:使用
fgets
支持带空格的命令输入,解决scanf
的局限性- 安全考量:通过
num
参数控制输入长度,防止缓冲区溢出- 异常处理:通过返回值区分成功 / 失败状态,便于后续扩展错误处理
- 细节处理:移除换行符确保命令字符串格式正确
二、分割字符串🎈
2.1 设计✍️
当我们获取到了命令字符串后,需要将字符串以空格为分隔符将字符串分割为子字符串,并将每一个子字符串的地址存入到一个指针数组中,这里给出一个字符串被分割的例子:"ls -l -a" -> "ls" "-l" "-a"。我们可以使用strtok函数来将字符串分割,使用strtok函数处理同一个字符串时,第一次需要传入字符串的地址,后面再次调用则只需要传入NULL即可。
#include <stdio.h> #include <string.h> #include <stdlib.h>#define NUM 1024 // 命令输入缓冲区大小 #define SIZE 64 // 命令参数数组的最大容量 #define SEP " " // 命令参数的分隔符(空格)// 获取用户名(从环境变量) const char* getUsername() {char* username = getenv("USER");if(username)return username;elsereturn "none"; }// 获取主机名(从环境变量) const char* getHostname() {char* hostname = getenv("HOSTNAME");if(hostname)return hostname;elsereturn "none"; }// 获取当前工作目录(从环境变量) const char* getCwd() {char* cwd = getenv("PWD"); if(cwd) return cwd; else return "none"; }// 注意:这里缺少了getUserCommand函数的定义,该函数在之前的代码中存在int main() { while(1) // 无限循环,模拟终端持续运行{ char usercommand[NUM]; // 存储用户输入的完整命令char* argv[SIZE]; // 存储解析后的命令参数列表(argv[0]是命令名)// 获取用户输入的命令,返回1表示成功,-1表示失败int x = getUserCommand(usercommand,NUM); if(x == -1) continue; // 若读取失败,跳过本次循环,重新等待输入 int argc = 0; // 记录参数的数量(包括命令名)// 第一次调用strtok:用SEP分隔符分割usercommand,获取第一个参数(命令名)argv[argc++] = strtok(usercommand,SEP); // 循环分割剩余参数:strtok(NULL, SEP)表示继续分割上一次的字符串// 当分割到末尾时返回NULL,循环结束,此时argv[argc]会被设为NULLwhile(argv[argc++] = strtok(NULL,SEP)); // 说明:strtok会保存分割状态,第一次调用后,后续用NULL作为第一个参数// 分割结果会依次存入argv数组,直到返回NULL,此时argc正好是参数总数+1// 打印解析后的所有参数(调试用)for(int i = 0 ; argv[i] ; i++) // 当argv[i]为NULL时结束循环{ printf("%d : %s \n",i,argv[i]); // 输出参数索引和参数值} }return 0; }
- 这是一个模拟命令行解释器的基础框架,通过无限循环持续接收用户命令
- 关键功能是使用
strtok
函数对用户输入的命令进行分割,解析出命令名和参数argv
数组存储解析后的参数列表,argc
记录参数数量,符合标准 C 程序的参数传递格式- 目前代码主要实现了解析命令的功能,后续可以扩展为执行相应命令的功能
⚠️注意:这段代码依赖之前定义的
getUserCommand
函数才能正常编译运行,该函数负责读取用户输入并处理换行符。
2.2 封装🔎
// 函数功能:将输入的命令字符串分割成参数列表 // 参数: // in - 输入的原始命令字符串(如"ls -l /home") // out - 输出的参数数组,存储分割后的各个参数 void SplitCommand(char* in , char* out[]) { int argc = 0; // 记录参数数量(包括命令名)// 第一次调用strtok:用分隔符SEP分割输入字符串,获取第一个参数(命令名)// 例如将"ls -l"分割后,out[0]会指向"ls"out[argc++] = strtok(in,SEP); // 循环分割剩余参数:// strtok(NULL, SEP)表示继续分割上一次未完成的字符串// 当没有更多参数时,strtok返回NULL,循环结束// 此时out数组最后一个元素会是NULL,符合标准参数列表格式while(out[argc++] = strtok(NULL,SEP)); // 工作原理:// 1. 每次调用strtok获取下一个参数// 2. 将参数地址存入out[argc]// 3. argc自增// 4. 当strtok返回NULL时,循环条件为假,退出循环// 备选分割方式(功能相同,更易理解的写法)// while(1) // { // out[argc] = strtok(NULL,SEP); // 获取下一个参数// if(out[argc] == NULL) // 如果没有更多参数// break; // 退出循环// argc++; // 参数计数+1// } // 调试模式:如果定义了debug宏,则打印分割后的参数列表 #ifdef debug // 循环打印所有参数,直到遇到NULL结束符for(int i = 0 ; out[i] ; i++) { printf("%d : %s \n",i,out[i]); // 输出参数索引和参数值 } #endif } // 主函数:程序入口点 int main() { // 无限循环,模拟终端持续运行while(1) { char usercommand[NUM]; // 存储用户输入的完整命令字符串char* argv[SIZE]; // 存储分割后的命令参数列表// 获取用户输入的命令,返回1表示成功,-1表示失败int x = getUserCommand(usercommand,NUM); // 调用SplitCommand函数分割命令字符串为参数列表SplitCommand(usercommand,argv); }return 0; }
核心要点说明:
SplitCommand
函数是这段代码的核心,负责将完整命令字符串分割成参数数组,模拟了系统处理命令行参数的方式- 使用
strtok
函数进行字符串分割,这是 C 语言中处理命令行参数的经典方法- 采用条件编译(
#ifdef debug
)实现调试输出功能,方便开发阶段验证分割结果- 主函数通过无限循环持续获取并处理用户命令,构建了一个简单的命令行交互框架
注意⚠️:这段代码依赖之前定义的
getUserCommand
函数、NUM
、SIZE
和SEP
宏才能正常工作。分割后的参数列表argv
可以直接用于后续的命令执行逻辑(如通过exec
系列函数执行系统命令)。
三、执行指令🎈
3.1 设计✍️
将命令字符串分割后,就需要执行命令了,我们知道bash需要一直运行,这里添加一个循环让他一直运行,我们可以使用前面学习过的进程替换来执行命令,但是不能使用当前进程来进程替换,当前进程还需要继续运行,所以我们可以创建一个子进程来执行命令,由于我们将字符串分割为子字符串存储在了指针数组中,这里可以使用execvp函数来进行进场替换。
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> // 提供fork()、execvp()等系统调用 #include <sys/types.h> // 提供pid_t等类型定义 #include <sys/wait.h> // 提供wait()等进程等待函数#define NUM 1024 // 命令输入缓冲区大小 #define SIZE 64 // 命令参数数组最大容量 #define SEP " " // 命令参数分隔符(空格) #define debug 1 // 开启调试模式(非0值表示开启)// 从环境变量获取用户名 const char* getUsername() {char* username = getenv("USER");if(username)return username;elsereturn "none"; }// 从环境变量获取主机名 const char* getHostname() {char* hostname = getenv("HOSTNAME");if(hostname)return hostname;elsereturn "none"; }// 从环境变量获取当前工作目录 const char* getCwd() {char* cwd = getenv("PWD"); if(cwd) return cwd; else return "none"; }// 获取用户输入的命令 // 参数:command-存储命令的缓冲区,num-缓冲区大小 // 返回值:1-成功,-1-失败 int getUserCommand(char* command , int num) { // 打印类似Linux终端的命令提示符 [用户名@主机名 工作目录]#printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd()); // 使用fgets读取整行输入(支持含空格的命令)char* r = fgets(command,num,stdin); if(r == NULL) return -1; // 读取失败(如EOF)返回-1 // 移除输入末尾的换行符(fgets会将回车符也读入)command[strlen(command)-1] = '\0'; return 1; }// 分割命令字符串为参数列表 // 参数:in-原始命令字符串,out-输出的参数数组 void SplitCommand(char* in , char* out[]) { int argc = 0; // 第一次调用strtok分割出第一个参数(命令名)out[argc++] = strtok(in,SEP); // 循环分割剩余参数,直到strtok返回NULLwhile(out[argc++] = strtok(NULL,SEP)); // 调试模式:打印分割后的参数列表 #ifdef debug for(int i = 0 ; out[i] ; i++) { printf("%d : %s \n",i,out[i]); } #endif } int main() { while(1) // 无限循环,模拟持续运行的终端{ char usercommand[NUM]; // 存储用户输入的命令字符串char* argv[SIZE]; // 存储分割后的命令参数列表// 获取用户输入的命令int x = getUserCommand(usercommand,NUM); if(x <= 0) continue; // 读取失败则重新等待输入// 分割命令字符串为参数数组(如"ls -l" -> ["ls", "-l", NULL])SplitCommand(usercommand,argv); // 创建子进程执行命令pid_t id = fork();if(id < 0) // 进程创建失败{return -1;}else if(id == 0) // 子进程执行分支{// 用新程序替换子进程(执行命令)// argv[0]是命令名,argv是参数列表execvp(argv[0],argv);// 如果execvp返回,说明执行失败(如命令不存在)exit(-1);}else // 父进程执行分支{// 等待子进程结束,回收资源pid_t rid = wait(NULL);if(rid>0){}; // 仅为避免编译警告,可扩展为错误处理}}return 0; }
代码核心功能说明:
- 这是一个简易的命令行解释器(Shell)实现,能够接收并执行用户输入的命令
- 核心工作流程:
- 打印命令提示符 → 获取用户输入 → 分割命令为参数 → 创建子进程 → 执行命令 → 等待命令完成
- 关键系统调用:
fork()
:创建子进程,用于执行命令(避免影响父进程)execvp()
:在子进程中加载并执行新程序(用户输入的命令)wait()
:父进程等待子进程结束,防止僵尸进程- 调试模式:通过
#define debug 1
开启后,会打印分割后的命令参数列表,方便开发调试该代码模拟了 Linux Shell 的基本工作原理,但缺少很多高级功能(如管道、重定向、环境变量设置等),可作为学习操作系统进程管理的基础示例。
3.2 封装🔎
// 函数功能:执行命令 // 参数:argv - 命令参数列表(格式:[命令名, 参数1, 参数2, ..., NULL]) // 返回值:0表示成功,-1表示进程创建失败 int execute(char* argv[]) {// 创建子进程,返回进程IDpid_t id = fork();if(id < 0) // 进程创建失败{return -1;}else if(id == 0) // 子进程执行分支{ // 用新程序替换子进程:// argv[0]是要执行的程序名,argv是完整参数列表// 若执行成功,此函数不会返回;若失败,才会执行后续代码execvp(argv[0],argv); // 若execvp返回,说明命令执行失败(如命令不存在)exit(-1); } else // 父进程执行分支{ // 等待子进程结束,回收其资源,防止僵尸进程pid_t rid = wait(NULL); if(rid>0){}; // 空语句,仅用于避免编译警告,可扩展为错误处理} return 0; // 成功执行命令 } int main() { while(1) // 无限循环,保持命令行持续运行{ char usercommand[NUM]; // 存储用户输入的完整命令字符串char* argv[SIZE]; // 存储分割后的命令参数列表// 打印命令提示符并获取用户输入的命令int x = getUserCommand(usercommand,NUM); if(x <= 0) continue; // 若获取命令失败,重新开始循环// 将用户输入的命令字符串分割为参数列表SplitCommand(usercommand,argv); // 调用execute函数执行命令execute(argv);}return 0; }
代码核心改进说明:
- 模块化设计:将命令执行逻辑封装到
execute
函数中,使代码结构更清晰- 功能分工:
main
函数:负责命令的获取、解析和调用执行函数,形成主循环execute
函数:专注于进程创建、命令执行和资源回收- 进程管理流程:
- 父进程通过
fork()
创建子进程- 子进程通过
execvp()
加载并执行新命令- 父进程通过
wait()
等待子进程完成并回收资源这种结构使代码更易于维护和扩展,后续可以在
execute
函数中添加更多功能,如命令执行结果判断、错误信息提示等。
四、处理內键命令的执行🎈
- 当我们使用上面的代码执行命令时,发现大部分命令都可以被执行,但是例如cd、export、echo这样的内建命令却不能被执行,原因是内建命令是作用与bash的也就是这里的父进程,并且内建命令是bash的一部分,与常见命令不同,执行内建命令时不需要创建新的子进程,所以这些内建命令需要被特殊处理一下。将命令字符串分割后就判断当前命令是否为内建命令,是则直接执行内建命令,否则认定为常见命令向下继续执行。内建命令如何处理,这里就不多讲解,详细处理方法在下面的代码中有详细的注释,有兴趣的可以看一下。
五、重定向(本文章所有代码)🎈
- 写完前面的代码后,发现这个代码并不能解决重定向的问题,没了解重定向的最好看一下后面一篇文章了解一下重定向是什么,在分割命令字符串之前,我们需要判断这个命令字符串是否需要进行重定向,需要重定向则需要对字符串进行处理,例如"ls -l -a > fortest.txt" -> "ls -l -a" 重定向类型 "fortest.txt",我们会得到三个部分,命令字符串、重定向类型、和文件名,在代码定义四种重定向类型,无重定向、输入重定向、输出重定向和追加重定向,默认情况下是无重定向,定义一个全局变量存储重定向类型,定义一个全局指针来指向文件名,然后我们针对不同的重定向类型使用dup2函数进行不同的处理,详细不同重定向的处理过程请查看代码中重定向的一部分。
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> // 提供chdir()/getcwd()/fork()/execvp()等系统调用 #include <sys/types.h> // 提供pid_t类型定义 #include <sys/wait.h> // 提供wait()/WEXITSTATUS()等进程等待相关函数#define NUM 1024 // 命令输入缓冲区大小(存储用户输入的完整命令) #define SIZE 64 // 命令参数数组最大容量(存储分割后的参数列表) #define SEP " " // 命令参数分隔符(空格) // #define debug 1 // 调试开关:取消注释可打印参数分割结果// 全局变量:存储当前工作目录(用于更新PWD环境变量) // 注:必须全局/静态,避免putenv时内存被释放导致野指针 char cwd[1024]; // 全局二维数组:存储自定义添加的环境变量(避免putenv时字符串被销毁) char myenv[128][1024]; // 全局变量:记录myenv数组中有效环境变量的个数 int cnt = 0; // 全局变量:记录上一个子进程的退出码(用于echo $?) int lastcode = 0; // 获取用户家目录(从环境变量HOME) char* getHomename() { char* homename = getenv("HOME"); // 读取系统环境变量HOMEif(homename) return homename; // 成功则返回家目录路径else return (char*)"none"; // 失败返回"none" } // 获取当前用户名(从环境变量USER) const char* getUsername() { char* username = getenv("USER");if(username) return username;else return "none"; } // 获取主机名(从环境变量HOSTNAME) const char* getHostname() {char* hostname = getenv("HOSTNAME");if(hostname)return hostname;elsereturn "none"; }// 获取当前工作目录(从环境变量PWD) const char* getCwd() {char* cwd = getenv("PWD");if(cwd)return cwd;elsereturn "none"; }// 功能:获取用户输入的命令 // 参数:command-存储命令的缓冲区,num-缓冲区最大容量 // 返回值:1-成功获取有效命令,0-仅输入回车(空命令),-1-读取失败(如EOF) int getUserCommand(char* command , int num) {// 打印类似Linux终端的提示符:[用户名@主机名 工作目录]#printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());// 读取整行输入(支持含空格的命令,如"ls -l")char* r = fgets(command,num,stdin);if(r == NULL) return -1; // 读取失败(如Ctrl+D触发EOF)返回-1// 移除fgets读取的换行符(用户输入回车会被存入缓冲区,需替换为字符串结束符)command[strlen(command)-1] = '\0';// 处理空命令:若仅输入回车,返回0(避免后续无意义的分割和执行)if(strlen(command) == 0)return 0;return 1; // 成功获取有效命令 }// 功能:将完整命令字符串分割为参数列表 // 参数:in-原始命令字符串(如"cd /home"),out-输出的参数数组(如["cd","/home",NULL]) void SplitCommand(char* in , char* out[]) { int argc = 0; // 第一次调用strtok:用SEP分割in,获取第一个参数(命令名,如"cd")out[argc++] = strtok(in,SEP); // 循环分割剩余参数:strtok(NULL,SEP)表示继续分割上一次的字符串// 分割失败时返回NULL,存入out后循环终止,最终out以NULL结尾(符合execvp要求)while(out[argc++] = strtok(NULL,SEP)); #ifdef debug // 调试模式:打印分割后的参数列表(如0:cd, 1:/home)for(int i = 0 ; out[i] ; i++) { printf("%d : %s \n",i,out[i]); } #endif }// 功能:执行非内建命令(通过创建子进程+execvp替换进程映像) // 参数:argv-分割后的命令参数列表(如["ls","-l",NULL]) // 返回值:0-执行流程正常,-1-子进程创建失败 int execute(char* argv[]) {// 创建子进程:父进程继续执行,子进程执行命令pid_t id = fork();if(id < 0) // fork失败(如系统资源不足){return -1;}else if(id == 0) // 子进程分支{// 用新程序替换子进程:argv[0]是命令名,argv是完整参数列表// 若execvp返回,说明执行失败(如命令不存在)execvp(argv[0],argv);exit(-1); // 执行失败,子进程退出(退出码为-1,实际会被转为255)}else // 父进程分支{int status = 0; // 存储子进程退出状态// 等待子进程结束,回收资源(防止僵尸进程),并获取退出状态pid_t rid = wait(&status);if(rid > 0) // 成功回收子进程{// 提取子进程的正常退出码(需用WEXITSTATUS宏处理status)lastcode = WEXITSTATUS(status);}}return 0; // 父进程执行流程正常结束 }// 功能:实现cd命令的核心逻辑(切换工作目录+更新PWD环境变量) // 参数:path-目标目录路径 void cd(char* path) {// 切换当前工作目录到path(系统调用)chdir(path);// 临时缓冲区:存储切换后的实际工作目录char tmp[1024];// 获取当前工作目录的绝对路径,存入tmp(避免PWD与实际目录不一致)getcwd(tmp,sizeof(tmp));// 拼接成"PWD=路径"格式(符合环境变量的标准格式),存入全局变量cwdsprintf(cwd,"PWD=%s",tmp);// 将新的PWD环境变量加入进程环境表,覆盖原有PWDputenv(cwd); }// 功能:判断并执行内建命令(cd/export/echo) // 内建命令需在父进程执行(如cd需修改父进程的工作目录),不能创建子进程 // 参数:argv-分割后的命令参数列表 // 返回值:1-是内建命令且执行成功,0-非内建命令 int dobuildin(char* argv[]) {// 1. 处理cd命令(切换工作目录)if(strcmp(argv[0],"cd") == 0){char* path = NULL;// 若cd后无参数(如"cd"),默认切换到用户家目录if(argv[1] == NULL)path = getHomename();else // 若有参数(如"cd /home"),使用指定路径path = argv[1];cd(path); // 调用cd函数执行切换return 1; // 标记为内建命令}// 2. 处理export命令(添加自定义环境变量)else if(strcmp(argv[0],"export") == 0){// 若export后无参数(如"export"),不做处理if(argv[1] == NULL)return 1;else {// 将环境变量(如"MYVAR=123")存入全局数组myenv// 注:必须用全局数组存储,避免局部变量销毁导致野指针strcpy(myenv[cnt],argv[1]);// 将环境变量加入进程环境表(putenv仅保存指针,需确保字符串长期有效)putenv(myenv[cnt++]);cnt %= 128; // 防止数组越界(简单循环覆盖)return 1; // 标记为内建命令}}// 3. 处理echo命令(输出字符串/环境变量/退出码)else if(strcmp(argv[0],"echo") == 0){// 若echo后无参数(如"echo"),默认输出回车if(argv[1] == NULL){printf("\n");return 1;}// 若参数以$开头(如"echo $HOME"或"echo $?"),处理变量/退出码else if(*(argv[1]) == '$' && strlen(argv[1]) >= 2){// 3.1 处理echo $?(输出上一个子进程的退出码)if(*(argv[1]+1) == '?'){printf("%d\n",lastcode); lastcode = 0; // 输出后重置退出码(可选逻辑)}// 3.2 处理echo $ENV(输出指定环境变量的值,如$HOME)else {// 从环境表中获取变量值(argv[1]+1跳过$,如"$HOME"→"HOME")const char* enval = getenv(argv[1]+1);if(enval) // 若变量存在,输出变量值{printf("%s\n",enval);}else // 若变量不存在,输出空行{printf("\n");}}}// 3.3 普通字符串(如"echo hello"),直接输出参数else {printf("%s\n",argv[1]);}return 1; // 标记为内建命令} // 其他命令(非内建),返回0else if(0){}return 0; }// 主函数:简易Shell的主循环 int main() { // 无限循环:持续接收并处理用户命令(模拟Shell常驻)while(1) { char usercommand[NUM]; // 存储用户输入的完整命令char* argv[SIZE]; // 存储分割后的命令参数列表// 1. 获取用户命令(打印提示符+读取输入)int x = getUserCommand(usercommand,NUM); if(x <= 0) // 读取失败(-1)或空命令(0),跳过后续处理continue; // 2. 分割命令字符串为参数列表(如"echo $HOME"→["echo","$HOME",NULL])SplitCommand(usercommand,argv); // 3. 判断是否为内建命令:是则执行,跳过子进程创建x = dobuildin(argv);if(x == 1)continue;// 4. 非内建命令:创建子进程执行(如ls、pwd等)execute(argv);}return 0; }
代码核心功能总结
这段代码实现了一个迷你 Shell,支持以下关键功能:
- 命令提示符:模拟 Linux 格式
[用户名@主机名 工作目录]#
- 内建命令(父进程执行,不可 fork):
cd [路径]
:切换工作目录,无参数默认切到家目录,自动更新PWD
环境变量export 变量=值
:添加自定义环境变量(如export MYVAR=123
)echo 内容
:输出普通字符串、环境变量(如echo $HOME
)、上一个进程退出码(echo $?
)- 非内建命令(子进程执行,如
ls
、pwd
、date
):
- 通过
fork()
创建子进程,execvp()
替换子进程映像执行命令- 父进程通过
wait()
回收子进程,记录退出码(供echo $?
使用)- 命令处理流程:获取命令 → 分割参数 → 判断内建命令 → 执行内建命令 / 创建子进程执行非内建命令
关键注意点
- 环境变量存储:
putenv()
仅保存字符串指针,需用全局数组(myenv
、cwd
)存储环境变量,避免局部变量销毁导致野指针。- 内建命令必要性:
cd
、export
等命令需修改父进程状态(工作目录、环境变量),必须在父进程执行,不能创建子进程(否则修改仅作用于子进程,子进程退出后失效)。- 进程退出码:通过
wait(&status)
获取子进程退出状态,用WEXITSTATUS(status)
提取正常退出码,存入lastcode
供echo $?
使用。- 空命令处理:用户仅输入回车时,返回
0
跳过后续分割和执行,避免无意义操作。
结束语
以上就是我对【Linux系统编程】自定义Shell的理解与实现
感谢你的三连支持!!!