从零手搓实现 Linux 简易 Shell:内建命令 + 环境变量 + 程序替换全解析

📅 发布时间:2026/7/13 12:14:04 👁️ 浏览次数:
从零手搓实现 Linux 简易 Shell:内建命令 + 环境变量 + 程序替换全解析
从零手搓实现 Linux 简易 Shell内建命令 环境变量 程序替换完整版下面我们用 C 语言一步步实现一个极简但功能相对完整的 shell支持读取用户输入、解析命令行支持参数内建命令cd、exit、pwd、echo、export环境变量的读取与修改支持P A T H 、 PATH、PATH、HOME 等通过 fork execvp 执行外部程序管道|的简单支持单级管道前后台进程基本的信号处理CtrlC 不退出 shell目标最终效果类似 bash 的极简版myshell$pwd/home/user myshell$cd/tmp myshell$echo$HOME/home/user myshell$exportMYVARhello myshell$echo$MYVARhello myshell$ls-l|greptxt -rw-r--r--1user user1234Feb4txtfile.txt myshell$sleep10[1]12345myshell$完整实现代码约 300 行#define_GNU_SOURCE#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includesys/wait.h#includesys/types.h#includesignal.h#includeerrno.h#includelimits.h#defineMAX_INPUT1024#defineMAX_ARGS64#defineMAX_PIPES2// 支持一级管道// 全局环境变量表我们自己维护一份副本externchar**environ;char**my_environNULL;// 前台进程组 pidpid_tfg_pgid0;// ---------------------- 辅助函数 ----------------------// 字符串 trim 两端空白voidtrim(char*str){char*startstr;char*end;// 跳过开头空白while(*start(*start ||*start\t||*start\n))start;if(*start0){*str\0;return;}// 找到结尾endstartstrlen(start)-1;while(endstart(*end ||*end\t||*end\n))end--;*(end1)\0;// 移动到开头if(start!str)memmove(str,start,end-start2);}// 解析一行输入为参数数组intparse_command(char*input,char**args){intargc0;char*token;trim(input);if(strlen(input)0)return0;tokenstrtok(input, \t\n);while(tokenargcMAX_ARGS-1){args[argc]token;tokenstrtok(NULL, \t\n);}args[argc]NULL;returnargc;}// 查找命令是否为内建intis_builtin(constchar*cmd){returnstrcmp(cmd,cd)0||strcmp(cmd,exit)0||strcmp(cmd,pwd)0||strcmp(cmd,echo)0||strcmp(cmd,export)0||strcmp(cmd,env)0;}// 内建命令执行intbuiltin_execute(char**args){if(!args[0])return1;if(strcmp(args[0],cd)0){if(!args[1]){chdir(getenv(HOME));}else{if(chdir(args[1])!0){perror(cd);}}return1;}if(strcmp(args[0],exit)0){exit(0);}if(strcmp(args[0],pwd)0){charcwd[PATH_MAX];if(getcwd(cwd,sizeof(cwd))!NULL){printf(%s\n,cwd);}else{perror(pwd);}return1;}if(strcmp(args[0],echo)0){inti1;while(args[i]){printf(%s,args[i]);if(args[i1])printf( );i;}printf(\n);return1;}if(strcmp(args[0],export)0){if(args[1]){// 支持 export VARvalue 或 export VARchar*eqstrchr(args[1],);if(eq){*eq\0;setenv(args[1],eq1,1);}else{setenv(args[1],,1);}}return1;}if(strcmp(args[0],env)0){char**envenviron;while(*env){printf(%s\n,*env);env;}return1;}return0;}// 简单信号处理CtrlC 只影响前台进程voidsigint_handler(intsig){if(fg_pgid0){kill(-fg_pgid,SIGINT);}// 不退出 shell}// 执行外部命令支持单级管道voidexecute_external(char**args,intbackground){intpipefd[2];inthas_pipe0;// 检测是否有管道for(inti0;args[i];i){if(strcmp(args[i],|)0){has_pipe1;args[i]NULL;// 分割成两个命令char**cmd2args[i1];pipe(pipefd);pid_tpid1fork();if(pid10){// 左命令dup2(pipefd[1],STDOUT_FILENO);close(pipefd[0]);close(pipefd[1]);execvp(args[0],args);perror(execvp left);_exit(1);}pid_tpid2fork();if(pid20){// 右命令dup2(pipefd[0],STDIN_FILENO);close(pipefd[0]);close(pipefd[1]);execvp(cmd2[0],cmd2);perror(execvp right);_exit(1);}close(pipefd[0]);close(pipefd[1]);if(!background){fg_pgidpid1;waitpid(pid1,NULL,0);waitpid(pid2,NULL,0);fg_pgid0;}else{printf([%d] %d\n,1,pid1);}return;}}// 无管道普通执行pid_tpidfork();if(pid0){// 子进程execvp(args[0],args);perror(execvp);_exit(1);}elseif(pid0){if(!background){fg_pgidpid;waitpid(pid,NULL,0);fg_pgid0;}else{printf([%d] %d\n,1,pid);}}}// 主循环intmain(){charinput[MAX_INPUT];char*args[MAX_ARGS];intargc;// 初始化环境变量副本可选setenv/putenv 实际操作的是 environ// my_environ environ; // 如果需要自己维护可复制signal(SIGINT,sigint_handler);signal(SIGTSTP,SIG_IGN);// 忽略 CtrlZprintf(欢迎使用 myshell (简易版)\n);printf(支持命令cd, pwd, echo, export, exit, env, 外部程序, 简单管道, 后台\n\n);while(1){charcwd[PATH_MAX];getcwd(cwd,sizeof(cwd));printf(myshell:%s$ ,strrchr(cwd,/)?strrchr(cwd,/):cwd);if(!fgets(input,MAX_INPUT,stdin)){printf(\n退出\n);break;}// 去掉换行input[strcspn(input,\n)]0;// 解析argcparse_command(input,args);if(argc0)continue;intbackground0;if(argc1strcmp(args[argc-1],)0){background1;args[--argc]NULL;}// 内建命令if(is_builtin(args[0])){builtin_execute(args);continue;}// 外部命令或管道execute_external(args,background);}return0;}编译 运行gcc -o myshell myshell.c ./myshell功能说明与扩展点已实现内建cd、exit、pwd、echo、export、env环境变量读取$PATH 等通过 getenv外部程序执行execvp简单单级管道ls | grep后台执行sleep 100 CtrlC 只杀前台进程可以继续扩展的方向支持多级管道需要多个 pipefd 数组 循环 fork支持重定向 → 解析时识别符号dup2支持环境变量展开$VAR→ 在 parse 前替换支持历史命令上下箭头→ readline 库支持 job controljobs、fg、bg→ 记录后台进程列表支持 alias小结这个版本大约 300 行代码已经包含了 shell 最核心的三大能力内建命令直接执行环境变量getenv / setenv程序替换fork execvp如果你想继续深入某个部分例如实现多级管道、重定向、变量展开、job control可以告诉我我可以继续补充对应代码和解析。