1. 目标

• 实现常规命令的处理功能

• 支持内建命令的执行

• 帮助理解内建命令、本地变量和环境变量的概念差异

• 深入理解shell的运行机制和原理


2. 实现原理

以下是一个典型的shell交互示例:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~$ ls
code.c  Linux_network  Linux_system  mydir
ltx@hcss-ecs-d90d:~$ ps
    PID TTY          TIME CMD
1230183 pts/0    00:00:00 bash
1230204 pts/0    00:00:00 ps
ltx@hcss-ecs-d90d:~$ 

请参考下图的时间轴展示事件发生顺序,时间从左向右流动。图中用标有"bash"的方块表示shell进程,它会随时间推移从左向右移动。具体流程如下:

  1. shell从用户处读取字符串"ls"
  2. shell创建一个新进程
  3. 在新进程中运行ls程序
  4. shell等待该进程结束

Shell会读取新的一行输入,创建新进程并在该进程中执行程序,同时等待进程结束。

因此,要实现一个Shell,需要循环执行以下步骤:

  1. 获取命令行输入
  2. 解析命令行参数
  3. 创建子进程(fork)
  4. 替换子进程映像(execvp)
  5. 父进程等待子进程终止(wait)

基于上述流程和之前学到的知识,我们现在可以尝试自己实现一个Shell了。


下面我们采用C/C++混编的方式来模拟实现

2.1 命令行提示符

首先我们需要和shell一样要有命令行提示符,我们先来介绍一下命令行提示符:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ 
用户名@主机名:当前目录$
  • ltx:当前登录的用户名(\u)。
  • @:分隔用户名和主机名的符号。
  • hcss-ecs-d90d:主机名(\h),表示用户登录的机器名称。
  • ::分隔主机名和当前目录的符号。
  • \~/Linux_system/lesson7:当前工作目录(\w):
    • \~:用户主目录的缩写(如/home/ltx)。
    • Linux_system/lesson7:当前目录的路径(相对主目录)。
  • $:命令提示符符号,表示当前为普通用户权限;若为#则表示超级用户(root)权限

我们先来直接打印看一下效果

代码语言:javascript

AI代码解释

#include <cstdio>
#include <stdlib.h>

int main()
{
    printf("ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ");
    return 0;
}

运行查看:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ make
g++ -o myshell myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ 

但是我们直接打印的话太死板了,就算用户,主机和当前目录改变,我们还是输出这个提示符,显然是不够的,那我们应该怎么做呢?

我们可以使用环境变量来拿到我们想要的用户名,主机名和当前目录

代码语言:javascript

AI代码解释

#define FORMAT "%s@%s:%s# "

const char* GetUserName()
{
    const char* name = getenv("USER");
    return name == NULL ? "None" : name;
}

const char* GetHostName()
{
    const char* hostname = getenv("HOSTNAME");
    return hostname == NULL ? "None" : hostname;
}

const char* GetPwd()
{
    const char* pwd = getenv("PWD");
    return pwd == NULL ? "None" : pwd;
}

int main()
{
    printf(FORMAT, GetUserName(), GetHostName(), GetPwd());
    return 0;
}

注意这里我们格式的提示符符号写死为#,为了和程序的($)进行区分,当然也可以通过判断当前用户是普通用户还是超级用户,来使用不同提示符符号,但是我们这里为了方便区分就直接写死为#

运行查看:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@None:/home/ltx/Linux_system/lesson7# ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ 

怎么回事,主机名怎么显示None,难道是没有HOSTNAME这个环境变量吗?我们来看一下

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ echo $HOSTNAME
hcss-ecs-d90d
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ env | grep HOSTNAME
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ 

通过echo $HOSTNAME来查看发现值存在,然后再通过env | grep HOSTNAME命令发现无输出,说明HOSTNAME不在环境变量列表中。

