Linux小程序(2)—— bash模拟实现

在学习了进程控制之后(进程创建、终止、等待、替换)之后,我们可以来实现bash的简单模拟。

进程创建与终止

进程等待

进程替换


什么是shell/bash?

shell/bash是操作系统的外壳程序,负责帮助用户进行指令的执行;拿到用户的命令后交给操作系统,再将结果返回给用户。shell/bash本质上也是一个可执行程序。

要模拟实现bash,我们要实现的功能主有三个:

  • 提示词,显示包括用户名和主机名等信息
  • 获取用户的输入,并能将命令和选项分割提取到
  • 将获取的命令交给操作系统执行并返回结果

获取用户名

我们可以看待bash的每行命令前都会显示当前用户名和主机名:

可以看到是 [用户名@主机名 当前路径] 的格式,用户名和主机名是存储于环境变量中的。我们可以使用env查看环境变量。

用户名存放于环境变量 USER 中;主机名存放于环境变量 HOSTNAME 中;当前路径存放于 PWD 中。因此我们需要使用getenv函数来获取环境变量。

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

const char* getusername()
{
    return getenv("USER");
}

const char* gethostname()
{
    return getenv("HOSTNAME");
}

const char* getpwd()
{
    return getenv("PWD");
}


int main()
{
    return 0;
}                 

之后我们仿照bash的格式,添加上[];为了美观我们可以define重新定义一下:

cpp 复制代码
#define LEFT "【"
#define RIGHT "】"
#define LABLE "¥"

之后再main函数中用printf打印:

cpp 复制代码
printf(LEFT"%s@%s %s"RIGHT,getusername(),gethostname(),getpwd());

括号和¥符号都是自定义的,换成其他符号也可以。


用户输入

输入

bash的一个重要功能就是要获取用户输入的指令。所以,下面我们来实现用户输入的部分。我们单独创建一个userInput的函数来实现这个功能。

我们先为用户输入开辟一块空间:

cpp 复制代码
#define Input_size 1024
char userline[Input_size];

之后让用户进行输入,并将输入的内容存放到该数组中。我们是否可以使用scanf函数来进行输入呢?先直接在main函数中测试:

cpp 复制代码
scanf("%s",userline);//将用户输入的内容放到数组

我们再加一行测试代码,打印出数组的内容,看是否将用户输入的内容存入数组:

cpp 复制代码
printf("echo:%s\n",userline);

运行一下,发现数组中只存进了ls:

这是因为scanf函数在读取到空格时就会终止。所以我们不能使用scanf函数来实现。

这个地方我们采用fgets函数。我们使用man命令查看关于它的说明:

它的头文件为stdio.h。如果读取成功则返回对应的字符串地址,失败则返回NULL。我们可以看到它有三个参数,第一个表示我们需要将文件放入哪个缓存区;第二个表示这个缓存区有多大;第三个则表示文件对象。

Linux系统会默认为我们打开三个输入输出流,分别为:

我们直接从stdin中读取即可。所以,我们的输入代码如下:

cpp 复制代码
void userInput(char* uline,int size)
{
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),getpwd());
    char* s = fgets(uline,size,stdin);
}

int main()
{
    char userline[Input_size];
    userInput(userline,sizeof(userline));
    printf("echo:%s\n",userline);
    return 0;    
}

这样我们就成功读取到了输入的全部内容。


不读取换行符

不过,我们可以看到显示的结果后面还有一个空行,这是因为我们在输入命令之后还要敲换行符,而这个换行符也会被读取,所以存放到数组中的最后一个元素是我们最后输入的换行符。

那么,怎么去掉这个空行,即不让数组读取到这个换行符呢?

因为换行符是存储在数组中的,我们通过数组下标的方式找到这个换行符替换为终止符即可。

cpp 复制代码
void userInput(char* uline,int size)
{
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),getpwd());
    char* s = fgets(uline,size,stdin);
    assert(s);//输入为空则报错
    uline[strlen(uline)-1] = '\0';
//strlen函数用于获取字符串长度;返回的是\0之前的字符长度
//我们的输入最后一个是换行符\n,其在数组中的下标就是长度-1;
//然后将该换行符替换为终止符\0 
}

持续使用

我们现在的代码执行一次后就退出了,而bash是可以一直使用的。所以,我们还需要实现循环使用的功能,让程序成为一个死循环,只要不退出就会一直使用:

cpp 复制代码
int main()
{
    char userline[Input_size];
    while(1)
    {
        userInput(userline,sizeof(userline));
        printf("echo:%s\n",userline);
    }
    return 0;    
}

字符串分割

不过,我们现在虽然能够获取输入的字符串,但是是整体的一整行;我们的一个命令是由多个字符串组成的,例如 ls -a -l ;是由三个字符串组成的,我们该怎样将它们分割开来?

这里我们使用strtok函数。

第一个参数表示要分割的子串;第二个参数表示分割符(默认为空格)。其返回值为分割出来的子串。不过调用一次,该函数只能分割处一个子串。所以如果想要全部分割,就需要循环去调用它。

