【Linux】shell的实现

1、命令行提示符

首先应明确,shell是死循环软件,本质是交互式命令解释器

命令行提示符的格式:用户名 @主机名 当前路径

cpp 复制代码
static std::string rfindDir(const std::string &p)
{
    // 如果路径是根目录 /,直接返回/
    if(p == "/")
        return p;
    const std::string psep = "/"; // 路径分隔符斜杠
    // rfind:从后往前找第一个 '/'
    auto pos = p.rfind(psep);
    // npos代表没找到斜杠(比如单纯字符串test,没有/)
    if(pos == std::string::npos)
        return std::string(); // 返回空字符串
    // substr(pos+1):截取斜杠后面所有字符
    // 例:/home/ubuntu -> 找到最后一个/,后面ubuntu,返回ubuntu
    return p.substr(pos+1);
}

const char *GetUserName()
{
		// getenv("USER") 读取环境变量USER,存当前用户名
    char *name = getenv("USER");
    if(name == NULL)  // 读取失败
        return "None";
    return name;
}

// 获取主机名
const char *GetHostName()
{
    static char buf[128];
    // 系统调用直接获取本机主机名
    if (gethostname(buf, sizeof(buf)-1) == 0)
    {
        return buf;
    }
    strcpy(buf, "None");
    return buf;
}

// 获取当前工作路径
const char *GetPwd()
{
    char *pwd = getenv("PWD");
    //char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd == NULL)
        return "None";
    return pwd;
}

// 打印shell提示符
void PrintCommandLine()
{
    // 拼接打印格式:[用户名@主机名 当前目录]#
    printf("[%s@%s %s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str());
    fflush(stdout); // 强制刷新输出缓冲区,不然提示符可能卡住不显示
}

2、获取键盘指令

cpp 复制代码
int GetCommand(char commandline[], int size)
{
    if(NULL == fgets(commandline, size, stdin))
        return 0;
    // 2.1 用户输入的时候,至少会摁一下回车\n abcd\n ,\n '\0'
    commandline[strlen(commandline)-1] = '\0';
    return strlen(commandline);
}

用fegts从键盘读取一行输入存到command,这里不能用scanf进行输入,scanf是以空格、回车为分隔符,无法一次性读取带多个空格的整行命令,命令参数的个数不固定,例如ls -a -l ,scanf无法一次性完整接收,fegts直接读取缓冲区一整行,完整保留所有空格,然后交给strtok分割

3、命令字符串切割

cpp 复制代码
#define MAXARGS 32
char *gargv[MAXARGS];
int gargc = 0;
const char *gsep = " ";

void ParseCommand()
{
    // 每次解析前清空全局参数表,防止上一条命令残留
    memset(gargv, 0, sizeof(gargv));
    gargc = 0;

    // strtok第一次调用:传入原始字符串command_line
    char* token = strtok(command_line, gsep);
    while(token != NULL)
    {
        gargv[gargc++] = token;
        // 后续分割:第一个参数传NULL,继续从上一次截断位置分割
        token = strtok(NULL, gsep);
    }
}

int main()
{
    // 0. 从配置文件中获取环境变量填充环境变量表
    // LoadEnv();
    char command_line[MAXSIZE] = {0};

    while(1)
    {
        // 1. 打印shell提示符
        PrintCommandLine();

        // 2. 获取用户输入命令
        if (0 == GetCommand(command_line, sizeof(command_line)))
        {
            continue;
        }

        // 3. 分割整行命令,填充全局参数数组gargv
        ParseCommand(command_line);

        // 循环打印分割后的每一个命令参数
        for (int i = 0; gargv[i]; i++)
        {
            printf("[%d]: %s\n", i, gargv[i]);
        }
        // 打印原始用户输入的完整命令字符串
        printf("echo: %s\n", command_line);
    }

    return 0;
}

借助strtok将字符串分割成一个个的子串,第一次调用后返回第一个子串指针,后续循环分割第一个参数必须传NULL,函数自动记住上次分割位置,注意不能传入字符串常量char* str = "aaa bbb" ,strtok会原地修改字符串,把找到的分隔符(空格)覆盖为\0截断子串

4、外部命令的执行

cpp 复制代码
int ExecuteCommand()
{
    // 能不能让你的bash自己执行命令:ls -a -l
    pid_t id = fork();
    if(id < 0)
        return -1;
    else if(id == 0)
    {
        // ls -a -l
        int fd = -1;  // 重定向文件描述符
        if(redir_type == NoneRedir)
        {
            // Do Nothing
        }
        else if(redir_type == OutputRedir)
        {
            // 子进程要进行输出重定向
            fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == AppRedir)
        {
		        // 追加重定向
            fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == InputRedir)
        {
            fd = open(filename, O_RDONLY);
            dup2(fd, 0);
        }
        else{}
        // 参数1:程序名(无需绝对路径,自动搜索PATH)
        // 参数2:命令参数数组gargv
        // 参数3:自定义环境变量数组genv
        execvpe(gargv[0], gargv, genv);
        // exec系列函数执行成功不会返回;返回=替换失败,子进程退出码1
        exit(1);
    }
    else
    {
        // 父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
		        // WEXITSTATUS提取子进程正常退出的退出码,存入全局lastcode,对应$?
            lastcode = WEXITSTATUS(status);
        }
    }
    return 0;
}