说明:

  • Shell 中的 $HOSTNAME 是 Shell 内置变量,而非标准环境变量。
  • 默认情况下,HOSTNAME 不会自动导出到子进程环境(如我们的 C 程序)。
  • 父 Shell 必须显式执行 export HOSTNAME 才能被子进程继承。

方案 1:导出变量(临时生效)

在运行程序前执行:

代码语言:javascript

AI代码解释

export HOSTNAME=$(hostname)  # 动态获取主机名并导出
./myshell                    # 再运行程序

效果:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ export HOSTNAME=$(hostname)
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ env | grep HOSTNAME
HOSTNAME=hcss-ecs-d90d
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:/home/ltx/Linux_system/lesson7# ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ 

方案 2:永久导出变量

在 Shell 配置文件(\~/.bashrc 或 \~/.zshrc)中添加:

代码语言:javascript

AI代码解释

export HOSTNAME=$(hostname)

执行 source \~/.bashrc 激活配置。

方案 3:改用系统调用(推荐)

使用 gethostname() 替代环境变量,直接获取系统主机名:

函数原型

代码语言:javascript

AI代码解释

#include <unistd.h>
int gethostname(char *name, size_t len);
  • 参数
    • name:指向字符缓冲区的指针,用于存储主机名
    • len:缓冲区长度(字节数)
  • 返回值
    • 成功时返回 0
    • 失败时返回 -1 并设置 errno

优势

  • 不依赖环境变量,避免导出问题。
  • 直接调用内核接口,可靠性更高。
  • 跨平台兼容性更好。

代码语言:javascript

AI代码解释

#include <cstdio>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <cstring>
#define FORMAT "%s@%s:%s# "

const char* GetUserName()
{
    const char* name = getenv("USER");
    return name == NULL ? "None" : name;
}

//const char* GetHostName()
//{
//    const char* hostname = getenv("HOSTNAME");
//    return hostname == NULL ? "None" : hostname;
//}

void GetHostName(char* buffer, size_t size)
{
    if (gethostname(buffer, size) != 0)
        strncpy(buffer, "None", size);
}

const char* GetPwd()
{
    const char* pwd = getenv("PWD");
    return pwd == NULL ? "None" : pwd;
}

int main()
{
    char hostname[256];
    GetHostName(hostname, sizeof(hostname));
    printf(FORMAT, GetUserName(), hostname, GetPwd());
    return 0;
}

运行结果:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ make
g++ -o myshell myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:/home/ltx/Linux_system/lesson7# ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ 

但是还有一个问题,那就是我们打印的目录并没有打印~(用户主目录的缩写),而是直接把我们的主目录给打印出来了,那这里我们需要再修改一下

代码语言:javascript

AI代码解释

const char* GetPwd()
{
    const char* pwd = getenv("PWD");
    const char* home = getenv("HOME");
    
    if(!pwd || !home) return "None";
    
    // 替换主目录为\~符号
    if(strncmp(pwd, home, strlen(home)) == 0)
    {
        static char formatted[1024];  // 静态缓冲区
        snprintf(formatted, sizeof(formatted), "~%s", pwd + strlen(home));
        return formatted;
    }
    return pwd;
}

关于strncmp和snprintf函数:

一、strncmp():安全字符串比较函数

1. 函数原型与核心功能

代码语言:javascript

AI代码解释

#include <string.h>
int strncmp(const char *str1, const char *str2, size_t n);
  • 功能:比较两个字符串的前 n 个字节(或直到发现不同字符)
  • 参数
    • str1str2:待比较的字符串
    • n:最大比较字符数(非字符串长度)
  • 返回值
    • < 0str1 小于 str2
    • = 0:两字符串前 n 字符相同
    • > 0str1 大于 str2

二、snprintf():安全格式化输出函数

1. 函数原型与核心功能

代码语言:javascript

AI代码解释

