手把手带你手撕一个shell

简介: 手把手带你手撕一个shell

什么是shell?

       Shell是一种应用程序,它连接了用户和Linux内核,让用户能够更加高效、安全、低成本地使用Linux内核。 Shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。 它接收用户输入的命令并把它送入内核去执行。Shell并不是内核的一部分,而是一个建立在内核基础上的应用程序,与QQ、迅雷、Firefox等其它软件类似。


       说大白话:他就是一个进程!作为一个进程,他当然是可以实现的啦,那么我们就简简单单手撕一个简易的进程吧!


怎么实现shell?

shell命令提示符的实现

       Shell提示符是Linux系统中的一种表示形式,它出现在用户登录并启动终端模拟包或从Linux控制台登录后。它是用户与Shell进行交互的重要途径,提示符就象征着通往Shell的大门,用户可以在提示符处输入Shell命令。对于普通用户而言,Base shell的默认提示符就是一个美元符号"$",表示等待用户输入命令。如下:

[amazon@iZ7xvfrafhk3mf5qwrf2gxZ myshell]$

 要实现这个命令提示符,我们需要获取当前的用户、当前主机以及当前路径,因此根据之前所学的知识,写出以下的函数获取对应的数据:


      对于getenv()的回忆—主要用于搜索和返回环境变量的值。这个函数的参数是环境变量的名称,如果对应的环境变量存在,那么getenv函数就会返回一个指向该环境变量值的指针。


//获取用户名
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";
}

在得到这些数据后我们就可以组装出一个简易的命令行提示符了!再接收命令行输入的命令就可以完成基本的命令输入。需要注意的是:这里使用fgets是为了将空格也接收进来为什么return strlen(command)呢?这是判断是否有命令输入,如果没有输入,即只是传了一个回车,那么会返回一个0,后续主函数中会用于接收,通过continue跳过后续的函数(总体是一个死循环)。

//获取用户输入命令
int getUserCommand(char* command, int num)
{
    printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
    char* r = fgets(command, num, stdin); // 最终你还是会输入\n
    if (r == NULL) return -1;
    // "abcd\n" "\n"
    command[strlen(command) - 1] = '\0'; // 为了将末尾的\n去掉,注意这样并不会越界 ,因为只要是输入都至少会有个\n
    return strlen(command);
}

创建子进程执行命令

       从上面的程序我们可知,存储的命令是整段的,对此我们需要进行命令的分割,用strtok按空格进行分割。从前面的知识中我们也知道:shell是创建子进程来让他执行任务的!对此,我们也创建一子进程,通过execvp的程序替换来执行对应的命令。而父进程需要执行的任务是waitpid等待子进程完成任务后储存对应的返回值,用于子进程任务完成怎么样的判断,对此shell的基本功能实现就完成了。但是还是需要改进的!

//按照空格分割命令
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) //child
    {
        //进程替换
        execvp(argv[0], argv); // cd ..
        exit(1);
    }
    else // father
    {
        int status = 0;//存储返回值,echo $?
        pid_t rid = waitpid(id, &status, 0);
        if (rid > 0) {
            lastcode = WEXITSTATUS(status);//存储返回值,echo $?
        }
    }
    return 0;
}

内建命令的引入

  由上图我们可知,当我们使用ls、pwd等等命令时自定义的shell是能够正常运行的,但是对于cd命令还有export是不能运行的。回过头想想,我们是怎么实现shell的呢?我们运用了进程的替换,我们替换了子进程中的进程而上面的shell中并没有实现!对此我们可以肯定的是:这些命令并不是外部的进程!这也引出了—内建命令。内建命令,如其名称所示,是由Shell自身提供的命令。这些命令已经和shell编译为一体,不需要借助外部程序文件来运行。这意味着它们执行速度更快,效率更高。通俗的讲,内建命令实际上就是shell中的一个函数

内建命令的实现

       由于内建命令已经和shell编译为一体,不需要借助外部程序文件来运行,因此我们需要一一的实现对应的内建命令,这里就先实现三个,如果要接着实现可以接着else if 实现下去。详细解释见代码:

