【Linux 学习计划】-- 简易版shell编写

发布于:2025-06-07 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

思路

创建自己的命令行

获取用户命令

分割命令

检查是否是内建命令

cd命令实现

进程程序替换执行程序

总代码

结语


思路

int main()
{
    while (1)
    {
        // 1. 自己的命令行
        PrintCommandLine();

        // 2. 获取用户命令
        char command[SIZE];
        int n = GetUserCommand(command, sizeof(command));
        if (n <= 0)
            return 1;

        // 3. 分割指令
        SplitCommand(command);

        // for(int i = 0; gArgv[i]; i++)
        //     printf("[%d]: %s\n", i, gArgv[i]);

        // 4. 检查是否是内建命令
        n = CheckBuildin();
        if(n) continue;

        // 5. 进程程序替换执行命令
        ExecuteCommand();
    }

    return 0;
}

上图中的五个函数,就是我们的思路

首先就是先写命令行:

就是这个,这个获取环境变量 + 指针操作 + 打印即可,较为简单

接着就是需要获取用户输入的指令,并且将其分割,放进数组里面,这里会涉及到strtok函数的使用

最后将我们的函数分割完之后,就是判断是否是内建命令了

如果是内建命令的话,这里就只实现cd和echo $?,我们就单独函数实现

如果不是内建命令的话,我们就正常进行程序替换即可

这里最关键的点就是程序替换了,因为我们要使用的所有的命令都在磁盘上保存着,这时我们fork创建子进程,并用程序替换将磁盘上待替换的程序加载到内存中并将这个子进程给覆盖,最终跑起来

创建自己的命令行

#define SkipPath(p)         \
    do                      \
    {                       \
        p += strlen(p) - 1; \
        while (*p != '/')   \
            p--;            \
    } while (0)

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


const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    return hostname;
}

const char *GetCwd()
{
    const char *cwd = getenv("PWD");
    return cwd;
}

const char *GetUserName()
{
    const char *name = getenv("USER");
    return name;
}

void PrintCommandLine()
{
    char line[SIZE];
    const char *UserName = GetUserName();
    const char *Cwd = GetCwd();
    const char *HostName = GetHostName();

    SkipPath(Cwd);
    snprintf(line, sizeof line, "[%s@%s %s]:>", UserName, HostName, strlen(Cwd) == 1 ? "/" : Cwd + 1);

    printf("%s", line);
    fflush(stdout);
}

这里没什么好说的,主要就是getenv获取环境变量,我们的参数就是从环境变量中来

需要讲的一点就是do...while(0)那个宏,这里我们如果直接获取PWD的话,就是所有都打出来:

这个是效果

但是整体是这样的

所以我们需要将指针移动到最后面那里,然后只显示最后一段

但是这里为什么用的是宏呢?因为这里是用 c 语言实现的,写一个函数的话还要二级指针,太麻烦了,宏方便点

而使用do...while(0)是为了形成一个代码块,我们可以整体使用,并且在下面调用的时候可以随便加 “ ; ”,这样调用起来就和函数一样了

最终效果如下:

获取用户命令

int GetUserCommand(char command[], size_t n)
{
    char *s = fgets(command, n, stdin);
    if (s == NULL)
        return 2;

    // 处理 \n
    command[strlen(command) - 1] = ZERO;

    // printf("命令: %s", command);
    return strlen(command);
}

其实如果是cpp的话,就直接getline了,但是c语言实现的话我们就用 fgets 获取一行

直接获取即可,完了之后放进提前开辟并传进函数里面的数组中

需要说的一点就是,我们用户输入的指令看起来是这样的:

ls -a -l

但其实最后还会加一个回车表示确定,所以实际上就是这样子的:

ls -a -l\n

综上,我们就需要将最后一个位置设置为\0,上面的ZERO其实就是\0,只不过设置成了宏而已

分割命令