#include <stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);
  • 功能:格式化输出到缓冲区,防止溢出 
  • 参数
    • str:目标缓冲区
    • size:缓冲区容量(含终止符)
    • format:格式化字符串(同 printf
  • 返回值
    • 成功:返回欲写入的字符数(不含 \0
    • 失败:返回负值

运行结果:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ make
g++ -o myshell myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ 

既然已经达到了我们需要的效果,那么接下来我们就来进行面向对象式的封装

制作命令行提示符

代码语言:javascript

AI代码解释

// 制作命令行
void MakeCommandLine(char cmd_prompt[], size_t size)
{
    char hostname[256];
    GetHostName(hostname, sizeof(hostname));
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), hostname, GetPwd());
}
打印命令行提示符

代码语言:javascript

AI代码解释

// 打印命令行提示符
void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE]; // COMMAND_SIZE为1024
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    ffulsh(stdout);
}

封装后的完整代码:

代码语言:javascript

AI代码解释

#include <cstdio>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <cstring>

#define COMMAND_SIZE 1024
#define FORMAT "%s@%s:%s# "

const char* GetUserName()
{
    const char* name = getenv("USER");
    return name == NULL ? "None" : name;
}

void GetHostName(char* buffer, size_t size)
{
    if (gethostname(buffer, size) != 0)
        strncpy(buffer, "None", size);
}

const char* GetPwd()
{
    const char* pwd = getenv("PWD");
    const char* home = getenv("HOME");
    
    if(!pwd || !home) return "None";
    
    // 替换主目录为\~符号
    if(strncmp(pwd, home, strlen(home)) == 0)
    {
        static char formatted[1024];  // 静态缓冲区
        snprintf(formatted, sizeof(formatted), "~%s", pwd + strlen(home));
        return formatted;
    }
    return pwd;
}

// 制作命令行
void MakeCommandLine(char cmd_prompt[], size_t size)
{
    char hostname[256];
    GetHostName(hostname, sizeof(hostname));
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), hostname, GetPwd());
}

// 打印命令行提示符
void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

int main()
{
    // 1. 打印命令行提示符
    PrintCommandPrompt();

    // 2. 获取用户输入的指令
    return 0;
}

运行查看:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ make
g++ -o myshell myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ 

没有问题


2.2 获取命令行输入

接下来就是获取用户的输入指令,如“ls -a -l\n”

代码语言:javascript

AI代码解释

// 获取命令行输入
bool GetCommandLine(char* out, size_t size)
{
     // ls -a -l => "ls -a -l\n" 字符串
     char* c = fgets(out, size, stdin);
     if(c == NULL) return false;
     out[strlen(out) - 1] = 0;// 清理\n
     if(strlen(out) == 0) return false;// 如果用户只按下enter,就不需要去解析
     return true;
}
为什么需要清理换行符(\n)?

在函数 GetCommandLine 中清理换行符 \n 是处理用户输入的关键步骤,其必要性源于 fgets() 函数的底层工作机制和命令行处理需求。以下从多个维度深入解析原因:


一、fgets() 函数的核心行为机制

代码语言:javascript

AI代码解释

