项目《基于Linux下的mybash命令解释器》(一)

一、bash的定义

        大家还记得小编曾经在哪一篇文章里介绍过bash呢?

        小编带领大家回顾一下,在《Linux(九)fork复制进程与写时拷贝技术》中,小编曾经介绍过shell。

        在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(command interpreter,命令解析器)。它类似于DOS下的COMMAND.COM和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。

        我们就是通过命令解释器(称为shell)(bash是命令解释器中的一种)和内核和系统进行交互的
(Windows通过图形界面进行交互的);例如我们把Is交给bash,bash帮我们运行Is,然后把结果给用
户;

        总结一下,这个项目是mybash(也可以叫myshell),目的是打造自己的命令解释器。目前我们Linux的系统默认的命令解释器是bash;命令解释器(也称为命令行解释器或shell)是计算机操作系统中的一个重要组件,它负责接收用户输入的命令,并解释和执行这些命令;其实命令解释器就是解析命令,执行命令,输出反馈。 

二、命令的分类

        我们在学习上一篇文章中的exec替换系列时,曾完成过“execl结合fork创建ps命令”的程序。在那里,我们总结了创建新进程是先fork后exec。那大家是否有想过,是所有的命令都可以用fork+execl完成吗?

        为了回答上面的问题,小编要和大家介绍一个新知识——内置命令和外置命令(普通命令)。

        内置命令是 Shell 程序的一部分,由Shell 自身提供。执行时,Shell 直接在内部调用相关函数来处理,无需创建新的进程。这使得它们执行速度快,能快速响应。例如,cd  exit 。

        外置命令是独立于 Shell 的可执行文件,通常存放在/usr/bin目录下(bin用于存放二进制可执行文件)。当执行外置命令时,系统会创建一个新的子进程来运行该命令。例如,ls  pwd  cp  ps等。

        在这里,小编也向大家分享一个区分内置和普通命令的方法——使用which查找。

        

        通过上面的图片,大家可以发现,能够使用which找到的都是外置(普通)命令,而内置命令是用which找不到的。

        而我们今天要模仿的对象bash也是一个可执行程序,使用which bash可以找到。

        简单来讲,就是普通命令是通过fork+exec实现的;而内置命令是Bash自身通过调用相应接口实现的。

三、项目框架

        下面,小编向大家介绍一下项目框架。

        大家可以和小编一起分析上面这张图,图片就是我们刚刚打开终端的样子。大家在使用过程中会发现,在终端界面只有使用exit才能退出,因此在框架中有了while(1)这个循环。

        接下来就是用户名,主机名,当前位置,用户角色这一行,我们直接使用printf打印,先将这一行定死,后面在完成细节时进行改进。

        然后创建了一个缓冲区buff,用于存放用户从键盘输入的命令。

        命令保存到缓冲区后,我们需要分割命令;这里就需要使用myargv和strtok 。strtok这个函数小编会在下面详细介绍。

        最后我们使用if else语句完成内置和普通命令的实现。在这里,小编也向大家分享一下使用if-else语句区分内置外置的原因。内置命令,大家可以理解为内部执行,也就是只能父进程执行,子进程无法替代,因为不同的进程执行结果不一样。外置(普通)命令,其实就是通过fork创建一个子进程,然后通过子进程完成,因为对于外置命令来说,父子进程执行的结果是一样的,所以谁执行都可以;也可以放在内部执行,但是这样就会很复杂。

四、strtok的介绍

        按照小编一直以来的方法,学习新的内容时要使用帮助手册。

        man strtok

注意:strtok线程不安全,原因就是函数实现使用了一个static的变量(指针记录下次分割的地址,再次调用要沿用上次的,所以需要静态变量)。在多线程中,如果两个线程都使用了strtok的话,这个变量的值就会被另一个线程不定期的进行修改。

五、各模块内容

//第一步
//大框架
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>

#define ARG_MAX 10//防止参数不够,可以做到一改全改

