Linux进程(九).自定义shell——深度理解命令行和shell

这里先补充一个vim在命令模式下的命令:

%s/src/dest/g

可将字段进行替换。

一.手搓一个命令行

1.命令行细解

首先我们要明晰命令行是干嘛的:它由命令行提示符+命令 组成,是人机交互的一种方式在命令行的"#"或"$"后面(命令行提示符后面)可以输入一行文本(命令),然后交由命令行解释器shell(bash),bash对命令进行翻译以后,翻译过的命令交由内核处理。

关于shell相关的知识点,可以看笔者Linux基本指令及知识点(二)的末尾。

这是centos版的命令行:

用户名+主机名+目录这三者组成的字符串命令行提示符

2.设计myshell的命令行提示符

初版:

复制代码
 #include<stdio.h>
  2 int main()
  3 {
  4     printf("laotian@VM-0-15-ubuntu:~/linux_storage/myshell$ ");                                                                                                        
  5     return 0;
  6 }

但是这个版怎么那么low啊,打的东西都是固定死的,我们需要去增加获取用户名,主机名,以及当前路径的函数。

再版:

①.获取用户名

这里用到一个函数:getenv 去获取环境变量USER里的用户名

②.获取主机名

同样使用getenv,只是把环境变量换成HOSTNAME就行了。

③.获取当前路径

用getenv获取环境变量PWD就行。

④.代码

复制代码
 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<string.h>
  4 const char* GetUserName()
  5 {//获得用户名
  6    const char* name = getenv("USER");
  7    return name == NULL ? "None" : name;
  8 }
  9 const char* GetHostName()
 10 {
 11     const char* hostname = getenv("HOSTNAME");
 12     return hostname == NULL ? "None" : hostname;
 13 }
 14 const char* GetPwd()
 15 {
 16     const char* pwd = getenv("PWD");
 17     return pwd == NULL ? "None" : pwd;
 18 }
 19 int main()
 20 {
 21     printf("%s@%s %s$ ",GetUserName(),GetHostName(),GetPwd());                                                                                                         
 22     return 0;
 23 }

3.命令行提示符再设计+获取用户输入信息

输入命令之前,命令行会卡住,等待用户输入,这该怎么实现呢?

由于解决方案跟c++有关,这里先用mv指令将myshell.c换成myshell.cc别忘了把makefile里的.c也换成.cc。再改一下.cc里包含的头文件,使得c和c++风格的代码都可以跑。

输入命令,在shell看来,就是输入一串字符串。既然是输入字符串,我们可以借用stdin输入流和fgets函数。

用fgets,可以从指定文件中读取数据,并存到指定容器中;stdin是个FILE*类型的指针,绑定了键盘,就是读取键盘输入。

所以能这么设计:

由于以上方式实现的命令行提示符,乱成一锅粥,一点也不美观,所以我们重新制作一个命令行:

①.首先来做命令行提示符。

MakeCommandLine()

我们单独封装一个MakeCommandLine:

这里会用到一个函数------snprintf,该函数可以格式化输出字符串,并将结果写入 指定的字符数组里:

复制代码
int snprintf(char *str, size_t size, const char *format, ...);

代码:

复制代码
void MakeCommandLine(char cmd_prompt[],int size)
 23 {   
 24     snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
 25 }   

封装该接口能够将命令行提示符这个字符串写入字符数组里然后将该字符串传给打印函数,即可实现对命令行提示符的打印。

PrintCommandPrompt()

打印装有命令行提示符的字符数组。

代码:

复制代码
void PrintCommandPrompt()
 27 {//打印命令行提示符
 28  char prompt[COMMAND_SIZE];//定义命令行提示符
 29  MakeCommandLine(prompt,sizeof(prompt));//制作命令行提示符
 30  printf("%s",prompt);                                        
 31  fflush(stdout);
 32 }   

②.实现获取用户输入信息,以及shell退出前一直打印命令行提示符

获取用户输入信息:

复制代码
bool GetCommandLine(char* out,int size)
   34 {
   35 
   36     char commandline[COMMAND_SIZE];
   37     char* c = fgets(out,size,stdin);
   38     if(c == NULL) return false;//fgets导入失败,返回false。
   39     out[strlen(commandline) - 1] = 0;//清理\n,防止出现一个空行。
   40     if(strlen(out) == 0)return false;//只输入一个回车,也要返回false
   41     return true;
   42 }

循环打印命令行提示符:

shell本质是个死循环,一直在打印提示符,获取用户输入信息,直到shell的进程退出才结束循环!

4.命令行分析(拆解)

由于执行指令需要先创建子进程,再使用exec系列函数作程序替换, 而将命令行参数(例如-l-a)传给execa系列函数时,不能整个字符串的传,需要将字符串切割成一个个的命令行参数的子串,才能传参。

那么如何快速找到每一个元素呢(子串)?

定义两个全局变量g_argv和g_argc,很熟悉,就是main函数的两个形参。

然后封装分析函数:CommandParse():

这里调用一个切割字符串的函数:**STRTOK,**将命令行commandline进行切割:

每切割下一个子串,就把子串放入g_argv。

复制代码
 bool CommandParse(char* commandline)
   48 {
   49 #define SEP " "
   50     g_argc = 0;
   51     g_argv[g_argc++] =  strtok(commandline,SEP);//首次切割需要传字符串首地址。
✹  52     while(g_argv[g_argc++] = strtok(nullptr,SEP));//后续调用传NULL。
   53     g_argc--;                                                                                                                                                        
   54     return true;
   55 }

因为argv的结尾有个null,所以argc++会多算一个null。所以要作调整,等for循环结束,argc--。

二.命令的处理

1.执行外部命令

执行命令时,需要先fork()一个子进程,然后用exec进行程序替换。

复制代码
 pid_t id =fork();
   85     if(id == 0)
   86     {//child
   87      //程序替换
   88      execvp(g_argv[0],g_argv);
   89      exit(1);
   90     }
   91     //father
   92    pid_t rid = waitpid(id,nullptr,0);
   93    (void)rid;//执行一下,防止报警告        

2.路径切割

由于命令行提示符只有当前目录,所以我们还要做路径的切割。

此处用cpp编写一个接口:

复制代码
std::string DirName(const char* pwd)
   32 {
   33 #define slash "/"
   34     std::string dir = pwd;
✹  35     if(pwd == slash) return slash;
   36     auto pos = dir.rfind(slash);
   37     if(pos == std::string::npos)return"BUG?";
   38     return dir.substr(pos + 1);
   39 }

rfind:从字符串末尾往前搜索,找到指定字符,就返回其所在的下标。

npos:

3.检测并处理内建命令

有些命令需要bash亲自去执行,那么用上面execvp,让子进程去执行就走不通了。譬如执行内建命令pwd,pwd的功能是显示**bash内容所在的目录的路径,**如果直接将pwd交给子进程,那么打出来的目录,就是子进程代码和数据所在的目录的路径了,就是不正确的。

这里设计一个CheckAndExecuteBuiltin() 接口:

在其内部,还要设计一个寻找家目录的接口,以应对cd命令没有命令行参数,直接跳转家目录。

复制代码
 const char* GetHome()
   32 {//找到家目录
   33     const char* home = getenv("HOME");
   34     return home == NULL ? NULL : home;
   35 }

那么如何更改bash所在目录的路径呢?

答案是chdir函数。

