Linux C下实现简单的Shell
宗旨:技术的学习是有限的,分享的精神的无限的。
【需求描述】
用各种C函数实现一个简单的交互式Shell:
1、给出提示符,让用户输入一行命令,识别程序名和参数并调用适当的exec函数执行程序,待执行完成后再次给出提示符。
2、识别和处理以下符号:简单的标准输入输出重定向( <和>),先dup2然后exec。管道(|): Shell进程先调用pipe创建一对管道描述符,然后fork出两个子进程,一个子进程关闭读端,调用dup2把写端赋给标准输出,另一个子进程关闭写端,调用dup2把读端赋给标准输入,两个子进程分别调用exec执行程序,而Shell进程把管道的两端都关闭,调用wait等待两个子进程终止。程序应该可以处理以下命令:
○ls△-l△-R○>○file1○
○cat○<○file1○|○wc△-c○>○file1○
○表示零个或多个空格,△表示一个或多个空格
//=====================================================================
一、结构定义
用户输入一行命令,将分解后的命令存放在arg中,最多10个。之所以定义输入、输出重定向文件名是为了方便使用。还要余外处理重定向和管道问题。必须创建子进程(有多少命令就有多少个子进程),然后调用exec函数族。
typedef struct command { char *arg[SHELL_CMD_MAX_LENGTH]; // 命令行参数,最多10个参数 char *input_file; // 存放输入重定向的文件名 char *output_file; // 存放输出重定向的文件名 } cmd_t;<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
二、接口定义
管道处理用strtok_r()函数,重定向处理使用strtok()函数,仔细研究一下他们的区别。
1、管道个数【管道的个数 =(命令个数– 1)】
static uint8_t shell_parse_pipe(char *data, cmd_tcmd[]);
2、重定向
static uint8_t shell_parse_cmd_line(char *data,cmd_t *cmd);
填充命令结构体中的各个成员,输入输出填充input_file,output_file成员,命令填充arg成员。命令成员的最后一个字符指向NULL。
3、内建命令的单独处理 --- cd【内建命令都需要一个个处理 --- 如type cd查看命令是内建还是外部shell】
int shell_cd_command(char *command, char *path);
/* 只能处理外部命令,内建命令不能处理,内建命令要一个一个单独实现 type命令可以查看命令是外部命令还是内建命令:type cd // 是shell的内建 */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #define SHELL_CMD_MAX_COUNT 10 #define SHELL_CMD_MAX_LENGTH 20 #define SHELL_IN_OUT_FILE_MAX_SIZE 20 #define SHELL_BUFFER 64 #define SHELL_PIPE_MAX_COUNT 10 #define SHELL_PIPE_READ_WRITE 2 typedef unsigned int uint32_t; typedef unsigned char uint8_t; typedef struct command { char *arg[SHELL_CMD_MAX_LENGTH]; // 命令行参数,最多10个参数 char *input_file; // 存放输入重定向的文件名 char *output_file; // 存放输出重定向的文件名 } cmd_t; /* 以空格符分开命令行字符串 */ static uint8_t shell_parse_cmd_line(char *data, cmd_t *cmd) { uint32_t i = 0; cmd->input_file = NULL; cmd->output_file = NULL; char *token = strtok(data, " "); while(token) { /*如果>后面有空格,那么执行完strtok后,空格被替换成'\0',*(p+1)就是'\0',为假,不执行cmd_buf->out=p+1*/ /*如果>后面没有空格,那么执行完strtok后,>符号被替换成'\0'了,直接调用strtok函数*/ if(*token == '>') { if(*(token + 1)) { cmd->output_file = token + 1; } else { cmd->output_file = strtok(NULL, " "); } } else if(*token == '<') { if(*(token + 1)) { cmd->input_file = token + 1; } else { cmd->input_file = strtok(NULL, " "); } } else { printf("token....\n"); /*如果获取的命令行参数不是>或者<,那么就将它们保存在arg中*/ cmd->arg[i++] = token; } token = strtok(NULL, " "); } cmd->arg[i] = NULL; return 0; } /* 以管道符分开命令行字符串 */ static uint8_t shell_parse_pipe(char *data, cmd_t cmd[]) { uint8_t count = 0; char *temp; char *token = strtok_r(data, "|", &temp); while(token) { /*以管道符分开的第一个字符串存放在结构数组cmd[0]中,第二个字符串存放在结构数组cmd[1]中,依次递推*/ shell_parse_cmd_line(token, &cmd[count++]); token = strtok_r(NULL, "|", &temp); } return count; } /* cd内部命令 */ int shell_cd_command(char *command, char *path) { int return_value = 0; if(strncmp(command, "cd", 2) == 0) if((return_value = chdir(path)) < 0) { perror("chdir"); } return return_value; } static void shell_process(void) { char cmd_buf[SHELL_BUFFER], pathname[SHELL_BUFFER]; cmd_t cmds[SHELL_CMD_MAX_COUNT]; pid_t pid; // 进程pid /* 输入输出重定向文件描述符, 管道个数, 10个管道描述符, 管道分隔的命令个数*/ uint8_t fd_in, fd_out, pipe_num, pipe_fd[SHELL_PIPE_MAX_COUNT][SHELL_PIPE_READ_WRITE], cmd_num = 0; uint8_t i = 0, j = 0; while(1) { memset(pathname, 0, sizeof(pathname)); getcwd(pathname, sizeof(pathname)); /* 获取当前工作路径 */ printf("[libang--%s--]$", pathname); fflush(stdout); /* 刷新缓冲区,这连续四行只是为了显示好看而已,不要也可以 */ memset(cmd_buf, 0, sizeof(cmd_buf)); fgets(cmd_buf, sizeof(cmd_buf), stdin); cmd_buf[strlen(cmd_buf) - 1] = '\0'; // 获取输入的命令到cmd_buf cmd_num = shell_parse_pipe(cmd_buf, cmds); shell_cd_command(cmds[0].arg[0], cmds[0].arg[1]); // 处理cd内建命令 pipe_num = cmd_num - 1; if(pipe_num > SHELL_PIPE_MAX_COUNT) { continue; } /* 一个管道有in和out, 创建pipe_num个管道 */ for(i = 0; i < pipe_num; ++i) { pipe(pipe_fd[i]); } /* 这一轮循环,创建了pipe_num+1个进程,其中一个父进程,pipe_num个子进程 */ for(i = 0; i < cmd_num; ++i) { if((pid = fork()) < 0) { printf("fork fail!\n"); exit(1); } if(pid == 0) { break; } } /* 有多少个命令就会执行多少个子进程,最终调用exec函数族 */ if(pid == 0) { /* 上面循环中,子进程break,所以执行下面的语句,此时i就和循环变量i一样 */ if(cmds[i].input_file) { /* 重定向输入 */ if((fd_in = open(cmds[i].input_file, O_RDONLY)) < 0) { perror("open fail!\n"); } dup2(fd_in, STDIN_FILENO); close(fd_in); } if(cmds[i].output_file) { /* 重定向输出 */ if((fd_out = open(cmds[i].output_file, O_RDWR | O_CREAT | O_TRUNC), 0644) < 0) { perror("open fail!\n"); } dup2(fd_out, STDOUT_FILENO); close(fd_out); } /* 管道是进程间通信的一种方式,输入命令中有管道 */ if(pipe_num) { /* 第一个子进程,读入写出,关闭读端,把标准输出重定向到写端*/ if(0 == i) { close(pipe_fd[i][0]); dup2(pipe_fd[i][1], STDOUT_FILENO); // 本来执行结果是在标准输出上 close(pipe_fd[i][1]); /* 关闭掉多余的管道 */ for(j = 1; j < pipe_num; ++j) { close(pipe_fd[j][0]); close(pipe_fd[j][1]); } } /* 最后一个子进程,关闭写端,把标准输入重定向到读端 */ else if(pipe_num == i) { close(pipe_fd[i - 1][0]); dup2(pipe_fd[i - 1][0], STDIN_FILENO); close(pipe_fd[i - 1][0]); /* 关闭掉多余的管道读写端 */ for(j = 0; j < pipe_num - 1; ++j) { close(pipe_fd[j][0]); close(pipe_fd[j][1]); } } /* 1~pipe_num-1, */ else { dup2(pipe_fd[i - 1][0], STDIN_FILENO); close(pipe_fd[i - 1][0]); dup2(pipe_fd[i][1], STDOUT_FILENO); close(pipe_fd[i][1]); for(j = 0; j < pipe_num; ++j) { if((j != i - 1) || j != i) { close(pipe_fd[j][0]); close(pipe_fd[j][1]); } } } } /* arg第1个参数是命令,后面的参数是命令选项如:-l */ execvp(cmds[i].arg[0], cmds[i].arg); } else // 父进程阻塞 { for(i = 0; i < pipe_num; ++i) { close(pipe_fd[i][0]); close(pipe_fd[i][1]); } for(i = 0; i < cmd_num; ++i) { wait(NULL); } } } } int main(int argc, char **argv) { shell_process(); return 0; }