int main()
{
    while(1)
    {
        printf("stu@localhost  ~$");//用户和路径先写定,后期会补充判断
        fflush(stdout);//刷新缓冲区

        char buff[128]={0};
        fgets(buff,128,stdin);

        char *myargv[ARG_MAX] = {0};

        char *cmd = get_cmd(buff,myargv);//提取buff里面的命令
        if(cmd == NULL)
        {
            continue;
        }
        else if(strcmp(cmd,"cd")==0){
            //内置命令
        }
        else if(strcmp(cmd,"exit")==0){
            break;//若为exit,则退出内部
            //exit(0);//可以,但不好
        }
        else{
            //普通命令
            //fork + exec
        }
    }
    exit(0);
}
//第二步
//完成get_cmd函数(也就是分割命令)
char *get_cmd(char *buff,char *myargv[]){
    if(buff == NULL||myargv == NULL){
        return NULL;//说明用户没有输入命令
    }   
    char *s = strtok(buff," ");//第一次调用strtok
    while(s!=NULL){//不确定用户输入的命令需要分割几次,只要分割到字符串末尾为'\0',就会结束
        myargv[i++] = s;//保存分割的结果
        s = strtok(NULL," ");//后续调用不用再传buff,因为内部有指针会记录
    }   
    return myargv[0];
}
//处理了exit和exit\n不匹配的问题
//第三步,实现普通命令
void run_cmd(char *path,char *myargv[]){//这个函数的作用是当输入命令为普通命令时,创建子进程执行
    if(path == NULL || myargv == NULL)
        return;
    pid_t pid = fork();
    if(pid == -1)
        return;
    //下面我们要让父子进程做不一样的事情
    if(pid == 0)//子进程进入if
    {
        execvp(path,myargv);
        //这里选择替换系列时,选带v,这样就可以传数组,选带p的,这样就不需要传环境变量
        perror("execvp error!\n");//一定要记得打印错误信息,这样方便我们明确的知道execvp是否执行成功
        exit(0);
    }
    else//父进程进入esle
    {
    //这里要记得处理将死进程
        wait(NULL);
    }
}

有小伙伴可能会注意到,在if语句中最后写了exit(0);那不写这个可不可以呢?那就回出现第二张图片的情况。

通过下面这张图片帮助大家更好的了解替换的过程。

在这里,我们简单运行一下程序,就可以发现我们自己完成运行的mybash的父进程是bash,子进程是执行的命令。

//第四步,将一开始定死的提示信息进行修改完善
void printf_info(){//打印提示信息,获取用户名,主机名,当前位置,获取用户角色(普通用户还是管理员)
    char *user_str="$";//默认为普通用户
    int user_id = getuid();
    if(user_id == 0){//当uid=0时为root,将$改为#
        user_str = "#";
    }
    struct passwd * ptr = getpwuid(user_id);//得到用户名
    if(ptr == NULL)//如果为NULL,也就是出现问题了,那么打印bash的版本号
    {
        printf("mybash1.0>> ");
        fflush(stdout);
        return;
    }
    //获取主机名
    char hostname[128] = {0};
    if(gethostname(hostname,128)==-1){//如果获取主机名失败,打印版本号
        printf("mybash1.0>> ");
        fflush(stdout);
        return;
    }
    //获取主机路径
    char dir[256] = {0};
    if(getcwd(dir,256)==NULL){//如果获取路径失败,打印版本号
        printf("mybash1.0>> ");
        fflush(stdout);
        return;
    }
    printf("%s@%s   %s%s ",ptr->pw_name,hostname,dir,user_str);
    fflush(stdout);
}

第四步就到了获取主机信息的部分,为了能够动态获取用户名,主机名等信息,这里小编要先向大家介绍几个函数。

获取用户名,在这个结构体中就可以得到。

获取主机名

获取当前路径。

