实现一个简易的shell

1、shell 的初步实现

在学习了进程创建、进程终止、进程等待以及进程程序替换系列进程控制相关知识后,我们就可以自己实现一个简易的 shell 命令行解释器了;实现一个简易的 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 OPT_NUM 64  //一个命令的最多选项

char lineCommand[NUM];
char* argv[OPT_NUM];

int main() {
    while(1) {

        //输出提示符
        printf("[用户名@主机名 当前路径]$ ");
        fflush(stdout);

        //获取输入
        char* ret = fgets(lineCommand, sizeof(lineCommand)-1, stdin);  //最后留一个位置来存放极端情况下的\0
        if( ret == NULL ) {
            perror("fgets");
            exit(1);
        }
        lineCommand[strlen(lineCommand) - 1] = '\0';  //消除命令行中最后的换行符

        //将输入解析为多个字符串存放到argv中,即字符串切割
        argv[0] = strtok(lineCommand, " ");
        int i = 1;
        while(argv[i++] = strtok(NULL, " "));

        //创建子进程
        pid_t id = fork();
        if(id == -1) {
            perror("fork");
            exit(1);
        } else if (id == 0) {  //子进程
            //进程程序替换
            int ret = execvp(argv[0], argv);
            if(ret == -1) {  
                printf("No such file or directory\n");
                exit(1);
            }
        } else {  //父进程
            //进程等待
            pid_t ret = waitpid(id, NULL, 0);
            if(ret == -1){
                perror("wait");
                exit(1);
            }
        } 
    }
    return 1;  //while循环正常情况下不会结束
}

2.功能完善:

2.1.ls颜色问题

ls 没有颜色功能,我们可以在 shell 对 ls 指令进行判断,然后手动为其加上 “–color=auto” 选项:

if(argv[0] != NULL && strcmp(argv[0], "ls") == 0)  //ls颜色显示
{
    argv[i++] = (char*)"--color=auto";
}

2.2.当前路径

当前路径我们运行上面的 myshell 可以发现,当我们 cd 更换路径后,pwd 命令还是显示原来的路径:

要理解并解决这个问题,我们首先要理解什么是当前路径:

可以看到,当 test 程序运行起来后,其在系统中一共有两个路径,其中 exe 路径是指 test 可执行程序在磁盘中的路径,而 cwd (current working directory) 则是指 当前进程的工作目录,它就是我们平时所说的 当前路径。

在 Linux 中,我们可以使用 chdir 系统调用来改变进程的工作目录:

问题:为什么我们的 shell 执行 cd 命令后目录不改变了?

原因

myshell 是通过创建子进程的方式去执行命令行中的各种指令的,也就是说,cd 命令是由子进程去执行的,那么自然被改变也是子进程的工作目录,父进程的工作目录不受影响;

而当我们使用 PWD 指令来查看当前路径时,cd 指令对应的子进程已经执行完毕退出了,此时 myshell 又会给 PWD 创建一个新的子进程,且这个子进程的工作目录和父进程 myshell 相同,所以 PWD 打印出来的路径不变。

解决

我们只需要对命令行传入的指令进行判断,如果是 cd 指令,就使用 chdir 将父进程的工作目录修改为指定的目录即可:

if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)  //cd改变父进程工作路径
{
    if(myargv[1] != NULL) 
        chdir(myargv[1]);  //myargv[1]中保存着指定路径
    continue;  //下面的语句不需要再执行,因为cd的目的已经达到了,直接读取下一条指令
}

2.3.内建命令

Linux 中的命令一共分为两种 – 内建命令和外部命令:

内建命令是 shell 程序的一部分,其功能实现在 bash 源代码中,不需要派生子进程来执行,也不需要借助外部程序文件来运行,而是由 shell 进程本身内部的逻辑来完成

外部命令则是通过创建子进程,然后进行进程程序替换,运行外部程序文件等方式来完成。

