2019.05.27更新:
这篇文章介绍了shell的基本原理, 在2018年暑假的时候, 我在里面又添加了小功能, 比如sudo, 这些功能有助于我更深入的了解Linux系统调用, 这里是源代码网页, 有需要的朋友可以自行下载源代码, 代码写的比较糟糕, 还请大佬们多多指教, 欢迎评论~
以下是原文
学习了进程一章之后, 可以尝试着写一个自己的shell
整个程序逻辑很简单:
输入->分析命令->创建子进程运行
在编写的时候, 我使用了一个结构体, 在结构体里存放各种数据信息
/* 创建命令控制节点 */
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); // 按照不同类别运行命令
void initNode(CMD_NODE *cmdNode) {
memset(cmdNode, 0, sizeof(CMD_NODE));
cmdNode->type = NORMAL;
}
这个函数十分简单, 先使整个控制头全部置零, 然后让输入命令类型变成NORMAL
void input(char cmd[COMMAND_MAX]) {
gets(cmd);
if(strlen(cmd) >= COMMAND_MAX - 1) {
printf("error: command too long!\n");
exit(-1);
}
}
利用gets函数得到命令,并进行字数检查
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中备用 . 等分段存入之后, 开始对命令的类型进行判断
int argIndex = 0;
int index = 0;
while(*cmd != '\0') {
if(*cmd == ' ') {
argIndex++;
index = -1;
}else {
arg[argIndex][index] = *cmd;
}
cmd++;
index++;
}
}
若是碰到有空格,则认为参数数量+1. 如果没有碰到的话, 则是当前参数数组的下标累加
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, 则认为命令有错误
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[]中
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就写完成了,在写的时候,在代码设计中,出现了许多的问题. 不过编程有意思的地方就是在于如何优化自己代码