[GNU/Linux] 自己实现shell
来源:互联网 发布:脸大又漂亮的女生知乎 编辑:程序博客网 时间:2024/06/05 08:37
写在前面
shell
作为一种与内核对话的一种方式,为我们使用操作系统服务,提供了很多便利。在我们使用Linux时,shell
是不得不接触的内容之一。为了学习和熟悉Linux
进程相关的内核函数,我们可以尝试着自己实现一个shell
。
源代码
src/Makefile
SOURCES = $(wildcard *.cpp)OBJECTS = $(patsubst %.cpp, %.o, $(SOURCES))CXXFLAG = -std=c++14 -I ../include -O2 -lreadlinepsh: $(OBJECTS) g++ $(CXXFLAG) -o psh $(OBJECTS)$(OBJECTS): $(SOURCES) g++ $(CXXFLAG) -c $(SOURCES).PHONY: cleanclean : -rm $(OBJECTS)
include/psh.h
#pragma once#ifndef _HEADER_PANGDA_SHELL__#define _HEADER_PANGDA_SHELL__#include<unistd.h>#include<cstdio>#include<string>#include<vector>#include<functional>#include<map>typedef std::vector<std::string> argument_t;struct command_t { int is_right_cmd = 0; //命令是否是正确的,若为0则说明是正确的,其他数字代表错误码 std::string execfile; //执行的文件名 argument_t arguments; //参数列表 bool is_redirect_stdin = false; //是否重定向了标准输入,即是否存在<语法元素 std::string filename_in; //新的标准输入名 bool is_redirect_stdout = false; //是否重定向了标准输出,即是否存在>语法元素 bool stdout_mode = false; //false表示截断输出,true表示追加输出 std::string filename_out; //新的标准输出文件名 bool is_background = false; //是否指定在后台运行,即是否存在&语法元素 bool is_pipe = false; //是否是一个管道,即是否存在|语法元素 std::string pipe_prompt[2]; //保存管道命令};command_t parse_command(std::string command);std::string string_trim(std::string s);std::string get_tip();int exec_command(command_t &cmd);int psh_error(int error);int shellfunc_exit(command_t);int shellfunc_logout(command_t);int shellfunc_cd(command_t);#endif
src/psh.cpp
#include<pangda/psh.h>#include<readline/readline.h>#include<readline/history.h>#include<cstdlib>#include<signal.h>extern std::map<std::string, std::function<int(command_t)> > shell_commands;int main(int argc, char *argv[], char **envp) { //构建内建命令与实现函数的映射 shell_commands["exit"] = shellfunc_exit; shell_commands["logout"] = shellfunc_logout; shell_commands["cd"] = shellfunc_cd; //阻断SIGINT SIGQUIT SIGSTOP SIGTSTP signal(SIGINT, SIG_IGN); signal(SIGQUIT, SIG_IGN); signal(SIGSTOP, SIG_IGN); signal(SIGTSTP, SIG_IGN); while (true) { std::string st = readline(get_tip().c_str()); //获得用户输入的内容 //若用户输入的不是全空格,则将这条命令保存在历史记录中。 //否则就不处理这条命令,直接获得下一条命令。 if (string_trim(st) == "") continue; else add_history(st.c_str()); //解析命令 command_t cmd = parse_command(st); //若命令是管道,则分别执行两条管道命令 if (cmd.is_pipe) { command_t pipe1 = parse_command(cmd.pipe_prompt[0]); command_t pipe2 = parse_command(cmd.pipe_prompt[1]); if (exec_command(pipe1) != 0) //若管道的第一条命令就是错误的,不再执行第二条命令 continue; exec_command(pipe2); } else if (cmd.is_right_cmd) { //若解析命令之后发现命令存在错误,则进入错误处理程序 psh_error(cmd.is_right_cmd); continue; } exec_command(cmd); //交由解释器解释执行命令 } return 0;}
src/psh_parser.cpp
#include<pangda/psh.h>#include<sstream>//分割命令中的各个参数static argument_t split_command(std::string command) { argument_t ret; std::stringstream out(command); //构建字符串流 std::string t; while (out >> t) { //若流中仍然有内容 ret.push_back(t); } return ret;}//分割语法,解决ls>c的问题static void stylize_command(std::string &cmd) { for (auto i = 0u; i < cmd.length(); i++) { //offset用于计算偏移值,若在语法元素左右插入了空格,那么cmd的长度会发生变化 //而i没有发生变化,因此需要计算偏移值。 int offset = 0; //若检测到了语法元素:><|& if (cmd[i] == '<' || cmd[i] == '|' || cmd[i] == '&') { if (i - 1 >= 0 && cmd[i - 1] != ' ') { //若语法元素左方没有空格 cmd.insert(i, " "); offset--; } if (i + 1 < cmd.length() && cmd[i + 1 + offset] != ' ') { //若语法元素右方无空格 cmd.insert(i + 1, " "); } } else if (cmd[i] == '>') { if (i - 1 >= 0 && cmd[i - 1] != ' ' && cmd[i - 1] != '>') { //若语法元素左方没有空格 cmd.insert(i, " "); offset--; } if (i + 1 < cmd.length() && cmd[i + 1 + offset] != ' ' && cmd[i + 1 + offset] != '>') { //若语法元素右方无空格 cmd.insert(i + 1, " "); } } }}//构建管道命令。管道的实现方式:将管道左右分别变为两个带重定向的命令,分别执行两个命令static void pipe_buildcmd(command_t &origin_cmd) { auto in = origin_cmd.arguments; origin_cmd.pipe_prompt[0] = origin_cmd.pipe_prompt[1] = ""; bool front = true; for (auto i : in) { if (i != "|") { //若未检测到|,说明仍然在管道左方 if (front) { origin_cmd.pipe_prompt[0] += i + " "; } else { origin_cmd.pipe_prompt[1] += i + " "; } } else { front = false; } } origin_cmd.pipe_prompt[0] += " > /tmp/psh_pipefile"; //补充管道左方命令 origin_cmd.pipe_prompt[1] += " < /tmp/psh_pipefile"; //补充管道右方命令}//删除命令中语法元素所占参数表位置static void setarg_command(command_t &cmdt) { std::vector<std::string> ret, in = cmdt.arguments; int sz = in.size(); //若命令中不含任何语法元素,不含管道原因是若含有管道会提前返回,不在这里执行。 if (!(cmdt.is_redirect_stdin || cmdt.is_redirect_stdout || cmdt.is_background)) { return; } //逐个检查参数列表中的内容 for (int i = 0; i < sz; i++) { if (in[i] == ">" || in[i] == "<" || in[i] == ">>") { //若为<>,需要多跳过一个内容 i++; } else if (in[i] == "&") { //若为&,直接跳过即可,循环结束会自动i++ continue; } else { ret.push_back(in[i]); //没有语法元素说明是参数的一部分,应当保留 } } cmdt.arguments = ret; //改变原本的参数表}//标记语法元素static void setmark_command(command_t &cmdt) { //从除过命令名之后的其他元素开始逐个检查 for (auto it = cmdt.arguments.begin() + 1; it != cmdt.arguments.end(); it++) { //若出现>元素 if (*it == ">" || *it == ">>") { cmdt.is_redirect_stdout = true; //标记>语法元素 if (*it == ">>") cmdt.stdout_mode = true; if (it + 1 == cmdt.arguments.end()) { cmdt.is_right_cmd = 400; //错误400:>语法元素后没有跟任何内容,错误语法 break; } cmdt.filename_out = *++it; //存储重定向的文件名 continue; } else if (*it == "<") { cmdt.is_redirect_stdin = true; //标记<语法元素 if (it + 1 == cmdt.arguments.end()) { cmdt.is_right_cmd = 401; //错误400:>语法元素后没有跟任何内容,错误语法 break; } cmdt.filename_in = *++it; //存储重定向的文件名 continue; } else if (*it == "|") { cmdt.is_pipe = true; //标记|语法元素 cmdt.is_right_cmd = 490; //错误490:命令是一个管道,应当使用管道的执行方法 pipe_buildcmd(cmdt); //构建管道命令 break; } else if (*it == "&") { if (it + 1 == cmdt.arguments.end()) { cmdt.is_background = true; //标记&语法元素 } else { cmdt.is_right_cmd = 403; //错误400:&语法元素出现在了命令中间,错误语法 break; } } } if (cmdt.is_right_cmd || cmdt.is_pipe) //若命令是错误的或者是一个管道,则交由各自的处理程序处理 return; setarg_command(cmdt); //删除句中出现的多余语法元素}command_t parse_command(std::string command) { command_t ret; stylize_command(command); //让句中的语法元素与其他元素分割开来 ret.arguments = split_command(command); //构建参数列表 ret.execfile = ret.arguments[0]; //指定要执行的命令名 setmark_command(ret); //标识命令中出现的语法元素 return ret;}
src/psh_shfunc.cpp
#include<pangda/psh.h>#include<sys/utsname.h>#include<pwd.h>#include<cstring>//获得当前用户名std::string get_username(uid_t uid) { passwd *ret = getpwuid(uid); if (ret == NULL) return ""; return std::string(ret->pw_name);}//获得shell提示符//返回类似于:"[username@hostname folder] #"std::string get_tip() { char hostname[65]; gethostname(hostname, 65); //获得主机名 char *curdir = getcwd(NULL, 0); //获得当前工作目录 std::string ret = "[" + get_username(getuid()) + "@" + hostname + " "; int lastpos = 0; for (auto i = 0u; i < strlen(curdir); i++) { if (curdir[i] == '/') lastpos = i; } std::string where = std::string(curdir).substr(lastpos + 1);//将工作目录前的内容删除 //构建shell提示符 if (where == "") where = "/"; //这里若where是空,那么这里工作目录在根目录下 ret += where; ret += "]"; ret += ((geteuid() == 0) ? "# " : "$ "); //当以root权限运行时,提示符为'#',否则为'$' free(curdir); //释放getcwd()以malloc()开辟的空间 return ret;}//根据错误码输出错误信息int psh_error(int error) { switch (error) { case 1: break; case 100: printf("psh: command not found.\n"); break; case 201:case 202: printf("psh: file doesn\'t exist.\n"); break; case 300: printf("psh: environment error.\n"); break; case 400:case 401:case 403: printf("psh: error present.\n"); break; } return -1;}//去除字符串两侧多余的空格std::string string_trim(std::string s) { if(s.empty()) { return s; } s.erase(0,s.find_first_not_of(" ")); s.erase(s.find_last_not_of(" ") + 1); return s;}//内建命令:cdint shellfunc_cd(command_t cmdt) { if (cmdt.arguments.size() == 1) { cmdt.arguments.push_back("."); //若cd无参数,默认给参数. } if (chdir(cmdt.arguments[1].c_str()) != 0) { //切换shell的工作目录,若失败则输出原因。 perror("psh"); return -1; } return 0;}//内建命令:exitint shellfunc_exit(command_t cmdt) { exit(0); return 0;}//内建命令:logoutint shellfunc_logout(command_t cmdt) { return shellfunc_exit(cmdt); //logout功能与exit相同,直接跳转使若exit有更多功能不必再次复制}
src/psh_explain.cpp
#include<pangda/psh.h>#include<sys/wait.h>#include<dirent.h>#include<fcntl.h>#include<cstring>//shell_commands:构建内建命令与处理函数的映射std::map<std::string, std::function<int(command_t)> > shell_commands;//分割字符串,主要用于分割PATH环境变量。让a:b:c变成["a","b","c"]的列表,方便查找static std::vector<std::string> split_string(std::string str, char sep) { std::vector<std::string> ret; unsigned int start = 0; //用于标记上一个分割符所在的位置 for (auto i = 0u; i < str.length(); i++) { if (str[i] == sep) { //若当前指向的位置就是分割符 ret.push_back(str.substr(start, i - start)); start = i + 1; } } ret.push_back(str.substr(start, str.length())); return ret;}static std::string find_exec(command_t &cmd) { //检查命令是否属于内建命令 if (shell_commands.find(cmd.execfile) != shell_commands.end()) { shell_commands[cmd.execfile](cmd); //若属于内建命令直接执行相关函数 cmd.is_right_cmd = 1; //错误1:命令是一个内建命令,无需处理 return ""; } //命令不属于内建命令,那么依次在环境变量目录中查找命令的可执行文件 std::vector<std::string> envpath = split_string(getenv("PATH"), ':'); //构建目录列表 envpath.push_back("./"); //将当前目录也放在查找列表中 for (auto it : envpath) { DIR *dp = opendir(it.c_str()); //打开相应目录,开始查找 dirent *dirp; if (dp == NULL) { cmd.is_right_cmd = 300; //错误300:环境变量中配置的PATH目录有错误 return ""; } while ((dirp = readdir(dp)) != NULL) { if (it != "./" && cmd.execfile == dirp->d_name) { //若不再当前目录下查找 std::string ret = it; if (ret[ret.length() - 1] != '/') { ret.push_back('/'); } ret = ret + cmd.execfile; return ret; } //在当前目录下要保证前方有./才去执行 if (it == "./" && cmd.execfile.length() >= 2 && cmd.execfile.substr(2) == dirp->d_name) { if (cmd.execfile.substr(0, 2) != "./") break; char *current_dir = getcwd(NULL, 0); cmd.execfile.erase(0, 1); std::string ret = current_dir + cmd.execfile; free(current_dir); return ret; } } } cmd.is_right_cmd = 100; //错误100:命令不存在 return "";}int exec_command(command_t &cmd) { //若命令存在错误,执行错误处理程序 if (cmd.is_right_cmd) { psh_error(cmd.is_right_cmd); return -1; } std::string path = find_exec(cmd); //查找到命令的绝对路径 //若命令未查找到,那么执行错误处理程序 if (cmd.is_right_cmd) { psh_error(cmd.is_right_cmd); return -1; } //构建符合系统调用要求的参数列表 char *arglist[cmd.arguments.size() + 1]; char args[200][256]; for (auto i = 0u; i < cmd.arguments.size(); i++) { strcpy(args[i], cmd.arguments[i].c_str()); arglist[i] = (char *)args[i]; } arglist[cmd.arguments.size()] = (char *)NULL; //参数列表要以NULL结尾,不然会出错误 pid_t child = fork(); //调用fork() if (child < 0) { //若fork<0,说明fork出错 psh_error(200); //错误200:无法fork()出子进程 return -1; } //若child>0,说明在执行的是父进程 if (child > 0) { //如果是后台进程,那么不再管他,输出完他的id后继续读取下一条命令 if (cmd.is_background) { printf("[Process id] %d\n", (int)child); return 0; } else { int ret; //等待子进程运行完毕,不使用wait()的原因是: //若在此之前有一个带有&语法元素的进程已经执行完毕,那么这里调用wait获取到的是上一个 //执行完毕的后台进程的状态,这样可能造成当前命令还没有执行完毕, //父进程已经退出了等待程序开始等待新的输入。 if (waitpid(child, &ret, 0) == -1) { //若出现错误 perror("psh"); return -1; } return 0; } } //若child=0,说明执行的是子进程 if (child == 0) { if (cmd.is_redirect_stdin) { //若有<语法元素 int fd = open(cmd.filename_in.c_str(), O_RDONLY); //打开重定向的stdin if (fd < 0) { psh_error(201); //错误201:打开文件出错 exit(0); } //不使用dup的原因是,dup只会复制文件描述符,不可以指定新的文件描述符是什么 //这里将新的描述符指定为标准输入的文件描述符,dup2会自动关闭原来的STDIN描述符 //并将fd的文件描述符设置为STDIN_FILENO dup2(fd, STDIN_FILENO); } if (cmd.is_redirect_stdout) { //若有>语法元素 mode_t mode = S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH; //配置文件属性 int flag = O_WRONLY | O_CREAT; flag |= (cmd.stdout_mode) ? O_APPEND : O_TRUNC; int fd = open(cmd.filename_out.c_str(), flag, mode); if (fd < 0) { psh_error(202); //错误202:打开文件出错 exit(0); } //不使用dup的原因同stdin dup2(fd, STDOUT_FILENO); } int ret = execv(path.c_str(), arglist); if (ret == -1) { perror("psh"); exit(-1); } exit(0); //结束掉子进程 } return -1;}
一些要注意的地方
- 父进程中应当使用
waitpid
来等待子进程结束,原因是若去使用wait
,当之前有一个后台运行的进程已经结束时,会获取到那个进程的状态,导致之后的提示符错位。 - 构建
argv[]
时,最后一个元素应该是(char *)NULL
,否则会出现Bad Address
的错误。 - 其实使用带
p
的exec
族函数可以自动在path
环境变量中查找,不需要手动进行。 - 记得忽略掉
SIGINT
、SIGQUIT
、SIGSTOP
、SIGTSTP
信号。 - 不知道还有什么了QAQ。
阅读全文
0 0
- [GNU/Linux] 自己实现shell
- [GNU/Linux] 自己实现ls
- Linux 下GNU Readline库函数安装用于实现shell内嵌补全
- GNU/Linux 线程实现
- shell的自己实现
- 实现自己的shell框架
- linux 编写自己的shell
- 【shell】linux shell 实现多线程
- GNU/LINUX
- GNU/Linux
- 用Shell脚本实现自动化完成属于自己的微型Linux!(一)
- 用Shell脚本实现自动化完成属于自己的微型Linux!(二)
- Linux下c++调用自己编写的matlab函数:通过shell script system command实现
- linux shell实现随机数
- linux 远程shell 实现
- linux shell 实现多线程
- linux shell 实现多线程
- linux shell 实现多线程
- 浮点型的存储方式
- 修改eclipse字体大小
- AndroidStudio工程中的build.gradle中的buildTypes、productFlavors的多构建、多渠道打包APK
- device setup classes和device interface classes区别
- Nginx实现高并发优化
- [GNU/Linux] 自己实现shell
- Linux下线程pid和tid
- vs2013+osg3.4+qt5.6.2编译过程
- A-Fast-RCNN: Hard Positive Generation via Adversary for Object Detection 笔记
- 1.16Android 学习+进度之十六-项目阶段性总结(没有更高的需求可能是最终总结)
- 九九乘法表
- 目前最值得学习的9种非主流的开发语言
- CSS中哪些属性可以被继承
- 动态sql使用