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

[Linux系统编程——Lesson13.自定义shell(讲解原理)]

前言

        经过之前的学习,我们已经掌握了一定的Linux基础与系统编程,接下我们将自主实现简易的自定义Shell来加深理解📖

一、打印命令提示符和获取命令行命令字符串🎈

1.1 设计✍️

  • 我们首先使用操作系统的bash看到了命令行提示符的组成为[用户名@主机名 当前工作目录]$,获取用户名、主机名和当前工作目录的函数在系统调用中都有,这里我们自己设计一个,这三个数据都是环境变量,我们可以通过getenv来获取到他们,获取后将他们按照操作系统bash的格式输出出来即可,通过下图我们可以发现,我们的当前工作目录与操作系统的有所区别,我们的当前工作目录是一条路径,可以通过裁剪得到与操作系统一样的效果,我这里为了区分与操作系统的区别,这里就不做裁剪了。

  • 输出完命令行提示符后,就需要向bash中输入命令了,这里我们就需要一个输入函数来读取命令字符串,需要注意⚠️的是这里不能使用scanf函数,因为scanf函数不能读取空格之后的内容,可以选择gets/fgets函数来读取,当我们输入完命令字符串后需要按回车,那么获取到的字符串中也会获取到这个’\n’,所以我们还需要将这个’\n’处理掉。
#include <stdio.h>    // 引入标准输入输出库,提供printf、fgets等函数
#include <string.h>   // 引入字符串处理库,提供strlen等字符串操作函数
#include <stdlib.h>   // 引入标准库,提供getenv等系统环境相关函数#define NUM 1024      // 定义宏NUM为1024,作为命令输入缓冲区的大小// 获取当前用户名
const char* getUsername()
{// 调用getenv函数从环境变量中获取"USER"的值,即用户名char* username = getenv("USER");if(username)  // 如果成功获取到用户名(不为空)return username;  // 返回获取到的用户名else           // 如果获取失败(环境变量不存在或为空)return "none";    // 返回"none"表示未获取到
}// 获取主机名
const char* getHostname()
{// 调用getenv函数从环境变量中获取"HOSTNAME"的值,即主机名char* hostname = getenv("HOSTNAME");if(hostname)  // 如果成功获取到主机名return hostname;  // 返回获取到的主机名else           // 如果获取失败return "none";    // 返回"none"表示未获取到
}// 获取当前工作目录
const char* getCwd()
{// 调用getenv函数从环境变量中获取"PWD"的值,即当前工作目录char* cwd = getenv("PWD");                                                                                                              if(cwd)  // 如果成功获取到当前工作目录                                                                                                                             return cwd;  // 返回获取到的当前工作目录                                                                                                                           else      // 如果获取失败                                                                                                                return "none";  // 返回"none"表示未获取到                                                                     
}int main()  
{char usercommand[NUM];  // 定义字符数组,用于存储用户输入的命令,大小为NUM(1024)// 打印命令行提示符,格式为"[用户名@主机名 工作目录]# "printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());// 读取用户输入的命令:// 使用fgets而不是scanf,因为scanf遇到空格会停止读取,而fgets可以读取整行输入(包括空格)// 参数说明:存储输入的缓冲区、缓冲区大小、输入流(stdin表示标准输入,即键盘)fgets(usercommand, sizeof(usercommand), stdin);// 处理输入的换行符:// fgets会将用户输入的回车符('\n')也读入缓冲区,这里将其替换为字符串结束符('\0')// strlen(usercommand)获取输入字符串的长度,减1得到换行符的位置usercommand[strlen(usercommand) - 1] = '\0';// 测试输出:打印用户输入的命令,验证是否正确读取和处理printf("%s", usercommand);return 0;  // 程序正常结束
}

1.2 封装🔎

        这里将打印命令行提示符与获取命令行字符串的工作统一封装到getUserCommand这个函数中。

