【Linux指南】进程控制系列(五)实战 —— 微型 Shell 命令行解释器实现

【Linux指南】进程控制系列(五)实战 —— 微型 Shell 命令行解释器实现
287e0d18392d0dc74c83c324a229e5c3.jpg

前面四篇文章,我们已经掌握了进程控制的 “全链路技能”:用fork创建子进程、exec替换程序、waitpid回收资源、exit终止进程。今天,我们将这些知识 “组装” 成一个能实际运行的工具 ——微型 Shell 命令行解释器(简称 “迷你 Shell”)。

这个迷你 Shell 将支持:命令行提示符(如[user@host dir]#)、内建命令(cd/export/env/echo)、外部命令(ls/ps等)、环境变量管理(继承与导出),完全遵循 Linux Shell 的核心工作逻辑。通过亲手实现,你会彻底明白 “输入一条命令后,Shell 到底在做什么”。

一、先搞懂:Shell 的本质是 “命令管家”

在写代码前,我们先回归本质:Shell 是一个 “命令管家”—— 它的核心工作是 “接收用户命令→解析命令→调度资源执行命令→反馈结果”,具体流程可拆解为一个无限循环:

  1. 展示提示符:打印[用户名@主机名 工作目录]$,告诉用户 “可以输入命令了”;
  2. 获取命令:读取用户输入的一行命令(如ls -lcd /home);
  3. 解析命令:将命令拆分为 “命令名 + 参数”(如ls -l拆成["ls", "-l", NULL]);
  4. 执行命令
    • 若为内建命令(如cd):Shell 自己执行(需修改 Shell 进程自身状态,不能用子进程);
    • 若为外部命令(如ls):Shell 创建子进程,子进程用exec替换为目标程序,Shell 等待子进程退出;
  5. 循环往复:回到第一步,等待用户输入下一条命令。

举个通俗的例子:Shell 就像餐厅服务员 —— 提示符是 “请问需要点什么?”,用户输入是 “一份牛排”(命令),解析是 “牛排 + 七分熟”(命令 + 参数),执行是:

  • 内建命令:服务员自己给你倒杯水(不用叫后厨);
  • 外部命令:服务员叫后厨(子进程)做牛排,自己在旁边等(waitpid),做好后给你端过来(反馈结果)。

二、迷你 Shell 的核心模块设计

我们将迷你 Shell 拆解为 5 个核心模块,逐个实现并讲解,最后整合为完整代码。每个模块都对应 Shell 的一个关键功能,且能复用前面学的进程控制知识。

2.1 模块 1:命令行提示符 —— 给用户 “交互的入口”

命令行提示符(如[ubuntu@localhost myshell]$)的作用是 “提示用户输入命令”,它需要包含三个关键信息:用户名主机名当前工作目录。我们通过 Linux 提供的函数获取这些信息:

  • 用户名:getenv("USER")(从环境变量中获取当前登录用户);
  • 主机名:getenv("HOSTNAME")(从环境变量中获取主机名);
  • 当前工作目录:getcwd()(获取当前进程的工作目录,比getenv("PWD")更准确,因为cdPWD可能未更新)。
实现细节:
  • 工作目录简化:默认显示完整路径(如/home/ubuntu/myshell),我们可以简化为 “最后一级目录”(如myshell),更符合常用 Shell 的习惯;
  • 缓冲区刷新:printf默认是 “行缓冲”,若提示符不含\n,需用fflush(stdout)强制刷新,否则提示符会 “卡着不显示”。
代码实现(提示符模块):

c

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>// 全局变量:存储当前工作目录(避免频繁分配内存)#defineBUF_SIZE1024char g_pwd[BUF_SIZE]={0};char g_last_pwd[BUF_SIZE]={0};// 存储上次工作目录,用于cd -// 1. 获取用户名staticchar*get_username(){char* username =getenv("USER");return(username ==NULL)?"unknown": username;}// 2. 获取主机名staticchar*get_hostname(){char* hostname =getenv("HOSTNAME");return(hostname ==NULL)?"unknown-host": hostname;}// 3. 获取当前工作目录(并更新PWD环境变量)staticchar*get_current_dir(){if(getcwd(g_pwd, BUF_SIZE)==NULL){// getcwd获取当前工作目录perror("getcwd failed");return"unknown-dir";}// 更新环境变量PWD(确保echo $PWD能显示正确路径)staticchar pwd_env[BUF_SIZE]={0};snprintf(pwd_env, BUF_SIZE,"PWD=%s", g_pwd);putenv(pwd_env);// putenv修改当前进程的环境变量return g_pwd;}// 4. 简化工作目录(只显示最后一级)staticchar*simplify_dir(char* full_dir){if(full_dir ==NULL||strcmp(full_dir,"/")==0){return"/";// 根目录直接返回/}// 逆序查找最后一个/(如/home/ubuntu/myshell → 找到最后一个/,返回myshell)char* last_slash =strrchr(full_dir,'/');return(last_slash ==NULL)? full_dir :(last_slash +1);}// 5. 打印命令行提示符voidprint_prompt(){char* username =get_username();char* hostname =get_hostname();char* full_dir =get_current_dir();char* simple_dir =simplify_dir(full_dir);// 格式:[用户名@主机名 简化目录]$ printf("[%s@%s %s]$ ", username, hostname, simple_dir);fflush(stdout);// 强制刷新缓冲区,确保提示符立即显示}
效果演示:

调用print_prompt()后,终端会显示类似:

plaintext

[ubuntu@localhost myshell]$ 

2.2 模块 2:命令获取与解析 —— 把 “字符串” 变成 “可执行指令”

用户输入的命令是 “字符串”(如ls -l /home),我们需要将其拆分为 “命令名 + 参数数组”(如["ls", "-l", "/home", NULL]),才能传给execvp执行。这一步分两个子任务:

子任务 1:获取用户输入(用 fgets 而非 scanf)
  • scanf的问题:遇到空格就停止读取,无法获取带空格的命令(如echo "hello world");
  • fgets的优势:读取一整行输入,包含空格,完美适配命令输入场景;
  • 处理细节:fgets会把用户输入的 “回车符\n” 也读入,需将其替换为字符串结束符\0;若输入为空行(只按回车),直接跳过。
子任务 2:解析命令(用 strtok 按空格拆分)

strtok是 C 标准库的字符串拆分函数,能按指定分隔符(这里是空格)拆分字符串:

  • 第一次调用:strtok(command, " "),传入要拆分的命令字符串,返回第一个 “非空格” 的子串(命令名);
  • 后续调用:strtok(NULL, " "),传入NULL表示 “继续拆分上次的字符串”,直到返回NULL(拆分结束);
  • 注意:拆分后的参数数组最后必须加NULL,因为execvp要求参数数组以NULL结尾。
代码实现(命令获取与解析模块):

c

#include<ctype.h>// 包含isspace函数// 全局变量:存储命令参数(命令名+参数,最后以NULL结尾)#defineARGV_MAX64char* g_argv[ARGV_MAX]={0};int g_argc =0;// 命令参数个数(包含命令名)// 1. 去除字符串前后的空格(避免空参数,如" ls -l " → "ls -l")staticvoidtrim_space(char* str){if(str ==NULL)return;// 去除前面的空格char* start = str;while(isspace(*start)) start++;// 去除后面的空格char* end = str +strlen(str)-1;while(end >= start &&isspace(*end)) end--;// 给字符串加结束符*(end +1)='\0';// 移动字符串(覆盖前面的空格)memmove(str, start, end - start +2);// +2:包含end+1的\0}// 2. 获取用户输入的命令intget_command(char* command_buf,int buf_size){// 读取一行命令(fgets会读入\n)if(fgets(command_buf, buf_size,stdin)==NULL){// 若用户按Ctrl+D(EOF),返回-1表示退出printf("\n");return-1;}// 去除换行符(将\n替换为\0) command_buf[strcspn(command_buf,"\n")]='\0';// 去除前后空格trim_space(command_buf);// 处理空行(用户只按了回车)if(strlen(command_buf)==0){return0;}return1;// 成功获取有效命令}// 3. 解析命令(拆分为g_argv数组)voidparse_command(char* command_buf){// 重置全局变量(避免上次命令的残留)memset(g_argv,0,sizeof(g_argv)); g_argc =0;// 第一次拆分:获取命令名char* token =strtok(command_buf," ");while(token !=NULL&& g_argc < ARGV_MAX -1){// 留一个位置放NULL g_argv[g_argc++]= token;// 后续拆分:获取参数 token =strtok(NULL," ");} g_argv[g_argc]=NULL;// 参数数组必须以NULL结尾}// 调试用:打印解析后的参数(可选)voiddebug_print_argv(){printf("解析结果:argc=%d\n", g_argc);for(int i =0; i < g_argc; i++){printf("argv[%d] = %s\n", i, g_argv[i]);}}
效果演示:

用户输入 ls -l /home ,解析后:

  • g_argc = 3
  • g_argv = ["ls", "-l", "/home", NULL]

2.3 模块 3:内建命令处理 ——Shell “自己动手” 的命令

内建命令(Built-in Command)是必须由 Shell 进程自身执行的命令,因为它们需要修改 Shell 的 “自身状态”(如cd修改工作目录、export修改环境变量)—— 若用子进程执行,修改的是子进程的状态,父进程(Shell)的状态不会变(进程具有独立性)。

迷你 Shell 将实现 4 个核心内建命令:cdexportenvecho,我们逐个实现。

3.3.1 内建命令 1:cd—— 切换工作目录

cd的核心是调用chdir系统函数修改当前进程的工作目录,但需要处理特殊场景:

  • cd(无参数):切换到用户家目录(getenv("HOME"));
  • cd ~:同无参数,切换到家目录;
  • cd -:切换到 “上次的工作目录”(需用g_last_pwd存储上次目录);
  • cd 目录路径:切换到指定目录(如cd /home)。
3.3.2 内建命令 2:export—— 导出环境变量

export的作用是 “将变量添加到当前进程的环境变量表中”,供后续执行的命令继承(如export MY_VAR=123):

  • 实现方式:用putenv函数(C 标准库),将 “KEY=VALUE” 格式的字符串添加到环境变量表;
  • 注意:export无参数时,可打印所有已导出的环境变量(可选功能)。
3.3.3 内建命令 3:env—— 打印所有环境变量

env的作用是 “打印当前进程的所有环境变量”,实现方式是遍历全局环境变量数组environextern char **environ),逐个打印每个环境变量(格式为 “KEY=VALUE”)。

