Linux 自主 shell 编写(C 语言实现)

效果

效果嘛和 命令行解释器 一模一样,这里就不贴图了

只是把 # (超管)$ (普通用户) 符号改为 > 以作区分

注意哦: 删除键不能直接使用,要配合 ctrl 键才行

主要步骤

打印命令行提示符

在 Linux 终端(命令行)里,首先看到的是 命令行提示符

[exercise@localhost my_shell]$ 

shell 一旦跑起来,定是要先打印 命令行提示符 的,但是这玩意对于不同的用户是不一样的呀,所以不能单纯的打印出来,而是要获取用户名,主机名等等,如何获取?目前来说对各种 系统接口还不熟,那就直接使用 环境变量

命令行执行 env 命令,就可以看到很多 环境变量 ^ ^

系统环境变量 很多,不容易直接得到想要的,所以可以使用库函数 getenv 来获取,需要包含头文件 #include <stdlib.h> ,函数原型如下:

char *getenv(const char *name);

那么 用户名主机名工作目录 分别在 USERHOSTNAMEPWD 内,直接使用 getenv 函数获取即可

最后使用 snprintf() 函数拼接成 命令行提示符 的格式即可,函数原型:

int snprintf(char *str, size_t size, const char *format, ...);

获取用户命令字符串

C 语言 获取键盘字符串 可以使用库函数 scanf() ,但它遇到空格可就不继续读取了,而它的高端玩法还不熟

咱就老老实实使用 fgets 函数,原型:

char *fgets(char *s, int size, FILE *stream);

切割用户命令字符串

这一步是必要的,因为日后一定是需要 进程替换 的,进程替换 就一定需要将用户命令以空格为分隔符打散分开,是库函数参数的原因,是刚需

如何实现呢?倒是也很简单,我们可以直接将空格替换为 '\0' ,那么一个长串就变为若干个子串

如果要执行用户输入的命令,是要创建子进程来完成的;那我们就需要为进程传递 命令行参数 来实现,毕竟不同的选项具有不同的功能,所以切割的字串分别放入 命令行参数表 argv[] 里即可,argv 的每一个元素都是一个指针,指向被切完成的子串(最后一个指针为 NULL

那么只需要将 argv 的第一个元素指向第一个子串,第二个元素指向第二个子串,以此类推

但这比较麻烦,咱可以使用库函数 strtok() 完成; 命令行参数表 也可以设置为全局的,好调用

执行命令

获取用户的命令后,不执行等啥呢?

当然啦,执行命令不是自己当前进程来执行,而是 创建子进程,在利用 进程替换,此时子进程就可以执行你想要的全新的代码

循环

一个 shell 怎么能只运行一条命令呢?所以我们需要将上述过程循环起来,这样就能无限制运行命令

至此,简易到不能再简易的 shell 就实现好了

至此源码(简易半成品)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32

char* gArgv[NUM];

const char* getUserName()
{
    const char* username = getenv("USER");
    if (username == NULL) return "None";
    return username;
}

const char* getHostName()
{
    const char* hostname = getenv("HOSTNAME");
    if (hostname == NULL) return "None";
    return hostname;
}

// 临时
const char* getCwd()
{
    const char* cwd = getenv("PWD");
    if (cwd == NULL) return "None";
    return cwd;
}

void MakeCommandLineAndPrint()
{
    char line[SIZE];
    const char* username = getUserName();
    const char* hostname = getHostName();
    const char* cwd = getCwd();
    snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, cwd);
    printf("%s", line);
    fflush(stdout);
}

int GetUserCommand(char command[], size_t size)
{
    char* s = fgets(command, size, stdin);
    if (s == NULL) return -1;
    command[strlen(command) - 1] = ZERO;
    return strlen(command);
}


void SplitCommand(char command[], int size)
{
    gArgv[0] = strtok(command, SEP);
    int index = 1;
    while ((gArgv[index++] = strtok(NULL, SEP)));

}

