【Linux】实现一个简易shell | 内键命令(八)

目录

前言:

一、总览

二、打印命令行提示符(PrintCommandLine函数)

三、获取用户命令(GetCommandLine函数)

四、分析命令(ParseCommandLine函数)

五、执行命令(ExecuteCommand函数)

六、shell前部分代码总览(不完善但能运行)

七、内键命令

1.cd命令

 1.1改善命令行参数提示符

2.export命令和echo命令

2.1实现echo $? 

八、shell的全部代码 

总结:


前言:

大家如果看过我之前的文章的话,就已经知道了几乎关于进程所有的操作和属性(当然关于信号和通信我们以后再提)。此时已经完全可以实现一个简易的shell程序了,注意,本篇代码含量很高,前方高能,做好准备!此外,还将补充内键命令。

一、总览

shell(bash)也是进程,我们完成它,一定要从最小的步骤开始,也要知道每一步是干什么的。分为4个大步:

#include<iostream>
#include<cstdio>

using namespace std;


int main()
{
    while(true)
    {

        PrintCommandLine(); //1.命令行提示符

        //GetCommandLine(); //2.获取用户命令

        //ParseCommandLine(); //3.分析命令

        //ExecuteCommand(); //4.执行命令
    }
    return 0;
}

二、打印命令行提示符(PrintCommandLine函数)

命令行提示符中,我们要获取用户名,主机名和路径,这些通过环境变量获取(getenv)。

(注意:这个shell用C++来完成)

我们使用snprintf函数对一个长字符串赋值:

#include<iostream>
#include<cstdio>

using namespace std;

const int basesize = 1024;

string GetUserName()
{
    string name = getenv("USER");
    return name.empty() ? "None" : name;
}

string GetHostName()
{
    string hostname= getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

string GetPwd()
{
    string pwd = getenv("PWD");
    return pwd.empty() ? "None" : pwd;
}

string MakeCommandLine()
{
    //[gan@localhost lesson16]$ 
    char command_line[basesize];
    //这里使用#来和本身的shell作区分
    snprintf(command_line, basesize, "[%s@%s %s]#", \
            GetUserName().c_str(), GetHostName().c_str(), GetPwd().c_str());
    return command_line;
}

void PrintCommandLine() //1.命令行提示符
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);
}

int main()
{
    while(true)
    {

        PrintCommandLine(); //1.命令行提示符
        
        //GetCommandLine(); //2.获取用户命令

        //ParseCommandLine(); //3.分析命令

        //ExecuteCommand(); //4.执行命令
    }
    return 0;
}

因为打印命令行中没有\n,所以使用fflush强制刷新一下。 

我们加上睡眠函数和回车,先来观察一下代码执行(不要偷懒,一步一步调试,否则后面就不知道哪里出错了):

#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<stdlib.h>

using namespace std;

const int basesize = 1024;

string GetUserName()
{
    string name = getenv("USER");
    return name.empty() ? "None" : name;
}

string GetHostName()
{
    string hostname= getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

string GetPwd()
{
    string pwd = getenv("PWD");
    return pwd.empty() ? "None" : pwd;
}

string MakeCommandLine()
{
    //[gan@localhost lesson16]$ 
    char command_line[basesize];
    //这里使用#来和本身的shell作区分
    snprintf(command_line, basesize, "[%s@%s %s]#", \
            GetUserName().c_str(), GetHostName().c_str(), GetPwd().c_str());
    return command_line;
}

void PrintCommandLine() //1.命令行提示符
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);
}

int main()
{
    while(true)
    {

        sleep(1);
        PrintCommandLine(); //1.命令行提示符
        
        printf("\n");
        //GetCommandLine(); //2.获取用户命令

        //ParseCommandLine(); //3.分析命令

        //ExecuteCommand(); //4.执行命令
    }
    return 0;
}

这里路径我们先以绝对路径的方式打印,以后再来优化。 

三、获取用户命令(GetCommandLine函数)

之后要获取用户命令,其实就是一段符串。用一种新方式获取字符串,fgets方式,从指定的文件描述符(后面讲)中获取指定大小的字符串函数:

bool GetCommandLine(char command_buffer[], int size) //2.获取用户命令
{
    //我们认为:我们要将用户输入的命令行 当做一个完整的字符串
    char* result = fgets(command_buffer, size, stdin); //从标准输入流获取
    if (!result)
    {
        return false;
    }
    return true;
}