#include <stdio.h>
char *fgets(char *str, int n, FILE *stream);
  • 参数解析
    • str:指向目标缓冲区的指针(存储读取结果)
    • n:最大读取字符数(包含终止符 \0
    • stream:输入流(stdin 表示标准输入)
  • 返回值
    • 成功:返回 str 指针
    • 失败/EOF:返回 NULL(必须检查!)

保留换行符的特性

当用户输入命令后按下回车键(Enter),系统会在输入流中插入换行符 \n

fgets() 会完整保留换行符作为字符串的一部分,例如输入 ls -a -l 后,实际存储为:

代码语言:javascript

AI代码解释

"ls -a -l\n"  // 末尾包含 \n

与危险函数 gets() 的对比

函数

换行符处理

安全性

缓冲区保护

fgets()

保留 \n

安全

通过 size 限制长度

gets()

替换为 \0

高危

无长度检查(已弃用)

gets() 因无缓冲区保护已被现代 C 标准废弃。


二、不清理换行符的严重后果

1. 命令解析失效

若保留 \n,命令行字符串变为:

代码语言:javascript

AI代码解释

"ls\n"   // 而非 "ls"

在解析命令时:

代码语言:javascript

AI代码解释

if (strcmp(command, "ls") == 0)  // 因 "ls" != "ls\n" 导致匹配失败

这会导致命令无法识别。

2. 路径操作错误

输入路径 \~/Documents 时:

代码语言:javascript

AI代码解释

"\~/Documents\n"  // 路径检测失败

系统调用(如 chdir())将返回错误,因为路径包含非法字符 \n

3. 输出格式破坏

使用 printf("Output: %s", command) 时:

代码语言:javascript

AI代码解释

Output: ls
        // 换行符导致额外空行

这会破坏命令行提示符的紧凑性。

测试效果:

代码语言:javascript

AI代码解释

int main()
{
    // 1. 打印命令行提示符
    PrintCommandPrompt();

    // 2. 获取用户输入的指令
    char commandline[COMMAND_SIZE];
    if(GetCommandLine(commandline, sizeof(commandline)))
        printf("echo %s\n", commandline);
    return 0;
}

这里我们printf("echo %s\n", commandline),将我们的输入的指令回显出来,方便我们观察

运行结果:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ make
g++ -o myshell myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls -a -l
echo ls -a -l

没有问题,但是这里打印一次就结束了,Shell不可能只打印一次就结束,而是一直都在死循环运行,所以我们再来修改一下

代码语言:javascript

AI代码解释

int main()
{
    while(1)
    {
        // 1. 打印命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的指令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))// 获取输入失败,如用户只按下回车键
            continue;    

        printf("echo %s\n", commandline);

    }
    return 0;
}

如果获取失败就继续打印命令行提示符,用户重新输入,命令行再进行获取输入;获取成功就回显命令,方便查看

运行查看:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls -a -l
echo ls -a -l
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls
echo ls
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# pwd
echo pwd
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# 

可以看到只要我们不退(如CTRL+ c),我们就可以一直运行


2.3 解析命令行参数

用户输入命令之后,我们需要解析用户输入的命令,然后去执行对应命令

我们先来进行命令行分析,那应该怎么分析呢?我们需要将获取到的命令行进行分析,怎么分析?通过我们自己的命令行参数表来分析,那么就得把获取到的命令分割到命令行参数表中,例如输入命令"ls -a -l",我们需要将命令分割为 "ls" "-a" "-l",放进参数表中。

那么如何分割字符串呢?这里我们先来认识一个函数strtok()

一、函数原型与核心机制

代码语言:javascript

AI代码解释

#include <string.h>
char *strtok(char *str, const char *delim);
  • 参数解析
    • str首次调用需传入待分割字符串,后续调用必须传 NULL(函数内部记录状态)。
    • delim:分隔符集合(如 "," 或 " \t\n"),任意字符均触发分割。
  • 返回值
    • 成功:指向当前子串的指针。
    • 失败:返回 NULL(无更多子串)。

二、底层工作流程

  1. 首次调用
    • 跳过 str 开头的所有分隔符(如 " hello" 会跳过空格)。
    • 找到第一个非分隔符字符作为子串起始位置。
    • 继续扫描直到遇到 delim 中的字符或字符串结尾,将该位置替换为 \0,并返回子串指针。
  2. 后续调用
    • 传入 NULL 作为 str 参数,函数从上一次替换的 \0 之后继续扫描。
    • 重复上述过程直至扫描完整个字符串。

关键特性修改原始字符串(分隔符被替换为 \0),因此不能用于常量字符串(如 char* s = "abc")。


三、典型代码示例

代码语言:javascript

AI代码解释

#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "apple,banana,orange"; // 必须为数组(可修改)
    const char delim[] = ",";
    char *token = strtok(str, delim);   // 首次调用:返回"apple"

    while (token != NULL) {
        printf("%s\n", token);
        token = strtok(NULL, delim);    // 后续调用:依次返回"banana"、"orange"
    }
    return 0;
}

