1、命令行提示符首先应明确shell是死循环软件本质是交互式命令解释器命令行提示符的格式用户名 主机名 当前路径staticstd::stringrfindDir(conststd::stringp){// 如果路径是根目录 /直接返回/if(p/)returnp;conststd::string psep/;// 路径分隔符斜杠// rfind从后往前找第一个 /autoposp.rfind(psep);// npos代表没找到斜杠比如单纯字符串test没有/if(posstd::string::npos)returnstd::string();// 返回空字符串// substr(pos1)截取斜杠后面所有字符// 例/home/ubuntu - 找到最后一个/后面ubuntu返回ubuntureturnp.substr(pos1);}constchar*GetUserName(){// getenv(USER) 读取环境变量USER存当前用户名char*namegetenv(USER);if(nameNULL)// 读取失败returnNone;returnname;}// 获取主机名constchar*GetHostName(){staticcharbuf[128];// 系统调用直接获取本机主机名if(gethostname(buf,sizeof(buf)-1)0){returnbuf;}strcpy(buf,None);returnbuf;}// 获取当前工作路径constchar*GetPwd(){char*pwdgetenv(PWD);//char *pwd getcwd(cwd, sizeof(cwd));if(pwdNULL)returnNone;returnpwd;}// 打印shell提示符voidPrintCommandLine(){// 拼接打印格式[用户名主机名 当前目录]#printf([%s%s %s]# ,GetUserName(),GetHostName(),rfindDir(GetPwd()).c_str());fflush(stdout);// 强制刷新输出缓冲区不然提示符可能卡住不显示}2、获取键盘指令intGetCommand(charcommandline[],intsize){if(NULLfgets(commandline,size,stdin))return0;// 2.1 用户输入的时候至少会摁一下回车\n abcd\n ,\n \0commandline[strlen(commandline)-1]\0;returnstrlen(commandline);}用fegts从键盘读取一行输入存到command这里不能用scanf进行输入scanf是以空格、回车为分隔符无法一次性读取带多个空格的整行命令命令参数的个数不固定例如ls -a -lscanf无法一次性完整接收fegts直接读取缓冲区一整行完整保留所有空格然后交给strtok分割3、命令字符串切割#defineMAXARGS32char*gargv[MAXARGS];intgargc0;constchar*gsep ;voidParseCommand(){// 每次解析前清空全局参数表防止上一条命令残留memset(gargv,0,sizeof(gargv));gargc0;// strtok第一次调用传入原始字符串command_linechar*tokenstrtok(command_line,gsep);while(token!NULL){gargv[gargc]token;// 后续分割第一个参数传NULL继续从上一次截断位置分割tokenstrtok(NULL,gsep);}}intmain(){// 0. 从配置文件中获取环境变量填充环境变量表// LoadEnv();charcommand_line[MAXSIZE]{0};while(1){// 1. 打印shell提示符PrintCommandLine();// 2. 获取用户输入命令if(0GetCommand(command_line,sizeof(command_line))){continue;}// 3. 分割整行命令填充全局参数数组gargvParseCommand(command_line);// 循环打印分割后的每一个命令参数for(inti0;gargv[i];i){printf([%d]: %s\n,i,gargv[i]);}// 打印原始用户输入的完整命令字符串printf(echo: %s\n,command_line);}return0;}借助strtok将字符串分割成一个个的子串第一次调用后返回第一个子串指针后续循环分割第一个参数必须传NULL函数自动记住上次分割位置注意不能传入字符串常量char* str aaa bbbstrtok会原地修改字符串把找到的分隔符空格覆盖为\0截断子串4、外部命令的执行intExecuteCommand(){// 能不能让你的bash自己执行命令ls -a -lpid_t idfork();if(id0)return-1;elseif(id0){// ls -a -lintfd-1;// 重定向文件描述符if(redir_typeNoneRedir){// Do Nothing}elseif(redir_typeOutputRedir){// 子进程要进行输出重定向fdopen(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);dup2(fd,1);}elseif(redir_typeAppRedir){// 追加重定向fdopen(filename,O_WRONLY|O_CREAT|O_APPEND,0666);dup2(fd,1);}elseif(redir_typeInputRedir){fdopen(filename,O_RDONLY);dup2(fd,0);}else{}// 参数1程序名(无需绝对路径自动搜索PATH)// 参数2命令参数数组gargv// 参数3自定义环境变量数组genvexecvpe(gargv[0],gargv,genv);// exec系列函数执行成功不会返回返回替换失败子进程退出码1exit(1);}else{// 父进程intstatus0;pid_t ridwaitpid(id,status,0);if(rid0){// WEXITSTATUS提取子进程正常退出的退出码存入全局lastcode对应$?lastcodeWEXITSTATUS(status);}}return0;}intmain(){// 存储用户输入整行命令的缓冲区charcommand_line[MAXSIZE]{0};while(1){// 打印命令行字符串PrintCommandLine();// 获取用户输入if(0GetCommand(command_line,sizeof(command_line)))continue;ParseRedir(command_line);// 解析字符串 - ls -a -l - ls -a -l 命令行解释器就要对用户输入的命令字符串首先进行解析ParseCommand(command_line);// 让子进程执行这个命令ExecuteCommand();}return0;}先通过fork创建子进程复制当前shell程序再调用execvp接口进行程序替换不能父进程直接调用exec系列函数exec 程序替换会完全覆盖当前进程代码、数据shell 程序直接消失无法循环接收下一条命令5、内建命令// 环境变量表存储拷贝过来的父进程环境变量char*genv[MAXARGS];// 环境变量条目计数intgenvc0;// 保存上一条执行命令的退出码对应$?功能intlastcode0;// retunr val:// 0 : 不是内建命令// 1 : 内建命令执行完毕intCheckBuiltinExecute(){// cd命令if(strcmp(gargv[0],cd)0){// 内建命令if(gargc2){// 新的目标路径: gargv[1]// 1. 更改进程内核中的路径chdir(gargv[1]);// 2. 更改环境变量charpwd[1024];getcwd(pwd,sizeof(pwd));// /home/whbsnprintf(cwd,sizeof(cwd),PWD%s,pwd);// cwd: PWD/home/homeputenv(cwd);lastcode0;}return1;}// echo命令elseif(strcmp(gargv[0],echo)0)// cd , echo , env , export 内建命令{if(gargc2){if(gargv[1][0]$){// $? ? : 看做一个变量名字if(strcmp(gargv[1]1,?)0){printf(lastcode: %d\n,lastcode);}elseif(strcmp(gargv[1]1,PATH)0){printf(%s\n,getenv(PATH));// putenv 和 getenv 是访问环境变量表}lastcode0;}return1;}}return0;}cd操作chdir(path)系统调用修改内核 PCB 记录的当前工作目录getcwd()获取最新路径snprintf拼接PWDxxxputenv()更新环境变量PWD操作系统不会同步更新PWD环境变量只改内核路径提示符读取getenv(PWD)会显示旧路径echo命令echo $?读取全局变量lastcode打印上一条外部命令退出码echo $PATHgetenv()读取进程环境变量表打印内建命令执行完成统一设置lastcode0代表执行成功进程创建时自动继承父进程环境变量系统全局指针extern char** environ存储getenv/putenv底层操作进程内部环境变量表修改后子进程 exec 时可通过execvpe传递