初识Linux · 自主Shell编写
本文介绍是自主Shell编写,对于shell,即外壳解释程序,我们目前接触到的命令行解释器,有bash,还有SSH,对于今天模拟实现的Shell编写,我们模拟的是bash,以及需要的预备知识前文已经介绍了,进程的多方面的知识,在自主Shell编写里面比较重要的是进程程序替换,进程终止,进程等待,进程状态什么的,都是自主Shell编写里面的辅助知识罢了。那么,话不多说,我们直接进入到Shell编写部
目录
前言:
本文介绍是自主Shell编写,对于shell,即外壳解释程序,我们目前接触到的命令行解释器,有bash,还有SSH,对于今天模拟实现的Shell编写,我们模拟的是bash,以及需要的预备知识前文已经介绍了,进程的多方面的知识,在自主Shell编写里面比较重要的是进程程序替换,进程终止,进程等待,进程状态什么的,都是自主Shell编写里面的辅助知识罢了。
那么,话不多说,我们直接进入到Shell编写部分。
1 命令行解释器部分
我们在Centos版本下进行演示,首先,我们平常看到的命令行解释器,呈现的都是这个模样,最开始的_lazy是当前的用户名,@后面的VM-12-14-centos代表的是当前主机名称,后面的~代表的我们所处的当前目录,那么我们这里,就应该要复刻一个一样的出来。
那么第一个问题来了,我们从哪里获取对应的用户名主机名以及目前的目录呢?
此时,前文引进的环境变量,就应该出场了:
输入了env之后,我们可以在环境变量表里面看到许多对应的环境变量,其中HOSTNAME,PWD,USER分别代表的就是主机名称,当前路径,当前用户名。
那么我们如何通过获取?我们已知的是有3种方式,一种是environ,一种是命令行参数表,一种是getenv。
我们这里使用getenv,相对于二级指针environ,getenv是我们最常见的选择,那么我们可以:
11 char* argv[] = {
12 getenv("HOSTNAME"),
13 getenv("USER"),
14 getenv("PWD")
15 };
将获取到的环境变量放在数组argv里面,随即进行打印:
我们直接使用printf打印数组的三个元素,看起来好像没有问题,因为命令行参数是在后面输入,所以我们不能使用\n作为结束,并且,这里介绍一个函数,snprintf,我们不妨使用该函数打印,把所有的环境变量放在一个字符串里面,似乎更好控制一点,这里如果有同学的man手册配置没有齐全的话,可以使用指令:
sudo yum install man-pages
snprintf就是将所有的输出,放到一个字符串里面,此时,我们直接打印该字符即可,所以第一部分的临时代码为:
34 void OutputBash()
35 {
36 char line[SIZE];
37
38 char* username = GetUser();
39 char* hostname = Gethost();
40 char* cwd = Getcwd();
41
42 snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,cwd);
43 printf("%s",line);
44 fflush(stdout);
45
46 // char* argv[] = {
47 // getenv("HOSTNAME"),
48 // getenv("USER"),
49 // getenv("PWD")
50 // };
51 // char* line;
52 // //printf("[%s@%s %s]>",argv[0],argv[1],argv[2]);
53 // fflush(stdout);
54 }
8 #define SIZE 512
9
10
11 char* GetUser()
12 {
13 char* user = getenv("USER");
14 if(user == NULL) return NULL;
15 return user;
16
17 }
18
19 char* Gethost()
20 {
21 char* host = getenv("HOSTNAME");
22 if(host == NULL) return NULL;
23 return host;
24 }
25
26 char* Getcwd()
27 {
28 char* cwd = getenv("PWD");
29 if(cwd == NULL) return NULL;
30 return cwd;
31
32 }
但是为什么要说这是临时的呢?因为我们的pwd并不完善:
目前,打印的出来并不是最完善的,较为完善的应该是只打印当前目录。
那么如何保证修饰一下呢?
我们可以将该字符串进行分割,也就是使用指针,将该指针的指向指到最后一个/指向的地方即可。但是这里不推荐使用函数,如果使用的是函数,我们就要使用二级指针,实属麻烦,所以可以使用宏即可:
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
那么判断的条件就是,只要p碰到了根目录就停下,但是有个缺陷就是:
/还是存在,那么我们可以这样操作:
snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,strlen(cwd) == 1 ? "/" : cwd + 1);
此时,较为完善的命令行解释器部分就打印出来了:
2 获取用户命令行参数
第一个问题我们解决了,我们现在该获取用户的命令行参数了。
在获取用户命令行参数这里,我们要注意的点是,我们应该使用什么函数来获取?
可不可以使用scanf来获取呢?如果使用scanf,那么ls -l -n -a,能获取到多少呢?
我们知道scanf是通过空格或者换行符来获取的,此时ls -l -n -a,就只能获取到ls,所以我们应该换个函数,这里推荐fgets,其实gets也是可以的,但是因为后面有文件的IO操作,所以我们使用fgets作为一个缓冲:
57 int GetUserCommand(char* usercommand,size_t n)
58 {
59 char* s = fgets(usercommand,n,stdin);
60 if(s == NULL) return -1;
61
62 return strlen(s);
63 }
但是该代码存在一定的缺陷。
在第4部分会有提示。
3 命令行参数进行分割
获取到了对应的命令,那么执行的时候,不能带空格去执行吧?所以我们要使用函数,将命令行参数进行分割,这里使用的函数是C语言的库函数,strtok,相信许多同学已经忘记了,不急:
第一个参数是分割的字符串,第二个参数是分割符,那么第一次分割之后,将第一个参数置为NULL,就会继续分割,我们要做的,就是将字符串分割之后,放到数组里面,有益于后面的进程替换工作。
这里定义一个全局变量,用于存在分割后的字符串变量:
#define SEP " "
char* gArgv[SIZE];
这里有一个非常细小的地方,如果我们使用单引号的空格,虽然也是空格,但是和strtok就不匹配了,因为这并不是cosnt char* ,这只是一个字符而已。
70 void SplitCommand(char* usercommand)
71 {
72 gArgv[0] = strtok(usercommand,SEP);
73 int index = 1;
74 while((gArgv[index++] = strtok(NULL,SEP)));//分割之后函数返回NULL 恰好作为结尾
75
76 }
此时有个很不错的代码细节,因为函数分割完返回的就是NULL,刚好可以作为数组的结束标志。
4 执行命令
到现在,我们可以不管三七二十一,直接执行命令了,至少我们现在先不用管命令是不是内建命令,我们就执行几个简单的即可。
那么要执行命令,我们肯定涉及到进程程序替换。因为分割好的命令我们已经放在了全局变量里面,所以我们可以直接创建函数了:
85 void ExcuteCommand()
86 {
87 pid_t id = fork();
88
89 if(id < 0) Die();
90 else if(id == 0)
91 {
92 //child
93 execvp(gArgv[0],gArgv);
94 exit(1);
95 }
96 else
97 {
98 //father
99 int status = 0;
100 pid_t rid = waitpid(id,&status,0);
101 if(rid > 0)
102 {
103 lastcode = WEXITSTATUS(status);
104 if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
105 }
106 }
107
108
109 }
这些代码都是进程替换的时候介绍过的了,无非是加修饰,让代码更加美观,此时,咱们就可以跑了,但是有同学仍会发现,不管怎么运行,都是不可以的,因为我们命令行输入的时候,都会自动的输入一个回车,这个回车,导致了我们跑不了,所以我们需要将回车干掉:
宏定义ZERO即可。
此时,我们就可以正常的执行了。
5 判断命令是否为内建命令
那么现在问题来了,如果我们是执行的ehco,cd这种内建命令,即只能父进程来执行的,我们就不能创建子进程了,判断是否为内建命令,条件成立就内建执行即可,并且跳过下一步:
那么判断内建命令的方式也是十分简单粗暴的,strcmp即可:
110 void Cd()
111 {
112 const char *path = gArgv[1];
113 if(path == NULL) path = Gethost();
114 // path 一定存在
115 chdir(path);
116
117 // 刷新环境变量
118 char temp[SIZE*2];
119 getcwd(temp, sizeof(temp));
120 snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
121 putenv(cwd); // OK
122 }
123
124 int IsInorder()
125 {
126 int yes = 0;
127 const char *enter_cmd = gArgv[0];
128 if(strcmp(enter_cmd, "cd") == 0)
129 {
130 yes = 1;
131 Cd();
132 }
133 else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
134 {
135 yes = 1;
136 printf("%d\n", lastcode);
137 lastcode = 0;
138 }
139 return yes;
140 }
这里拿cd举例子,判断cd是内建命令之后,在cd函数实现,因为我们要该目录,所以使用函数chdir,改变当前工作目录,改变了之后,改变环境变量中的PATH即可。此时自主shell编写就差不多了。
感谢阅读!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)