1. 打印命令行提示符
在我们使用系统提供的shell时,每次都会打印出一行字符串,这其实就是命令行提示符,那我们自定义的shell当然也需要这一行字符串。
这一行字符串包含用户名,主机名,当前工作路径,所以,我们在打印这行字符串时,需要获取这些信息。根据我们之前学过的知识,我们可以用getenv系统调用来获取!
这里有一个接口gethostname,我试过了用getenv来获取系统的主机名,但是,在我的系统上似乎无法获取,这可能和系统有关,但是我们用gethostname这个接口也可以很安全的获得主机名,具体用法用man手册看一看也就会了。
结果没有问题:
2. 获取用户输入
当我们解决了命令行提示符的问题后,接下来我们就会注意到每次执行指令都会有一个光标在闪烁等待用户输入指令!所以我们现在就要解决这个问题!
如果用scanf来获取缓冲区的字符串坑定是不行的,因为scanf默认以空格作为分隔符,而我们在输入指令带选项时,就会有空格!
那我们就用fgets:
可是为什么回显时有两次换行呢?
原因含很简单:我们在输入指令时,最后输入的换行也在缓冲区中被fges获取保留在数组commandline的最后一个字符,解决方法也很简单,只需要把最后一个字符置为0即可!
现在写的代码还是不够优雅,我们稍微封装一下:
1 #include <iostream>
2 #include <cstdlib>
3 #include <cstdio>
4 #include <unistd.h>
5 #include <cstring>
6
7 using namespace std;
8
9 #define COMMAND_SIZE 1024
10 #define FORMAT "%s@%s:%s$ "
11
12 const char* get_user_name()
13 {
14 const char* user=getenv("USER");
15 return user==NULL?"NONE":user;
16 //return user;
17 }
18
19 const char* get_pwd()
20 {
21 const char* pwd=getenv("PWD");
22 return pwd==NULL?"NONE":pwd;
23 }
24
25 //制作命令行提示符Command Prompt
26 void make_command_prompt(char cmd_prompt[],int size)
27{
28 char hostname[256];
29 gethostname(hostname,sizeof(hostname));
30 snprintf(cmd_prompt,size,FORMAT,get_user_name(),hostname,get_pwd());
31 }
32
33 //打印命令行提示符
34 void print_cmd_prompt()
35 {
36 char prompt[COMMAND_SIZE];
37 make_command_prompt(prompt,sizeof(prompt));
38 printf("%s",prompt);
39 fflush(stdout);
40 }
41
42 //获取用户输入的命令
43 bool get_command(char* out,int size)
44 {
45 char* c=fgets(out,size,stdin);
46 if(c==NULL) return false;
47 out[strlen(out)-1]=0;
48 //如果用户什么都没有输入则返回false
49 if(strlen(out)==0) return false;
50 return true;
51 }
52
53 int main()
54 {
55 //1.打印命令行提示法
56 print_cmd_prompt();
57 //2.获取用户输入的命令
58 char commandline[COMMAND_SIZE];
59 if(get_command(commandline,sizeof(commandline)))
60 {
61 printf("%s\n",commandline);
62 }
63 return 0;
64 }
我们使用的shell是不断在获取用户的指令的,也就是说shell一旦跑起来就是一个死循环,直到我们退出shell!所以我们还应该将我们的主体逻辑改一下!
3. 解析命令行
我们获取了用户输入的字符串后【ls -a -l】,我们不可能用这一长串字符串去执行我们的指令,我们需要做的下一步就是将我们获取的字符串按空格切割!具体如何做到如下:
我们先在全局定义一个命令行参数表char* g_argv[MAXARGC]来记录我们切割的命令行参数
接下来,我们封装一个函数来完成我们的切割任务:
测试函数:
测试结果:
4. 执行命令
执行命令也非常简单,这需要用到我们之前学过的知识,创建子进程,将子进程进行程序替换!
5.简化工作路径的显示
通过上图我们可以观察到我们自定义的shell显示的工作路径太长了,为了和原shell尽可能保持一致,所以我们封装一个函数来解决这个问题!
6. 检测并处理内建命令
我们在输入ls,pwd等命令时,我们自定义的shell雀氏可以很好的帮我们完成工作。但是,当我们输入cd,export等命令时,此时的shell就不再适用了。cd命令是改变当前的工作路径,但是我们自定义的shell是子进程通过进程替换的方式帮我们执行命令,而cd这类命令是去环境变量表中那到当前的工作路径,我们需要更改父进程bash的环境变量。所以对于cd这类的命令,我们需要用父进程去执行。而cd这类的命令我们又称为内建命令,因此,在执行命令之前,我们需要一个检测并处理内建命令的操作!
下面是测试结果:
我们发现工作路径果然发生改变了,但是命令行显示的路径为什么没有发生改变呢?
但cd命令执行时,先是进程的工作路径发生改变,然后环境变量中记录的工作路径再改变,而这个工作也是由shell来完成的,但是目前我们的自定义shell还没有实现这个功能!并且,我们获取当前工作路径是通过获取环境变量的方式拿到的,所以我们在命令行中显示的工作路径永远是久的!
因此,获取当前工作路径有一个更好的方式->系统调用【getcwd】!
下面的测试就符合预期了!
但是,环境变量中的pwd是实实在在发生了变化的,所以我们自定义的shell也应该实现这一个功能!
所以,我们仅需要在获取当前工作路径之后,用puenv导入到环境变量中即可!
当然,还有许多内建命令,比如echo,我们可以完善这些内建命令,这里就不写了【比较懒】。
7. 完善环境变量表
目前这里自定义的shell只有命令行参数表,还缺少一张环境变量表。父进程bash在启动时,从配置文件中获取环境变量,子进程则继承父进程的环境变量。如果我们要模拟bash获取环境变量的方式,就必须从配置文件中那数据。但是,这里目前是做不到的【没办法到配置文件中拿数据】。
不过,我们自定义的shell本质上还是bash的子进程,所以我们可以到父进程中获取环境变量!