复制代码
 bool CheckAndExecBuiltin()
   85 {
   86     std::string cmd = g_argv[0];
   87     if(cmd == "cd")
   88     {
   89         if(g_argc == 1)
   90         {//没有命令行参数,就跳转到家目录下。
   91             std::string home = GetHome();
   92             if(home.empty())return true;//获取家目录路径失败,就返回。
   93             chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
   94         }
   95         else
   96         {//有命令行参数
   97             std::string where = g_argv[1];
   98             //cd -/cd ~
   99             if(where == "-")
  100             {
  101             std::string home = GetHome();
  102             if(home.empty())return true;//获取家目录路径失败,就返回。
  103             chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
  105             else if(where == "~")
  106             {
  107             std::string home = GetHome();
  108             if(home.empty())return true;//获取家目录路径失败,就返回。
  109             chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
  110             }
  111             else
  112             {
  113                 chdir(where.c_str());
  114                 return true;
  115             }
  116         }
  117         return true;
  118     }
  119     return false;
  120 }

可以看出cd的底层其实就是调用chdir,去变更bash所在的目录的路径。

4.重构GetPwd()

cd以后,我们发现了一个问题,命令行提示符上的当前bash的目录的路径不是实时变化的,这是什么原因呢?该如何解决呢?

cd以后,当前bash进程的路径发生变化,但环境变量PWD没有更新 ,导致GetPwd函数通过环境变量获取到的bash所在目录的路径也是没有更新过的老路径 ,所以上面GetPwd通过环境变量得路径的方式是不行的,需要重新设计,以便获得实时的路径

如何获得实时的路径呢?

答案是getcwd()函数:

这里定义了一个全局变量。

代码:

复制代码
const char* pwd = getcwd(cwd,sizeof(cwd));

同时还要更新环境变量

用到putenv :将环境变量(字符串)导给当前进程(bash),但是有格式的要求:

先使用snprintf把格式化的环境变量的字符串导入全局变量cwdenv里,再把cwdenc传给getenv:

复制代码
const char* GetPwd()   
   30 {                      
   31     //const char* pwd = getenv("PWD");
   32     const char* pwd = getcwd(cwd,sizeof(cwd));
   33     //更新环境变量     
   34     if(pwd != NULL)    
   35     {                  
   36         snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",pwd);//将格式化的字符串导入cwdenv
   37         getenv(cwdenv);//将swdenv导给当前bash进程,以实现环境变量的更新。                                                                                            
   38     }
   39     return pwd == NULL ? "None" : pwd;                                                                                                                               
   40 }

pwd指令不也是实时显示当前bash的路径吗?所以还能输出一个结论------pwd的底层也是调用了getcwd()函数!

三.最终版的shell

复制代码
#include <cerrno>
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<wait.h>
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]"//宏定义一个版式
#define MAXARGC 128
//定义shell的全局变量
char* g_argv[MAXARGC];
int g_argc = 0;//g_是global,全局的意思。
//for test
char cwd[1024];
char cwdenv[1024];

//last exit code最近一个程序的退出码
int lastcode = 0;

const char* GetUserName()
{//获得用户名
   const char* name = getenv("USER");
   return name == NULL ? "None" : name;
}
const char* GetHostName()
{
    const char* hostname = getenv("HOSTNAME");
    return hostname == NULL ? "None" : hostname;
}
const char* GetPwd()
{
    //const char* pwd = getenv("PWD");
    const char* pwd = getcwd(cwd,sizeof(cwd));
    //更新环境变量
    if(pwd != NULL)
    {
        snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",pwd);//将格式化的字符串导入cwdenv
        getenv(cwdenv);//将swdenv导给当前bash进程,以实现环境变量的更新。
    }
    return pwd == NULL ? "None" : pwd;
}
const char* GetHome()
{//找到家目录
    const char* home = getenv("HOME");
    return home == NULL ? NULL : home;
}
std::string DirName(const char* pwd)
{
#define slash "/"
    std::string dir = pwd;
    if(pwd == slash) return slash;
    auto pos = dir.rfind(slash);
    if(pos == std::string::npos)return"BUG?";
    return dir.substr(pos + 1);
}
void MakeCommandLine(char cmd_prompt[],int size)
{
    snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());
}
void PrintCommandPrompt()
{//打印命令行提示符
 char prompt[COMMAND_SIZE];//定义命令行提示符
 MakeCommandLine(prompt,sizeof(prompt));//制作命令行提示符
 printf("%s",prompt);                                      
 fflush(stdout);
}
bool GetCommandLine(char* out,int size)
{

    char* c = fgets(out,size,stdin);
    if(c == NULL) return false;//fgets导入失败,返回false。
    out[strlen(out) - 1] = 0;//清理\n,防止出现一个空行。
    if(strlen(out) == 0)return false;//只输入一个回车,也要返回false
    return true;
}
bool CommandParse(char* commandline)
{
#define SEP " "
    g_argc = 0;
    g_argv[g_argc++] =  strtok(commandline,SEP);//首次切割需要传字符串首地址。
    while(g_argv[g_argc++] = strtok(nullptr,SEP));//后续调用传NULL。
    g_argc--;
    return g_argc < 0 ? false : true;
}
void PrintArgv()
{
    for(int i = 0;g_argv[i];i++)
    {//argv以null为结尾,所以可以这样设计循环条件
        printf("argv[%d]->%s\n",i,g_argv[i]);
    }
    printf("argc:%d",g_argc);
    printf("\n");
}

bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        if(g_argc == 1)
        {//没有命令行参数,就跳转到家目录下。
            std::string home = GetHome();
            if(home.empty())return true;//获取家目录路径失败,就返回。
            chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
        }
        else
        {//有命令行参数
            std::string where = g_argv[1];
            //cd -/cd ~
            if(where == "-")
            {
            std::string home = GetHome();
            if(home.empty())return true;//获取家目录路径失败,就返回。
            chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
            }
            else if(where == "~")
            {
            std::string home = GetHome();
            if(home.empty())return true;//获取家目录路径失败,就返回。
            chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
            }
            else
            {
                chdir(where.c_str());
                return true;
            }
        }        return true;
    }
    else if(cmd == "echo")
    {
        if(g_argc == 2)
        {
            std::string opt = g_argv[1];
            if(opt == "$?")
            {
                //显示上个退出进程的退出码。
                std::cout << lastcode << std::endl;
                lastcode = 0;//需要清零
                return true;
            }
            if(opt[0] == '$')
            {//echo +$环境变量,可以查环境变量
                std::string env_name = opt.substr(1);//下标1后面的就是环境变量。
                const char* env_value = getenv(env_name.c_str());//获得对应环境变量的内容
                if(env_value)
                {
                    std:: cout << env_value << std::endl;
                }
            }
        }
        return true;
    }
    return false;
}
int Execute()
{//执行程序

    pid_t id =fork();
    if(id == 0)
    {//child
     //程序替换
     execvp(g_argv[0],g_argv);
     exit(1);
    }
    int status = 0;
    //father
   pid_t rid = waitpid(id,&status,0);
   if(rid > 0)
   {//状态码
        lastcode = WEXITSTATUS(status);//用宏获取状态码
   }
   return 0;
}
int main()
{
    while(true)
    {
    //1.输出命令行提示符
    PrintCommandPrompt();
    //2.获取用户输入信息
    char commandline[COMMAND_SIZE];
    if(!GetCommandLine(commandline,sizeof(commandline)))
    {//获取用户输入信息失败,依旧继续跑。
        continue;
    }
    //3.命令行解析
    if(!CommandParse(commandline))
        continue;
    //3.5检测并处理内建命令
   if(CheckAndExecBuiltin())
    continue;//Check函数判断并处理了内建命令,就开启下一轮循环,不需要继续执行普通命令了。
    //4.执行命令
    Execute();
    }
    return 0;
}
相关推荐
前端 贾公子1 小时前
Milvus使用指南 (下)
java·服务器·前端
snow@li1 小时前
服务器运维:mysql 安装笔记
运维·服务器·mysql
天道jimmy1 小时前
VulnHub 系列:View2aKill,1
linux·运维·服务器
荀彧原名苟或1 小时前
SAP 执行 CMD命令(GUI_EXEC 执行电脑本机命令、SXPG_COMMAND_EXECUTE函数执行服务器上的外部程序)<转载>
运维·服务器·sap·abap
帷幕落秋1 小时前
Nginx 系列实战(三):LNMP 完整搭建,动静分离的学习总结
运维·自动化运维
Jae den1 小时前
服务器 CPU 核心越多越好吗?
运维·服务器
先吃饱再说1 小时前
从本地跑通到公网访问:前后端项目部署实战
运维·全栈
帷幕落秋1 小时前
一文吃透 Nginx 虚拟主机:原理 + 双站点实战 + SELinux 排错
运维·自动化运维
mengge.cloud1 小时前
0909华为云网络类服务实践小白实验教程
linux·运维·服务器·网络·华为云·php