输出

代码语言:javascript

AI代码解释

apple
banana
orange

所以我们就可以通过strtok函数来实现分割字符串

代码语言:javascript

AI代码解释

// 全局定义
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc;


// 解析命令行
bool CommandParse(char* commandline)
{
    // 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
    g_argc = 0;
    char* token = strtok(commandline, " ");
    while(token)
    {
        g_argv[g_argc++] = token;
        token = strtok(NULL, " ");
    }
    g_argv[g_argc] = token; // NULL也要存入参数表中
    return g_argc > 0 ? true:false;
}

为了方便我们能够在程序运行时,可以看到分割后的字符串命令是否正确分割到了我们的命令行参数表中,我们可以打印出来观察

代码语言:javascript

AI代码解释

// 打印命令行参数表
void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]->%s\n", i, g_argv[i]);
    }
    printf("argc: %d\n", g_argc);
}

测试:

代码语言:javascript

AI代码解释

int main()
{
    while(1)
    {
        // 1. 打印命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的指令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))// 获取输入失败,如用户只按下回车键
            continue;    
        
        //printf("echo %s\n", commandline);
        // 3. 解析命令行参数
        if(!CommandParse(commandline))
            continue;
        PrintArgv();
    }
    return 0;
}

运行结果:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ make
g++ -o myshell myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls -a -l
argv[0]->ls
argv[1]->-a
argv[2]->-l
argc: 3
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls -l
argv[0]->ls
argv[1]->-l
argc: 2
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls-^[[D^[[C^C
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls -a -l
argv[0]->ls
argv[1]->-a
argv[2]->-l
argc: 3
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls -a
argv[0]->ls
argv[1]->-a
argc: 2
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls -a -b -c -d
argv[0]->ls
argv[1]->-a
argv[2]->-b
argv[3]->-c
argv[4]->-d
argc: 5
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls
argv[0]->ls
argc: 1
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# 
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ^C  #CRTL + c退出
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ 

运行结果没有问题,一切都在掌握中


2.4 执行命令

获取到用户输入的命令之后,我们现在需要给用户反馈,去执行用户输入的命令才行

那要怎么执行命令呢?其实在前面实现原理我们就介绍过了,Shell的bash进程,会创建子进程,通过进程替换去执行用户输入的命令,bash进程则阻塞等待子进程退出。不过这里有一个问题,既然要进程替换,那就要使用exec系列函数,但是exec系列函数那么多,应该要使用哪个呢?其实不难想到,已知我们现在拿着分割后的命令——命令行参数表,也就是一个数组,所以我们肯定需要execv系列的函数,那么就是execv,execvp和execve三个中选,那肯定选execvp呀,因为另外两个需要完整路径,而execvp自己会搜索PATH环境变量,这不就简单多了。

注意:进程替换可在上一篇文章【进程控制】中具体了解

代码语言:javascript

AI代码解释

int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        execvp(g_argv[0], g_argv);
        exit(1);
    }

    // father
    pid_t rid = waitpid(id, NULL, 0);// 阻塞等待指定子进程id,目前不需要获取子进程的退出信息
    (void)rid;// 暂时先不判断返回值
    return 0;
}

测试:

代码语言:javascript

AI代码解释

int main()
{
    while(1)
    {
        // 1. 打印命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的指令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))// 获取输入失败,如用户只按下回车键
            continue;    
        
        //printf("echo %s\n", commandline);
        // 3. 解析命令行参数
        if(!CommandParse(commandline))
            continue;
        // PrintArgv();

        // 4. 执行命令
        Execute();
    }
    return 0;
}

运行结果:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ make
g++ -o myshell myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls
Makefile  myshell  myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls -l
total 28
-rw-rw-r-- 1 ltx ltx    68 Jul 20 16:39 Makefile
-rwxrwxr-x 1 ltx ltx 17432 Jul 21 17:09 myshell
-rw-rw-r-- 1 ltx ltx  3179 Jul 21 17:05 myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# pwd
/home/ltx/Linux_system/lesson7
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# cd ..
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# pwd
/home/ltx/Linux_system/lesson7

可以看到确实实现了对应指令的功能,但是其实还是存在一些问题,我们使用 cd .. (进入上级目录),并没有达到想要的效果,pwd打印的还是原来的路径,这是什么原因呢?

其实是因为子进程在执行命令,我们子进程修改路径,但是我们父进程没改呀,我们执行cd命令,真正要修改的路径是谁的路径?其实是要修改父进程的路径。因为我们只是创建子进程去执行命令,执行命令前的工作我们都是在父进程中完成的,子进程只是执行对应命令,执行完之后就退出了,父进程进行等待回收避免成为僵尸进程,所以我们修改子进程的路径达不到想要的效果,修改子进程的路径压根就不会影响父进程。

注意:对于这种需要父进程去执行的命令,就是内建命令


2.5 内建命令

所以在执行命令前,我们需要先判断一下是不是内建命令,如果是内建命令就父进程自己执行,不是就让子进程去执行

代码语言:javascript

AI代码解释

void Cd()
{
    if(g_argc == 1) // cd不带参数,直接进入用户主目录
    {
        const char* home = GetHome();
        if(home) 
        {
            chdir(home);
            setenv("PWD", home, 1); // 更新PWD环境变量
        }
    }
    else
    {
        char* where = g_argv[1];
        chdir(where);
        setenv("PWD", where, 1); // 更新PWD环境变量
    }
}


// 检测并处理内建命令
bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }

    return false;
}

