[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的错误。
  • 其实使用带pexec族函数可以自动在path环境变量中查找,不需要手动进行。
  • 记得忽略掉SIGINTSIGQUITSIGSTOPSIGTSTP信号。
  • 不知道还有什么了QAQ。

这里写图片描述