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;
}
相关推荐
Doro再努力1 小时前
Vim 快速上手实操手册:从入门到生产环境实战
linux·编辑器·vim
wypywyp2 小时前
8. ubuntu 虚拟机 linux 服务器 TCP/IP 概念辨析
linux·服务器·ubuntu
Doro再努力2 小时前
【Linux操作系统10】Makefile深度解析:从依赖推导到有效编译
android·linux·运维·服务器·编辑器·vim
senijusene2 小时前
Linux软件编程:IO编程,标准IO(1)
linux·运维·服务器
忧郁的橙子.2 小时前
02-本地部署Ollama、Python
linux·运维·服务器
醇氧2 小时前
【linux】查看发行版信息
linux·运维·服务器
No8g攻城狮3 小时前
【Linux】Windows11 安装 WSL2 并运行 Ubuntu 22.04 详细操作步骤
linux·运维·ubuntu
XiaoFan0123 小时前
免密批量抓取日志并集中输出
java·linux·服务器
souyuanzhanvip3 小时前
ServerBox v1.0.1316 跨平台 Linux 服务器管理工具
linux·运维·服务器
HalvmånEver5 小时前
Linux:线程互斥
java·linux·运维