自制简易 Shell:像搭建积木小屋一样打造命令交互小天地

目录

准备工作:搭建小屋的材料

[打造小屋的 "身份牌"](#打造小屋的 “身份牌”)

[接收指令:小屋的 "对讲机"](#接收指令:小屋的 “对讲机”)

拆解指令:把大任务拆成小积木

[执行指令:小屋的 "行动队"](#执行指令:小屋的 “行动队”)

[特殊指令:小屋的 "特色功能"](#特殊指令:小屋的 “特色功能”)

小屋的日常运转

完整代码


啥是 Shell 呢?打个比方,**它就像是你和计算机底层系统之间的 "翻译官" 和 "大管家"。**你给它下指令,它就能帮你调动计算机的各种资源去完成任务,就像你跟管家说 "我要整理文件",管家就会去安排人手(计算机资源)帮你搞定

准备工作:搭建小屋的材料

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define NUM 1024
#define SIZE 64
#define SEP " "

char cwd[1024];
char enval[1024];
int lastcode = 0;

看代码开头,引入了一堆头文件,这就好比搭建积木小屋前得准备好各种工具。<stdio.h>是负责输入输出的,就像小屋的 "门窗",能让信息进进出出;<stdlib.h>提供了一些通用的工具函数,像是搭建小屋时的 "万能螺丝刀";<string.h>专门处理字符串,就像给积木贴上好看标签的工具;<unistd.h><sys/types.h><sys/wait.h>则是和系统交互的关键,好比是和外界沟通获取搭建材料的渠道。

后面定义的几个宏和变量也很重要。NUMSIZE就像是规定了积木的大小和数量上限,给我们的小屋搭建划定了一个范围。cwdenvallastcode则像是小屋里的 "储物箱",用来存放当前工作目录、环境变量相关信息以及上一个命令的退出码。

打造小屋的 "身份牌"

cpp 复制代码
const char *getUsername()
{
    const char *name = getenv("USER");
    if (name)
        return name;
    else
        return "none";
}

const char *getHostname()
{
    const char *hostname = getenv("HOSTNAME");
    if (hostname)
        return hostname;
    else
        return "none";
}

const char *getCwd()
{
    const char *cwd = getenv("PWD");
    if (cwd)
        return cwd;
    else
        return "none";
}

这几个函数就像是在给咱们的小屋制作 "身份牌"。getUsername函数去系统环境变量里找 "USER",就像在小屋里翻翻找找,看看住在这里的人叫啥名字。要是找到了,就把名字亮出来,找不到就说 "none"。getHostnamegetCwd函数也是类似的道理,一个是找计算机的主机名,一个是找当前工作目录,就好比确定小屋在哪个小区(主机),具体门牌号是啥(工作目录)。

接收指令:小屋的 "对讲机"

cpp 复制代码
int getUserCommand(char *command, int num)
{
    printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
    char *r = fgets(command, num, stdin);
    if (r == NULL)
        return -1;
    command[strlen(command) - 1] = '\0';
    return strlen(command);
}

getUserCommand函数就像是小屋里的 "对讲机"。它先打印出一个提示符,格式是[用户名@主机名 当前工作目录]# ,就像管家在小屋门口喊 "我准备好接收指令啦!" 然后通过fgets从标准输入(也就是你在键盘上敲的内容)读取用户输入的命令,存到command这个 "小纸条" 里。要是没读取成功,就返回 -1 ,读取成功了就把命令末尾的换行符去掉,整理得干干净净,再返回命令的长度。

拆解指令:把大任务拆成小积木

cpp 复制代码
void commandSplit(char *in, char *out[])
{
    int argc = 0;
    out[argc++] = strtok(in, SEP);
    while (out[argc++] = strtok(NULL, SEP))
        ;
}

commandSplit函数干的活,就像是把一大块复杂的积木拆解成一个个小积木。它用strtok函数把用户输入的命令字符串(in),按照SEP(这里是空格)这个分隔符,拆分成一个个单词,然后把这些单词分别放到out这个 "小格子" 数组里。这样,原本一长串的命令,就被整理得井井有条,方便后续处理。

执行指令:小屋的 "行动队"

cpp 复制代码
int execute(char *argv[])
{
    pid_t id = fork();
    if (id < 0)
    {
        return -1;
    }
    else if (id == 0)
    {
        execvp(argv[0], argv);
        exit(1);
    }
    else
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid > 0)
        {
            lastcode = WEXITSTATUS(status);
        }
    }
    return 0;
}

execute函数就是小屋里的 "行动队"。它先用fork函数创建一个子进程,就像是派出一个分身去干活。要是创建失败(id < 0 ),就返回 -1 表示任务失败。要是分身(子进程,id == 0 )创建成功,就通过execvp函数去执行用户输入的命令,要是执行出问题了,就用exit(1)结束子进程。而原来的进程(父进程)则通过waitpid函数等着分身回来报告情况,要是等回来了(rid > 0 ),就把分身的退出码存到lastcode里,就像记录下这次行动的结果。

特殊指令:小屋的 "特色功能"

cpp 复制代码
void cd(const char *path)
{
    chdir(path);
    char tmp[1024];
    getcwd(tmp, sizeof(tmp));
    sprintf(cwd, "PWD=%s", tmp);
    putenv(cwd);
}

int doBuildin(char *argv[])
{
    if (strcmp(argv[0], "cd") == 0)
    {
        char *path = NULL;
        if (argv[1] == NULL)
            path = ".";
        else
            path = argv[1];
        cd(path);
        return 1;
    }
    else if (strcmp(argv[0], "export") == 0)
    {
        if (argv[1] == NULL)
            return 1;
        strcpy(enval, argv[1]);
        putenv(enval); 
        return 1;
    }
    else if (strcmp(argv[0], "echo") == 0)
    {
        char *val = argv[1] + 1;
        if (strcmp(val, "?") == 0)
        {
            printf("%d\n", lastcode);
            lastcode = 0;
        }
        else
        {
            printf("%s\n", getenv(val));
        }
        return 1;
    }
    else if (0)
    {
    }

    return 0;
}

这里定义的cddoBuildin函数,给小屋增添了一些 "特色功能"。cd函数就像小屋的 "搬家工具",它能改变当前工作目录,然后更新环境变量里的PWD,就像你给小屋换了个地方,还得把新地址告诉大家。

doBuildin函数则像是一个 "功能调度员",它判断用户输入的是不是像cdexportecho这种内置命令。要是cd命令,就调用cd函数去切换目录;要是export命令,就把相关环境变量设置好;要是echo命令,根据不同情况,要么打印上一个命令的退出码,要么打印指定环境变量的值。这些内置命令让小屋能实现一些特殊的、常用的功能,就像给小屋装上了一些好用的小机关。

小屋的日常运转

cpp 复制代码
int main()
{
    while (1)
    {
        char usercommand[NUM];
        char *argv[SIZE];
        // 1. 打印提示符&&获取用户命令字符串获取成功
        int n = getUserCommand(usercommand, sizeof(usercommand));
        if (n <= 0)
            continue;
        // 2. 分割字符串
        // "ls -a -l" -> "ls" "-a" "-l"
        commandSplit(usercommand, argv);
        // 3. check build-in command
        n = doBuildin(argv);
        if (n)
            continue;
        // 4. 执行对应的命令
        execute(argv);
    }
}

main函数就是小屋日常运转的 "总导演"。它在一个无限循环里,不断地做这几件事:先通过getUserCommand获取用户输入的命令,要是没获取到有效命令就跳过这次循环;然后用commandSplit把命令拆分开;接着看看是不是内置命令,是的话就用doBuildin处理,处理完了就跳过这次循环;最后要是不是内置命令,就用execute去执行这个命令。

完整代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define NUM 1024
#define SIZE 64
#define SEP " "

char cwd[1024];
char enval[1024];
int lastcode = 0;

const char *getUsername()
{
    const char *name = getenv("USER");
    if (name)
        return name;
    else
        return "none";
}

const char *getHostname()
{
    const char *hostname = getenv("HOSTNAME");
    if (hostname)
        return hostname;
    else
        return "none";
}

const char *getCwd()
{
    const char *cwd = getenv("PWD");
    if (cwd)
        return cwd;
    else
        return "none";
}

int getUserCommand(char *command, int num)
{
    printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
    char *r = fgets(command, num, stdin);
    if (r == NULL)
        return -1;
    command[strlen(command) - 1] = '\0';
    return strlen(command);
}

void commandSplit(char *in, char *out[])
{
    int argc = 0;
    out[argc++] = strtok(in, SEP);
    while (out[argc++] = strtok(NULL, SEP))
        ;
}

int execute(char *argv[])
{
    pid_t id = fork();
    if (id < 0)
    {
        return -1;
    }
    else if (id == 0)
    {
        execvp(argv[0], argv);
        exit(1);
    }
    else
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid > 0)
        {
            lastcode = WEXITSTATUS(status);
        }
    }
    return 0;
}

