送给大家一句话:
不管前方的路有多苦,只要走的方向正确,不管多么崎岖不平,都比站在原地更接近幸福。 —— 宫崎骏《千与千寻》
自主shell命令编写
1 前言
前几篇文章,我们学习进程的相关知识:进程概念,进程替换,进程控制。熟悉了进程到底是个什么事情,接下来我们来做一个实践,来运用我们所学的相关知识。这个项目就是手搓一个shell模块,模拟实现Xshell中的命令行输入。
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束:
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。
所以要写一个shell,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork),防止打扰主程序的运行
- 替换子进程(execvp),来执行对应功能。
- 父进程等待子进程退出(wait)
根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了
2 项目实现
为了保证项目文件的优雅美观,我们按照功能来书写不同函数:
- 创建自己的命令行
- 获取命令
- 分割命令
- 创建进程执行命令
2.1 创建命令行
该模块我们需要实现类似:
获取这些信息大家应该都知道吧!通过对环境变量我们就可以获取到这些信息。使用getenv()
函数就可以完成操作。
#include<stdio.h> 2 #include<sys/types.h> 3 #include<sys/wait.h> 4 #include<stdlib.h> 5 #include<unistd.h> 6 #include<string.h> 7 //大小宏 8 #define SIZE 256 9 //获取用户名 10 const char* GetUsername() 11 { 12 const char* name = getenv("USER"); 13 if(name == NULL) return "NONE"; 14 return name; 15 } 16 //获取机器信息 17 const char* GetHostName() 18 { 19 const char* hostname = getenv("HOSTNAME"); 20 return hostname; 21 } 22 //获取当前目录 23 const char* GetCwd() 24 { 25 const char* cwd = getenv("PWD"); 26 if(cwd == NULL) return "NONE"; 27 return cwd; 28 } 29 30 void MakeCommandLineAndPrint() 31 { //设置命令行字符串 32 char line[SIZE]; 33 const char* username = GetUsername(); 34 const char* hostname = GetHostName(); 35 const char* cwd = GetCwd(); //将获取的三个数据写入命令行中 36 sprintf(line,"[%s@%s %s]> ",username,hostname,cwd); 37 printf("%s",line); 38 fflush(stdout);//为了将命令行刷新出来 39 } 40 41 int main() 42 { 43 //创建我们自己的命令行 44 MakeCommandLineAndPrint(); 45 int a = 0; scanf("%d",&a); //阻断一下方便查看 46 return 0; 47 }
这里使用的sprintf()
函数是向流中写入格式化信息的好工具。这一段函数大家都可以看明白,就是获取三个变量,然后通过Line数组进行中转,然后打印出来。来看效果:
这时候发现,我们的所在目录全部都别打印出来了,我们可以进行一下优化:
#define SkipPath(p) do{ p += strlen(p)-1 ;while(*p != '/') p--; p++; }while(0);
通过这个宏定义就可以只保留最后的目录。
这里之所以不使用函数,是因为使用函数会涉及二级指针,会比较复杂!!!
来看效果:
这样就非常完美了!!
2.2 获取命令
这个模块可以说是非常关键的一步了,只有正确获取了对应命令,我们才好打开新进程来执行命令。
#define ZERO '\0' 45 int GetUserCommand(char* command,int n) 46 { 47 if(command == NULL) return -1; 48 fgets(command,n,stdin); 49 command[strlen(command) - 1] = ZERO; 50 return strlen(command); 51 }
这样我们就可以获取命令行输入的字符串了。
2.3 分割命令
获取命令之后,我们还需要对输入的一串命令来进行分割,来保证我们可以正常执行命令
12 #define SEP " " ... 14 //全局命令 方便操作 15 char* gArgv[NUM]; ... 58 void SplitCommand(char command[] , size_t n) 59 { 60 (void)n; 61 gArgv[0] = strtok(command,SEP); 62 int index = 1; 63 // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束 64 while((gArgv[index++] = strtok(NULL,SEP))); 65 }
我们使用来strtok()
函数:
char *strtok(char *str, const char *delim)
- str—要被分解的字符串
- delim—用作分隔符的字符(可以是一个,也可以是集合)在这里我们使用宏定义SEP( 代表 “ ” )
- 第一次调用strtok(),传入的参数str是要被分割的字符串{aaa - bbb -ccc},而成功后返回的是第一个子字符串{aaa};
- 第二次调用strtok的时候,传入的参数应该为NULL,这样使该函数默认使用上一次未分割完的字符串继续分割 ,就从上一次分割的位置作为本次分割的起始位置,直到分割结束。(strtok内部会记录相应信息)
这样就成功分割命令,来看效果:
我们的准备工作做完了,接下来就可以进行最终的操作:创建新进程来执行命令!
2.4 运行命令
运行命令就要使用:
- 创建子进程
- 进程替换
这两个加在一起就有了非常牛批的力量,究极POWER!。
68 //执行命令 69 void ExecuteCommand() 70 {//创建子进程 71 pid_t id = fork(); 72 if(id == 0) 73 { //进程替换 74 execvp(gArgv[0],gArgv); 75 exit(errno); 76 } 77 else 78 { 79 int status = 0; 80 pid_t rid = waitpid(id,&status,0);//进程等待 81 if(rid > 0) 82 { //如果错误打印错误信息 83 int lastcode = WEXITSTATUS(status); 84 if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode); 85 } 86 } 87 }
前面已经做好大部分工作了,执行命令这一步就很简单了。来看效果:
这样就完成了绝大部分的代码编写。我们在加上一个while循环,让命令行一直运行试试:
这样就实现了shell的大部分功能,但是还是有一些功能没有做到:比如我们运行cd等内建命令时,会无法运行,所以我要加上特殊情况来保证内建命令可以执行!!!
90 char* GetHome() 91 { 92 char* home = getenv("HOME"); 93 return home; 94 } 95 96 char cwd[SIZE]; 97 98 void cd() 99 { 100 const char* path = gArgv[1]; 101 if(path == NULL) path = GetHome(); 102 chdir(path); 103 104 char temp[SIZE]; 105 getcwd(temp,sizeof(temp)); 106 snprintf(cwd,sizeof(cwd),"PWD=%s",temp); 107 putenv(cwd); 108 } 109 110 //检查是否为内建命令 并单独执行 111 bool CheckBuildin() 112 { 113 bool yes = false; 114 //if语句判断即可,内建命令是有限的 115 if(strcmp(gArgv[0],"cd") == 0) 116 { 117 cd(); 118 yes = true; 119 } 120 return yes; 121 } 123 int main() 124 { 125 int quit = 0; 126 127 while(!quit) 128 { 129 130 //创建我们自己的命令行 131 MakeCommandLineAndPrint(); 132 133 //获取命令行信息 134 char usercommand[SIZE]; 135 int n = GetUserCommand(usercommand,sizeof(usercommand)); 136 if(n <= 0) return 1; 137 138 //分割命令行信息 139 SplitCommand(usercommand, sizeof(usercommand)); 140 141 bool judge = CheckBuildin(); 142 if(judge) continue; 143 144 //执行命令 145 ExecuteCommand(); 146 } 147 148 149 return 0; 150 }
这样把内建命令单独进行运行就可以了,我这里只写了一个cd
命令。来看效果:
这样就完成了我们的自主shell编写!!!
3 加入文件操作(4.19 更新)
学习过文件操作的重定向之后,我们也可以向我们的shell外壳中添加上文件操作的功能。
3.1 识别判断重定向
和上面的内建命令一样,我们需要进行一下判断,看看是否使用了重定向> < >>
//文件打开模式 18 #define None_Redir 0 19 #define In_Redir 1 // 输入 20 #define Out_Redir 2 // 输出 21 #define App_Redir 3 // 追加 22 #define Skip_Space(cmd ,pos) do{ while(1) if(isspace(cmd[pos])) pos++; else break;}while(0) 23 int redir_type = None_Redir; 24 char *filename = NULL; ..... 134 void CheckRedir(char* cmd) 135 { 136 // ls -a -l > myfile.txt 137 int pos = 0 ; 138 int end = strlen(cmd); 139 //左闭右开 140 while(pos != end) 141 { 142 if(cmd[pos] == '>') 143 { 144 if(cmd[pos+1] == '>') 145 { 146 cmd[pos++] = 0; 147 pos++; 148 redir_type = App_Redir; 149 Skip_Space(cmd,pos); 150 filename = cmd + pos; 151 } 152 else 153 { 154 cmd[pos++] = 0; 155 redir_type = Out_Redir; 156 Skip_Space(cmd,pos); 157 filename = cmd + pos; 158 159 } 160 } 161 else if(cmd[pos] == '>') 162 { 163 cmd[pos++] = 0 ; 164 redir_type = In_Redir; 165 Skip_Space(cmd,pos); 166 filename = cmd + pos; 167 } 168 else 169 { 170 pos++; 171 } 172 } 173 }
逻辑是很简单的:
- 遍历一遍命令行数组,检查里面是否有
> < >>
- 在pos位置设置为‘\0’,截断命令。
- 跳过重定向符号与文件名之间的空格
- 文件名即为此时pos后的字符串。
重定向符号的对应数字就可以我们可以提前对重定向模式进行宏定义,方便使用。
3.2 执行重定向
进行重定向的判断之后,我们可以在执行命令的模块函数中进行一个判断,按照对应的重定向模式进行重定向。子进程中使用dup2函数,不会影响到父进程的文件指向,就可以完成任务。
86 void ExecuteCommand() 87 { 88 pid_t id = fork(); 89 if(id == 0) 90 { 91 if(filename != NULL) 92 { 93 94 if(redir_type == In_Redir) 95 { 96 int fd = open(filename,O_RDONLY); 97 dup2(fd , 0); 98 } 99 else if(redir_type == Out_Redir) 100 { 101 int fd = open(filename,O_WRONLY | O_TRUNC | O_CREAT, 0666 ); 102 dup2(fd , 1); 103 } 104 else if(redir_type == App_Redir) 105 { 106 int fd = open(filename,O_WRONLY | O_CREAT | O_TRUNC, 0666 ); 107 dup2(fd , 1); 108 109 } 110 else 111 { 112 113 } 114 } 115 execvp(gArgv[0],gArgv); 116 exit(errno); 117 } 118 else 119 { 120 int status = 0; 121 pid_t rid = waitpid(id,&status,0); 122 if(rid > 0) 123 { 124 lastcode = WEXITSTATUS(status); 125 if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode); 126 } 127 } 128 }
来看效果
这样就实现了shell里的文件操作
4 源代码
#include<stdio.h> 2 #include<sys/types.h> 3 #include<sys/wait.h> 4 #include<stdlib.h> 5 #include<unistd.h> 6 #include<string.h> 7 #include<errno.h> 8 #include<stdbool.h> 9 10 #define SIZE 256 11 #define SkipPath(p) do{ p += strlen(p)-1 ;while(*p != '/') p--; }while(0); 12 #define ZERO '\0' 13 #define NUM 32 14 #define SEP " " 15 16 //命令 17 char* gArgv[NUM]; 18 int lastcode = 0; 19 char cwd[SIZE]; 20 21 const char* GetUsername() 22 { 23 const char* name = getenv("USER"); 24 if(name == NULL) return "NONE"; 25 return name; 26 } 27 28 const char* GetHostName() 29 { 30 const char* hostname = getenv("HOSTNAME"); 31 return hostname; 32 } 33 34 const char* GetCwd() 35 { 36 const char* cwd = getenv("PWD"); 37 if(cwd == NULL) return "NONE"; 38 return cwd; 39 } 40 41 void MakeCommandLineAndPrint() 42 { 43 char line[SIZE]; 44 const char* username = GetUsername(); 45 const char* hostname = GetHostName(); 46 const char* cwd = GetCwd(); 47 SkipPath(cwd); 48 sprintf(line,"[%s@%s %s]> ",username,hostname,strlen(cwd) == 1?"/":cwd + 1); 49 printf("%s",line); 50 fflush(stdout); 51 } 52 53 int GetUserCommand(char command[] ,size_t n) 54 { 55 char* s = fgets(command,n,stdin); 56 if(s == NULL) return -1; 57 command[strlen(command) - 1] = ZERO; 58 return strlen(command); 59 } 60 61 void SplitCommand(char command[] , size_t n) 62 { 63 (void)n; 64 gArgv[0] = strtok(command,SEP); 65 int index = 1; 66 // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束 67 while((gArgv[index++] = strtok(NULL,SEP))); 68 } 69 70 //执行命令 71 void ExecuteCommand() 72 { 73 pid_t id = fork(); 74 if(id == 0) 75 { 76 execvp(gArgv[0],gArgv); 77 exit(errno); 78 } 79 else 80 { 81 int status = 0; 82 pid_t rid = waitpid(id,&status,0); 83 if(rid > 0) 84 { 85 lastcode = WEXITSTATUS(status); 86 if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode); 87 } 88 } 89 } 90 91 char* GetHome() 92 { 93 char* home = getenv("HOME"); 94 return home; 95 } 96 97 98 void cd() 99 { 100 const char* path = gArgv[1]; 101 if(path == NULL) path = GetHome(); 102 chdir(path); 103 104 char temp[SIZE]; 105 getcwd(temp,sizeof(temp)); 106 snprintf(cwd,sizeof(cwd),"PWD=%s",temp); 107 putenv(cwd); 108 } 109 110 //检查是否为内建命令 并单独执行 111 bool CheckBuildin() 112 { 113 bool yes = false; 114 //if语句判断即可,内建命令是有限的 115 if(strcmp(gArgv[0],"cd") == 0) 116 { 117 cd(); 118 yes = true; 119 } 120 return yes; 121 } 122 123 int main() 124 { 125 int quit = 0; 126 127 while(!quit) 128 { 129 130 //创建我们自己的命令行 131 MakeCommandLineAndPrint(); 132 133 //获取命令行信息 134 char usercommand[SIZE]; 135 int n = GetUserCommand(usercommand,sizeof(usercommand)); 136 if(n <= 0) return 1; 137 138 //分割命令行信息 139 SplitCommand(usercommand, sizeof(usercommand)); 140 141 bool judge = CheckBuildin(); 142 if(judge) continue; 143 144 //执行命令 145 ExecuteCommand(); 146 } 147 148 149 return 0; 150 }