chdir:

1. 函数原型

代码语言:javascript

AI代码解释

#include <unistd.h>
int chdir(const char *path);  // 通过路径修改工作目录
int fchdir(int fd);           // 通过文件描述符修改

2. 关键行为特性

  • 进程隔离性chdir() 仅影响调用进程及其子进程,不会修改父进程(如 Shell)的工作目录 。
  • 环境变量无关联chdir() 不会自动更新 PWD 环境变量,需手动调用 setenv("PWD", path, 1) 同步 。
  • 符号链接处理: 若 path 是符号链接,chdir() 会跟随链接指向的实际目录 。

setenv:

1. 函数定义

代码语言:javascript

AI代码解释

#include <stdlib.h>  
int setenv(const char *name, const char *value, int overwrite);  
  • 功能
    • 若环境变量 name 不存在,则创建该变量并赋值为 value
    • 若 name 已存在且 overwrite ≠ 0,则将其值更新为 value
    • 若 name 已存在且 overwrite = 0,则保留原值不变 。

2. 底层行为

  • 内存管理: 函数内部会复制 name 和 value 字符串,调用者无需维护其生命周期 。
  • 环境存储: 修改后的环境变量存储在全局指针 environ 指向的字符串数组中 。

3. 关键特性

特性

说明

进程隔离性

修改仅影响当前进程及其子进程,不影响父进程(如 Shell)

线程安全性

POSIX.1-2008 要求线程安全,但实际实现需验证(如 glibc 支持)

平台兼容性

支持 Linux/Unix,Windows 需用 _putenv() 替代

符号处理

value 中的特殊字符(如 $, :, %)不会被解析,按字面存储


参数与返回值深度解析

1. 参数规范

参数

类型

约束条件

name

const char*

非空;长度 >0;不得包含 = 字符(否则返回 EINVAL)

value

const char*

可为空字符串(""),此时变量值为空;若为 NULL 则等效于删除变量

overwrite

int

非零值强制更新;零值保护现有变量

2. 返回值与错误码

返回值

含义

错误码

触发条件

0

成功

-

-

-1

失败

EINVAL

name 含 = 或为空

ENOMEM

内存不足,无法分配新环境空间