void Die()
{
    exit(1);
}

void ExecuteCommmand()
{
    pid_t id = fork();
    if (id < 0) Die();
    else if (id == 0)
    {
        // child
        execvp(gArgv[0], gArgv);
        exit(errno);
    }
    else 
    {
        // parent
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid > 0)
        {}
    }
}

int main()
{
    int quit = 0;
    while (!quit)
    {
        // 自己需要输出一个命令行
        MakeCommandLineAndPrint();

        // 获取用户命令字符串
        char usercommand[SIZE];
        int num = GetUserCommand(usercommand, sizeof(usercommand));
        if (num < 0) return 1;

        // 分割用户命令字符串
        SplitCommand(usercommand, sizeof(usercommand));

        // 执行命令
        ExecuteCommmand();
    }
    return 0;
}

细节

上面的代码虽然说可以运行,但有很多漏洞和细节尚未修补实现,接下来一一填补:

内建命令问题

cd

举个例子吧,上面的代码先跑起来,不说其他的,试试 cd 命令能不能正常运行

不能正常运行!!! 这个的漏洞不是一般的大,并不是不能使用 cd 命令,而是命令 cd 对咱这个 shell 不起任何作用

而如果你们运行上面的残本代码,会发现当前的工作路径是一串绝对路径,要想切割最后一个目录拿过来倒也容易,但 cd 还是无法生效啊

为什么?

其实很简单,我们是实现 shell 的方法是 创建子进程,然后拿想要的进程去替换这个子进程;由于进程的独立性,子进程会影响父进程吗?肯定不会,那子进程执行 cd 命令和你父进程有什么关系呢?子进程执行 cd 命令的时候父进程在干嘛?在那 wait 呢!!!

所以这样实现父进程 shell 的工作路径改不了的,那如何能改?当然是父进程自己执行咯

所以像 cd 这样的命令是 内建命令

既如此,观察上述代码,在执行命令之前 需要检查是否有 内建命令

如何检查?

直接判断不就行了,它有几个 内建命令,咱就判断几次,如果用户输入的是 cd 命令,shell 就自己执行

如何执行?这种涉及系统的东西当然要 系统调用 嘛,chdir 可以将当前进程的工作路径,切换至你想要的路径,那咱们就可以直接 将用户输入的路径 传进 chdir 的参数里即可

注意如果直接运行 cd 命令,是返回用户家目录的;所以如果切割后的子串只有 cd ,第二个元素路径为 NULL 的话,可直接返回 用户家目录(可函数实现)

改完之后记得要修改 shell 下一次打印出来的命令行路径,因为这是被我封装为函数的,直接修改较为麻烦,但我是从环境变量里获取的,所以直接修改环境变量即可:
首先使用函数 getcwd ,此函数可以直接获取真正的工作路径,然后拼接 PWD ,再使用函数 putenv() 来刷新环境变量

// 内建命令 cd 的执行过程
void Cd()
{
    // 获取 cd 路径
    const char* path = gArgv[1];
    if (path == NULL) path = getHome();

    // 此时 path 一定存在,那么可以直接使用 系统调用 修改工作路径
    chdir(path);
 
    // 获取此时的工作路径
    char temp[SIZE * 2];
    getcwd(temp, sizeof(temp));

    //  拼接 PWD 环境变量
    snprintf(Cwd, sizeof(Cwd), "PWD=%s", temp);

    // 刷新环境变量
    putenv(Cwd);
}

// 检查是否有内建命令
int CheckBuildIn()
{
    int yes = 0;
    const char* enter_cmd = gArgv[0];
    if (strcmp(enter_cmd, "cd") == 0)
    {
        yes = 1;
        Cd();
    }
    // 继续判断其他内建命令...
    return yes;
}

至此,最大的坑已经被补上了,至于命令行解释器里,当前工作目录的切割,使用宏函数可直接实现(后附完整源码),这里不做解释