int main()
{
    char command_buffer[basesize];
    while(true)
    {
        PrintCommandLine(); //1.命令行提示符
        
        if (!GetCommandLine(command_buffer, basesize)) //2.获取用户命令
        {
            //此时接受的是false
            continue;
        }
        
        printf("%s", command_buffer);
        //ParseCommandLine(); //3.分析命令

        //ExecuteCommand(); //4.执行命令
    }
    return 0;
}

使用fgets接收参数,之后我们进行打印调试代码。

我们并没有在printf中输入"\n"但是每次都在换行,是因为我们每次最后都会输入回车,所以我们要让最后的回车消失。

bool GetCommandLine(char command_buffer[], int size) //2.获取用户命令
{
    //我们认为:我们要将用户输入的命令行 当做一个完整的字符串
    char* result = fgets(command_buffer, size, stdin); //从标准输入流获取
    if (!result)
    {
        return false;
    }
    command_buffer[strlen(command_buffer) - 1] = 0; //将最后的 \n 覆盖
    return true;
}

int main()
{
    char command_buffer[basesize];
    while(true)
    {
        PrintCommandLine(); //1.命令行提示符
        
        if (!GetCommandLine(command_buffer, basesize)) //2.获取用户命令
        {
            //此时接受的是false
            continue;
        }
        
        //ParseCommandLine(); //3.分析命令

        //ExecuteCommand(); //4.执行命令
    }
    return 0;
}

四、分析命令(ParseCommandLine函数)

接下来分析命令,假设最多传入64个参数,我们再定义三个全局变量:

const int argvnum = 64;

//全局命令行参数表
char* gargv[argvnum];
int gargc = 0;

使用strtok对字符串进行分割,为保证安全每次命令行解析都对参数列表进行清空。

这里的makefile文件如下(使用C++11标准):

CC:=g++
myshell:myshell.cc
	$(CC) -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f myshell
const int basesize = 1024;
const int argvnum = 64;

//全局命令行参数表
char* gargv[argvnum];
int gargc = 0;

void ParseCommandLine(char command_buffer[], int len) //3.分析命令
{
    (void)len;
    //为保证安全每次命令行解析都对参数列表进行清空
    memset(gargv, 0, sizeof(gargv)); 
    gargc = 0;
    const char* sep = " "; //定义分隔符
    gargv[gargc++] = strtok(command_buffer, sep); //对字符串切分

    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    //这里会多加一个1,所以自减1
    gargc--;
}

void debug()
{
    printf("argc: %d\n", gargc);
    for (int i = 0; gargv[i]; ++i)
    {
        printf("argv[%d] = %s\n", i, gargv[i]);
    }
}

int main()
{
    char command_buffer[basesize];
    while(true)
    {
        PrintCommandLine(); //1.命令行提示符
        
        if (!GetCommandLine(command_buffer, basesize)) //2.获取用户命令
        {
            //此时接受的是false
            continue;
        }
        
        ParseCommandLine(command_buffer, strlen(command_buffer)); //3.分析命令
        debug();

        //ExecuteCommand(); //4.执行命令
    }
    return 0;
}

我们先debug一下:

当然也有可能直接输入回车,需要再更正代码,直接在GetCommandLine中判断command_buffer是否为0,是返回false,否返回true:

bool GetCommandLine(char command_buffer[], int size) //2.获取用户命令
{
    //我们认为:我们要将用户输入的命令行 当做一个完整的字符串
    char* result = fgets(command_buffer, size, stdin); //从标准输入流获取
    if (!result)
    {
        return false;
    }
    command_buffer[strlen(command_buffer) - 1] = 0; //将最后的 \n 覆盖

    //当用户直接输入回车时判断长度是否为0
    if(strlen(command_buffer) == 0) return false;
    return true;
}

五、执行命令(ExecuteCommand函数)

不能让shell自己执行命令,因为一旦用户输入错误,shell自身就可能崩溃,所以我们要创建子进程执行命令。

六、shell前部分代码总览(不完善但能运行)

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

using namespace std;

const int basesize = 1024;
const int argvnum = 64;

//全局命令行参数表
char* gargv[argvnum];
int gargc = 0;

string GetUserName()
{
    string name = getenv("USER");
    return name.empty() ? "None" : name;
}

string GetHostName()
{
    string hostname= getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

string GetPwd()
{
    string pwd = getenv("PWD");
    return pwd.empty() ? "None" : pwd;
}

string MakeCommandLine()
{
    //[gan@localhost lesson16]$ 
    char command_line[basesize];
    //这里使用#来和本身的shell作区分
    snprintf(command_line, basesize, "[%s@%s %s]#", \
            GetUserName().c_str(), GetHostName().c_str(), GetPwd().c_str());
    return command_line;
}

void PrintCommandLine() //1.命令行提示符
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);
}