// 函数功能:获取用户输入的命令并处理
// 参数:
//   command - 用于存储命令的字符数组指针
//   num     - 数组最大容量(防止缓冲区溢出)
// 返回值:1表示成功,-1表示失败
int getUserCommand(char* command , int num)    
{    // 打印命令行提示符(类似Linux终端样式)// 依赖之前定义的getUsername()、getHostname()、getCwd()获取环境信息printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());    // 读取用户输入:// 使用fgets而非scanf,因为fgets可以读取含空格的完整命令// 最多读取num-1个字符(预留1个位置给字符串结束符'\0')char* r = fgets(command,num,stdin);    // 错误处理:若读取失败(如遇EOF),返回-1if(r == NULL) return -1;    // 关键处理:移除输入末尾的换行符// fgets会将用户输入的回车('\n')也存入数组,需替换为字符串结束符'\0'command[strlen(command)-1] = '\0';    // 测试输出:验证输入处理是否正确printf("%s",command);// 成功执行返回1return 1;                 
}int main()
{// 定义命令缓冲区,大小为宏NUM(1024)char usercommand[NUM]; // 调用函数获取用户命令getUserCommand(usercommand,NUM);return 0;
}

核心要点说明:

  1. 函数封装:将命令读取逻辑独立为getUserCommand,提高代码复用性
  2. 输入处理:使用fgets支持带空格的命令输入,解决scanf的局限性
  3. 安全考量:通过num参数控制输入长度,防止缓冲区溢出
  4. 异常处理:通过返回值区分成功 / 失败状态,便于后续扩展错误处理
  5. 细节处理:移除换行符确保命令字符串格式正确

二、分割字符串🎈

2.1 设计✍️        

        当我们获取到了命令字符串后,需要将字符串以空格为分隔符将字符串分割为子字符串,并将每一个子字符串的地址存入到一个指针数组中,这里给出一个字符串被分割的例子:"ls -l -a" -> "ls" "-l" "-a"。我们可以使用strtok函数来将字符串分割,使用strtok函数处理同一个字符串时,第一次需要传入字符串的地址,后面再次调用则只需要传入NULL即可。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>#define NUM 1024                    // 命令输入缓冲区大小
#define SIZE 64                     // 命令参数数组的最大容量
#define SEP " "                     // 命令参数的分隔符(空格)// 获取用户名(从环境变量)
const char* getUsername()
{char* username = getenv("USER");if(username)return username;elsereturn "none"; 
}// 获取主机名(从环境变量)
const char* getHostname()
{char* hostname = getenv("HOSTNAME");if(hostname)return hostname;elsereturn "none"; 
}// 获取当前工作目录(从环境变量)
const char* getCwd()
{char* cwd = getenv("PWD");                                                                                                               if(cwd)                                                                                                                                  return cwd;                                                                                                                                      else                                                                                                                                     return "none";     
}// 注意:这里缺少了getUserCommand函数的定义,该函数在之前的代码中存在int main()  
{                                                                                                                                                        while(1)  // 无限循环,模拟终端持续运行{         char usercommand[NUM];  // 存储用户输入的完整命令char* argv[SIZE];       // 存储解析后的命令参数列表(argv[0]是命令名)// 获取用户输入的命令,返回1表示成功,-1表示失败int x = getUserCommand(usercommand,NUM);  if(x == -1) continue;   // 若读取失败,跳过本次循环,重新等待输入                                int argc = 0;           // 记录参数的数量(包括命令名)// 第一次调用strtok:用SEP分隔符分割usercommand,获取第一个参数(命令名)argv[argc++] = strtok(usercommand,SEP);  // 循环分割剩余参数:strtok(NULL, SEP)表示继续分割上一次的字符串// 当分割到末尾时返回NULL,循环结束,此时argv[argc]会被设为NULLwhile(argv[argc++] = strtok(NULL,SEP));          // 说明:strtok会保存分割状态,第一次调用后,后续用NULL作为第一个参数// 分割结果会依次存入argv数组,直到返回NULL,此时argc正好是参数总数+1// 打印解析后的所有参数(调试用)for(int i = 0 ; argv[i] ; i++)  // 当argv[i]为NULL时结束循环{                               printf("%d : %s \n",i,argv[i]);  // 输出参数索引和参数值}                                    }return 0;
}
  • 这是一个模拟命令行解释器的基础框架,通过无限循环持续接收用户命令
  • 关键功能是使用strtok函数对用户输入的命令进行分割,解析出命令名和参数
  • argv数组存储解析后的参数列表,argc记录参数数量,符合标准 C 程序的参数传递格式
  • 目前代码主要实现了解析命令的功能,后续可以扩展为执行相应命令的功能