3.3.4 内建命令 4:echo—— 打印内容或环境变量

echo支持三种场景:

  • echo 文本:直接打印文本(如echo hello → 输出hello);
  • echo $环境变量:打印指定环境变量的值(如echo $PATH → 输出/bin:/usr/bin);
  • echo $?:打印 “上次命令的退出码”(需用全局变量g_last_code存储)。
代码实现(内建命令模块):

c

#include<sys/wait.h>// 全局变量:存储上次命令的退出码(供echo $?使用)int g_last_code =0;// 全局变量:存储环境变量表(从父进程继承)externchar**environ;// 1. 判断是否为内建命令(返回1=是,0=否)intis_builtin_command(){if(g_argc ==0|| g_argv[0]==NULL)return0;// 支持的内建命令列表constchar* builtin_list[]={"cd","export","env","echo",NULL};for(int i =0; builtin_list[i]!=NULL; i++){if(strcmp(g_argv[0], builtin_list[i])==0){return1;}}return0;}// 2. 执行内建命令cdstaticvoidexec_cd(){char* target_dir =NULL;// 保存当前目录(用于cd -)strncpy(g_last_pwd, g_pwd, BUF_SIZE);// 处理不同参数场景if(g_argc ==1){// cd无参数 → 切换到家目录 target_dir =getenv("HOME");}elseif(strcmp(g_argv[1],"~")==0){// cd ~ → 切换到家目录 target_dir =getenv("HOME");}elseif(strcmp(g_argv[1],"-")==0){// cd - → 切换到上次目录 target_dir = g_last_pwd;printf("%s\n", target_dir);// 模仿bash,打印切换后的目录}else{// cd 目录路径 → 切换到指定目录 target_dir = g_argv[1];}// 调用chdir切换目录if(chdir(target_dir)==-1){perror("cd failed"); g_last_code =1;// 退出码设为1(表示失败)}else{ g_last_code =0;// 退出码设为0(表示成功)}}// 3. 执行内建命令exportstaticvoidexec_export(){if(g_argc !=2){// 参数错误(如export无参数或多参数)fprintf(stderr,"用法:export KEY=VALUE\n"); g_last_code =2;return;}// 检查参数格式(必须包含=,如MY_VAR=123)if(strchr(g_argv[1],'=')==NULL){fprintf(stderr,"错误:export参数必须包含'='\n"); g_last_code =2;return;}// 调用putenv添加环境变量if(putenv(g_argv[1])!=0){perror("export failed"); g_last_code =1;}else{ g_last_code =0;}}// 4. 执行内建命令envstaticvoidexec_env(){// 遍历environ数组,打印所有环境变量for(int i =0; environ[i]!=NULL; i++){printf("%s\n", environ[i]);} g_last_code =0;}// 5. 执行内建命令echostaticvoidexec_echo(){if(g_argc <2){// echo无参数 → 打印空行printf("\n"); g_last_code =0;return;}char* content = g_argv[1];if(content[0]=='$'){// 场景1:echo $变量(如$PATH、$?)if(strcmp(content,"$?")==0){// echo $? → 打印上次命令的退出码printf("%d\n", g_last_code);}else{// echo $环境变量 → 打印环境变量的值char* var_name = content +1;// 跳过$,取变量名(如$PATH → PATH)char* var_value =getenv(var_name);if(var_value !=NULL){printf("%s\n", var_value);}// 变量不存在时,不打印(模仿bash)}}else{// 场景2:echo 文本 → 打印文本(支持多参数,如echo hello world)for(int i =1; i < g_argc; i++){printf("%s ", g_argv[i]);}printf("\n");} g_last_code =0;}// 6. 统一执行内建命令voidexec_builtin_command(){if(strcmp(g_argv[0],"cd")==0){exec_cd();}elseif(strcmp(g_argv[0],"export")==0){exec_export();}elseif(strcmp(g_argv[0],"env")==0){exec_env();}elseif(strcmp(g_argv[0],"echo")==0){exec_echo();}}

