朋友们、伙计们,我们又见面了,本期来给大家带来一期自定义shell,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
引言:
1. 命令行提示符
2. 获取用户指令
2.1 封装命令行和获取指令
3. 执行用户输入的命令
3.1 分割命令字符串
3.2 创建子进程执行指令
3.3 封装分割字符串和执行命令
4. 内建命令的执行
4.1 cd命令
4.2 export命令
4.3 echo命令
5. 检查重定向
6. 完整代码
引言:
Linux命令行的功能非常强大,我们用了这么长时间的shell,那么本期我们来自己实现一个简单版的shell。
1. 命令行提示符
我们可以先看一下原本的shell:
要实现一个shell先得有一个命令行提示符,这个命令行提示符前面是用户名,中间是主机号,后面是工作目录,这些属性我们可以直接定义,但是在Linux环境变量中都保存着这些属性,并且是动态的,所以我们直接使用环境变量中的这些属性;
在程序中获取环境变量的接口叫做getenv:
#include <stdlib.h> char *getenv(const char *name);
接下来我们先使用getenv实现三个获取命令行提示符的函数:
// 获取用户名 const char* getUsername() {const char* name = getenv("USER");if(name) return name;return "none"; } // 获取主机名 const char* getHostname() {const char* hostname = getenv("HOSTNAME");if(hostname) return hostname;return "none"; } // 获取当前工作目录 const char* getCwd() {const char* cwd = getenv("PWD");if(cwd) return cwd;return "none"; }
获取完成之后,紧接着按照原本shell的格式来把他们拼接在一起打印出来看一看:
int main() {// 打印命令行提示符printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());return 0; }
可以看到我们的shell命令行提示符已经完成了,接下来就需要获取用户输入的命令了。
2. 获取用户指令
要获取用户输入的指令,我们先定义一个用来保存指令的字符数组,由于我们输入的指令可能不只是单纯的一个,还会输入一些带选项的指令,此时就不能使用scanf来读取,需要用按行读取的函数接口,我们选择fgets:
C语言会默认打开三个流:
- ① 标准输入流:stdin
- ② 标准输出流:stdout
- ③ 标准错误流:stderr
所以我们从标准输入流中读取用户写入的命令:
#define NUM 1024int main() {char usercommand[NUM];// 打印命令行提示符printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());// 获取用户输入命令char* r = fgets(usercommand, sizeof(usercommand), stdin);if(r == NULL) return 1;return 0; }
当我们输入命令时,最后肯定会跟上一个'\n',所以我们需要将命令行中的最后一个位置的'\n'设置为'\0'。
int main() {char usercommand[NUM];// 打印命令行提示符printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());// 获取用户输入命令char* r = fgets(usercommand, sizeof(usercommand), stdin);if(r == NULL) return 1;// 命令规范化usercommand[strlen(usercommand) - 1] = '\0'; // 不会存在越界问题return 0; }
这样写是不存在越界的问题,假设我们什么命令都不输入,但是最后还是会敲一下回车,所以只有一个'\n',将这个'\n'改为'\0'也符合预期。
2.1 封装命令行和获取指令
将打印命令行提示符和获取用户指令封装称为一个函数,后面我们直接调用这个函数即可。
#include <stdio.h> #include <stdlib.h>#define NUM 1024int getUserCommand(char *command, int sz) {// 打印命令行提示符printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());// 获取用户输入命令char* r = fgets(command, sz, stdin);if(r == NULL) return -1;// 命令规范化command[strlen(command) - 1] = '\0'; // 不会存在越界问题return strlen(command); }int main() {char usercommand[NUM];getUserCommand(usercommand, sizeof(usercommand));//printf("%s", usercommand);return 0; }
3. 执行用户输入的命令
获取完用户输入的指令,接下来就需要执行对应的指令了。
3.1 分割命令字符串
要能正确执行对应的命令,就需要将输入的命令以及选项做合理的分割:分割命令字符串我们选用C语言中的strtok函数:
将分割好的字符串存放在一个字符指针数组中,最后以NULL结尾即可:
int main() {char usercommand[NUM];// 打印命令行提示符 + 获取用户指令getUserCommand(usercommand, sizeof(usercommand));// 分割命令字符串char *argv[SIZE];int argc = 0;argv[argc++] = strtok(usercommand, SEP);while (argv[argc++] = strtok(NULL, SEP)){};return 0; }
3.2 创建子进程执行指令
因为要执行指令就需要进行程序替换,如果单进程替换的话后面的代码就不能运行,所以创建子进程,让子进程去执行对应的指令。
在这里的程序替换选择的函数接口是:execvp
我们使用命令行输入指令肯定是想要我输完一个结果显示紧接着就可以输入下一个,所以需要设置一个循环的结构,并且为了效率,当获取用户指令失败、或者用户没有输入有效指令,就继续获取,就不用进行下面分割字符串、创建子进程的过程了。
#define SEP " " #define SIZE 64int main() {while (1){char usercommand[NUM];// 打印命令行提示符 + 获取用户指令int flag = getUserCommand(usercommand, sizeof(usercommand));if (flag <= 0)continue;// 分割命令字符串char *argv[SIZE];int argc = 0;argv[argc++] = strtok(getUserCommand, SEP);while (argv[argc++] = strtok(NULL, SEP)){};// 创建子进程执行命令pid_t id = fork();if (id < 0)return 1;else if (id == 0) // child{// 程序替换执行命令execvp(argv[0], argv);exit(1);}else // parent{pid_t rid = waitpid(id, NULL, 0);}}return 0; }
3.3 封装分割字符串和执行命令
为了使用方便以及代码简介,直接将这两个接口封装称为一个函数即可:
// 分割命令字符串 void commandSplit(char *in, char *out[]) {int argc = 0;out[argc++] = strtok(in, SEP);while (out[argc++] = strtok(NULL, SEP)){}; }// 执行命令 int excuteCommand(char *argv[]) {// 创建子进程执行命令pid_t id = fork();if (id < 0)return -1;else if (id == 0) // child{// 程序替换执行命令execvp(argv[0], argv);exit(1);}else // parent{pid_t rid = waitpid(id, NULL, 0);}return 0; }
4. 内建命令的执行
在Linux中有一批命令,需要我们的bash自己去执行,类似于自己内部的一个函数,这批命令就叫做内建命令,例如:cd、export、echo的特殊用法等,因此我们需要在执行命令之前,判断用户输入的命令是否为内建命令,如果为内建命令,直接让我们的父进程直接去执行内建命令,同样的我们将判断内建命令封装成一个函数,直接在这个函数里面调用执行对应的内建命令:
4.1 cd命令
cd命令是更改我们的工作目录,更改工作目录的接口是chdir:
路径更改完之后,还需要更改环境变量中的PWD,所以先用获取当前路径的接口getcwd:
然后使用sprintf将当前路径导出到环境变量中。
cd命令还需要注意一个小细节,cd后面没有路径默认情况是cd到家目录。
char cwd[1024]; // 接受当前工作目录的缓冲区void cd(char *path) {chdir(path); // 改变当前工作目录char tmp[1024]; // getcwd(tmp, sizeof(tmp)); // 获取当前工作目录sprintf(cwd, "PWD=%s", tmp); // 写入到缓冲区中putenv(cwd); // 导出环境变量 }// 判断内建命令 int isBuildinCommand(char *argv[]) {if (strcmp(argv[0], "cd") == 0) // cd path{char *path = NULL;if(argv[1] == NULL) home = homePath();else path = argv[1];cd(path);return 1;}// 可以再列举更多内建命令return 0; }
4.2 export命令
我们使用export命令来在命令行中导出环境变量,所以我们设置全局的缓冲区来接收要导出的环境变量,然后使用putenv来导出环境变量。
int isBuildinCommand(char *argv[]) {if (strcmp(argv[0], "cd") == 0) // cd path{char *path = NULL;if(argv[1] == NULL) path = getHomePath();else path = argv[1];cd(path);return 1;}else if(strcmp(argv[0], "export") == 0){if(argv[1] == NULL) return 1;strcpy(enval, argv[1]); // 拷贝到缓冲区putenv(enval); // 导出return 1;}// 可以再列举更多内建命令return 0; }
4.3 echo命令
我们使用echo命令首先打印普通变量(echo 1244),其次打印退出码(echo $?),还会打印环境变量(echo $PATH),所以这些特殊用法都要实现:
// 判断内建命令 int isBuildinCommand(char *argv[]) {if (strcmp(argv[0], "cd") == 0) // cd path{char *path = NULL;if (argv[1] == NULL)path = getHomePath();elsepath = argv[1];cd(path);return 1;}else if (strcmp(argv[0], "export") == 0){if (argv[1] == NULL)return 1;strcpy(enval, argv[1]); // 拷贝到缓冲区putenv(enval); // 导出return 1;}else if (strcmp(argv[0], "echo") == 0){if (argv[1] == NULL) // echo{printf("\n");return 1;}else if (*(argv[1]) == '$' && strlen(argv[1]) > 1) // echo $...{char *val = argv[1] + 1;if (strcmp(val, "?") == 0){printf("%d\n", lastcode);lastcode = 0;}else{const char *env_val = getenv(val);if (env_val)printf("%s\n", env_val);elseprintf("\n");}return 1;}else // echo 1234{printf("%s\n", argv[1]);return 1;}}// 可以再列举更多内建命令return 0; }
5. 检查重定向
在获取完指令后先检测一下指令中是否存在重定向。
先获取重定向的类型,再拆分要重定向的文件。
获取完成之后在执行命令的时候直接使用重定向完成操作。
// 检查重定向 void checkRedir(char command[], int len) {char *end = command + len - 1; // 从后向前面检查char *start = command;while (end > start){if (*end == '>') // 输出重定向 ls -a -l >> log.txt{if (*(end - 1) == '>') // 追加重定向{*(end - 1) = '\0';filename = end + 1;SkipSpace(filename);redir = AppendRedir;break;}else{*end = '\0';filename = end + 1;SkipSpace(filename);redir = OutputRedir;break;}}else if (*end == '<') // 输入重定向{*end = '\0';filename = end + 1;SkipSpace(filename);redir = InputRedir;break;}else{end--;}} }
6. 完整代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <fcntl.h>#define NUM 1024 #define SEP " " #define SIZE 64#define NoneRedir 0 #define OutputRedir 1 #define AppendRedir 2 #define InputRedir 3int redir = NoneRedir; char *filename = NULL;char cwd[1024]; int lastcode = 0; char enval[1024];// 获取用户名 const char *getUsername() {const char *name = getenv("USER");if (name)return name;return "none"; } // 获取主机名 const char *getHostname() {const char *hostname = getenv("HOSTNAME");if (hostname)return hostname;return "none"; } // 获取当前工作目录 const char *getCwd() {const char *cwd = getenv("PWD");if (cwd)return cwd;return "none"; } // 获取家目录 char *getHomePath() {char *home = getenv("HOME");if (home)return home;elsereturn (char *)"."; }// 打印命令行提示符 + 获取用户指令 int getUserCommand(char *command, int sz) {// 打印命令行提示符printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());// 获取用户输入命令char *r = fgets(command, sz, stdin);if (r == NULL)return 1;// 命令规范化command[strlen(command) - 1] = '\0'; // 不会存在越界问题return strlen(command); }// 分割命令字符串 void commandSplit(char *in, char *out[]) {int argc = 0;out[argc++] = strtok(in, SEP);while (out[argc++] = strtok(NULL, SEP)){}; }// 执行命令 int excuteCommand(char *argv[]) {// 创建子进程执行命令pid_t id = fork();if (id < 0)return -1;else if (id == 0) // child{int fd = 0;if (redir == InputRedir) // 输入重定向{fd = open(filename, O_RDONLY);dup2(fd, 0);}else if (redir == OutputRedir) // 输出重定向{fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);dup2(fd, 1);}else if (redir == AppendRedir) // 追加重定向{fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);dup2(fd, 1);}else{// do nothing}// 程序替换执行命令execvp(argv[0], argv);exit(1);}else // parent{int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){lastcode = WEXITSTATUS(status);}}return 0; }void cd(char *path) {chdir(path); // 改变当前工作目录char tmp[1024]; //getcwd(tmp, sizeof(tmp)); // 获取当前工作目录sprintf(cwd, "PWD=%s", tmp); // 写入到缓冲区中putenv(cwd); // 导出环境变量 }// 判断内建命令 int isBuildinCommand(char *argv[]) {if (strcmp(argv[0], "cd") == 0) // cd path{char *path = NULL;if (argv[1] == NULL)path = getHomePath();elsepath = argv[1];cd(path);return 1;}else if (strcmp(argv[0], "export") == 0){if (argv[1] == NULL)return 1;strcpy(enval, argv[1]); // 拷贝到缓冲区putenv(enval); // 导出return 1;}else if (strcmp(argv[0], "echo") == 0){if (argv[1] == NULL){printf("\n");return 1;}else if (*(argv[1]) == '$' && strlen(argv[1]) > 1){char *val = argv[1] + 1;if (strcmp(val, "?") == 0){printf("%d\n", lastcode);lastcode = 0;}else{const char *env_val = getenv(val);if (env_val)printf("%s\n", env_val);elseprintf("\n");}return 1;}else{printf("%s\n", argv[1]);return 1;}}// 可以再列举更多内建命令return 0; }#define SkipSpace(pos) \do \{ \while (isspace(*pos)) \pos++; \} while (0)// 检查重定向 void checkRedir(char command[], int len) {char *end = command + len - 1; // 从后向前面检查char *start = command;while (end > start){if (*end == '>') // 输出重定向 ls -a -l >> log.txt{if (*(end - 1) == '>') // 追加重定向{*(end - 1) = '\0';filename = end + 1;SkipSpace(filename);redir = AppendRedir;break;}else{*end = '\0';filename = end + 1;SkipSpace(filename);redir = OutputRedir;break;}}else if (*end == '<') // 输入重定向{*end = '\0';filename = end + 1;SkipSpace(filename);redir = InputRedir;break;}else{end--;}} }int main() {while (1){char usercommand[NUM];// 打印命令行提示符 + 获取用户指令int flag = getUserCommand(usercommand, sizeof(usercommand));if (flag <= 0)continue;// 检测重定向checkRedir(usercommand, sizeof(usercommand));// 分割命令字符串char *argv[SIZE];commandSplit(usercommand, argv);// 判断内建命令int sign = isBuildinCommand(argv);if (sign)continue;// 执行命令excuteCommand(argv);}return 0; }
朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!