tip:我们上面对 cd 指令就是以内置命令的逻辑来处理的 – myshell 遇到 cd 命令时,由自己直接来改变进程工作目录,处理完毕直接 continue,而不会创建子进程;

2.4.echo命令

同时,我们发现 echo 命令也是一个内置命令,这其实也很好的解释了 为什么 “echo $变量” 可以查看本地变量以及为什么 “echo $?” 可以获取最近一个进程的退出码 了:

虽然本地变量只在当前进程有效,但是使用 echo 查看本地变量时,shell 并不会创建子进程,而是直接在当前进程中查找,自然可以找到本地变量;

shell 可以通过进程等待的方式获取上一个子进程的退出状态,然后将其保存在 ? 变量中,当命令行输入 “echo $?” 时,直接输出 ? 变量中的内容,然后将 ? 置为0 (echo 正常退出的退出码),也不需要创建子进程。

int EXIT_CODE;  //退出码 -- 全局变量

if(argv[0] != NULL && strcmp(argv[0], "echo") == 0)  //处理echo内建命令
{
    if(strcmp(argv[1], "$?") == 0){  //echo $?
        printf("%d\n", EXIT_CODE);
        EXIT_CODE = 0;
    } else {  //echo $变量
        printf("%s\n", argv[1]+1);
    }
    continue;
}

//fork后面的内容
} else {  //父进程
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);   //进程等待
    EXIT_CODE = (status >> 8) & 0xFF;   //获取退出码
    if(ret == -1){
        perror("wait");
        exit(1);
    }
} 


3.终极具体代码实现:

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

#define NUM 1024    //一个命令的最大长度
#define OPT_NUM 64  //一个命令的做多选项

char lineCommand[NUM];
char* argv[OPT_NUM];  
int EXIT_CODE;  //保存进程退出码

int main() {
    while(1) {

        //输出提示符
        printf("[用户名@主机名 当前路径]$ ");
        fflush(stdout);

        //获取输入
        char* ret = fgets(lineCommand, sizeof(lineCommand)-1, stdin);  //最后留一个位置来存放极端情况下的\0
        if( ret == NULL ) {
            perror("fgets");
            exit(1);
        }
        lineCommand[strlen(lineCommand) - 1] = '\0';  //消除命令行中最后的换行符

        //将输入解析为多个字符串存放到argv中,即字符串切割
        argv[0] = strtok(lineCommand, " ");
        int i = 1;

        if(argv[0] != NULL && strcmp(argv[0], "ls") == 0)  //ls颜色显示
        {
            argv[i++] = (char*)"--color=auto";
        }

        while(argv[i++] = strtok(NULL, " "));

        if(argv[0] != NULL && strcmp(argv[0], "cd") == 0)  //cd改变父进程工作路径
        {
            if(argv[1] != NULL) 
                chdir(argv[1]);  //myargv[1]中保存着指定路径
            continue;
        }
        
        if(argv[0] != NULL && strcmp(argv[0], "echo") == 0)  //处理echo内建命令
        {
            if(strcmp(argv[1], "$?") == 0){  //echo $?
                printf("%d\n", EXIT_CODE);
                EXIT_CODE = 0;
            } else {  //echo $变量
                printf("%s\n", argv[1]+1);
            }
            continue;
        }

        //创建子进程
        pid_t id = fork();
        if(id == -1) {
            perror("fork");
            exit(1);
        } else if (id == 0) {  //子进程
            int ret = execvp(argv[0], argv);  //进程程序替换
            if(ret == -1) {  
                printf("No such file or directory\n");
                exit(1);
            }
        } else {  //父进程
            int status = 0;
            pid_t ret = waitpid(id, &status, 0);  //进程等待
            EXIT_CODE = (status >> 8) & 0xFF;  //获取退出状态
            if(ret == -1){
                perror("wait");
                exit(1);
            }
        } 
    }
    return 1;  //while循环正常情况下不会结束
}

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值