void cd(const char *path)
{
    chdir(path);
    char tmp[1024];
    getcwd(tmp, sizeof(tmp));
    sprintf(cwd, "PWD=%s", tmp);
    putenv(cwd);
}

int doBuildin(char *argv[])
{
    if (strcmp(argv[0], "cd") == 0)
    {
        char *path = NULL;
        if (argv[1] == NULL)
            path = ".";
        else
            path = argv[1];
        cd(path);
        return 1;
    }
    else if (strcmp(argv[0], "export") == 0)
    {
        if (argv[1] == NULL)
            return 1;
        strcpy(enval, argv[1]);
        putenv(enval); // ???
        return 1;
    }
    else if (strcmp(argv[0], "echo") == 0)
    {
        char *val = argv[1] + 1;
        if (strcmp(val, "?") == 0)
        {
            printf("%d\n", lastcode);
            lastcode = 0;
        }
        else
        {
            printf("%s\n", getenv(val));
        }
        return 1;
    }
    else if (0)
    {
    }

    return 0;
}

int main()
{
    while (1)
    {
        char usercommand[NUM];
        char *argv[SIZE];
        // 1. 打印提示符&&获取用户命令字符串获取成功
        int n = getUserCommand(usercommand, sizeof(usercommand));
        if (n <= 0)
            continue;
        // 2. 分割字符串
        // "ls -a -l" -> "ls" "-a" "-l"
        commandSplit(usercommand, argv);
        // 3. check build-in command
        n = doBuildin(argv);
        if (n)
            continue;
        // 4. 执行对应的命令
        execute(argv);
    }
}