//获取家目录
char* homepath()
{
    char* home = getenv("HOME");
    if (home) return home;
    else return (char*)".";
}
void cd(const char* path)//用于cd命令
{
    chdir(path);//用于改变当前工作目录
    char tmp[1024];//临时储存,但不能直接使用,需要全局变量,否则栈帧销毁也会销毁
    getcwd(tmp, sizeof(tmp));//获取当前工作目录的绝对路径
    sprintf(cwd, "PWD=%s", tmp); //输出到全局变量中,保证不会失效
    putenv(cwd);//改变或增加环境变量的内容
}
// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char* argv[])
{
    if (strcmp(argv[0], "cd") == 0)//通过改变当前环境变量的内容从而改变->命令行提示符路径
    {
        char* path = NULL;
        if (argv[1] == NULL) path = homepath();//cd 回到家目录
        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)
    {
        if (argv[1] == NULL) {//echo 输出换行
            printf("\n");
            return 1;
        }
        if (*(argv[1]) == '$' && strlen(argv[1]) > 1) {//根据指令输出
            char* val = argv[1] + 1; // $PATH $?
            if (strcmp(val, "?") == 0)
            {
                printf("%d\n", lastcode);
                lastcode = 0;
            }
            else {
                const char* enval = getenv(val);
                if (enval) printf("%s\n", enval);
                else printf("\n");
            }
            return 1;
        }
        else {//只是输出到屏幕
            printf("%s\n", argv[1]);
            return 1;
        }
    }
    else if (0) {}//接下来的内建命令
    return 0;
}

shell的拼接

       shell在Linux中是一直运行的,因此为一个死循环!,我们定义一个usercommand字符数组用于接收储存从命令行收到的初步命令,定义一个字符串数组来接收通过commandSplit分割后的字符串,接下来区分是否为内建命令,按照argv里面的命令进行执行程序替换或者执行内建命令。

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的总体代码

#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]; // for test
int lastcode = 0;
//获取家目录
char* homepath()
{
    char* home = getenv("HOME");
    if (home) return home;
    else return (char*)".";
}
//获取用户名
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); // 最终你还是会输入\n
    if (r == NULL) return -1;
    // "abcd\n" "\n"
    command[strlen(command) - 1] = '\0'; // 为了将末尾的\n去掉,注意这样并不会越界 ,因为只要是输入都至少会有个\n
    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) //child
    {
        //进程替换
        execvp(argv[0], argv); // cd ..
        exit(1);
    }
    else // father
    {
        int status = 0;//存储返回值,echo $?
        pid_t rid = waitpid(id, &status, 0);
        if (rid > 0) {
            lastcode = WEXITSTATUS(status);//存储返回值,echo $?
        }
    }
    return 0;
}
void cd(const char* path)
{
    chdir(path);//用于改变当前工作目录
    char tmp[1024];//临时储存,但不能直接使用,需要全局变量,否则栈帧销毁也会销毁
    getcwd(tmp, sizeof(tmp));//获取当前工作目录的绝对路径
    sprintf(cwd, "PWD=%s", tmp); //输出到全局变量中,保证不会失效
    putenv(cwd);//改变或增加环境变量的内容
}
// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char* argv[])
{
    if (strcmp(argv[0], "cd") == 0)
    {
        char* path = NULL;
        if (argv[1] == NULL) path = homepath();
        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)
    {
        if (argv[1] == NULL) {
            printf("\n");
            return 1;
        }
        if (*(argv[1]) == '$' && strlen(argv[1]) > 1) {
            char* val = argv[1] + 1; // $PATH $?
            if (strcmp(val, "?") == 0)
            {
                printf("%d\n", lastcode);
                lastcode = 0;
            }
            else {
                const char* enval = getenv(val);
                if (enval) printf("%s\n", enval);
                else printf("\n");
            }
            return 1;
        }
        else {
            printf("%s\n", argv[1]);
            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);
    }
}

                       感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!


相关文章
|
JavaScript 前端开发 Shell
用shell脚本写一个坦克大战的游戏的思路
用shell脚本写一个坦克大战的游戏思路
314 1
|
Python
送给她最最浪漫的表白(Python代码实现)
送给她最最浪漫的表白(Python代码实现)
142 0
|
6月前
|
安全 Linux 应用服务中间件
简简单单之Linux命令入门
简简单单之Linux命令入门
|
7月前
|
存储 Shell 程序员
第一次使用脚本后想学点东西
第一次使用脚本后想学点东西
43 0
|
数据采集 XML 人工智能
|
数据采集 运维 搜索推荐
|
开发框架 安全 前端开发
实战 | 记一次本升砖某学校拿shell的回忆录
实战 | 记一次本升砖某学校拿shell的回忆录
155 0
|
域名解析 Linux 编译器
几个好玩的Linux命令
几个好玩的Linux命令
374 0
几个好玩的Linux命令
|
Python
Python经典编程习题100例:第20例:落体反弹问题
Python经典编程习题100例:第20例:落体反弹问题
109 0