bool GetCommandLine(char command_buffer[], int size) //2.获取用户命令
{
    //我们认为:我们要将用户输入的命令行 当做一个完整的字符串
    char* result = fgets(command_buffer, size, stdin); //从标准输入流获取
    if (!result)
    {
        return false;
    }
    command_buffer[strlen(command_buffer) - 1] = 0; //将最后的 \n 覆盖

    //当用户直接输入回车时判断长度是否为0
    if(strlen(command_buffer) == 0) return false;
    return true;
}

void ParseCommandLine(char command_buffer[], int len) //3.分析命令
{
    (void)len;
    //为保证安全每次命令行解析都对参数列表进行清空
    memset(gargv, 0, sizeof(gargv)); 
    gargc = 0;
    const char* sep = " "; //定义分隔符
    gargv[gargc++] = strtok(command_buffer, sep); //对字符串切分

    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    //这里会多加一个1,所以自减1
    gargc--;
}

bool ExecuteCommand() //4.执行命令
{
    //创建子进程 让子进程执行命令
    pid_t id = fork();
    if (id < 0) return false;

    if (id == 0)
    {
        //子进程
        //1.执行命令
        execvp(gargv[0], gargv);
        //2.退出
        exit(1);
    }

    int status = 0;
    pid_t rid = waitpid(id, &status, 0); //以阻塞等待方式
    if (rid > 0)
    {
        return true;
    }
    return false;
}

int main()
{
    char command_buffer[basesize];
    while(true)
    {
        PrintCommandLine(); //1.命令行提示符
        
        if (!GetCommandLine(command_buffer, basesize)) //2.获取用户命令
        {
            //此时接受的是false
            continue;
        }
        
        ParseCommandLine(command_buffer, strlen(command_buffer)); //3.分析命令

        ExecuteCommand(); //4.执行命令
    }
    return 0;
}

七、内键命令

1.cd命令

此时代码如果执行cd切换路径的代码,会发现切换不了。每一个进程都有一个叫做当前路径的概念(cwd),所以切换的是子进程的路径也就是把其cwd改变了(chdir函数改变,这我们之前都讲过)。

cd命令不是让子进程执行的,而是让父进程执行的。

这些不能由子进程执行的命令,叫做內键命令!

所以有一小部分指令不需要创建子进程,而是自己调用函数。当我们更改路径以后,需要把自己的PWD环境变量改变,chdir不会改变PWD。

这里必须再次说明cwd和PWD不是同一个东西,PWD是环境变量,CWD是进程属性

在shell命令行中执行,cd本质就是chdir,先改变cwd,之后由OS改变PWD!

  • chdir:改变调用它的进程的 CWD。

  • PWD:由 shell 维护,需手动更新。

进程所在路径(PWD)实在自己的PCB中的,也就是环境变量中:

所以我们需要使用shell自己更新。 使用getcwd获取当前工作路径:

当然还需要把环境变量更新,使用putenv更新(其可以新增也可以更新)。

所以我们先来完善更改路径的函数(这里也就是更新PWD环境变量):

//全局shell工作路径
char pwd[basesize];
char pwdenv[basesize]; //定义环境全局变量

string GetPwd()
{
    if(!getcwd(pwd, sizeof(pwd))) return "None";
    //修改环境变量
    snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
    putenv(pwdenv);

    return pwd;
    //string pwd = getenv("PWD");
    //return pwd.empty() ? "None" : pwd;
}

环境变量是由shell自己维护的,我们写的shell也并不是从头写的,而是从父进程(bash)继承的,所以当putenv时会发生写时拷贝。

 1.1改善命令行参数提示符

我们将命令行提示符前面打印绝对路径修改为最后所在目录名:

string LastDir()
{
    string curr = GetPwd();
    if (curr == "/" || curr == "None") return curr;

    size_t pos = curr.rfind("/");
    if (pos == std::string::npos) return curr;
    return curr.substr(pos + 1);
}