通过这段代码,咱们就像是亲手搭建了一个简易的 Shell "积木小屋",虽然它还很简单,但却有着强大的潜力。希望大家都能从这个小小的 Shell 代码里,感受到编程的乐趣和神奇。

相关推荐
曼岛_6 分钟前
CentOS 7 强制升级Docker 24.x终极指南(解决MySQL8镜像兼容性问题)
linux·docker·centos
Richard20126 分钟前
Linux Command Recap
linux·前端
W说编程7 分钟前
《UNIX网络编程卷1:套接字联网API》第4章 基本TCP套接字编程
c语言·网络·网络协议·tcp/ip·架构·unix·tcp
Tadecanlan17 分钟前
[C++面试] C++中各类括号的差异:[]、{}、<>、()
开发语言·c++·面试
_GR26 分钟前
2023年蓝桥杯第十四届C&C++大学B组真题及代码
c语言·c++·蓝桥杯
努力努力再努力wz30 分钟前
【c++深入系列】:类和对象详解(下)
java·运维·c语言·开发语言·c++
BuHuaX34 分钟前
C#的反射机制
服务器·unity·c#·游戏引擎·游戏程序
对方正在长头发丿36 分钟前
P1162 填涂颜色(BFS)
数据结构·c++·算法·蓝桥杯·宽度优先
不爱学英文的码字机器1 小时前
隐私计算的崛起:数据安全的未来守护者
服务器·算法
菜就多练,以前是以前,现在是现在1 小时前
Codeforces Round 1000 (Div. 2)
数据结构·c++·算法