当前位置: 首页 > article >正文

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

目录自主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重定向等核心机制。

相关文章:

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

目录 自主Shell命令行解释器 第1步 : 打印命令行字符串 第2步 : 从键盘中获取用户的字符串输入 第3步 : 解析命令行字符串 第4步 : 利用程序替换函数执行解析完的命令 第 5 步 : 内建命令的特殊处理 第6步 : 解析重定向命令 自主Shell命令行解释器 在前面学习完进程的创…...

在资源优化调度场景中,基于多源数据(如地磁/视频/雷达检测、浮动车GPS、手机信令、互联网地图API等)构建关联规则

在资源优化调度场景中,基于多源数据(如地磁/视频/雷达检测、浮动车GPS、手机信令、互联网地图API等)构建关联规则,可实现对城市交通系统的动态感知与协同优化。其核心逻辑包括:多源数据融合建模:通过时空对…...

基于VSG控制的MMC并网逆变器仿真模型(Simulink仿真实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...

Ambari Metrics 是 Apache Ambari 提供的轻量级、嵌入式集群监控子系统,用于收集、聚合、存储和展示 Hadoop 生态组件

Ambari Metrics 是 Apache Ambari 提供的轻量级、嵌入式集群监控子系统,用于收集、聚合、存储和展示 Hadoop 生态组件(如 HDFS、YARN、HBase、Kafka 等)的关键性能指标(Metrics)。它基于时间序列数据库(默认…...

Apache Hadoop 生态系统(或与其深度集成)的企业级大数据平台核心服务,常见于 Hortonworks Data Platform(HDP)、Cloudera Data Platform

Apache Hadoop 生态系统(或与其深度集成)的企业级大数据平台核心服务,常见于 Hortonworks Data Platform(HDP)、Cloudera Data Platform(CDP)等发行版中,各自承担关键职能&#xff1…...

手机短信误删!4 个实用恢复方法,一文看懂轻松

你是否有过这样的懊恼时刻:手滑删除了银行的验证码短信,或是亲友的珍贵问候,急需时才发现信息已消失无踪?其实,被删除的短信并不会立刻从手机中彻底清除,只是被系统标记为 “可覆盖空间”—— 这个特性为恢…...

每天了解几个MCP SERVER:工具百宝箱!200+应用集成,Composio 让 AI 连接一切

每天了解几个MCP SERVER:🔧 Composio来源: https://docs.composio.dev/docs/mcp-overview简介 Composio MCP Server 提供200工具集成平台,让AI可以连接各种第三方应用和服务。 能做什么 工具集成:200应用集成工作流编排&#xff1…...

每天了解几个MCP SERVER:云端媒体库!AI 自动处理图片视频,Cloudinary 让媒体管理更简单

每天了解几个MCP SERVER:🖼️ Cloudinary来源: https://github.com/cloudinary/mcp-servers简介 Cloudinary MCP Server 提供云端图片管理和CDN服务,让AI可以进行图片上传、处理和优化。 能做什么 图片上传:上传图片和视频图片处理…...

每天了解几个MCP SERVER:极速分析神器!亿级数据秒级查询,ClickHouse 让大数据分析飞起

每天了解几个MCP SERVER:⚡ ClickHouse来源: https://github.com/ClickHouse/mcp-clickhouse简介 ClickHouse MCP Server 为AI Agent提供ClickHouse列式数据库的查询能力,以极高的性能分析大数据。 能做什么 SQL查询:执行高性能SQL查询实时分…...

刷题笔记:力扣第48题-旋转图像

1.拿到这道题目,第一反应是再创建一个新的矩阵,按照顺时针旋转90的方式遍历原来的矩阵,将旋转后的矩阵存入新矩阵中,输出即可。这种方法的时间复杂度和空间复杂度均为O(n2)。2.但本题不允许使用新的矩阵,这意味着一切修…...

OpenClaw使用教程 + 获取API + 踩坑

新手建议:如果研究不下去,直接用第三方的Oneclaw,字节、阿里、腾讯出品的第三方。 可以等一等,国产有替代。 别焦虑,没用,23年gpt,24年sora,25deepseek和agent,26开年o…...

免费查AI率完全攻略:5种不花钱的检测方法

免费查AI率完全攻略:5种不花钱的检测方法 写完论文之后最怕什么?不是导师让你改格式,而是交上去才发现AI率高得离谱。现在各高校对AI率查得越来越严,知网、维普、万方都上了AIGC检测模块,一旦被标记"疑似AI生成&q…...

免费降AI率软件排行:从白嫖到付费怎么选

免费降AI率软件排行:从白嫖到付费怎么选 写这篇文章的起因是,上个月有三个不同的朋友问了我同一个问题:“有没有免费降AI率的软件?” 一个是大四写毕业论文的,一个是在读研二准备投期刊的,还有一个是帮老板…...

毕业论文免费查AI率+降AI率一站式攻略

毕业论文免费查AI率降AI率一站式攻略 答辩季快到了,论文的事情一件接一件。查重过了还有AI检测,AI检测过了还有格式审查,感觉毕业比入学还难。 这篇文章解决一个具体问题:怎么用免费工具完成论文AI率的检测和修改。从"查出来…...

免费降ai工具实测:哪个免费额度最良心

免费降ai工具实测:哪个免费额度最良心 网上但凡说"免费"两个字的工具,十个里有八个是标题党。要么注册完发现免费额度只有100字,跟没有一样;要么做一堆任务才能解锁,转发三个群、邀请两个好友,折…...

HJ132 小红走网格

中等 通过率:31.75% 时间限制:1秒 空间限制:1024M 知识点数论 校招时部分企业笔试将禁止编程题跳出页面,为提前适应,练习时请使用在线自测,而非本地IDE。 描述 在二维平面坐标系中,小红初…...

2026 学术诚信必备!10 款 AI 论文查重工具盘点:查重 + AI 率双检测,告别毕业 / 投稿焦虑

又到本科毕设、期刊投稿的关键节点,「重复率飘红」「AI 率超标」成了悬在每一位学术人头顶的达摩克利斯之剑。传统查重工具只能检测文字重复,面对 AI 生成内容却束手无策;而专业 AI 检测又价格高昂、操作繁琐。 今天为大家整理了10 款兼顾查…...

在服务器上通过git仓库进行多开发者协同工作

本文旨在解决一个需求:在同一服务器上的若干个开发人员能够在自己的账户下面抓取和更新一个项目文件夹。1.首先是root权限创建git仓库:创建用于共享的裸仓库:git clone --bare 项目文件夹名 项目文件夹名.git创建用户组:sudo grou…...

C语言程序设计第四版(何钦铭、颜晖)第八章指针之拆分实数的整数与小数部分

1. 拆分实数的整数与小数部分:要求自定义一个函数 void splitfloat (float x, int* intpart,float *fracpart);其中x是被拆分的实数,*intpart和*fracpart分别是将实数x拆分出来的整数部分与小数部分。编写主函数,并在其中调用函数splitfloat()。试编写相应程序。#include<st…...

苍穹外卖WebSocket连接问题

在调试过程中发现&#xff0c;当前端应用部署在Nginx下访问localhost时&#xff0c;WebSocket连接无法建立&#xff1b;而直接运行前端项目时则可以正常连接。通过浏览器F12排查问题后&#xff0c;发现是WebSocket的URL路径不同&#xff1a;我们实际需要的地址为ws://localhost…...

c++ 类和对象(全)

本文只是把之前上中下三篇文章集合了起来&#xff0c;后面跟着补充一点示例代码&#xff0c;也只是为了方便大家一下子全部观看。 类和对象&#xff08;上&#xff09; 一.类的定义 1.类定义格式 我们可以先看一个类的例子(栈)&#xff1a; class Stack { private:int* a;in…...

优化Docker镜像下载速度:国内镜像源配置指南

1. 为什么需要配置国内Docker镜像源 第一次用Docker拉取镜像时&#xff0c;看着进度条像蜗牛一样缓慢移动&#xff0c;我盯着屏幕足足等了半小时。后来才发现&#xff0c;默认的Docker Hub服务器在国外&#xff0c;国内直接访问速度感人。这就像你在北京点外卖&#xff0c;却非…...

学生专属福利:如何免费获取JetBrains和Navicat全家桶

1. 学生专属福利&#xff1a;为什么你需要它们&#xff1f; 还在为开发工具的高昂费用发愁吗&#xff1f;作为一个过来人&#xff0c;我太懂学生时代那种“想学技术&#xff0c;却被工具卡住”的窘迫了。一套正版的JetBrains全家桶&#xff08;比如IntelliJ IDEA、PyCharm、Web…...

Markdown写作技巧:LaTeX公式+代码块高亮全攻略

Markdown写作技巧&#xff1a;LaTeX公式代码块高亮全攻略 在技术文档和学术研究的写作中&#xff0c;Markdown因其简洁性和强大功能已成为首选工具。但对于需要表达复杂数学公式或展示多语言代码的专业作者来说&#xff0c;如何充分利用Markdown的高级功能仍是一个挑战。本文将…...

基于PEX88096的PCIe 4.0八盘M.2扩展卡设计

1. 项目概述PCIe 4.0 M.2扩展卡长期面临市场定价畸高问题&#xff0c;主流商用产品普遍标价数千元&#xff0c;严重制约了高性能存储系统在DIY、边缘计算及小型服务器场景中的普及应用。本项目以PEX88048 PCIe 4.0交换芯片为核心&#xff0c;构建一款支持8路独立M.2 NVMe插槽的…...

Python+Selenium实现抖音博主批量监控:300+账号实时更新通知(附完整代码)

PythonSelenium构建高可用抖音博主监控系统&#xff1a;从零到一的实战架构与性能调优 最近在技术社群里&#xff0c;经常看到有朋友在讨论如何批量追踪抖音博主的更新动态。无论是做内容分析、竞品研究&#xff0c;还是个人兴趣追踪&#xff0c;手动刷新几百个主页显然不现实。…...

告别数据孤岛:基于WebDAV的Zotero与InfiniCLOUD跨平台同步实战

1. 为什么需要跨平台文献同步&#xff1f; 作为一名常年泡在实验室的研究生&#xff0c;我经历过无数次这样的崩溃瞬间&#xff1a;在实验室电脑上整理好的文献库&#xff0c;回到宿舍打开笔记本发现参考文献全乱了&#xff1b;出差路上想用平板查篇论文&#xff0c;却发现最新…...

【解刊】IEEE Trans系列新宠:中科院1区TOP期刊,国人作者占比近八成领跑全球!

1. IEEE Transactions on Cybernetics&#xff1a;控制论领域的黄金期刊 最近在学术圈里&#xff0c;IEEE Transactions on Cybernetics&#xff08;IEEE控制论汇刊&#xff09;成了热门话题。这本期刊不仅稳居中科院1区TOP&#xff0c;更让人惊讶的是&#xff0c;中国学者在这…...

社区分享 | 从零开始学习 TinyML(三)

1. TinyML模型部署后的性能优化挑战 当你第一次把训练好的TinyML模型部署到Arduino或Cortex-M系列MCU上时&#xff0c;可能会遇到一些令人头疼的问题。我清楚地记得自己早期的一个项目&#xff0c;模型在PC上测试时运行良好&#xff0c;但移植到开发板上后&#xff0c;推理速度…...

基于Mirage Flow的个性化学习推荐系统构建

基于Mirage Flow的个性化学习推荐系统构建 1. 引言 你有没有过这样的经历&#xff1f;同一个班级&#xff0c;同样的老师&#xff0c;同样的教材&#xff0c;但有的同学学得飞快&#xff0c;有的同学却总是卡在某个知识点上&#xff0c;怎么都绕不过去。传统的在线教育平台&a…...