//第五步,实现cd命令
else if(strcmp(cmd,"cd")==0){
    if(myargv[1] != NULL){//当cd后面有路径时
        if(chdir(myargv[1])==-1){
            perror("cd err!\n");        
        }
     }
    //当用户只输入cd时,会进入家目录(和系统保持一致)
}
//第六步
//为了和系统保持一致,使字体有颜色(使用printf)
printf("\033[1;32m%s@%s\033[0m  \033[1;34m%s\033[0m%s ",ptr->pw_name,hostname,d    ir,user_str);

六、项目改进

        在上述代码中,我们普通命令的实现都是依靠系统完成的,也就是/user/bin里的二进制可执行程序(在标准路径下找到的)。

        那么,这也就是我们改进的方向,就是写入自己的环境变量,调用自己实现的程序实现MyBash命令解释器(就是将自己实现的命令放入mybin下)。实际上,我们已经自己实现了3个命令了,分别是mycp、mykill、myps。

        下面,我们还将实现一些其他的命令。在实现之前,我们在存放mybash.c的路径mkdir mybin这个目录存放我们自己完成的命令,和系统的user/bin进行区分。

首先是clear命令。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main(){
    printf("\033[2J\033[0;0H");
}
//pwd.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main(){
    char path[256] = {0};
    if(getcwd(path,256) == NULL) 
    {   
        perror("getcwd error!\n");
        exit(1);
    }   
    printf("%s\n",path);
    exit(0);   
}

 现在我们的mybin下面有两个命令了。

大家和小编一起运行一下看看能否成功。

大家要注意,我们在通过gcc生成可执行文件后,把.c文件删除是否还可以成功运行呢?答案当然是可以的,大家可以自行尝试。

接下来就是实现ls命令了,ls命令稍微有点复杂,大家要注意哦。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<dirent.h>
#include<sys/stat.h>
int main(){
    char path[256] = {0};
    if(getcwd(path,256) == NULL){
        perror("getcwd error!\n");
        exit(1);
    }   
    DIR *pdir = opendir(path);
    if(pdir == NULL)
    {   
        perror("opendir error!\n");
        exit(1);
    }   
    struct dirent *s = NULL;
    while((s=readdir(pdir))!=NULL)
    {   
     if(strncmp(s->d_name,".",1)==0)
     {   
        continue;
     }   
     //是目录文件打印为蓝色,不是目录文件分为两种,普通文件是黑色,可执行文件是绿色
     struct stat filestat;
     stat(s->d_name,&filestat);
     if(S_ISDIR(filestat.st_mode))
     {
        printf("\033[1;34m%s\033[0m ",s->d_name);
     }
     else{
        if(filestat.st_mode &(S_IXUSR|S_IXGRP|S_IXOTH))
        {
            printf("\033[1;32m%s\033[0m ",s->d_name);
        }
        else
            printf("%s  ",s->d_name);
     }
    }
    printf("\n");
    closedir(pdir);
    exit(0);
}

 小编在这里就只写这三个命令啦,其他命令小伙伴也能自己写。接下来就是要在前面代码的基础上使用我们自己的环境变量mybin了。

首先,我们要知道我们自己的环境变量的路径是什么,这个是小编正确的路径 /home/stu/quzijie/class03/test15/mybin,那么我们在最上面定义一个宏。大家的mybin在哪,就放哪的路径,不是和小编完全一样噢!还要注意在mybin后面加一个/,因为我们还要拼接路径。

让我们编译运行一下看看能否成功。

现在使用的是我们的mybin里面的命令,我们没有写ps所以无法执行;但我们输入/usr/bin/ps就可以执行系统的ps命令了。

在这里也告诉大家一个偷懒的办法,我们可以使用which查找命令的路径,然后复制到mybin下面就可以使用啦。

七、相关知识点回顾

Linux(九)fork复制进程与写时拷贝技术:回顾bash

Linux(十三)fork + exec进程创建:回顾fork+exec创建进程

Linux(七)进程管理、用户管理和文件压缩命令:回顾用户管理(用户信息保存的位置)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值