shell程序的简单实现
本章思维导图:
注:本章思维导图对应的.xmind
和.png
文件都已同步导入至资源,可免费查阅
在学习完有关进程的知识后,我们就可以开始尝试自己实现一个简单的shell
程序了。
注:在编写简单的shell
程序之前,你首先需要掌握:
👉进程控制
👉环境变量
👉进程替换
1. 实现交互 interact()
首先,和真正的shell
程序一样,我们启动程序,shell
就会打印出命令行提示符,并等待用户的输入:
因此,我们首先要做的,就是要正确打印出命令行提示符,并等待接收用户输入的命令。
注:
命令行提示符的基本格式为:
[用户名@主机名 当前路径]&
- 需要注意,如果当前用户为
root
用户,那么&
就应该变为#
那么,我们该如何获取我们需要的有用户名、主机名和路径信息呢?答案便是通过环境变量来获取
- 环境变量
USER
记录了当前的用户信息 - 环境变量
HOSTNAME
记录了当前的主机信息 - 环境变量
PWD
记录了当前的路径信息
可以利用系统调用getenv()
来获取对应的信息,并进行打印
等待并接受用户的输入这一操作十分简单,定义一个字符数组,并用函数fgets()
进行接收即可。
这样,我们就实现了第一部分的功能:
//形参out为一个输出型参数,用于接收用户输入的命令 void interact(char* out) { printf("[%s@%s %s]$ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD")); fgets(out, SIZE, stdin); out[strlen(out) - 1] = '\0'; //fgets()会将用户输入的换行符读入,因此要将这个符号去除 }
2. 分割命令 split()
在进程替换一节中我们提到,如果要将当前的进程替换为另一个程序,那么就需要使用exec
系列函数来进行进程程序替换:
int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);
- 命令参数要么以参数列表的形式
arg, ...
传入,要么以字符串数组argv
的方式传入 - 但是我们在第一步
interact()
的过程中只接受了用户的一长串命令,这并不能直接作为参数传入程序替换函数中 - 因此,我们就需要对之前输入的字符串以空格
‘ ’
为分隔符进行分割
如何分割?——可以用库函数strtok解决
char * strtok ( char * str, const char * delimiters );
delimiters
即分割符- 返回值即为被分割的字符串,分割结束返回
NULL
- 关于参数
str
,当要对用一个字符串多次调用时:
- 第一次调用时,即为要被分割字符串
str
- 之后的所有调用,参数
str
都为NULL
如此,我们便可以实现功能分割功能了:
//参数command为用户输入的命令 //参数out为输出型参数,用于存储被分割的字符串集合 void split(char* command, char** out) { int i = 0; out[i++] = strtok(command, " "); while (out[i++] = strtok(NULL, " ")); }
3. 执行命令
获得了正确的命令参数后,我们就可以开始程序替换了。
但是应该注意,如果程序替换成功,那么原程序之后的所有代码便都不会再执行了。
因此,为了确保shell
能够一直处理用户输入的命令,我们应该创建一个子进程来进行进程程序替换
我们可以很容易的写出这样的代码:
//参数argv即为存储命令字符串的数组 void execute(char** argv) { //创建子进程 pid_t pid = fork(); if (pid == 0) { //子进程进行进程程序替换 execvp(argv[0], argv); exit(1); } //子进程退出后父进程进行等待,并获取子进程的退出码 int status; pid_t rid = waitpid(pid, &status, 0); EXIT = WEXITSTATUS(status); }
我们再对上面两部分代码进行整合,就可以得到我们shell
的简单版本了:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> #define SIZE 1024 #define ARGC 64 int EXIT = 0; //进程退出码 void interact(char* out) { printf("[%s@%s %s]$ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD")); fgets(out, SIZE, stdin); out[strlen(out) - 1] = '\0'; } void split(char* command, char** out) { int i = 0; out[i++] = strtok(command, " "); while (out[i++] = strtok(NULL, " ")); } void execute(char** argv) { pid_t pid = fork(); if (pid == 0) { execvp(argv[0], argv); exit(1); } int status; pid_t rid = waitpid(pid, &status, 0); EXIT = WEXITSTATUS(status); } int main() { while(1) { //获取命令行参数 char command[SIZE] = {0}; interact(command); if (strlen(command) == 0) continue; //将命令行拆分成多个字符串 char* argv[ARGC]; split(command, argv); execute(argv); } return 0; }
我么可以执行来看看:
可以发现我们执行cat
、ls
、clear
这些命令的时候没有出现问题,但是当我们执行cd
、echo
、export
这些命令的时候,却得不到正确的结果。这是为什么?
- 应该清楚,我们是用子进程进行的进程替换,子进程执行完后便会退出终止。
- 因此,子进程的改变不会影响到父进程,即不会影响到
shell
进程- 例如我们使用
cd
命令修改当前路径,我们修改的只是子进程的路径,而其父进程shell
并未受任何影响- 同样,对于
export
,我们只是对子进程添加了环境变量,父进程的环境变量同样不会改变
所以,当遇到类似cd
这种命令时,我们要对其进行特殊处理
3.1 执行内建命令
在Linux中,诸如echo
、cd
、export
这样的命令我们称其为内建命令
我们可以利用枚举的方法来对内建命令进行处理
//参数argv即为命令字符串集合 //返回值如果为0,说明不是内建命令;如果是1,说明是内建命令 int buildCommand(char** argv) { int ret = 0; //处理“cd” if (strcmp(argv[0], "cd") == 0) { ret = 1; char* path = argv[1]; //命令cd后面跟的就是新的路径 char put[SIZE]; char absolutePath[SIZE]; if (path == NULL) path = getenv("HOME"); chdir(path); getcwd(absolutePath, SIZE); //将新路径存入字符数组absolutePath snprintf(put, SIZE, "%s%s", "PWD=", absolutePath); //修改环境变量PWD putenv(put); } //处理”export“ else if (strcmp(argv[0], "export") == 0) { ret = 1; char env[SIZE]; if (argv[1]) { strcpy(env, argv[1]); putenv(env); } /* 一定不能直接写成:putenv(argv[1]); 否则当输入新的命令时,argv[1]的值就会改变,环境变量也会跟着变 */ } //处理“echo” else if (strcmp(argv[0], "echo") == 0) { ret = 1; if (argv[1] == NULL) printf("\n"); else { if (argv[1][0] == '$') { //"echo $?"即输出最近一个进程的退出码 if (argv[1][1] == '?') { printf("%d\n", EXIT); EXIT = 0; } //否则输出对应的环境变量 else { char* env = getenv(argv[1] + 1); if (env == NULL) printf("\n"); else printf("%s\n", env); } } //否则为向屏幕输出字符串 else printf("%s\n", argv[1]); } } return ret; }
3.2 执行非内建命令
利用buildCommand()
的返回值
- 如果返回值为0,那么就说明该命令为非内建命令,开始创建子进程进行进程替换
- 如果返回值为1,那么就说明该命令为内建命令,已经经过处理,直接等待下一条命令的输入即可
//处理内建命令 int ret = buildCommand(argv); //执行命令 if (!ret) execute(argv);
4. 实现代码
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> #define SIZE 1024 #define ARGC 64 int EXIT = 0; void interact(char* out) { printf("[%s@%s %s]$ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD")); fgets(out, SIZE, stdin); out[strlen(out) - 1] = '\0'; } void split(char* command, char** out) { int i = 0; out[i++] = strtok(command, " "); while (out[i++] = strtok(NULL, " ")); } int buildCommand(char** argv) { int ret = 0; if (strcmp(argv[0], "cd") == 0) { ret = 1; char* path = argv[1]; char put[SIZE]; char absolutePath[SIZE]; if (path == NULL) path = getenv("HOME"); chdir(path); getcwd(absolutePath, SIZE); snprintf(put, SIZE, "%s%s", "PWD=", absolutePath); putenv(put); } else if (strcmp(argv[0], "export") == 0) { ret = 1; char env[SIZE]; if (argv[1]) { strcpy(env, argv[1]); putenv(env); } } else if (strcmp(argv[0], "echo") == 0) { ret = 1; if (argv[1] == NULL) printf("\n"); else { if (argv[1][0] == '$') { if (argv[1][1] == '?') { printf("%d\n", EXIT); EXIT = 0; } else { char* env = getenv(argv[1] + 1); if (env == NULL) printf("\n"); else printf("%s\n", env); } } else printf("%s\n", argv[1]); } } return ret; } void execute(char** argv) { pid_t pid = fork(); if (pid == 0) { execvp(argv[0], argv); exit(1); } int status; pid_t rid = waitpid(pid, &status, 0); EXIT = WEXITSTATUS(status); } int main() { while(1) { //获取命令行参数 char command[SIZE] = {0}; interact(command); if (strlen(command) == 0) continue; //将命令行拆分成多个字符串 char* argv[ARGC]; split(command, argv); //处理内建命令 int ret = buildCommand(argv); //执行命令 if (!ret) execute(argv); } return 0; }
本篇完
如果错误敬请指正