void SplitCommand(char *command)
{
    gArgv[0] = strtok(command, SEP);
    int index = 1;
    while (gArgv[index++] = strtok(NULL, SEP))
        ;
    // 在最后一次判断的时候,发现没有字符串了,strtok 就会返回NULL
    // 正好让 gArgv 的最后一个位置变为 NULL,这样在后面进程程序替换的时候就可以直接用
}

这里其实就是strtok函数的使用技巧

先说说为什么要分割:

我们获取了程序之后,不是说所有的功能都是我们自己写,像ls、cd、mkdir等等,这些指令在我们电脑中的磁盘上人家都帮我们写好了,shell也是调用的这些文件

所以我们要做的,就是在用户输入指令的时候,我们能将这些程序加载进来

这里就需要用到进程程序替换了,并且我们现在只有用户输入的数据,我们到后面肯定是execvp用起来最好,所以分割完直接放函数里面当参数即可,就实现这个功能了

再来说说 strtok

当你第一次使用strtok的时候,传入一个数组当参数(第一个参数位置,第二个是分隔符)

当你从第二次开始,第一个参数直接传NULL,就代表使用你一刚开始用个的那个数组

这也就是为什么一开始传command数组,后面传NULL的原因

接着最妙的就是,当strtok检测到没有可以分割的了,就会返回NULL,而我们的数组(execvp函数要求)最后一个位置必须以NULL结尾

检查是否是内建命令

int CheckBuildin()
{
    int yes = 0;
    const char *enter_cmd = gArgv[0];
    if(strcmp(enter_cmd, "cd") == 0)
    {
        yes = 1;
        Cd();
    }
    else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
    {
        yes = 1;
        printf("%d\n", lastcode);
        lastcode = 0;
    }
    return yes;
}

直接就是if、else检查判断就行,有多少个内建命令就有多少个if、else

cd命令实现

void Cd()
{
    const char* path = gArgv[1];
    if(path == NULL) path = GetHome();

    chdir(path);

    // 更新环境变量
    
    char cwd[SIZE];
    char temp[SIZE];
    getcwd(temp, sizeof temp);
    snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
    setenv("PWD", cwd, 1);
}

这里主要用到的是chdir函数,ps:chdir支持直接使用 . 或者 ..

先说获取path,程序走到这里,那么用户输入的指令一定是cd,那么就会有像cd ..这样的指令

那么我们获取完之后,分割的指令中,第二个就一定是path,直接获取就好(下标是1)

当然也有可能用户直接输入一个cd,后面啥也不跟,我们随便处理一下,给他返回到家目录即可

最后我们在chdir之后,还需要更新一下路径

这里我们先获取环境变量中的PWD,然后使用snprintf写入数组中,以PWD:%s的格式

最后直接用setenv函数修改(或者说更新)环境变量即可

当然你也可以在最后使用putenv,这样的话,我们的cwd数组就需要开辟在全局,不然函数运行结束之后,cwd数组也就没了,这样putenv找不到他,就会直接异常终止我们的程序,显示段错误(Segmentation fault)

setenv就不用,这里推荐这个,因为他更现代、好用

进程程序替换执行程序

void ExecuteCommand()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        execvp(gArgv[0], gArgv);
        exit(errno);
    }
    else
    {
        // 父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid > 0)
        {
            if(WIFEXITED(status) == 0)
            {
                lastcode = WEXITSTATUS(status);
                printf("exit code: %d, exit signal:%d\n", WEXITSTATUS(status), WTERMSIG(status));
            }
        }
    }
}

就是execvp函数的使用,创建子进程直接替换,然后父进程在外面waitpid阻塞等待即可,也不用父进程干什么事情

总代码

MyShell.c

点开这个链接,里面就是代码,放在gitee里面了

结语

这篇文章到这里就结束啦!!~( ̄▽ ̄)~*

如果觉得对你有帮助的,可以多多关注一下喔


网站公告

今日签到

点亮在社区的每一天
去签到