在学习了进程控制之后(进程创建、终止、等待、替换)之后,我们可以来实现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;
}