[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),返回-1
    if(r == NULL) return -1;    
    
    // 关键处理:移除输入末尾的换行符
    // fgets会将用户输入的回车('\n')也存入数组,需替换为字符串结束符'\0'
    command[strlen(command)-1] = '\0';    
        
    // 测试输出:验证输入处理是否正确
    printf("%s",command);
    
    // 成功执行返回1
    return 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;
    else
        return "none"; 
}

// 获取主机名(从环境变量)
const char* getHostname()
{
    char* hostname = getenv("HOSTNAME");
    if(hostname)
        return hostname;
    else
        return "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]会被设为NULL
        while(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;
    else
        return "none"; 
}

// 从环境变量获取主机名
const char* getHostname()
{
    char* hostname = getenv("HOSTNAME");
    if(hostname)
        return hostname;
    else
        return "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返回NULL
    while(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[])
{
    // 创建子进程,返回进程ID
    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;  // 成功执行命令
}    

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");  // 读取系统环境变量HOME
    if(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;
    else
        return "none"; 
}

// 获取当前工作目录(从环境变量PWD)
const char* getCwd()
{
    char* cwd = getenv("PWD");
    if(cwd)
        return cwd;
    else
        return "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=路径"格式(符合环境变量的标准格式),存入全局变量cwd
    sprintf(cwd,"PWD=%s",tmp);
    // 将新的PWD环境变量加入进程环境表,覆盖原有PWD
    putenv(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;  // 标记为内建命令
	}                                                                                                                                                  
    // 其他命令(非内建),返回0
    else 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的理解与实现

感谢你的三连支持!!!

评论 8
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值