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

请看上面的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;
}
相关推荐
zcmodeltech9 分钟前
智能矿井沙盘模型多系统协同控制系统设计:基于STM32与Modbus RTU的感知-传输-控制一体化方案
服务器·数据库·分布式·stm32·单片机·嵌入式硬件
江湖有缘20 分钟前
Docker实战 | 使用Docker部署Donetick任务与家务管理应用
运维·docker·容器
实心儿儿29 分钟前
Linux —— 应用层协议HTTP
linux·网络·http
流星白龙33 分钟前
【Docker】4.NameSpace空间隔离实战
java·运维·docker
有续技术1 小时前
哈斯 (Haas) 机床 IP、端口号配置内容总结
运维·服务器
小张同学a.2 小时前
LAMP架构2
linux·运维·网络·架构·负载均衡
千河殇 多想和她2 小时前
如何在Reaonix中使用CodeGraph以及CodeGraph效果实测经验分享
linux·运维·ubuntu
VortMall2 小时前
出海引流运维双升级|VortMall微服务商城系统v1.3.12 升级更新
运维·微服务·facebook
心念枕惊2 小时前
新写了个直播录制工具,可录制抖音快手斗鱼直播
运维·服务器·数据库
躺不平的理查德2 小时前
CMAKE备忘录
linux·运维·服务器