int main()
{
		// 存储用户输入整行命令的缓冲区
    char command_line[MAXSIZE] = {0};
    while(1)
    {
        // 打印命令行字符串
        PrintCommandLine();
        // 获取用户输入
        if(0 == GetCommand(command_line, sizeof(command_line)))
            continue;

        ParseRedir(command_line);

        // 解析字符串 -> "ls -a -l" -> "ls" "-a" "-l" 命令行解释器,就要对用户输入的命令字符串首先进行解析!
        ParseCommand(command_line);
    
        // 让子进程执行这个命令
        ExecuteCommand();
    }

    return 0;
}

先通过fork创建子进程,复制当前shell程序,再调用execvp接口进行程序替换,不能父进程直接调用exec系列函数,exec 程序替换会完全覆盖当前进程代码、数据,shell 程序直接消失,无法循环接收下一条命令

5、内建命令

cpp 复制代码
// 环境变量表,存储拷贝过来的父进程环境变量
char *genv[MAXARGS];
// 环境变量条目计数
int genvc = 0;
// 保存上一条执行命令的退出码,对应$?功能
int lastcode = 0;

// retunr val:
// 0 : 不是内建命令
// 1 : 内建命令&&执行完毕
int CheckBuiltinExecute()
{
    // cd命令
    if(strcmp(gargv[0], "cd") == 0)
    {
        // 内建命令
        if(gargc == 2)
        {
            // 新的目标路径: gargv[1]
            // 1. 更改进程内核中的路径
            chdir(gargv[1]);
            // 2. 更改环境变量
            char pwd[1024];
            getcwd(pwd, sizeof(pwd)); // /home/whb
            snprintf(cwd, sizeof(cwd), "PWD=%s", pwd); // cwd: PWD=/home/home
            putenv(cwd);
            lastcode = 0;
        }
        return 1;
    }
    
    // echo命令
    else if(strcmp(gargv[0], "echo") == 0) // cd , echo , env , export 内建命令
    {
        if(gargc == 2)
        {
            if(gargv[1][0] == '$')
            {
                // $? ? : 看做一个变量名字
                if(strcmp(gargv[1]+1, "?") == 0)
                {
                    printf("lastcode: %d\n", lastcode);
                }
                else if(strcmp(gargv[1]+1, "PATH") == 0)
                {
                    printf("%s\n", getenv("PATH")); // putenv 和 getenv 是访问环境变量表!
                }
                lastcode = 0;
            }
            return 1;
        }
    }

    return 0;
}

cd操作:chdir(path)系统调用:修改内核 PCB 记录的当前工作目录,getcwd() 获取最新路径,snprintf拼接PWD=xxx,putenv()更新环境变量PWD,操作系统不会同步更新PWD环境变量,只改内核路径,提示符读取getenv("PWD")会显示旧路径

echo命令:echo $?:读取全局变量lastcode,打印上一条外部命令退出码,echo $PATHgetenv()读取进程环境变量表打印,内建命令执行完成统一设置lastcode=0,代表执行成功

进程创建时自动继承父进程环境变量,系统全局指针extern char** environ存储,getenv/putenv底层操作进程内部环境变量表,修改后子进程 exec 时可通过execvpe传递

相关推荐
果果燕14 小时前
实习笔记(二)NFS 嵌套挂载问题完整复盘
linux·服务器·网络
g3voip15 小时前
工业防爆电话选型指南:防爆参数、SIP接入与调度系统集成
linux·服务器·网络·信息与通信·ip·pbx
沫璃染墨15 小时前
《从零入门Linux系统篇(十四):系统工具篇·五——Git版本控制:从版本管理到协同开发》
linux·运维·服务器·git·gitee·github
PGCCC15 小时前
Ubuntu 安装 PostgreSQL 16 完整教程(2026):从仓库配置到远程连接
linux·ubuntu·postgresql
常乐か15 小时前
在 Ubuntu 24.04 中编译 Qt 5.15.2 项目完全指南
linux·qt·ubuntu
垂直昵称16 小时前
多线程实现网页接口并发压力测试
linux·服务器·windows
jufeng130717 小时前
【系列:MiniKV 原理剖析 · 第 2 篇】
linux·c++·软件工程
王琦031817 小时前
Linux的管理程序
linux·运维·服务器
Crazy________18 小时前
Redis02:库切换、监控与安全配置
linux·redis·云原生·mybatis
SakitamaX18 小时前
高可用集群KEEPALIVED
linux·运维·服务器·keepalived