用C实现Linux shell

2019.05.27更新:
这篇文章介绍了shell的基本原理, 在2018年暑假的时候, 我在里面又添加了小功能, 比如sudo, 这些功能有助于我更深入的了解Linux系统调用, 这里是源代码网页, 有需要的朋友可以自行下载源代码, 代码写的比较糟糕, 还请大佬们多多指教, 欢迎评论~


以下是原文

学习了进程一章之后, 可以尝试着写一个自己的shell

  • 程序目标
  • 单个命令: ls
  • 带-l到多个参数的命令: ls -l /temp
  • 带一个输出重定向的命令: ls -l > a
  • 带一个输入重定向的命令: wc -c < a
  • 带一个管道命令: ls -l | wc -c
  • 后台运行符可以添加到命令的最后面: ls &

整个程序逻辑很简单:
输入->分析命令->创建子进程运行

在编写的时候, 我使用了一个结构体, 在结构体里存放各种数据信息

/* 创建命令控制节点 */
typedef struct commandNode {
COMMAND_TYPE type; // 用户输入的命令类型
char cmd[COMMAND_MAX]; // 用户输入的命令
char arg[ARGLIST_NUM_MAX][COMMAND_MAX]; // 存放所有分解命令的数组
char *argList[ARGLIST_NUM_MAX + 1]; // 对用户输入的命令进行解析之后的字符串数组 , +1 是为了execvp操作,具体百度
char *argNext[ARGLIST_NUM_MAX]; // 管道之后命令的字符串数组
char file[FILE_PATH_MAX]; // 存放文件路径的数组
}CMD_NODE;

我们需要以下函数:

void initNode(CMD_NODE *cmdNode); // 初始化控制节点
void input(char cmd[COMMAND_MAX]); // 输入函数
void analysis_command(CMD_NODE *cmdNode); // 将命令进行分解
void put_into_arr(char argList[ARGLIST_NUM_MAX][COMMAND_MAX], char *cmd); // 将命令分解并放入数组中
void get_flag(char argList[ARGLIST_NUM_MAX][COMMAND_MAX], COMMAND_TYPE *flag); // 得到命令类型
void other_work(); // 处理一些命令特有的事情
void run(CMD_NODE *cmdNode); // 按照不同类别运行命令

  • initNode函数
void initNode(CMD_NODE *cmdNode) {
	        memset(cmdNode, 0, sizeof(CMD_NODE));
	        cmdNode->type = NORMAL;
	        }

这个函数十分简单, 先使整个控制头全部置零, 然后让输入命令类型变成NORMAL

  • input函数
 void input(char cmd[COMMAND_MAX]) {
    gets(cmd);
    if(strlen(cmd) >= COMMAND_MAX - 1) {
        printf("error: command too long!\n");
        exit(-1);
    }
}

利用gets函数得到命令,并进行字数检查

  • analysis_command函数
void analysis_command(CMD_NODE *cmdNode) {
    /* 自定义命令 */
    if(strcmp(cmdNode->cmd, "exit") == 0) {
        exit(-1);
    }
    if(strcmp(cmdNode->cmd, "pause") == 0) {
        char ch;
        while((ch = getchar()) == '\r');    // 在linux中的pause和windows里的不一样
    }
    /* 结束 */
    put_into_arr(cmdNode->arg, cmdNode->cmd); 
    get_flag(cmdNode->arg, &(cmdNode->type));
}

分析命令函数, 先进行内部命令处理, 如果命令不是内部命令, 则将命令分段存入arg中备用 . 等分段存入之后, 开始对命令的类型进行判断

  • put_into_arr函数
    int argIndex = 0;
    int index = 0;
    while(*cmd != '\0') {
        if(*cmd == ' ') {
            argIndex++;
            index = -1;
        }else {
            arg[argIndex][index] = *cmd;
        }
        cmd++;
        index++;
    }
}

若是碰到有空格,则认为参数数量+1. 如果没有碰到的话, 则是当前参数数组的下标累加

  • get_flag()函数
void get_flag(char arg[ARGLIST_NUM_MAX][COMMAND_MAX], COMMAND_TYPE *flag) {
    int argIndex = 0;
    int count = 0;
    while(arg[argIndex][0] != '\0') {
        if(strcmp("|", arg[argIndex]) == 0) {    // 将命令的类型置为管道命令
            *flag = HAVE_PIPE;
            count++;
        }
        if(strcmp(">", arg[argIndex]) == 0) {    // 将命令的类型置为输出重定向
            *flag = OUT_REDIRECT;
            count++;
        }
        if(strcmp("<", arg[argIndex]) == 0) {    // 将命令的类型置为输入重定向
            *flag = IN_REDIRECT;
            count++;             
        }
        if(strcmp("&", arg[argIndex]) == 0) {    // 将命令的类型置为后台运行
            *flag = BACKSTAGE;
            count++;                
        }
        argIndex++;
    }   
    if(count > 1) { // 命令类型重复定义
        printf("error: have too many args\n");
        exit(-1);
        *flag = COMMAND_ERR;
    }
}

