【Linux】第二个小程序--简易shell

时间:2024-04-08 21:49:05

请看上面的shell,其本质就是一个字符串,我们知道bash本质上就是一个进程,只不过命令行就是一个输出的字符串,

我们输入的命令“ls -a -l”实际上是我们在输入行输入的字符串,所以,如果我们想要做一个简易的shell的时候,首先要输出“[ghs@hecs-406886 myshell]$”这样的字符串,然后再接收并解析写入的命令字符串,然后一条命令就可以执行了。在“[ghs@hecs-406886 myshell]$”这样一行字符串中,ghs表示用户名,hecs-406886表示当前主机的主机名,myshell表示当前所处的路径,[ @ ]$为提示符。

所以,我们也需要构建一个类似的命令行,首先,创建一个myshell.c,

在main函数中,第一步我们需要自己输出一个命令行:命令行包括用户名、主机名、当前路径, 这些内容可以从环境变量中获取,

可以通过getenv函数获取环境变量内容,我们自定义三个函数来获取上面三个环境变量:

const char* getusername()
{
    const char* name = getenv("USER");
    if(name == NULL) return "None";
    return name;
}
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;
}

先介绍一个函数snprintf:把指定参数按照特定格式写到指定长度的内存里,

然后综合上面三个函数,封装出MakeCommandLineAndPrint()函数,制作并打印命令行:

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

第二步就是获取用户命令字符串,输入的指令(“ls -l -a”)站在开发者的角度本质是一个字符串,我们想一下,可以用scanf获取指令吗?不可以!因为指令选项个数不定。其实,我们想按行来获取字符串,在C语言中,我们可以使用fgets函数,它可以按行从特定的文件流中获取指定内容,获取内容指向由指针s指向的缓冲区,

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

定义usercommand数组用于存储字符串,然后封装GetUserCommand()函数用于获取用户命令字符串,

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

第三步就是命令行字符串分割,使用‘ ’(空格)作为分隔符,我们想要得到一个数组,这个数组叫做char *argv[],对于这样“ls -l -a”一个字符串,定义指针指向第一个‘l’,再定义另一个指针也指向第一个‘l’,往后遍历,遇到第一个空格把其置为‘\0’,把指向第一个‘l’的指针放到agrv数组的第一个位置,然后指针往后走,指向‘-’,再定义一个指针向后走,遇到第一个空格把其置为‘\0’,把指向第一个‘-’的指针放到agrv数组的第二个位置,依次类推。为了实现这个功能,我们使用strtok函数:

#define NUM 32
char* gArgv[NUM];
void SplitCommand(char command[],size_t n)
{
    gArgv[0] = strtok(command,SEP);
    int index = 1;
    while((gArgv[index++] = strtok(NULL,SEP)));//故意写成=,表示先赋值再判断,分割之后,strtok 
                                               //会返回NULL,刚好让gArgv最后一个元素是NULL, 
                                               //并且while判断结束
}

下一步就要执行命令,我们需要创建子进程去执行,在子进程中我们要选择使用哪一个程序替换函数,第一,由于上面分割出来的命令是不带路径的,我要执行的命令全是系统的默认路径,应该让它在环境变量中找,所以选择的函数一定带p,第二,由于我提供的是一个命令数组argv,所以一定要带v,所以,选择execvp函数,这个函数在失败时返回-1,同时会设置错误码errno。

pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
    //child
    execvp(gArgv[0],gArgv);
    exit(errno);
}

但是,上面这个代码只能执行一次,为了能一直执行下去,我们创建一个循环:

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

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

         //3.命令行字符串分割
         SplitCommand(usercommand,sizeof(usercommand));
         
         //n.执行命令
         ExecuteCommand();
    }
    return 0;
}

上面我们完成一个粗犷的shell,但是有一点:

我们无法使用cd ..完成路径回退,这是为什么呢?因为我们上面的程序是在子进程中进行的,cd ..是在子进程中进行的,是把子进程的路径进行了回退,但是和父进程无关,父进程没有回退,cd这样的命令应该让父进程进程回退,而不应该让子进程回退,因此,需要对这些内建命令进行单独处理,在执行命令之前,要检查命令是否是内建命令

//3.命令行字符串分割
SplitCommand(usercommand,sizeof(usercommand));
//4.检查命令是否是内建命令
n = CheckBuilding();
if(n) continue; 
void Cd()
{
   const char* path = gArgv[1];
   if(path == NULL) path = GetHome();
   //path一定存在
   chdir(path);
}

上面代码的意思是,如果“cd”,那么回退到用户家目录,否则把命令行里的路径更改为当前路径。 

int CheckBuilding()
{
    int yes = 0;
    const char* enter_cmd = gArgv[0];
    if(strcmp("cd",enter_cmd) == 0)
    {
        yes = 1;
        Cd();
    }

    return yes;
}

当时,运行上面代码后,发现命令行的当前路径提示一直不变,只有pwd里的路径才改变,导致这样的原因是没有对环境变量进行更新,需要导入环境变量,将当前的路径放到temp中,然后将cwd导入环境变量中。

char cwd[SIZE*2];
void Cd()
{
   const char* path = gArgv[1];
   if(path == NULL) path = GetHome();
   //path一定存在
   chdir(path);
   //刷新环境变量
   char temp[SIZE*2];
   getcwd(temp,sizeof(temp));
   snprintf(cwd,sizeof(cwd),"PWD=%s",temp);
   putenv(cwd);
}

执行结果虽然正确了,但是我们只想让最后一个路径显示出来(和正常的shell一样),因此,我定义了一个宏函数:

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

这个宏将穿进去的路径字符串指向最后一个/,

最后的效果如上图。

但是还有一个问题,当我们回退到根目录后,命令行不显示路径了,需要做一下特殊处理:

此外,当进程退出时,我们也想用echo看一下退出码,由于echo也是一种内建命令,因此也需要在第四步特殊判断一下:

int CheckBuilding()
{
    int yes = 0;
    const char* enter_cmd = gArgv[0];
    if(strcmp("cd",enter_cmd) == 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;
}

至此,简易的shell完成。

下面附上完整代码:

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

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

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

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

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

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 n)
{

    char* s = fgets(command, n ,stdin);
    if(s == NULL) return -1;
    command[strlen(command)-1] = ZERO;
    return strlen(command);
}
void SplitCommand(char command[],size_t n)
{
    gArgv[0] = strtok(command,SEP);
    int index = 1;
    while((gArgv[index++] = strtok(NULL,SEP)));//故意写成=,表示先赋值再判断,分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL,并且while判断结束
}
void Die()
{
    exit(-1);
}

void ExecuteCommand()
{
    pid_t id = fork();
    if(id < 0) Die();
    else if(id == 0)
    {
        //child
        execvp(gArgv[0],gArgv);
        exit(errno);
    }
    else 
    {
        //father
        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()
{
   const char* path = gArgv[1];
   if(path == NULL) path = GetHome();
   //path一定存在
   chdir(path);
   //刷新环境变量
   char temp[SIZE*2];
   getcwd(temp,sizeof(temp));
   snprintf(cwd,sizeof(cwd),"PWD=%s",temp);
   putenv(cwd);
}
int CheckBuilding()
{
    int yes = 0;
    const char* enter_cmd = gArgv[0];
    if(strcmp("cd",enter_cmd) == 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)
    {
         //1.我们需要自己输出一个命令行
         MakeCommandLineAndPrint();

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

         //3.命令行字符串分割
         SplitCommand(usercommand,sizeof(usercommand));
         //4.检查命令是否是内建命令
         n = CheckBuilding();
         if(n) continue; 
         //5.执行命令
         ExecuteCommand();
    }
    return 0;
}