初识Linux · 自主Shell编写

_lazy. 2024-10-12 14:07:06 阅读 56

目录

前言:

1 命令行解释器部分

2 获取用户命令行参数

3 命令行参数进行分割

4 执行命令

5 判断命令是否为内建命令


前言:

本文介绍是自主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是我们最常见的选择,那么我们可以:

<code> 11 char* argv[] = {

12 getenv("HOSTNAME"),

13 getenv("USER"),

14 getenv("PWD")

15 };

将获取到的环境变量放在数组argv里面,随即进行打印:

我们直接使用printf打印数组的三个元素,看起来好像没有问题,因为命令行参数是在后面输入,所以我们不能使用\n作为结束,并且,这里介绍一个函数,snprintf,我们不妨使用该函数打印,把所有的环境变量放在一个字符串里面,似乎更好控制一点,这里如果有同学的man手册配置没有齐全的话,可以使用指令:

sudo yum install man-pages

snprintf就是将所有的输出,放到一个字符串里面,此时,我们直接打印该字符即可,所以第一部分的临时代码为:

<code> 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作为一个缓冲:

<code> 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,就会继续分割,我们要做的,就是将字符串分割之后,放到数组里面,有益于后面的进程替换工作。

这里定义一个全局变量,用于存在分割后的字符串变量:

<code>#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即可:

<code>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编写就差不多了。


感谢阅读!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。