上个函数,我们将命令的每个参数分别存入不同的数组中, 所以在此函数中, 我们对这些数组进行判断 . 如果遇到关键字,则count自增. 正确的命令应该只带有一个类型符, 因此如果count大于1, 则认为命令有错误

  • other_work()函数
void other_work(CMD_NODE *cmdNode) {
    int index = 0;
    int type = cmdNode->type;
    while(cmdNode->arg[index][0] != '\0') {
        cmdNode->argList[index] = cmdNode->arg[index];
        index++;
    }
    if(type == BACKSTAGE) { // 带有后台运行命令
        cmdNode->argList[index - 1] = NULL; // 将后台运行&屏蔽掉
    }
    index = 0;  // 使得下面代码能够正常使用index   
    if(type == OUT_REDIRECT || type == IN_REDIRECT) { // 带有重定向命令
        while(strcmp(">", cmdNode->argList[index]) && strcmp("<", cmdNode->argList[index])) {
                index++;
        }
        index++;    // 跳过'>'或者'<'   
        strcpy(cmdNode->file, cmdNode->argList[index]);
        cmdNode->argList[index - 1] = NULL;  // 使得argList只存放>或者<之前的命令
        return ;
    }

虽然我们将命令存放在了数组中, 但这是远远不够的, 我们还需要判断命令类型,以进行进一步的操作. 比如将>后的命令存放在file[]中

  • run()函数
void run(CMD_NODE *cmdNode) {
    pid_t pid = 0;
    pid = fork();   // 创建子进程
    if(pid < 0) {
        printf("创建子进程失败");
        exit(-1);
    }
    if(pid == 0) {  // 子进程
        if(cmdNode->type == NORMAL || cmdNode->type == BACKSTAGE) {
            execvp(cmdNode->argList[0], cmdNode->argList);
            exit(0);
        }
        if(cmdNode->type == IN_REDIRECT) {
            int fd = open(cmdNode->file, O_RDONLY|O_CREAT);
            dup2(fd, STDIN);   // 将输入流切换成指定的文件路径
            execvp(cmdNode->argList[0], cmdNode->argList);
            exit(0);
        }
        if(cmdNode->type == OUT_REDIRECT) {
            int fd = open(cmdNode->file, O_WRONLY|O_CREAT, O_TRUNC);    // 只写模式打开, 存在,则并且清空以前记录 不存在,则创建
            dup2(fd, STDOUT);   // 将输出流切换成指定的文件路径
            execvp(cmdNode->argList[0], cmdNode->argList);
            exit(0);
        }
        if(cmdNode->type == HAVE_PIPE) {
            pid_t pid2 = 0;
            int fd = 0;
            pid2 = fork();  // 再创建一个进程, 子子进程运行管道前命令, 子进程运行管道后命令
            if(pid2 < 0) {
                printf("管道命令运行失败\n");
                exit(-1);
            }
            if(pid2 == 0) { // 子子进程部分
            printf("进入子子进程\n");
            fd = open("/tmp/shellTemp", O_WRONLY|O_CREAT|O_TRUNC, 0644);
            dup2(fd, STDOUT);
            execvp(cmdNode->argList[0], cmdNode->argList);
            close(fd);
            exit(0);
            }
            if(waitpid(pid2, 0, 0) == -1) { // 子进程等待子子进程结束运行
                printf("error: 管道命令运行失败\n");
                exit(-1);
            }
            fd = open("/tmp/shellTemp", O_RDONLY);
            dup2(fd, STDIN);
            execvp(cmdNode->argNext[0], cmdNode->argNext);
            exit(0);
        }
    }
    if(cmdNode->type == BACKSTAGE) {
        printf("子进程pid为%d\n", pid);
        return ;
    }
    if(waitpid(pid, 0, 0) == -1) {
        printf("等待子进程失败\n");
    }
}

这儿是命令真正运行的地方

普通命令不再赘述, 他的原理就是打开子进程,然后使用execvp函数使得子进程运行命令.
重定向命令, 我们使用了dup2()函数来改变stdin或者stdout.
管道命令, 则是在子进程中再生成子进程
后台运行命令, 我们让父进程不再等待子进程结束, 从而达到后台运行的结果.


至此, 整个shell就写完成了,在写的时候,在代码设计中,出现了许多的问题. 不过编程有意思的地方就是在于如何优化自己代码

展开阅读全文

没有更多推荐了,返回首页