特殊场景

  • 在 z/OS 中,以 .BPXK_.EDC_.CEE_ 开头的变量名被保留,禁止修改 。
  • Windows 平台:空值 setenv(name, "") 等效于删除变量 。

测试:

代码语言:javascript

AI代码解释

int main()
{
    while(1)
    {
        // 1. 打印命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的指令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))// 获取输入失败,如用户只按下回车键
            continue;    
        
        //printf("echo %s\n", commandline);
        // 3. 解析命令行参数
        if(!CommandParse(commandline))
            continue;
        // PrintArgv();

        // 4. 检测并处理内建命令
        if(CheckAndExecBuiltin())
            continue;

        // 5. 执行命令
        Execute();
    }
    return 0;
}

运行结果:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ make
g++ -o myshell myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# ls
Makefile  myshell  myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# pwd
/home/ltx/Linux_system/lesson7
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# cd ..
ltx@hcss-ecs-d90d:..# pwd
/home/ltx/Linux_system
ltx@hcss-ecs-d90d:..# cd
ltx@hcss-ecs-d90d:~# pwd
/home/ltx
ltx@hcss-ecs-d90d:~# cd /
ltx@hcss-ecs-d90d:/# pwd
/
ltx@hcss-ecs-d90d:/# cd /home/ltx/Linux_system/lesson7
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# pwd
/home/ltx/Linux_system/lesson7

我们发现我们通过 cd.. 进入上级目录时,我们的当前目录也变成了 .. ,这是因为 .. 是相对路径,相对当前目录的上一级目录,但是我们的当前目录需要获取绝对路径,所以还需要再修改一下GetPwd()函数。

代码语言:javascript

AI代码解释

void Cd()
{
    char cwd[1024]; // 用于存储当前工作目录
    if(g_argc == 1) // cd不带参数,直接进入用户主目录
    {
        const char* home = GetHome();
        if (home && chdir(home) == 0)
        {
            if (getcwd(cwd, sizeof(cwd)))
            {
                setenv("PWD", cwd, 1);
            }
        }
    }
    else
    {
        char* where = g_argv[1];
        if (chdir(where) == 0)
        {
            // 关键修复:获取实际的绝对路径
            if (getcwd(cwd, sizeof(cwd)))
            {
                setenv("PWD", cwd, 1);
            }
        }
        else
        {
            perror("cd");
        }
    }
}

getcwd:

1. 函数原型

代码语言:javascript

AI代码解释

#include <unistd.h>
char *getcwd(char *buf, size_t size);
  • 功能:将当前工作目录的绝对路径写入缓冲区 buf
  • 参数
    • buf:目标缓冲区指针(可为 NULL
    • size:缓冲区大小(字节数)
  • 返回值
    • 成功:返回 buf(若 buf 非 NULL)或新分配内存指针(若 buf 为 NULL
    • 失败:返回 NULL 并设置 errno

2. 底层行为

运行结果:

代码语言:javascript

AI代码解释

ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ make
g++ -o myshell myshell.cc
ltx@hcss-ecs-d90d:~/Linux_system/lesson7$ ./myshell
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# pwd
/home/ltx/Linux_system/lesson7
ltx@hcss-ecs-d90d:~/Linux_system/lesson7# cd ..
ltx@hcss-ecs-d90d:~/Linux_system# pwd
/home/ltx/Linux_system
ltx@hcss-ecs-d90d:~/Linux_system# cd ..
ltx@hcss-ecs-d90d:~# pwd
/home/ltx
ltx@hcss-ecs-d90d:~# cd /
ltx@hcss-ecs-d90d:/# pwd
/

其实例如cd ~(切换到用户主目录($HOME))和cd -(返回上一个工作目录),我们模拟实现的shell并不能执行这样的命令,还需要针对一些特殊情况来进行完善,同样内建命令也不止cd,常见内建命令包括:cdpwdecho(部分实现)、exportsource等。还有除了命令行参数表,也还有环境变量表,也都可以去实现。但是我们就不一一去模拟实现了,感兴趣可以自行去实现,我们这次模拟实现自定义shell同时,也是对之前知识的一种巩固。

源码:

代码语言:javascript

AI代码解释

#include <cstdio>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>

#define COMMAND_SIZE 1024
#define FORMAT "%s@%s:%s# "

#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc;

const char* GetUserName()
{
    const char* name = getenv("USER");
    return name == NULL ? "None" : name;
}

//const char* GetHostName()
//{
//    const char* hostname = getenv("HOSTNAME");
//    return hostname == NULL ? "None" : hostname;
//}

void GetHostName(char* buffer, size_t size)
{
    if (gethostname(buffer, size) != 0)
        strncpy(buffer, "None", size);
}

const char* GetPwd()
{
    const char* pwd = getenv("PWD");
    const char* home = getenv("HOME");
    
    if(!pwd || !home) return "None";
    
    // 替换主目录为\~符号
    if(strncmp(pwd, home, strlen(home)) == 0)
    {
        static char formatted[1024];  // 静态缓冲区
        snprintf(formatted, sizeof(formatted), "~%s", pwd + strlen(home));
        return formatted;
    }
    return pwd;
}

const char* GetHome()
{
    const char* home = getenv("HOME");
    return home;
}

// 制作命令行
void MakeCommandLine(char cmd_prompt[], size_t size)
{
    char hostname[256];
    GetHostName(hostname, sizeof(hostname));
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), hostname, GetPwd());
}