⚠️注意:这段代码依赖之前定义的getUserCommand函数才能正常编译运行,该函数负责读取用户输入并处理换行符。

2.2 封装🔎

// 函数功能:将输入的命令字符串分割成参数列表
// 参数:
//   in  - 输入的原始命令字符串(如"ls -l /home")
//   out - 输出的参数数组,存储分割后的各个参数
void SplitCommand(char* in , char* out[])      
{      int argc = 0;  // 记录参数数量(包括命令名)// 第一次调用strtok:用分隔符SEP分割输入字符串,获取第一个参数(命令名)// 例如将"ls -l"分割后,out[0]会指向"ls"out[argc++] = strtok(in,SEP);      // 循环分割剩余参数:// strtok(NULL, SEP)表示继续分割上一次未完成的字符串// 当没有更多参数时,strtok返回NULL,循环结束// 此时out数组最后一个元素会是NULL,符合标准参数列表格式while(out[argc++] = strtok(NULL,SEP));        // 工作原理:// 1. 每次调用strtok获取下一个参数// 2. 将参数地址存入out[argc]// 3. argc自增// 4. 当strtok返回NULL时,循环条件为假,退出循环// 备选分割方式(功能相同,更易理解的写法)// while(1)    // {    //     out[argc] = strtok(NULL,SEP);  // 获取下一个参数//     if(out[argc] == NULL)          // 如果没有更多参数//         break;                     // 退出循环//     argc++;                        // 参数计数+1// }                                                                                                                                                  // 调试模式:如果定义了debug宏,则打印分割后的参数列表
#ifdef debug // 循环打印所有参数,直到遇到NULL结束符for(int i = 0 ; out[i] ; i++)                                                                                                          {                                                                                                                                      printf("%d : %s \n",i,out[i]);  // 输出参数索引和参数值                                                                                                    }        
#endif                                                                                                                                     
}                                                                                                                                          // 主函数:程序入口点
int main()                                                                                                                                 
{                                                                                                                                          // 无限循环,模拟终端持续运行while(1)                                                                                                                               {                                                                                                                                      char usercommand[NUM];  // 存储用户输入的完整命令字符串char* argv[SIZE];       // 存储分割后的命令参数列表// 获取用户输入的命令,返回1表示成功,-1表示失败int x = getUserCommand(usercommand,NUM);    // 调用SplitCommand函数分割命令字符串为参数列表SplitCommand(usercommand,argv); }return 0;
}

