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

news2026/3/19 11:48:53
目录自主Shell命令行解释器第1步 : 打印命令行字符串第2步 : 从键盘中获取用户的字符串输入第3步 : 解析命令行字符串第4步 : 利用程序替换函数执行解析完的命令第 5 步 : 内建命令的特殊处理第6步 : 解析重定向命令自主Shell命令行解释器在前面学习完进程的创建进程的等待以及进程替换等函数之后我们可以自己设计一个shell命令行解释器shell的本质就是一个死循环因为当我们输入完一条命令后, 它执行完后不能直接退出还要等你下一条命令, 所以必须while(1)死循环顶着。第1步 : 打印命令行字符串void PrintCommandLine() { printf([%s%s:%s]# , GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); // 用户名 主机名 : 当前路径 fflush(stdout); }用GetUserNane GetHostName GetPwd这三个函数分别来获取用户名 主机名 当前路径怎么获取用户名 主机名 当前路径这三个变量呢?我们可以通过环境变量来获取 其实真正的shell是通过专门的系统调用来获取的, 但是我们在自己设计的时候就通过getenv从环境变量中获取, 这样更方便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(pos1); // /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() 实现读取字符串操作#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 切割后就是argv[0] ls argv[1] -a argv[2] -l argv[3] NULL所以我们在自己设计时还要在代码中自己维护一张 argv[] 命令行参数表, 我们取名为 gargv[]#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));的作用是每次解析新命令前必须重置参数表和计数避免上一次解析的残留数据干扰本次结果。2. 第一次切割提取命令名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 代码。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.只是读取、打印、创建文件…3. 跑完就结束不影响 Shell 的状态内建命令为什么不能是独立程序cd、exit、export、pwd 这些命令做的事情是修改 Shell 自己本身父子进程的目录、环境变量、文件描述符……全都存在各自的 PCB 里互相独立、互不干扰。因为每个进程的 PCBtask_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 里的目录、环境变量 , 只是读文件、打印、计算 , 所以它们不需要在父进程执行所以这里我们需要对这种内建命令进行特殊处理:// 我们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 这样的重定向命令 , 所以我们还要对这样的重定向命令进行处理:// 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 commandlinestrlen(commandline); //定义头尾指针 while(start end) //从左到右扫描找 、 、 { if(*start ) { if(*(start1) ) { // 追加重定向 *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 重定向做好准备。// 空格空格空格filename.txt #define TrimSpace(start) do{\ while(isspace(*start)) start;\ }while(0)TrimSpace(start) 这个宏函数的作用是提取的是后面的文件名把指针 start 前面所有的空格全部跳过让 start 直接指向第一个不是空格的字符。去掉字符串左边的所有空格只保留后面的有效内容。因为 TrimSpace 是在找到 / / 之后才调用的这时候 start 指针已经指向符号后面的内容了所以它清理的是文件名前面的空格。但是总的来说ParseRedir 是只解析命令并不执行命令 , 它只负责从命令行里提取出 2 个关键信息存到全局变量redir_type 哪种重定向 / / /无, filename 重定向到哪个文件 , 但是它不打开文件不重定向不执行命令。所以下一步还是得回到第四步的 ExecuteCommand 解析命令函数中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不能污染父进程只能在子进程里做。举个例子: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三个参数就是告诉它你是谁、你带什么参数、你用什么环境。execvpe(gargv[0], gargv, genv);第一个参数 gargv[0] 意思就是你要执行谁程序名字比如 ls 就会告诉 execvpe我要运行 ls 这个程序第二个参数 gargv 意思就是 命令 参数列表 , 是一个字符串数组格式固定{ls, -a, -l, NULL} , 告诉 ls 你运行的时候要带上 -a -l 这些参数。- gargv[0] 命令本身 ls - gargv[1] 参数 -a - gargv[2] 参数 -l - 最后必须以 NULL 结尾第三个参数 genv 意思是环境变量 , 就是系统里的 PATH、HOME 这些。 , 一般直接传父进程的环境变量就行。作用是让系统能找到 ls 在哪里不然它不知道 ls 在哪个目录。好了到这里 , 自主shell的基本内容也就完了完整代码如下:#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; // vectorstd::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(pos1); // /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 commandlinestrlen(commandline); while(start end) { if(*start ) { if(*(start1) ) { // 追加重定向 *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重定向等核心机制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2420977.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…