2.4 模块 4:外部命令执行 ——Shell “找帮手” 的命令

外部命令(如ls/ps/gcc)是 “独立的可执行程序”,需要 Shell 创建子进程执行(避免覆盖 Shell 自身代码),核心流程是:

  1. fork创建子进程;
  2. 子进程用execvp替换为目标程序(自动从PATH查找命令路径);
  3. 父进程用waitpid等待子进程退出,获取退出码(更新g_last_code);
  4. execvp失败(如命令不存在),子进程退出并设置错误退出码。
代码实现(外部命令模块):

c

voidexec_external_command(){pid_t pid =fork();if(pid ==-1){// fork失败(如系统进程过多)perror("fork failed"); g_last_code =1;return;}if(pid ==0){// 子进程:执行外部命令execvp(g_argv[0], g_argv);// 只有execvp失败时,才会执行到这里(成功则子进程代码被覆盖)perror("command not found");// 错误原因:命令不存在、权限不足等exit(127);// 退出码127:标准的“命令未找到”错误码}else{// 父进程:等待子进程退出,获取退出码int status;waitpid(pid,&status,0);// 阻塞等待子进程// 解析子进程的退出状态,更新g_last_codeif(WIFEXITED(status)){// 正常退出:获取退出码 g_last_code =WEXITSTATUS(status);}elseif(WIFSIGNALED(status)){// 被信号终止(如Ctrl+C → SIGINT,kill -9 → SIGKILL) g_last_code =128+WTERMSIG(status);// 符合Linux标准(如128+2=130)}}}
效果演示:

用户输入ls -l,执行流程:

  1. Shell 判断ls是外部命令,fork子进程;
  2. 子进程调用execvp("ls", ["ls", "-l", NULL]),从PATH找到/bin/ls,替换为ls程序;
  3. ls执行完毕后退出,父进程waitpid获取退出码(0 表示成功);
  4. g_last_code更新为 0,下次执行echo $?会输出 0。

2.5 模块 5:环境变量初始化 —— 继承父进程的 “配置”

Shell 启动时,需要继承父进程的环境变量(如PATH/HOME/USER),这些环境变量存储在全局数组environ中(无需手动定义,只需用extern声明)。我们可以在 Shell 启动时,打印关键环境变量(可选),确保继承正常。

代码实现(环境变量初始化):

c

// 初始化环境变量(打印关键变量,验证继承)voidinit_env(){printf("=== 迷你Shell启动 ===");// 打印关键环境变量(可选,用于调试)char* path =getenv("PATH");char* home =getenv("HOME");char* user =getenv("USER");if(path !=NULL)printf("\nPATH: %s", path);if(home !=NULL)printf("\nHOME: %s", home);if(user !=NULL)printf("\nUSER: %s", user);printf("\n====================\n\n");// 初始化上次工作目录(启动时的当前目录)get_current_dir();strncpy(g_last_pwd, g_pwd, BUF_SIZE);}

三、迷你 Shell 完整源码与运行演示

将上述模块整合为完整代码(mini_shell.c),添加main函数实现 “无限循环” 的核心逻辑,再编译运行验证功能。

3.1 完整源码

c

#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<ctype.h>#include<sys/types.h>#include<sys/wait.h>// 全局常量定义#defineBUF_SIZE1024// 命令缓冲区大小#defineARGV_MAX64// 命令参数最大个数// 全局变量定义char g_pwd[BUF_SIZE]={0};// 当前工作目录char g_last_pwd[BUF_SIZE]={0};// 上次工作目录(用于cd -)char* g_argv[ARGV_MAX]={0};// 命令参数数组int g_argc =0;// 命令参数个数int g_last_code =0;// 上次命令的退出码externchar**environ;// 全局环境变量数组// ------------------------------ 模块1:命令行提示符 ------------------------------staticchar*get_username(){char* username =getenv("USER");return(username ==NULL)?"unknown": username;}staticchar*get_hostname(){char* hostname =getenv("HOSTNAME");return(hostname ==NULL)?"unknown-host": hostname;}staticchar*get_current_dir(){if(getcwd(g_pwd, BUF_SIZE)==NULL){perror("getcwd failed");return"unknown-dir";}staticchar pwd_env[BUF_SIZE]={0};snprintf(pwd_env, BUF_SIZE,"PWD=%s", g_pwd);putenv(pwd_env);return g_pwd;}staticchar*simplify_dir(char* full_dir){if(full_dir ==NULL||strcmp(full_dir,"/")==0){return"/";}char* last_slash =strrchr(full_dir,'/');return(last_slash ==NULL)? full_dir :(last_slash +1);}voidprint_prompt(){char* username =get_username();char* hostname =get_hostname();char* full_dir =get_current_dir();char* simple_dir =simplify_dir(full_dir);printf("[%s@%s %s]$ ", username, hostname, simple_dir);fflush(stdout);}// ------------------------------ 模块2:命令获取与解析 ------------------------------staticvoidtrim_space(char* str){if(str ==NULL)return;char* start = str;while(isspace(*start)) start++;char* end = str +strlen(str)-1;while(end >= start &&isspace(*end)) end--;*(end +1)='\0';memmove(str, start, end - start +2);}intget_command(char* command_buf,int buf_size){if(fgets(command_buf, buf_size,stdin)==NULL){printf("\n");return-1;} command_buf[strcspn(command_buf,"\n")]='\0';trim_space(command_buf);if(strlen(command_buf)==0){return0;}return1;}voidparse_command(char* command_buf){memset(g_argv,0,sizeof(g_argv)); g_argc =0;char* token =strtok(command_buf," ");while(token !=NULL&& g_argc < ARGV_MAX -1){ g_argv[g_argc++]= token; token =strtok(NULL," ");} g_argv[g_argc]=NULL;}// ------------------------------ 模块3:内建命令处理 ------------------------------intis_builtin_command(){if(g_argc ==0|| g_argv[0]==NULL)return0;constchar* builtin_list[]={"cd","export","env","echo",NULL};for(int i =0; builtin_list[i]!=NULL; i++){if(strcmp(g_argv[0], builtin_list[i])==0){return1;}}return0;}staticvoidexec_cd(){char* target_dir =NULL;strncpy(g_last_pwd, g_pwd, BUF_SIZE);if(g_argc ==1){ target_dir =getenv("HOME");}elseif(strcmp(g_argv[1],"~")==0){ target_dir =getenv("HOME");}elseif(strcmp(g_argv[1],"-")==0){ target_dir = g_last_pwd;printf("%s\n", target_dir);}else{ target_dir = g_argv[1];}if(chdir(target_dir)==-1){perror("cd failed"); g_last_code =1;}else{ g_last_code =0;}}staticvoidexec_export(){if(g_argc !=2){fprintf(stderr,"用法:export KEY=VALUE\n"); g_last_code =2;return;}if(strchr(g_argv[1],'=')==NULL){fprintf(stderr,"错误:export参数必须包含'='\n"); g_last_code =2;return;}if(putenv(g_argv[1])!=0){perror("export failed"); g_last_code =1;}else{ g_last_code =0;}}staticvoidexec_env(){for(int i =0; environ[i]!=NULL; i++){printf("%s\n", environ[i]);} g_last_code =0;}staticvoidexec_echo(){if(g_argc <2){printf("\n"); g_last_code =0;return;}char* content = g_argv[1];if(content[0]=='$'){if(strcmp(content,"$?")==0){printf("%d\n", g_last_code);}else{char* var_name = content +1;char* var_value =getenv(var_name);if(var_value !=NULL){printf("%s\n", var_value);}}}else{for(int i =1; i < g_argc; i++){printf("%s ", g_argv[i]);}printf("\n");} g_last_code =0;}voidexec_builtin_command(){if(strcmp(g_argv[0],"cd")==0){exec_cd();}elseif(strcmp(g_argv[0],"export")==0){exec_export();}elseif(strcmp(g_argv[0],"env")==0){exec_env();}elseif(strcmp(g_argv[0],"echo")==0){exec_echo();}}// ------------------------------ 模块4:外部命令执行 ------------------------------voidexec_external_command(){pid_t pid =fork();if(pid ==-1){perror("fork failed"); g_last_code =1;return;}if(pid ==0){execvp(g_argv[0], g_argv);perror("command not found");exit(127);}else{int status;waitpid(pid,&status,0);if(WIFEXITED(status)){ g_last_code =WEXITSTATUS(status);}elseif(WIFSIGNALED(status)){ g_last_code =128+WTERMSIG(status);}}}// ------------------------------ 模块5:环境变量初始化 ------------------------------voidinit_env(){printf("=== 迷你Shell启动 ===");char* path =getenv("PATH");char* home =getenv("HOME");char* user =getenv("USER");if(path !=NULL)printf("\nPATH: %s", path);if(home !=NULL)printf("\nHOME: %s", home);if(user !=NULL)printf("\nUSER: %s", user);printf("\n====================\n\n");get_current_dir();strncpy(g_last_pwd, g_pwd, BUF_SIZE);}// ------------------------------ 主函数(核心循环) ------------------------------intmain(){char command_buf[BUF_SIZE]={0};init_env();// 初始化环境变量// Shell核心循环:获取命令→解析→执行→循环while(1){print_prompt();// 1. 打印提示符int ret =get_command(command_buf, BUF_SIZE);// 2. 获取命令if(ret ==-1){// 用户按Ctrl+D,退出Shellprintf("=== 迷你Shell退出 ===\n");break;}elseif(ret ==0){// 空行,跳过continue;}parse_command(command_buf);// 3. 解析命令// debug_print_argv(); // 调试用:打印解析结果if(is_builtin_command()){// 4. 执行内建命令exec_builtin_command();}else{// 5. 执行外部命令exec_external_command();}}return0;}

3.2 编译与运行演示

步骤 1:编译代码

在 Linux 终端中,执行编译命令:

bash

gcc mini_shell.c -o mini_shell -Wall 
  • -o mini_shell:指定输出文件名为mini_shell
  • -Wall:显示所有警告(避免潜在错误)。
步骤 2:运行迷你 Shell

bash

./mini_shell 

启动后会显示初始化信息和提示符:

plaintext

=== 迷你Shell启动 === PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin HOME: /home/ubuntu USER: ubuntu ==================== [ubuntu@localhost myshell]$ 
步骤 3:测试核心功能

退出迷你 Shell:按Ctrl+D或输入exit(可扩展exit内建命令),Shell 会退出:bash

[ubuntu@localhost ubuntu]$ === 迷你Shell退出 ===

测试echo $?(查看退出码):bash

[ubuntu@localhost ubuntu]$ ls -l # 成功执行,退出码0[ubuntu@localhost ubuntu]$ echo$?0[ubuntu@localhost ubuntu]$ lss # 命令不存在,退出码127command not found: lss [ubuntu@localhost ubuntu]$ echo$?127

测试外部命令lsps:bash

[ubuntu@localhost ubuntu]$ ls -l # 执行外部命令ls total 4 drwxrwxr-x 2 ubuntu ubuntu 4096 Oct 116:00 myshell [ubuntu@localhost ubuntu]$ ps# 执行外部命令ps PID TTY TIME CMD 1234 pts/0 00:00:00 bash5678 pts/0 00:00:00 mini_shell 5679 pts/0 00:00:00 ps

测试内建命令env:bash

[ubuntu@localhost ubuntu]$ env|grep MY_VAR # 查看导出的环境变量MY_VAR=hello_mini_shell 

测试内建命令exportecho:bash

[ubuntu@localhost ubuntu]$ exportMY_VAR=hello_mini_shell [ubuntu@localhost ubuntu]$ echo$MY_VAR# 打印环境变量 hello_mini_shell [ubuntu@localhost ubuntu]$ echo$PATH# 打印系统环境变量 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin [ubuntu@localhost ubuntu]$ echo hello world # 打印文本 hello world 

测试内建命令cd:bash

[ubuntu@localhost myshell]$ cd /home [ubuntu@localhost home]$ cd ubuntu [ubuntu@localhost ubuntu]$ cd - # 切换到上次目录(/home) /home [ubuntu@localhost home]$ cd ~ # 切换到家目录(/home/ubuntu)[ubuntu@localhost ubuntu]$ 

四、进阶扩展:让迷你 Shell 更实用

当前的迷你 Shell 已实现核心功能,但还可以扩展以下进阶功能,使其更接近真实 Shell(如 bash):

4.1 扩展 1:支持命令别名(如ll=ls -l

  • 实现思路:用哈希表(如struct alias_map)存储 “别名→原命令” 的映射;
  • 新增内建命令aliasalias ll='ls -l',将别名添加到哈希表;
  • 命令解析时:若命令是别名,替换为原命令(如llls -l)。

4.2 扩展 2:支持命令补全(按 Tab 补全命令 / 路径)

  • 实现思路:监听Tab键(需关闭终端的 “行缓冲”,用tcsetattr修改终端属性);
  • 补全逻辑:若输入的是命令前缀(如l),遍历PATH目录,找出以l开头的命令(如ls/less);若输入的是路径前缀(如/hom),调用readdir遍历目录,补全为/home

4.3 扩展 3:支持重定向(如ls > file.txt

  • 实现思路:解析命令时识别>/<等重定向符号,拆分 “命令” 与 “重定向目标”;
  • 子进程执行前:调用open打开目标文件,用dup2重定向标准输出(stdout)或标准输入(stdin),再执行execvp

4.4 扩展 4:支持管道(如ls | grep txt

  • 实现思路:用pipe创建管道,fork两个子进程:
    • 子进程 1:执行ls,将标准输出重定向到管道写入端;
    • 子进程 2:执行grep txt,将标准输入重定向到管道读取端;
    • 父进程:等待两个子进程退出,关闭管道两端。

五、总结:迷你 Shell 背后的 “进程控制逻辑”

这个迷你 Shell 虽然简单,但完全基于前面四篇文章的核心知识,是进程控制的 “集大成者”。我们可以用一张图总结其核心逻辑:

plaintext

用户输入 → 提示符(print_prompt) ↓ 获取命令(get_command)→ 空行/EOF处理 ↓ 解析命令(parse_command)→ 生成g_argv数组 ↓ 判断命令类型: ├─ 内建命令(is_builtin_command)→ Shell自身执行(如cd修改工作目录) └─ 外部命令 → fork子进程 → 子进程execvp替换 → 父进程waitpid回收 ↓ 更新退出码(g_last_code)→ 回到提示符,循环 

通过这个实战,你应该能深刻理解:

  • 内建命令与外部命令的本质区别:是否需要修改 Shell 自身状态;
  • 进程控制的 “全链路” 应用:fork(创建子进程)→ exec(替换程序)→ waitpid(回收资源)→ exit(终止进程);
  • Linux Shell 的工作原理:不是 “自己执行命令”,而是 “管理子进程执行命令” 的 “管家”。

至此,Linux 进程控制系统系列文章已全部完成。从进程的创建、终止、等待、替换,到最终实现迷你 Shell,我们走过了 “理论→实践” 的完整路径。希望你能亲手运行代码,修改扩展,真正将这些知识内化为自己的技能。

Read more

红绿重构:TDD 如何让我写出更好的 Python 代码

红绿重构:TDD 如何让我写出更好的 Python 代码

红绿重构:TDD 如何让我写出更好的 Python 代码 “先写测试,再写代码。” 第一次听到这句话时,我以为这是某种程序员的玄学。直到我在一个真实项目中被 bug 折磨了三天,才终于决定认真对待它。 一、TDD 是什么?为什么它能改变你的编码方式? 测试驱动开发(Test-Driven Development,TDD)并不是一种测试技术,而是一种设计哲学。 它的核心节奏只有三个步骤,被称为"红绿重构循环": 🔴 Red → 写一个会失败的测试 🟢 Green → 写最少的代码让测试通过 🔵 Refactor → 在测试保护下重构代码 听起来简单,但真正实践后你会发现,这个节奏从根本上改变了你思考问题的顺序——你不再先想"怎么实现",而是先想"这个函数应该如何被使用"。这个微小的视角转变,会让你写出接口更清晰、耦合更低、更容易维护的代码。 二、

By Ne0inhk
基于Python+PyGame实现的一款功能完整的数独游戏,支持多难度选择、实时验证、提示系统、成绩记录,并采用多线程优化加载体验。(文末附全部代码)

基于Python+PyGame实现的一款功能完整的数独游戏,支持多难度选择、实时验证、提示系统、成绩记录,并采用多线程优化加载体验。(文末附全部代码)

✨ 项目亮点 * ✅ 三种难度可选(初级30空/中级45空/高级60空) * ✅ 实时颜色提示(正确蓝色/错误红色) * ✅ 内置“提示”功能(显示答案2秒) * ✅ 成绩记录系统(分难度保存最快用时) * ✅ 流畅界面 + 加载进度优化 * ✅ 完整游戏交互(键盘+鼠标) 📌 一、功能概览 功能说明难度切换初级/中级/高级,对应不同空格数量实时验证输入后立即颜色反馈(蓝/红)提示系统按T键或按钮显示当前格答案(持续2秒)成绩记录自动保存各难度最佳用时,支持查看历史记录重置游戏可随时重置当前难度盘面帮助说明内置游戏规则与操作指南 🧩 二、核心技术实现 1. 游戏状态管理 python game_states = { "初级": {"matrix": ..., "blank_ij": ..., "start_

By Ne0inhk

python与Java差别

Python与Java核心差异深度解析:从语法到场景,一篇讲透如何选择 在编程语言生态中,Python和Java是两大支柱级存在,前者以“高效开发”著称,后者凭“稳定高性能”立足。无论是编程新手入门选型,还是开发者根据项目需求切换技术栈,二者的差异对比都是绕不开的话题。本文将从核心特性、应用场景、优劣势等维度展开,帮你系统理清二者的区别,精准匹配实际需求。 一、核心特性对比:一张表看懂关键差异 对比维度 Python Java  语言类型 解释型语言,动态类型(弱类型),执行时逐行解释代码 编译型语言(先编译为字节码),静态类型(强类型),编译后通过JVM执行  语法风格 极简灵活,采用缩进(4个空格或Tab)划分代码块,无需显式声明变量类型,一行代码可完成复杂逻辑 严谨规范,必须用大括号{}划分代码块,变量声明时需指定数据类型,语法结构更规整  执行速度 相对较慢,解释执行无预编译优化,

By Ne0inhk