cpp 复制代码
int devideString(char userline[],char* argv[])
{
    int i = 0;
    argv[i++] = strtok(userline,DELIM);
    while(argv[i++] = strtok(NULL,DELIM));
    return i-1;//返回有几个子字符串
}

并套用在bash的循环中:

cpp 复制代码
while(1)
{
    userInput(userline,sizeof(userline));
    printf("echo:%s\n",userline);
    int argc = devideString(userline,argv);
    if(argc == 0) continue;//如果为空继续输入
    for(int i = 0;argv[i];i++) printf("[%d]:%s\n",i,argv[i]);//观察是否分割成功
}

运行后发现成功分割:


执行命令

另外一个重要的功能就是将获取到的命令交给操作系统去执行,并返回结果。而这个流程不就是整个进程控制的内容吗?

所以在main函数中:

cpp 复制代码
int main()
{
    extern char** environ;
    int i = 0;
    char* argv[ARGC_SIZE];
    char userline[Input_size];
    while(1)
    {
        userInput(userline,sizeof(userline));
        printf("echo:%s\n",userline);
        int argc = devideString(userline,argv);
        if(argc == 0) continue;//如果为空继续输入
        for(int i = 0;argv[i];i++) printf("[%d]:%s\n",i,argv[i]);

        pid_t id = fork();
        if(id<0)
        {
            perror("fork failed");
            continue;
        }
        else if(id == 0)
        {
        //子进程执行命令
            execvpe(argv[0],argv,environ);
            exit(199);//随便给一个退出码
//由于执行到这里必定是替换出现问题,所以给一个退出码这样我们就能拿到异常信息
        } 
        else
        {
            int status = 0;
            pid_t rid = waitpid(id,&status,0);
            if(rid == id)//等待成功
            {printf("等待成功\n");}       
        }
    }
    return 0;
}

完整代码

通过上面的实现,我们已经基本完成了bash的模拟,可以获取用户输入的指令并交给操作系统执行。完整代码如下:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>

#define LEFT "【"
#define RIGHT "】"
#define LABLE "¥"
#define DELIM " "
#define Input_size 1024
#define ARGC_SIZE 32
#define EXIT_CODE 199

const char* getusername()
{
    return getenv("USER");
}

const char* gethostname()
{
    return getenv("HOSTNAME");
}

const char* getpwd()
{
    return getenv("PWD");
}

void userInput(char* uline,int size)
{
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),getpwd());
    char* s = fgets(uline,size,stdin);
    assert(s);//输入为空则报错
    uline[strlen(uline)-1] = '\0';
//strlen函数用于获取字符串长度;返回的是\0之前的字符长度
//我们的输入最后一个是换行符\n,其在数组中的下标就是长度-1;
//然后将该换行符替换为终止符\0 
}

int devideString(char userline[],char* argv[])
{
    int i = 0;
    argv[i++] = strtok(userline,DELIM);
    while(argv[i++] = strtok(NULL,DELIM));
    return i-1;//返回有几个子字符串
}

int main()
{
    extern char** environ;
    int i = 0;
    char* argv[ARGC_SIZE];
    char userline[Input_size];
    while(1)
    {
        userInput(userline,sizeof(userline));
        printf("echo:%s\n",userline);
        int argc = devideString(userline,argv);
        if(argc == 0) continue;//如果为空继续输入
        for(int i = 0;argv[i];i++) printf("[%d]:%s\n",i,argv[i]);

        pid_t id = fork();
        if(id<0)
        {
            perror("fork failed");
            continue;
        }
        else if(id == 0)
        {
        //子进程执行命令
            execvpe(argv[0],argv,environ);
            exit(199);//随便给一个退出码
//由于执行到这里必定是替换出现问题,所以给一个退出码这样我们就能拿到异常信息
        } 
        else
        {
            int status = 0;
            pid_t rid = waitpid(id,&status,0);
            if(rid == id)//等待成功
            {printf("等待成功\n");}       
        }
    }
    return 0;
}
相关推荐
嵌入式-老费2 小时前
Linux camera驱动开发(开篇)
linux·运维·驱动开发
Python-AI Xenon2 小时前
RHEL / CentOs 7.9 离线升级OpenSSH完整指南
linux·centos·numpy
蜡笔小新拯救世界2 小时前
简单rce的ctf题目绕过
linux·c++·web安全·c#
运维有小邓@2 小时前
如何在 CentOS 主机上配置集中式 Syslog 服务器
linux·服务器·centos
市安2 小时前
负载均衡入门:HAProxy 双 Web 节点集群配置与验证
linux·运维·服务器·网络·nginx·负载均衡·haproxy
强风7942 小时前
Linux—Socket编程TCP
linux·服务器·tcp/ip
_OP_CHEN2 小时前
【Linux系统编程】(二十二)从磁盘物理结构到地址映射:Ext 系列文件系统硬件底层原理深度剖析
linux·操作系统·文件系统·c/c++·计算机硬件·ext文件系统·磁盘寻址
一直跑2 小时前
通过所里的服务器连接到组里的服务器,然后可视化组里的文件和代码,并修改等操作(VScode/vscode/mobaxterm)
linux·运维·服务器
码农编程录3 小时前
【notes11】并发与竞争
linux