核心要点说明:

  • SplitCommand函数是这段代码的核心,负责将完整命令字符串分割成参数数组,模拟了系统处理命令行参数的方式
  • 使用strtok函数进行字符串分割,这是 C 语言中处理命令行参数的经典方法
  • 采用条件编译(#ifdef debug)实现调试输出功能,方便开发阶段验证分割结果
  • 主函数通过无限循环持续获取并处理用户命令,构建了一个简单的命令行交互框架

注意⚠️:这段代码依赖之前定义的getUserCommand函数、NUMSIZESEP宏才能正常工作。分割后的参数列表argv可以直接用于后续的命令执行逻辑(如通过exec系列函数执行系统命令)。


三、执行指令🎈

3.1 设计✍️

        将命令字符串分割后,就需要执行命令了,我们知道bash需要一直运行,这里添加一个循环让他一直运行,我们可以使用前面学习过的进程替换来执行命令,但是不能使用当前进程来进程替换,当前进程还需要继续运行,所以我们可以创建一个子进程来执行命令,由于我们将字符串分割为子字符串存储在了指针数组中,这里可以使用execvp函数来进行进场替换。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>         // 提供fork()、execvp()等系统调用
#include <sys/types.h>      // 提供pid_t等类型定义
#include <sys/wait.h>       // 提供wait()等进程等待函数#define NUM 1024            // 命令输入缓冲区大小
#define SIZE 64             // 命令参数数组最大容量
#define SEP " "             // 命令参数分隔符(空格)
#define debug 1             // 开启调试模式(非0值表示开启)// 从环境变量获取用户名
const char* getUsername()
{char* username = getenv("USER");if(username)return username;elsereturn "none"; 
}// 从环境变量获取主机名
const char* getHostname()
{char* hostname = getenv("HOSTNAME");if(hostname)return hostname;elsereturn "none"; 
}// 从环境变量获取当前工作目录
const char* getCwd()
{char* cwd = getenv("PWD");                                                                                                               if(cwd)                                                                                                                                  return cwd;                                                                                                                                      else                                                                                                                                     return "none";     
}// 获取用户输入的命令
// 参数:command-存储命令的缓冲区,num-缓冲区大小
// 返回值:1-成功,-1-失败
int getUserCommand(char* command , int num)    
{    // 打印类似Linux终端的命令提示符 [用户名@主机名 工作目录]#printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());    // 使用fgets读取整行输入(支持含空格的命令)char* r = fgets(command,num,stdin);    if(r == NULL) return -1;  // 读取失败(如EOF)返回-1    // 移除输入末尾的换行符(fgets会将回车符也读入)command[strlen(command)-1] = '\0';    return 1;                 
}// 分割命令字符串为参数列表
// 参数:in-原始命令字符串,out-输出的参数数组
void SplitCommand(char* in , char* out[])      
{      int argc = 0;      // 第一次调用strtok分割出第一个参数(命令名)out[argc++] = strtok(in,SEP);      // 循环分割剩余参数,直到strtok返回NULLwhile(out[argc++] = strtok(NULL,SEP));        // 调试模式:打印分割后的参数列表
#ifdef debug for(int i = 0 ; out[i] ; i++)                                                                                                          {                                                                                                                                      printf("%d : %s \n",i,out[i]);                                                                                                     }        #endif                                                                                                                                    
}  int main()                                                                                                                                 
{                                                                                                                                          while(1)  // 无限循环,模拟持续运行的终端{                                                                                                                                                  char usercommand[NUM];  // 存储用户输入的命令字符串char* argv[SIZE];       // 存储分割后的命令参数列表// 获取用户输入的命令int x = getUserCommand(usercommand,NUM);    if(x <= 0) continue;    // 读取失败则重新等待输入// 分割命令字符串为参数数组(如"ls -l" -> ["ls", "-l", NULL])SplitCommand(usercommand,argv); // 创建子进程执行命令pid_t id = fork();if(id < 0)  // 进程创建失败{return -1;}else if(id == 0)  // 子进程执行分支{// 用新程序替换子进程(执行命令)// argv[0]是命令名,argv是参数列表execvp(argv[0],argv);// 如果execvp返回,说明执行失败(如命令不存在)exit(-1);}else  // 父进程执行分支{// 等待子进程结束,回收资源pid_t rid = wait(NULL);if(rid>0){};  // 仅为避免编译警告,可扩展为错误处理}}return 0;
}

代码核心功能说明:

  • 这是一个简易的命令行解释器(Shell)实现,能够接收并执行用户输入的命令
  • 核心工作流程
    • 打印命令提示符 → 获取用户输入 → 分割命令为参数 → 创建子进程 → 执行命令 → 等待命令完成
  • 关键系统调用
    • fork()创建子进程,用于执行命令(避免影响父进程)
    • execvp()在子进程中加载并执行新程序(用户输入的命令)
    • wait()父进程等待子进程结束,防止僵尸进程
  • 调试模式:通过#define debug 1开启后,会打印分割后的命令参数列表,方便开发调试

        该代码模拟了 Linux Shell 的基本工作原理,但缺少很多高级功能(如管道、重定向、环境变量设置等),可作为学习操作系统进程管理的基础示例。

3.2 封装🔎

// 函数功能:执行命令
// 参数:argv - 命令参数列表(格式:[命令名, 参数1, 参数2, ..., NULL])
// 返回值:0表示成功,-1表示进程创建失败
int execute(char* argv[])
{// 创建子进程,返回进程IDpid_t id = fork();if(id < 0)  // 进程创建失败{return -1;}else if(id == 0)  // 子进程执行分支{    // 用新程序替换子进程:// argv[0]是要执行的程序名,argv是完整参数列表// 若执行成功,此函数不会返回;若失败,才会执行后续代码execvp(argv[0],argv);    // 若execvp返回,说明命令执行失败(如命令不存在)exit(-1);    }    else  // 父进程执行分支{    // 等待子进程结束,回收其资源,防止僵尸进程pid_t rid = wait(NULL);    if(rid>0){};  // 空语句,仅用于避免编译警告,可扩展为错误处理}    return 0;  // 成功执行命令
}    int main()    
{    while(1)  // 无限循环,保持命令行持续运行{    char usercommand[NUM];  // 存储用户输入的完整命令字符串char* argv[SIZE];       // 存储分割后的命令参数列表// 打印命令提示符并获取用户输入的命令int x = getUserCommand(usercommand,NUM);    if(x <= 0) continue;  // 若获取命令失败,重新开始循环// 将用户输入的命令字符串分割为参数列表SplitCommand(usercommand,argv); // 调用execute函数执行命令execute(argv);}return 0;
}

代码核心改进说明

  • 模块化设计:将命令执行逻辑封装到execute函数中,使代码结构更清晰
  • 功能分工
    • main函数:负责命令的获取、解析和调用执行函数,形成主循环
    • execute函数:专注于进程创建、命令执行和资源回收
  • 进程管理流程:
    • 父进程通过fork()创建子进程
    • 子进程通过execvp()加载并执行新命令
    • 父进程通过wait()等待子进程完成并回收资源

        这种结构使代码更易于维护和扩展,后续可以在execute函数中添加更多功能,如命令执行结果判断、错误信息提示等。


四、处理內键命令的执行🎈

  •         当我们使用上面的代码执行命令时,发现大部分命令都可以被执行,但是例如cd、export、echo这样的内建命令却不能被执行,原因是内建命令是作用与bash的也就是这里的父进程,并且内建命令是bash的一部分,与常见命令不同,执行内建命令时不需要创建新的子进程,所以这些内建命令需要被特殊处理一下。将命令字符串分割后就判断当前命令是否为内建命令,是则直接执行内建命令,否则认定为常见命令向下继续执行。内建命令如何处理,这里就不多讲解,详细处理方法在下面的代码中有详细的注释,有兴趣的可以看一下。

五、重定向(本文章所有代码)🎈

  • 写完前面的代码后,发现这个代码并不能解决重定向的问题,没了解重定向的最好看一下后面一篇文章了解一下重定向是什么,在分割命令字符串之前,我们需要判断这个命令字符串是否需要进行重定向,需要重定向则需要对字符串进行处理,例如"ls -l -a > fortest.txt" -> "ls -l -a" 重定向类型 "fortest.txt",我们会得到三个部分,命令字符串、重定向类型、和文件名,在代码定义四种重定向类型,无重定向、输入重定向、输出重定向和追加重定向,默认情况下是无重定向,定义一个全局变量存储重定向类型,定义一个全局指针来指向文件名,然后我们针对不同的重定向类型使用dup2函数进行不同的处理,详细不同重定向的处理过程请查看代码中重定向的一部分。
#include <stdio.h>    
#include <string.h>    
#include <stdlib.h>    
#include <unistd.h>         // 提供chdir()/getcwd()/fork()/execvp()等系统调用
#include <sys/types.h>      // 提供pid_t类型定义
#include <sys/wait.h>       // 提供wait()/WEXITSTATUS()等进程等待相关函数#define NUM 1024            // 命令输入缓冲区大小(存储用户输入的完整命令)
#define SIZE 64             // 命令参数数组最大容量(存储分割后的参数列表)
#define SEP " "             // 命令参数分隔符(空格)
// #define debug 1           // 调试开关:取消注释可打印参数分割结果// 全局变量:存储当前工作目录(用于更新PWD环境变量)
// 注:必须全局/静态,避免putenv时内存被释放导致野指针
char cwd[1024];    
// 全局二维数组:存储自定义添加的环境变量(避免putenv时字符串被销毁)
char myenv[128][1024];    
// 全局变量:记录myenv数组中有效环境变量的个数
int cnt = 0;    
// 全局变量:记录上一个子进程的退出码(用于echo $?)
int lastcode = 0;                                                                                                                           // 获取用户家目录(从环境变量HOME)
char* getHomename()    
{                                       char* homename = getenv("HOME");  // 读取系统环境变量HOMEif(homename)            return homename;              // 成功则返回家目录路径else                          return (char*)"none";         // 失败返回"none"
}                                // 获取当前用户名(从环境变量USER)
const char* getUsername()    
{                                       char* username = getenv("USER");if(username)            return username;else                                                                                                                                               return "none";    
}    // 获取主机名(从环境变量HOSTNAME)
const char* getHostname()
{char* hostname = getenv("HOSTNAME");if(hostname)return hostname;elsereturn "none"; 
}// 获取当前工作目录(从环境变量PWD)
const char* getCwd()
{char* cwd = getenv("PWD");if(cwd)return cwd;elsereturn "none"; 
}// 功能:获取用户输入的命令
// 参数:command-存储命令的缓冲区,num-缓冲区最大容量
// 返回值:1-成功获取有效命令,0-仅输入回车(空命令),-1-读取失败(如EOF)
int getUserCommand(char* command , int num)
{// 打印类似Linux终端的提示符:[用户名@主机名 工作目录]#printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());// 读取整行输入(支持含空格的命令,如"ls -l")char* r = fgets(command,num,stdin);if(r == NULL) return -1;  // 读取失败(如Ctrl+D触发EOF)返回-1// 移除fgets读取的换行符(用户输入回车会被存入缓冲区,需替换为字符串结束符)command[strlen(command)-1] = '\0';// 处理空命令:若仅输入回车,返回0(避免后续无意义的分割和执行)if(strlen(command) == 0)return 0;return 1;  // 成功获取有效命令
}// 功能:将完整命令字符串分割为参数列表
// 参数:in-原始命令字符串(如"cd /home"),out-输出的参数数组(如["cd","/home",NULL])
void SplitCommand(char* in , char* out[])    
{    int argc = 0;    // 第一次调用strtok:用SEP分割in,获取第一个参数(命令名,如"cd")out[argc++] = strtok(in,SEP);    // 循环分割剩余参数:strtok(NULL,SEP)表示继续分割上一次的字符串// 分割失败时返回NULL,存入out后循环终止,最终out以NULL结尾(符合execvp要求)while(out[argc++] = strtok(NULL,SEP));                                                                                                             #ifdef debug  // 调试模式:打印分割后的参数列表(如0:cd, 1:/home)for(int i = 0 ; out[i] ; i++)                                                                                                          {                                                                                                                                      printf("%d : %s \n",i,out[i]);                                                                                                     }        
#endif
}// 功能:执行非内建命令(通过创建子进程+execvp替换进程映像)
// 参数:argv-分割后的命令参数列表(如["ls","-l",NULL])
// 返回值:0-执行流程正常,-1-子进程创建失败
int execute(char* argv[])
{// 创建子进程:父进程继续执行,子进程执行命令pid_t id = fork();if(id < 0)  // fork失败(如系统资源不足){return -1;}else if(id == 0)  // 子进程分支{// 用新程序替换子进程:argv[0]是命令名,argv是完整参数列表// 若execvp返回,说明执行失败(如命令不存在)execvp(argv[0],argv);exit(-1);  // 执行失败,子进程退出(退出码为-1,实际会被转为255)}else  // 父进程分支{int status = 0;  // 存储子进程退出状态// 等待子进程结束,回收资源(防止僵尸进程),并获取退出状态pid_t rid = wait(&status);if(rid > 0)  // 成功回收子进程{// 提取子进程的正常退出码(需用WEXITSTATUS宏处理status)lastcode = WEXITSTATUS(status);}}return 0;  // 父进程执行流程正常结束
}// 功能:实现cd命令的核心逻辑(切换工作目录+更新PWD环境变量)
// 参数:path-目标目录路径
void cd(char* path)
{// 切换当前工作目录到path(系统调用)chdir(path);// 临时缓冲区:存储切换后的实际工作目录char tmp[1024];// 获取当前工作目录的绝对路径,存入tmp(避免PWD与实际目录不一致)getcwd(tmp,sizeof(tmp));// 拼接成"PWD=路径"格式(符合环境变量的标准格式),存入全局变量cwdsprintf(cwd,"PWD=%s",tmp);// 将新的PWD环境变量加入进程环境表,覆盖原有PWDputenv(cwd);
}// 功能:判断并执行内建命令(cd/export/echo)
// 内建命令需在父进程执行(如cd需修改父进程的工作目录),不能创建子进程
// 参数:argv-分割后的命令参数列表
// 返回值:1-是内建命令且执行成功,0-非内建命令
int dobuildin(char* argv[])
{// 1. 处理cd命令(切换工作目录)if(strcmp(argv[0],"cd") == 0){char* path = NULL;// 若cd后无参数(如"cd"),默认切换到用户家目录if(argv[1] == NULL)path = getHomename();else  // 若有参数(如"cd /home"),使用指定路径path = argv[1];cd(path);  // 调用cd函数执行切换return 1;  // 标记为内建命令}// 2. 处理export命令(添加自定义环境变量)else if(strcmp(argv[0],"export") == 0){// 若export后无参数(如"export"),不做处理if(argv[1] == NULL)return 1;else {// 将环境变量(如"MYVAR=123")存入全局数组myenv// 注:必须用全局数组存储,避免局部变量销毁导致野指针strcpy(myenv[cnt],argv[1]);// 将环境变量加入进程环境表(putenv仅保存指针,需确保字符串长期有效)putenv(myenv[cnt++]);cnt %= 128;  // 防止数组越界(简单循环覆盖)return 1;  // 标记为内建命令}}// 3. 处理echo命令(输出字符串/环境变量/退出码)else if(strcmp(argv[0],"echo") == 0){// 若echo后无参数(如"echo"),默认输出回车if(argv[1] == NULL){printf("\n");return 1;}// 若参数以$开头(如"echo $HOME"或"echo $?"),处理变量/退出码else if(*(argv[1]) == '$' && strlen(argv[1]) >= 2){// 3.1 处理echo $?(输出上一个子进程的退出码)if(*(argv[1]+1) == '?'){printf("%d\n",lastcode);            lastcode = 0;  // 输出后重置退出码(可选逻辑)}// 3.2 处理echo $ENV(输出指定环境变量的值,如$HOME)else  {// 从环境表中获取变量值(argv[1]+1跳过$,如"$HOME"→"HOME")const char* enval = getenv(argv[1]+1);if(enval)  // 若变量存在,输出变量值{printf("%s\n",enval);}else  // 若变量不存在,输出空行{printf("\n");}}}// 3.3 普通字符串(如"echo hello"),直接输出参数else {printf("%s\n",argv[1]);}return 1;  // 标记为内建命令}                                                                                                                                                  // 其他命令(非内建),返回0else if(0){}return 0;
}// 主函数:简易Shell的主循环
int main()                                                                                                                                 
{                                                                                                                                          // 无限循环:持续接收并处理用户命令(模拟Shell常驻)while(1)                                                                                                                               {                                                                                                                                      char usercommand[NUM];  // 存储用户输入的完整命令char* argv[SIZE];       // 存储分割后的命令参数列表// 1. 获取用户命令(打印提示符+读取输入)int x = getUserCommand(usercommand,NUM);    if(x <= 0)  // 读取失败(-1)或空命令(0),跳过后续处理continue;    // 2. 分割命令字符串为参数列表(如"echo $HOME"→["echo","$HOME",NULL])SplitCommand(usercommand,argv); // 3. 判断是否为内建命令:是则执行,跳过子进程创建x = dobuildin(argv);if(x == 1)continue;// 4. 非内建命令:创建子进程执行(如ls、pwd等)execute(argv);}return 0;
}

代码核心功能总结

这段代码实现了一个迷你 Shell,支持以下关键功能:

  • 命令提示符:模拟 Linux 格式 [用户名@主机名 工作目录]#
  • 内建命令(父进程执行,不可 fork):
    • cd [路径]:切换工作目录,无参数默认切到家目录,自动更新PWD环境变量
    • export 变量=值:添加自定义环境变量(如export MYVAR=123
    • echo 内容:输出普通字符串、环境变量(如echo $HOME)、上一个进程退出码(echo $?
  • 非内建命令(子进程执行,如lspwddate):
    • 通过fork()创建子进程,execvp()替换子进程映像执行命令
    • 父进程通过wait()回收子进程,记录退出码(供echo $?使用)
  • 命令处理流程获取命令 → 分割参数 → 判断内建命令 → 执行内建命令 / 创建子进程执行非内建命令

关键注意点

  • 环境变量存储putenv()仅保存字符串指针,需用全局数组(myenvcwd)存储环境变量,避免局部变量销毁导致野指针。
  • 内建命令必要性cdexport等命令需修改父进程状态(工作目录、环境变量),必须在父进程执行,不能创建子进程(否则修改仅作用于子进程,子进程退出后失效)。
  • 进程退出码:通过wait(&status)获取子进程退出状态,用WEXITSTATUS(status)提取正常退出码,存入lastcodeecho $?使用。
  • 空命令处理:用户仅输入回车时,返回0跳过后续分割和执行,避免无意义操作。

结束语

以上就是我对【Linux系统编程】自定义Shell的理解与实现

感谢你的三连支持!!!

http://www.dtcms.com/a/483864.html

相关文章:

  • 微信开放平台官方网站家电电商平台排名
  • 贵州建设厅考试网站二建成绩自己动手建设公司门户网站
  • 湛江网站建设方案托管个人网页设计html加js代码
  • 崇左网站建设网站如何做绿标
  • 湛江制作网站公司互联网站的建设维护营销
  • 陪跑教学大纲:PowerBI QuickBI FineBI 数据运营 面试 简历修改等
  • 做海报有什么好的网站推荐网络营销网站建设诊断报告
  • 郑州网站推建设河南省建设科技会网站
  • 电商网站建设方案道客巴巴北京提供24小时医疗服务
  • 网站打开是别人的哪个网站做设计兼职不用压金
  • MySQL新增插入,重复更新操作业务实现
  • linux重启网络(systemctl restart network)会不会导致连接断开?
  • 怎么用云服务器建设网站wordpress 商务主题
  • Monitoring: 1靶场渗透
  • 网店网站建设哪家小吃培训机构排名前十
  • 科技类网站源码百度h5游戏中心
  • 对伯努利过程的理解
  • 临沂网站建设步骤广州网站建设推广公司有哪些
  • 法律网站的建设流程提交网址
  • 广州市住房和城乡建设厅网站首页怎么注册微网站吗
  • 10年网站设计祥汇云江西建设厅网站查询施工员
  • dedecms网站关键词网址之家哪个好
  • Linux1014 shell:sed c s/ ,#!/bin/sed -f ./sed.sh 1.txt ,1.sed 1.txt, ,
  • HyperWorks许可证与其他软件的卓越集成
  • 辽源建站公司免费聊天软件
  • 如何让便笺实现开机自启动
  • 工业网站模板wordpress 不在根目录
  • 湖北智能网站建设找哪家图床外链生成器
  • 做销售如何在网站上搜集资料织梦网站后台模版更换
  • 做网站最快的编程语言注册wordpress账号