string MakeCommandLine()
{
    //[gan@localhost lesson16]$ 
    char command_line[basesize];
    //这里使用#来和本身的shell作区分
    snprintf(command_line, basesize, "[%s@%s %s]#", \
            GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
    return command_line;
}

2.export命令和echo命令

其实当我们export和echo不需要创建子进程,也是内键命令。但是我们目前还是使用的shell的环境变量,我们现在开始自己创建一个自己的环境变量表。这里因为无法读取系统配置,所以先从父shell中继承下来环境变量。

所以进程地址空间中有一段“命令行参数环境变量”就是专门存储这部分内容的:

因为export是导入环境变量,所以其是一个内键命令。 输入env会进行进程替换,但是我们目前先查当前进程的环境变量,所以将env先设置成内键命令。

const int envnum = 64;
//我的系统环境变量
char* genv[envnum];

void AddEnv(const char* item)
{
    int index = 0;
    while(genv[index++]);

    genv[--index] = (char*)malloc(strlen(item) + 1);
    strncpy(genv[index], item, strlen(item) + 1);
    genv[++index] = nullptr;
}

bool CheckAndExecBuiltCommand()
{
    if (strcmp(gargv[0], "cd") == 0) 
    {
        if (gargc == 2)
        {
            chdir(gargv[1]);
            return true;
        }
    }
    else if (strcmp(gargv[0], "export") == 0)
    {
        if (gargc == 2)
        {
            //export也是内键命令
            AddEnv(gargv[1]);
            return true;
        }
    }
    else if (strcmp(gargv[0], "env") == 0)
    {
        if (gargc == 1)
        {
            for (int i = 0; genv[i]; ++i)
            {
                printf("%s\n", genv[i]);
            }
            return true;
        }
    }

    return false;
}

//初始化环境变量 
//作为一个shell获取环境变量应该从系统配置来
//我们今天就直接从父shell中获取环境变量
void InitEnv()
{
    //我们从父进程拷贝下来环境变量
    extern char** environ;
    int index = 0;
    while(environ[index]) 
    {
        genv[index] = (char*)malloc(strlen(environ[index]) + 1);
        strncpy(genv[index], environ[index], strlen(environ[index]) + 1);
        index++;
    }
    genv[index] = nullptr;
}

但是此时我们子进程并不会继承当前父进程,因为当前代码并没有让子进程获取,所以我们程序替换的函数就需要使用execvpe(可以传入环境变量):

bool ExecuteCommand() //4.执行命令
{
    //创建子进程 让子进程执行命令
    pid_t id = fork();
    if (id < 0) return false;

    if (id == 0)
    {
        //子进程
        //1.执行命令
        //execvp(gargv[0], gargv);
        execvpe(gargv[0], gargv, genv);
        //2.退出
        exit(1);
    }

    int status = 0;
    pid_t rid = waitpid(id, &status, 0); //以阻塞等待方式
    if (rid > 0)
    {
        return true;
    }
    return false;
}

我们写一个打印环境变量的程序other,之后调用,看子进程是否继承:

2.1实现echo $? 

最后,我们将echo $?这个內键命令实现。先设置一个全局变量lastcode记录上次退出码,并在CheckAndExecBuiltCommand函数中添加:

else if (strcmp(gargv[0], "echo") == 0)
{
    if (gargc == 2)
    {
        //echo $?
        //echo $PATH
        //echo hello
        if (strcmp(gargv[1], "$?") == 0)
        {
            //打印退出码
            printf("%d\n", lastcode);
        }
        else 
        {
            printf("%s\n", gargv[1]);
        }
        lastcode = 0;
        return true;
    }
    else 
    {
        lastcode = 3;
    }
}

至此,我们已经完成了一个简易的shell,还有很多并没有完善,但是我们已经知道了其大致的原理。

八、shell的全部代码 

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

using namespace std;

const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;

//全局命令行参数表
char* gargv[argvnum];
int gargc = 0;

int lastcode = 0;

//全局shell工作路径
char pwd[basesize];
char pwdenv[basesize]; //定义环境全局变量

//我的系统环境变量
char* genv[envnum];

string GetUserName()
{
    string name = getenv("USER");
    return name.empty() ? "None" : name;
}

string GetHostName()
{
    string hostname= getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

string GetPwd()
{
    if(!getcwd(pwd, sizeof(pwd))) return "None";
    //修改环境变量
    snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
    putenv(pwdenv);

    return pwd;
    //string pwd = getenv("PWD");
    //return pwd.empty() ? "None" : pwd;
}

string LastDir()
{
    string curr = GetPwd();
    if (curr == "/" || curr == "None") return curr;

    size_t pos = curr.rfind("/");
    if (pos == std::string::npos) return curr;
    return curr.substr(pos + 1);
}

string MakeCommandLine()
{
    //[gan@localhost lesson16]$ 
    char command_line[basesize];
    //这里使用#来和本身的shell作区分
    snprintf(command_line, basesize, "[%s@%s %s]#", \
            GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
    return command_line;
}

void PrintCommandLine() //1.命令行提示符
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);
}

bool GetCommandLine(char command_buffer[], int size) //2.获取用户命令
{
    //我们认为:我们要将用户输入的命令行 当做一个完整的字符串
    char* result = fgets(command_buffer, size, stdin); //从标准输入流获取
    if (!result)
    {
        return false;
    }
    command_buffer[strlen(command_buffer) - 1] = 0; //将最后的 \n 覆盖

    //当用户直接输入回车时判断长度是否为0
    if(strlen(command_buffer) == 0) return false;
    return true;
}

void ParseCommandLine(char command_buffer[], int len) //3.分析命令
{
    (void)len;
    //为保证安全每次命令行解析都对参数列表进行清空
    memset(gargv, 0, sizeof(gargv)); 
    gargc = 0;
    const char* sep = " "; //定义分隔符
    gargv[gargc++] = strtok(command_buffer, sep); //对字符串切分

    while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
    //这里会多加一个1,所以自减1
    gargc--;
}

bool ExecuteCommand() //4.执行命令
{
    //创建子进程 让子进程执行命令
    pid_t id = fork();
    if (id < 0) return false;

    if (id == 0)
    {
        //子进程
        //1.执行命令
        //execvp(gargv[0], gargv);
        execvpe(gargv[0], gargv, genv);
        //2.退出
        exit(1);
    }

    int status = 0;
    pid_t rid = waitpid(id, &status, 0); //以阻塞等待方式
    if (rid > 0)
    {
        if(WIFEXITED(status))
        {
            lastcode = WEXITSTATUS(status);
        }
        else 
        {
            lastcode = 100;
        }
        return true;
    }
    return false;
}

void AddEnv(const char* item)
{
    int index = 0;
    while(genv[index++]);

    genv[--index] = (char*)malloc(strlen(item) + 1);
    strncpy(genv[index], item, strlen(item) + 1);
    genv[++index] = nullptr;
}

bool CheckAndExecBuiltCommand()
{
    if (strcmp(gargv[0], "cd") == 0) 
    {
        if (gargc == 2)
        {
            chdir(gargv[1]);
        }
        else 
        {
            lastcode = 1;
        }
        return true;
    }
    else if (strcmp(gargv[0], "export") == 0)
    {
        if (gargc == 2)
        {
            //export也是内键命令
            AddEnv(gargv[1]);
        } 
        else 
        {
            lastcode = 2;
        }
        return true;
    }
    else if (strcmp(gargv[0], "env") == 0)
    {
        if (gargc == 1)
        {
            for (int i = 0; genv[i]; ++i)
            {
                printf("%s\n", genv[i]);
            }
            lastcode = 0;
            return true;
        }
    }
    else if (strcmp(gargv[0], "echo") == 0)
    {
        if (gargc == 2)
        {
            //echo $?
            //echo $PATH
            //echo hello
            if (strcmp(gargv[1], "$?") == 0)
            {
                //打印退出码
                printf("%d\n", lastcode);
            }
            else 
            {
                printf("%s\n", gargv[1]);
            }
            lastcode = 0;
            return true;
        }
        else 
        {
            lastcode = 3;
        }
    }

    return false;
}

//初始化环境变量 
//作为一个shell获取环境变量应该从系统配置来
//我们今天就直接从父shell中获取环境变量
void InitEnv()
{
    //我们从父进程拷贝下来环境变量
    extern char** environ;
    int index = 0;
    while(environ[index]) 
    {
        genv[index] = (char*)malloc(strlen(environ[index]) + 1);
        strncpy(genv[index], environ[index], strlen(environ[index]) + 1);
        index++;
    }
    genv[index] = nullptr;
}

int main()
{
    //启动前初始化环境变量表
    InitEnv();
    char command_buffer[basesize];
    while(true)
    {
        PrintCommandLine(); //1.命令行提示符
        
        if (!GetCommandLine(command_buffer, basesize)) //2.获取用户命令
        {
            //此时接受的是false
            continue;
        }
        
        ParseCommandLine(command_buffer, strlen(command_buffer)); //3.分析命令

        //判断是否为内键命令
        if(CheckAndExecBuiltCommand())
        {
            continue;
        }

        ExecuteCommand(); //4.执行命令
    }
    return 0;
}

总结:

这酸爽!大家如果能比着实现一遍的话,实力绝对突飞猛进!如果能自己敲一遍更是无敌!进程讲解很多了,至少你已经知道了很多关于进程的知识,在生活中也能理解很多现象。但是我们说过一句话叫做Linux下一切皆文件!但是此时我们还没有提到关于任何关于文件的知识。因为这个也是Linux的核心内容,接下来,我们就要开始文件的篇章了,不要走开,敬请期待!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值