Loading... ## 实现的功能 1. 类 UNIX 操作系统控制台的提示信息 - 当前用户信息 - 当前工作目录 2. 获取命令字符串 - 分号分隔 - 多行输入 3. 解析命令 - 分析指令及其参数 - 管道 - 输入输出重定向 - 变量存储和替换 4. 指令执行 - exec 函数族实现前台执行 - 实现部分内建命令 ## 大致框架 我们使用的 shell 一般有以下流程:**初始化,打印提示符,解析命令,执行内置命令,执行外部命令,循环上述过程**。所以可以根据这个流程来写出大致框架: ```c++ int main() { while (true) { init(); fflush(stdin); print_prompt(); int len = get_input(); if (len <= 0) continue; parse_token(); for (auto i = 0; i < cmdNum; i++) { cmd_info* curCmd = cmds + i; if (exec_inner(curCmd) == 1) // if not shell builtin { // execvp will not return if it runs successfully // so use child proc to execute other command. pid_t pid = fork(); if (pid < 0) printf("[Error] Fork error in main."); else if (pid == 0) { exec_outer(curCmd); continue; } if ((cmds[i].type & BACKGROUND) == 0) waitpid(pid, nullptr, 0); } // free cmd node init_node(curCmd); } } } ``` ## 功能实现 ### 头文件和全局变量等 ```c #include <cstdio> #include <cstdlib> #include <cerrno> #include <cstring> #include <fcntl.h> #include <pwd.h> #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/wait.h> #define MAX_CMD 100 // Maximum number of read commands per line #define MAX_VAR_NUM 100 // Maximum number of variables #define BUFFER_SIZE 1024 // Maximum buffer size #define MAX_PATH_LENGTH 1024 // Maximum length of file path struct cmd_info cmds[MAX_CMD]; char cmdStr[BUFFER_SIZE]; int cmdNum = 0; char envKey[MAX_VAR_NUM][MAX_PATH_LENGTH]; char envVal[MAX_VAR_NUM][MAX_PATH_LENGTH]; int varNum = 0; ``` ### cmd_info 结构体 首先我们需要一个结构体来保存命令的信息。根据要实现的功能,这个结构体应该保存有以下信息: - 指令名 - 指令参数 - 输入输出重定向 - 管道信息 所以我设计结构体如下: ```c // type of command, use binary #define BACKGROUND 1 #define LEFT_REDIRECT 2 #define RIGHT_REDIRECT 4 #define REDIRECT_APPEND 8 struct cmd_info { // command and parameters int argc = 0; char** argv{}; // Input and output redirection char* input{}; char* output{}; // next command struct cmd_info* next = nullptr; // command attributes int type = 0; }; ``` **argc** 和 **argv** 记录命令和参数个数;**input** 和 **output** 记录输入输出重定向;**next** 指针记录管道的下一个指令;**type** 记录重定向类型。 ### 输出提示符 一般入门编程学的就是这个,就不需要讲什么了吧。 ```c void print_prompt() { char host_name[MAX_PATH_LENGTH]; // user's hostname char path_name[MAX_PATH_LENGTH]; // current working path passwd* pwd = getpwuid(getuid()); // user's information getcwd(path_name, MAX_PATH_LENGTH); // get user's hostname if (gethostname(host_name, MAX_PATH_LENGTH)) strcpy(host_name, "unknown"); // if current directory is not user's home, print it directory if (strlen ( path_name ) < strlen ( pwd->pw_dir ) || strncmp(path_name , pwd->pw_dir , strlen ( pwd->pw_dir)) != 0) printf("\033[;32m%s@%s\033[0m:\033[;34m%s\033[0m", pwd->pw_name, host_name, path_name); else // else use '~' instead of user's home printf("\033[;32m%s@%s\033[0m:\033[;34m~%s\033[0m", pwd->pw_name, host_name, path_name + strlen(pwd->pw_dir)); // if the current user is root if (getuid() == 0) printf("# "); // is root else printf("$ "); // isn't root } ``` ### 解析命令字符串 这个功能代码量比较大,具体代码见文末吧。 这些函数解析命令字符串, 能支持多个空格,支持多行输入,支持多条命令 `;`,支持了变量`$`, 支持引号`'` ,同时为重定向 `<`,`>`,`<<`,以及管道 `|`做好准备。 > 这一部分有些可能处理不是很恰当,参考一下就好了吧 qwq ### 内建命令 对于内建命令, 比如 `cd, pwd, exit, echo, export, unset` 可以直接执行 在代码中, 内建命令的实现都在 `exec_inner` 函数中, 如果不是内建命令, 则返回 1 , 然后会调用执行外部命令的函数 `exec_outer` ### 外部命令 实现的函数是 `exec_outer`,里面包括了重定向,管道,下面再介绍。 对于外部命令, 可以 fork 一个子进程,然后使用 exec 函数族让程序在子进程执行并返回。这里我使用 `execvp` 函数。 如果当前命令 的 `next` 为 `nullptr`,即没有下一条管道命令,那么直接将标准文件描述符传给 `setIO` 处理好文件 IO,然后调用 `execvp` 执行外部命令即可。 如果不为`nullptr`,说明有管道。建立管道,并用 fork 来新建子进程执行管道命令。这时传递到 `setIO` 函数的是**对应管道文件描述符**的输入输出, 然后如果有多个管道,可以递归地调用 `execOuter`函数。 ```c void exec_outer(cmd_info* cmd) { // the last command runs directly and exit if (cmd->next == nullptr) { setIO(cmd, STDIN_FILENO, STDOUT_FILENO); execvp(cmd->argv[0], cmd->argv); printf("[Error] Execute command %s error.\n", cmd->argv[0]); return; } // pipe int fd[2]; pipe(fd); pid_t pid = fork(); if (pid < 0) { printf("[Error] Fork error in exec_outer."); return; } else if (pid == 0) { close(fd[0]); setIO(cmd, STDIN_FILENO, fd[1]); execvp(cmd->argv[0], cmd->argv); // if execute success, the following code will not be executed printf("[Error] Execute command %s error.\n", cmd->argv[0]); return; } else { wait(nullptr); close(fd[1]); // execute all commands recursively cmd = cmd->next; setIO(cmd, fd[0], STDOUT_FILENO); exec_outer(cmd); } } ``` ### 管道与重定向 重定向的 I/O 以及管道的 I/O,我都放在 `setIO` 函数中处理,如下. 这个函数接受的参数包括一个命令指针 `cmd` (以;分隔的, 包括管道中的命令), 以及 一个输入文件描述符`rfd`,一个输出文件描述符`wfd`. **文件重定向** 如果命令的 `type` 属性中的 `LEFT_REDIRECTION/ RIGHT_REDIRECTION` 标志位设置为 1 的话,就打开重定向的文件得到其文件描述符, 然后将标准输入/输出文件描述符关闭,再修改为此文件描述符。 > 注意最后用完此该文件描述符要用 close 关闭 **管道重定向** 分别检查**传入的文件描述符参数**是否是**标准输入,输出**。如果不是, 说明传递的是管道的文件描述符, 就将相应的 标准输入/输出 关闭 ,再复制到 `rfd/wfd`, 最后``close rfd/wfd` ```c void setIO(cmd_info* cmd, const int rfd, const int wfd) { int type = cmd->type; // > or >> if ((type & RIGHT_REDIRECT) != 0) { int mode = 0; if ((type & REDIRECT_APPEND) != 0) mode = O_WRONLY | O_TRUNC | O_CREAT; else mode = O_WRONLY | O_APPEND | O_CREAT; // change output file int wport = open(cmd->output, mode); dup2(wport, STDOUT_FILENO); close(wport); } // < if ((type & LEFT_REDIRECT) != 0) { int rport = open(cmd->input, O_RDONLY); dup2(rport, STDIN_FILENO); close(rport); } // pipe if (rfd != STDIN_FILENO) { dup2(rfd, STDIN_FILENO); close(rfd); } if (wfd != STDOUT_FILENO) { dup2(wfd, STDOUT_FILENO); close(wfd); } } ``` ## 完整代码 [Github](https://gist.github.com/TruthTyl/bcfecfc8a342a7821fd8cf3bd0caccb2) 最后修改:2020 年 04 月 27 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