// 宏函数
#define SkipPath(pCwd) do { pCwd += (strlen(pCwd) - 1); while (*pCwd != '/') --pCwd; } while (0)

退出码问题

父进程是一定要得到子进程的退出码的,不然有问题无法准确反馈给用户

具体实现也是进程替换的内容,非常简单,看源码

echo 查看退出码

当然是下面这个命令啦:

echo $?

和上面 cd 命令一样,需要在 CheckBuildIn 函数里进行判断是否有 echo $? 命令,逻辑编写十分简单,在 CheckBuildIn 函数里编写即可:

else if (strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
{
    yes = 1;
    printf("%d\n", lastcode);
    lastcode = 0;
}

完整源码

CentOS 7.9 平台 gcc 编译测试,进入 可执行文件 MyShell 所在目录下, ./MyShell 即可运行

makefile

bin=MyShell
src=myshell.c

$(bin):$(src)
	gcc $^ -o $@
.PHONY:clean
clean:
	rm -f $(bin)

myshell.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(pCwd) do { pCwd += (strlen(pCwd) - 1); while (*pCwd != '/') --pCwd; } while (0)

char* gArgv[NUM];
char Cwd[SIZE];
int lastcode = 0;

const char* getHome()
{
    const char* home = getenv("HOME");
    if (home == NULL) return "/";
    return home;
}

const char* getUserName()
{
    const char* username = getenv("USER");
    if (username == NULL) return "None";
    return username;
}

const char* getHostName()
{
    const char* hostname = getenv("HOSTNAME");
    if (hostname == NULL) return "None";
    return hostname;
}

// 临时
const char* getCwd()
{
    const char* cwd = getenv("PWD");
    if (cwd == NULL) return "None";
    return cwd;
}

void MakeCommandLineAndPrint()
{
    char line[SIZE];
    const char* username = getUserName();
    const char* hostname = getHostName();
    const char* cwd = getCwd();
    SkipPath(cwd);
    snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : (cwd + 1));
    printf("%s", line);
    fflush(stdout);
}

int GetUserCommand(char command[], size_t size)
{
    char* s = fgets(command, size, stdin);
    if (s == NULL) return -1;
    command[strlen(command) - 1] = ZERO;
    return strlen(command);
}


void SplitCommand(char command[], int size)
{
    (void)size;
    gArgv[0] = strtok(command, SEP);
    int index = 1;
    while ((gArgv[index++] = strtok(NULL, SEP)));

}

void Die()
{
    exit(1);
}

void ExecuteCommmand()
{
    pid_t id = fork();
    if (id < 0) Die();
    else if (id == 0)
    {
        // child
        execvp(gArgv[0], gArgv);
        exit(errno);
    }
    else 
    {
        // parent
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid > 0)
        {
            lastcode = WEXITSTATUS(status);
            if (lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
        }

    }
}

void Cd()
{
    // 获取 cd 路径
    const char* path = gArgv[1];
    if (path == NULL) path = getHome();

    // 此时 path 一定存在,那么可以直接使用 系统调用 修改工作路径
    chdir(path);
 
    // 获取此时的工作路径
    char temp[SIZE * 2];
    getcwd(temp, sizeof(temp));

    //  拼接 PWD 环境变量
    snprintf(Cwd, sizeof(Cwd), "PWD=%s", temp);

    // 刷新环境变量
    putenv(Cwd);
}

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;
}

int main()
{
    int quit = 0;
    while (!quit)
    {
        // 自己需要输出一个命令行
        MakeCommandLineAndPrint();

        // 获取用户命令字符串
        char usercommand[SIZE];
        int num = GetUserCommand(usercommand, sizeof(usercommand));
        if (num < 0) return 1;
        else if (num == 0) continue;

        // 分割用户命令字符串
        SplitCommand(usercommand, sizeof(usercommand));

        // 检查命令是否为内建命令
        num = CheckBuildIn();
        if (num) continue;

        // 执行命令
        ExecuteCommmand();
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值