// 打印命令行提示符
void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

// 获取命令行输入
bool GetCommandLine(char* out, size_t size)
{
     // ls -a -l => "ls -a -l\n" 字符串
     char* c = fgets(out, size, stdin);
     if(c == NULL) return false;
     out[strlen(out) - 1] = 0;// 清理\n
     if(strlen(out) == 0) return false;// 如果用户只按下enter
     return true;
}

// 解析命令行
bool CommandParse(char* commandline)
{
    // 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
    g_argc = 0;
    char* token = strtok(commandline, " ");
    while(token)
    {
        g_argv[g_argc++] = token;
        token = strtok(NULL, " ");
    }
    g_argv[g_argc] = token;// NULL也要存入参数表中
    return g_argc > 0 ? true : false;
}

// 打印命令行参数表
void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]->%s\n", i, g_argv[i]);
    }
    printf("argc: %d\n", g_argc);
}

void Cd()
{
    char cwd[1024]; // 用于存储当前工作目录
    if(g_argc == 1) // cd不带参数,直接进入用户主目录
    {
        const char* home = GetHome();
        if (home && chdir(home) == 0)
        {
            if (getcwd(cwd, sizeof(cwd)))
            {
                setenv("PWD", cwd, 1);
            }
        }
    }
    else
    {
        char* where = g_argv[1];
        if (chdir(where) == 0)
        {
            // 关键修复:获取实际的绝对路径
            if (getcwd(cwd, sizeof(cwd)))
            {
                setenv("PWD", cwd, 1);
            }
        }
        else
        {
            perror("cd");
        }
    }
}

// 检测并处理内建命令
bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }

    return false;
}

// 执行命令
int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        execvp(g_argv[0], g_argv);
        exit(1);
    }

    // father
    pid_t rid = waitpid(id, NULL, 0);// 阻塞等待指定子进程id,目前不需要获取子进程的退出信息
    (void)rid;// 暂时先不判断返回值
    return 0;
}

int main()
{
    while(1)
    {
        // 1. 打印命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的指令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))// 获取输入失败,如用户只按下回车键
            continue;    
        
        //printf("echo %s\n", commandline);
        // 3. 解析命令行参数
        if(!CommandParse(commandline))
            continue;
        // PrintArgv();

        // 4. 检测并处理内建命令
        if(CheckAndExecBuiltin())
            continue;

        // 5. 执行命令
        Execute();
    }
    return 0;
}


 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