shell操作系统的一个外壳程序。
shell/bash也是一个进程,执行指令的时候,本质就是自己创建子进程执行的!
当我们进入shell的时候,我们知道会出现一个这样的东西
我们先来实现一下它
其实关于这些信息,在我们的环境变量里面刚好就有
所以这里,我们就不用系统调用了,而是通过环境变量来完成
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
const char* getpwd()
{
return getenv("PWD");
}
int main()
{
printf(LEFT"%s@%s %s"RIGHT LABLE"\n",getusername(),gethostname(),getpwd());
return 0;
}
如上代码所示,我们现在就可以简单的实现打开shell时候要输入命令行的界面
运行结果为
不过由于我们后序要输入一个命令,由于输入结束后本身带有换行,所以我们这里可以去掉这个换行
我们可以先直接加上一个scanf,这样的话它就会去接收命令了
但是这样对吗,我们可以加上一个打印来看看结果
运行结果如下,我们可以看到其实打印结果并非我们的预期
这其实是因为scanf它本身读取到空格就结束了,所以我们需要一次读取一行
在c语言中有一个函数叫做fgets
char *fgets(char *s, int size, FILE *stream);
这个函数的意思是,从stream这个流中,读取size个字符,放入s中。(s是放在哪里,size是可以访问多少)
如下所示,在这里我们需要注意的是,这里对于27行,我们可以不用对这个大小减一。都是可以的
运行结果为
那么在这里我们有一个问题,这里的s有可能为空吗,即有可能输入失败吗?
在不考虑设备出现问题的情况下,其实是不可能的,即便我们什么都不输入,它也会由于我们按了\n
而将这个字符给输入进去。
而且我们发现我们在打印的时候,多了一个空行,这个空行其实就是因为我们输入了这个\n
字符所导致的,所以我们可以进行一次调整
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define LINE_SIZE 1024
char commandline[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
const char* getpwd()
{
return getenv("PWD");
}
int main()
{
printf(LEFT"%s@%s %s"RIGHT LABLE" ",getusername(),gethostname(),getpwd());
char* s = fgets(commandline,sizeof(commandline),stdin);
assert(s);
commandline[strlen(commandline) - 1] = '\0';
if(s)
printf("echo : %s",commandline);
return 0;
}
运行结果为
为了方便,我们可以将对应的代码给写为一个函数
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define LINE_SIZE 1024
char commandline[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
const char* getpwd()
{
return getenv("PWD");
}
void Interact(char* cline,int size)
{
printf(LEFT"%s@%s %s"RIGHT LABLE" ",getusername(),gethostname(),getpwd());
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
}
int main()
{
Interact(commandline,sizeof(commandline));
printf("echo : %s",commandline);
return 0;
}
然后运行结果为
我们的前面的代码还有的缺陷就是只能用一次,所以我们不妨直接将其改为一个循环结构
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define LINE_SIZE 1024
char commandline[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
const char* getpwd()
{
return getenv("PWD");
}
void Interact(char* cline,int size)
{
printf(LEFT"%s@%s %s"RIGHT LABLE" ",getusername(),gethostname(),getpwd());
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
}
int main()
{
int quit = 0;
while(!quit)
{
Interact(commandline,sizeof(commandline));
printf("echo : %s\n",commandline);
}
return 0;
}
这样的话,看起来就已经像回事了
为了分析这个指令,我们首先要做的就是先将字符串给切割出来。放到一个数组里面去
于是我们可以利用这个函数
char *strtok(char *str, const char *delim);
第一个参数是待分割的字符串,第二个参数是分割的字符
这个函数每调用一次,便会截取一次字符串。具体操作方法,可见下文
字符串函数详解
最终代码如下所示
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
char commandline[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
const char* getpwd()
{
return getenv("PWD");
}
void Interact(char* cline,int size)
{
printf(LEFT"%s@%s %s"RIGHT LABLE" ",getusername(),gethostname(),getpwd());
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
}
int splitstring(char* argv[])
{
int i = 0;
argv[i++] = strtok(commandline,DELIM);
while(argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
int main()
{
char* argv[ARGC_SIZE] = {NULL};
int quit = 0;
while(!quit)
{
Interact(commandline,sizeof(commandline));
int argc = splitstring(argv);
if(argc == 0) continue;
//测试分割
for(int i = 0; argv[i]; i++)
{
printf("[%d] : %s\n",i,argv[i]);
}
}
return 0;
}
运行结果为
由于我们的命令分为内建命令和普通命令。这里我们先考虑普通命令
对于普通命令,我们的基本思路是创建一个子进程,然后通过程序替换来实现。
这里的程序替换根据我们已有的条件最好选择时候execvp/execvpe。环境变量可带可不带,但是v和p一定要带上,因为我们需要从PATH环境变量中去找指令且我们已经有了一个数组了,所以使用v形式更加方便
如下所示,我们就可以实现一个简单的普通命令的执行了
#include
#include
#include
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
char commandline[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
const char* getpwd()
{
return getenv("PWD");
}
void Interact(char* cline,int size)
{
printf(LEFT"%s@%s %s"RIGHT LABLE" ",getusername(),gethostname(),getpwd());
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
}
int splitstring(char* argv[])
{
int i = 0;
argv[i++] = strtok(commandline,DELIM);
while(argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
int main()
{
extern char** environ;
char* argv[ARGC_SIZE] = {NULL};
int quit = 0;
while(!quit)
{
//2.交互问题,解决命令行
Interact(commandline,sizeof(commandline));
//3.子串分割问题,解析命令行
int argc = splitstring(argv);
if(argc == 0) continue;
// for(int i = 0; argv[i]; i++)
// {
// printf("[%d] : %s\n",i,argv[i]);
// }
//4.指令的判断(内建命令和普通命令)
//5.普通命令的执行
pid_t id = fork();
if(id < 0)
{
perror("fork");
}
else if (id == 0)
{
//子进程执行命令
execvpe(argv[0],argv,environ);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
}
}
}
return 0;
}
我们可以简单的使用一下
我们发现这些命令都可以正常的使用。
不过相比原本的shell还是有一些区别的,比如说,我们的shell中像目录文件,可执行程序没有颜色。ll指令还没有办法解析(因为没有支持改名)等问题。
而且还有当我们想要使用内建命令的时候,是没有任何效果的
因为这是子进程在跑这个命令,最后变化的都是子进程的目录等,最后子进程退出了。所以最终就没有任何变化。
我们现在可以将普通指令的执行给封装为一个函数,如下所示
#include
#include
#include
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE] = {NULL};
extern char** environ;
int last_code = 0;
int quit = 0;
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
const char* getpwd()
{
return getenv("PWD");
}
void Interact(char* cline,int size)
{
printf(LEFT"%s@%s %s"RIGHT LABLE" ",getusername(),gethostname(),getpwd());
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
}
int splitstring(char cline[],char* _argv[])
{
int i = 0;
_argv[i++] = strtok(cline,DELIM);
while(_argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
void NormalExcute(char* _argv[])
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if (id == 0)
{
//子进程执行命令
execvpe(_argv[0],_argv,environ);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
last_code = WEXITSTATUS(status);
}
}
}
int main()
{
while(!quit)
{
//2.交互问题,解决命令行
Interact(commandline,sizeof(commandline));
//3.子串分割问题,解析命令行
int argc = splitstring(commandline,argv);
if(argc == 0) continue;
//4.指令的判断(内建命令和普通命令)
//5.普通命令的执行
NormalExcute(argv);
}
return 0;
}
对于内建命令,我们则是直接判断即可
比如下面
运行结果最终为,我们可以发现确实更改当前目录了
不过我们会发现,这个前面字符串的显示还是存在一些问题的
这是因为我们当前所使用的是环境变量的方式,我们可以去sprintf函数去修改环境变量。不过这样有很多比较麻烦的事情
最简单的方式是,我们不用环境变量来获取当前工作目录了,我们可以直接用getcwd这个函数来使用,它可以获得当前的工作目录。放入一个数组中。
如下代码所示,完成了cd的内建命令,并且我们实现了ls在显示时候的颜色输出。
#include
#include
#include
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE] = {NULL};
extern char** environ;
int last_code = 0;
int quit = 0;
char pwd[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
void Interact(char* cline,int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),pwd);
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
}
int splitstring(char cline[],char* _argv[])
{
if(strcmp(cline,"") == 0) return 0;
int i = 0;
_argv[i++] = strtok(cline,DELIM);
while(_argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
void NormalExcute(char* _argv[])
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if (id == 0)
{
//子进程执行命令
execvpe(_argv[0],_argv,environ);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
last_code = WEXITSTATUS(status);
}
}
}
int BuildCommand(char* _argv[],int _argc)
{
if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
if(_argc > 0 && strcmp(_argv[0],"ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit)
{
//2.交互问题,解决命令行
Interact(commandline,sizeof(commandline));
//3.子串分割问题,解析命令行
int argc = splitstring(commandline,argv);
if(argc == 0) continue;
//4.指令的判断(内建命令和普通命令)
int n = BuildCommand(argv,argc);
//5.普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
我们再来实现一下其他的内建命令,比如echo命令,如果我们直接使用的话,相当于是调用了子进程中的echo命令,它会出现这样的问题,无论我们输入什么,最终也依然输出什么
而且如果我们导入环境变量的时候,直接使用export也是不可以的
因为这是创建了子进程才导入的,我们不希望创建子进程,所以就需要内建命令
直接在这里putenv即可
这样的话,就有了这个环境变量了
不过上面的其实还存在很多问题
因为如果我们再次使用一下export的话,我们会发现原来加入的环境变量消失了
这其实是因为我们前面的代码是将_argv中的内容直接导入到了环境变量中。这里的导入不是说拷贝一份。而是将这个地址写入到了环境变量所对应的空间中。而我们后面再次导入的时候已经将这块的内容修改了。所以说原来的就不见了。总之环境变量表保存的不是字符串,而是地址。
所以我们需要专门开辟一块空间用来存储环境变量
然后我们最后将echo给实现一下即可。
#include
#include
#include
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE] = {NULL};
extern char** environ;
int last_code = 0;
int quit = 0;
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
void Interact(char* cline,int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),pwd);
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
}
int splitstring(char cline[],char* _argv[])
{
if(strcmp(cline,"") == 0) return 0;
int i = 0;
_argv[i++] = strtok(cline,DELIM);
while(_argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
void NormalExcute(char* _argv[])
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if (id == 0)
{
//子进程执行命令
execvpe(_argv[0],_argv,environ);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
last_code = WEXITSTATUS(status);
}
}
}
int BuildCommand(char* _argv[],int _argc)
{
if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
{
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
{
if(*_argv[1] == '$')
{
char* val = getenv(_argv[1] + 1);
if(val) printf("%s\n",val);
}
else
{
printf("%s\n",_argv[1]);
}
return 1;
}
if(_argc > 0 && strcmp(_argv[0],"ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit)
{
//2.交互问题,解决命令行
Interact(commandline,sizeof(commandline));
//3.子串分割问题,解析命令行
int argc = splitstring(commandline,argv);
if(argc == 0) continue;
//4.指令的判断(内建命令和普通命令)
int n = BuildCommand(argv,argc);
//5.普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
当子进程退出的时候,它会有一个退出码。
所以我们可以对于echo的内建命令在加上一个打印退出码的操作
#include
#include
#include
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE] = {NULL};
extern char** environ;
int last_code = 0;
int quit = 0;
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
void Interact(char* cline,int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),pwd);
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
}
int splitstring(char cline[],char* _argv[])
{
if(strcmp(cline,"") == 0) return 0;
int i = 0;
_argv[i++] = strtok(cline,DELIM);
while(_argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
void NormalExcute(char* _argv[])
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if (id == 0)
{
//子进程执行命令
execvpe(_argv[0],_argv,environ);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
last_code = WEXITSTATUS(status);
}
}
}
int BuildCommand(char* _argv[],int _argc)
{
if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
{
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
{
if(strcmp(_argv[1],"$?") == 0)
{
printf("%d\n",last_code);
last_code = 0;
}
else if(*_argv[1] == '$')
{
char* val = getenv(_argv[1] + 1);
if(val) printf("%s\n",val);
}
else
{
printf("%s\n",_argv[1]);
}
return 1;
}
if(_argc > 0 && strcmp(_argv[0],"ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit)
{
//2.交互问题,解决命令行
Interact(commandline,sizeof(commandline));
//3.子串分割问题,解析命令行
int argc = splitstring(commandline,argv);
if(argc == 0) continue;
//4.指令的判断(内建命令和普通命令)
int n = BuildCommand(argv,argc);
//5.普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}
运行效果如下
所以,当我们进行登录的时候,系统就要启动一个shell进程,那么我们shell本身的环境变量是从哪里来的???
其实在我们用户的目录下,就一个bash_profile文件
它里面的内容是这样的,这里面就会帮助我们导入环境变量
类似的,还有.bashrc文件等
所以说
当用户登录的时候,shell会读取用户目录下的.bashprofile文件,里面保存了导入环境变量的方式!
#include
#include
#include
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE] = {NULL};
extern char** environ;
int last_code = 0;
int quit = 0;
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
void Interact(char* cline,int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),pwd);
char* s = fgets(cline,size,stdin);
(void)s;
assert(s);
commandline[strlen(cline) - 1] = '\0';
}
int splitstring(char cline[],char* _argv[])
{
if(strcmp(cline,"") == 0) return 0;
int i = 0;
_argv[i++] = strtok(cline,DELIM);
while(_argv[i++] = strtok(NULL,DELIM));
return i - 1;
}
void NormalExcute(char* _argv[])
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if (id == 0)
{
//子进程执行命令
execvpe(_argv[0],_argv,environ);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid == id)
{
last_code = WEXITSTATUS(status);
}
}
}
int BuildCommand(char* _argv[],int _argc)
{
if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
{
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
{
if(strcmp(_argv[1],"$?") == 0)
{
printf("%d\n",last_code);
last_code = 0;
}
else if(*_argv[1] == '$')
{
char* val = getenv(_argv[1] + 1);
if(val) printf("%s\n",val);
}
else
{
printf("%s\n",_argv[1]);
}
return 1;
}
if(_argc > 0 && strcmp(_argv[0],"ls") == 0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit)
{
//2.交互问题,解决命令行
Interact(commandline,sizeof(commandline));
//3.子串分割问题,解析命令行
int argc = splitstring(commandline,argv);
if(argc == 0) continue;
//4.指令的判断(内建命令和普通命令)
int n = BuildCommand(argv,argc);
//5.普通命令的执行
if(!n) NormalExcute(argv);
}
return 0;
}