Linux下写一个简陋的shell程序(2)
Linux下写一个简陋的shell程序(2)
第一版的shell程序在这:Linux下写一个简陋的shell程序-CSDN博客
这一节的内容主要增加一个小功能:重定向。也就是在Linux中使用的重定向指令:>
,<
,>>
。
补充:这里的重定向暂时只是支持不是内建指令的指令重定向(也就是外部指令),这里只是做学习使用,没想搞那么复杂。
代码
完整shell程序代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
#define STREND '\0'char* argv[MAX_ARGC];
int exitcode = 0;
char pwd[SIZE];
char env[SIZE];//重定向类型标记
#define NoneRedir -1//没有重定向
#define StdinRedir 0//输入重定向
#define StdoutRedir 1//输出重定向
#define AppendRedir 2//追加重定向//一个跳过字符串中空格字符的小函数,调用了isspace().
//写的是do while 因为常规的while循环后面跟不了';' 所以用do while
//当然了,你也可以单独写一个函数,不用宏定义.
//其实我个人也是更趋向于写函数定义而不是宏定义,看起来怪怪的
#define IgnSpace(buf,pos) do{ while(isspace(buf[pos])) pos++; }while(0)int redir_type = NoneRedir;//重定向类型,默认是没有重定向
char *filename = NULL;//存储文件名字符串int Interactive(char out[],int size)
{const char* hostname = getenv("HOSTNAME");const char* username = getenv("USER");const char* p_wd = getenv("PWD");if(hostname == NULL) hostname = "None";if(username == NULL) username = "None";if(p_wd == NULL) p_wd = "None";printf("[%s@%s %s]$ ",username, hostname, p_wd);fgets(out,size,stdin);out[strlen(out)-1] = 0;return strlen(out);
}//判断指令中是否有重定向指令
void CheckRedir(char in[])
{// ls -a -l 没有重定向的指令// ls -a -l > log.txt 输出重定向// ls -a -l >> log.txt 追加重定向// cat < log.txt 输入重定向redir_type = NoneRedir;//默认没有重定向指令filename = NULL;int pos = strlen(in) - 1;//从指令字符串尾部开始遍历while(pos >= 0){if(in[pos] == '>'){//分两种情况,一种是输出重定向,一种是追加重定向//因为我们是从后向前遍历,所以如果前一个字符也是'>',则为追加重定向if(in[pos - 1] == '>'){redir_type = AppendRedir;//将前面的指令和后面的内容分开//就好比如完整指令是ls -a -l >> log.txt ,我们找到重定向符号后//就ls -a -l \0 >> log.txt 这样后续执行指令的时候就不会受到后面内容的影响in[pos-2] = STREND;//确定好重定向类型后,pos++到下一个字符,我们要越过空格去找重定向的文件//比如ls -a -l >> log.txt ,pos本来在">>"中的第二个'>'位置处//pos++后,就来到了">>"后面的空格处,然后开始跳过空格找log.txtpos++;IgnSpace(in,pos);filename = in + pos;break;}else//输出重定向{//解析和上面同理redir_type = StdoutRedir;in[pos - 1] = STREND;pos++;IgnSpace(in,pos);filename = in + pos;break;}}else if(in[pos] == '<'){redir_type = StdinRedir;in[pos - 1] = STREND;pos++;IgnSpace(in,pos);filename = in + pos;break;}else{pos--;}}
}void Split(char in[])
{//在分离指令之前先判断一下指令中是否有重定向的指令CheckRedir(in);int i = 0;argv[i++] = strtok(in,SEP);while(argv[i++] = strtok(NULL,SEP));if(strcmp(argv[0],"ls") == 0){argv[i-1] = (char*)"--color";argv[i] = NULL;}
}void Execute()
{pid_t id = fork();if(id == 0){int fd = -1;if(redir_type == StdinRedir){fd = open(filename,O_RDONLY);dup2(fd,0);}else if(redir_type == StdoutRedir){fd = open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);dup2(fd,1);}else if(redir_type == AppendRedir){fd = open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);dup2(fd,1);}else{// nothing to do;}//子进程替换程序execvp(argv[0],argv);//程序替换失败就退出exit(1);}//父进程后续工作,回收子进程资源int status = 0;pid_t rid = waitpid(id, &status, 0);//阻塞等待//处理一下进程退出码if(rid == id) exitcode = WEXITSTATUS(status);}int BuildinCmd()
{int ret = 0 ;//我们只能穷举判断是否是内建命令了//我们这里只列举一些常用的内建命令作为例子if(strcmp("cd",argv[0]) == 0){//执行内建命令ret = 1;char* target = argv[1];//cd xxx or cd if(!target) target = getenv("HOME");chdir(target);char temp[1024];getcwd(temp,1024);snprintf(pwd,SIZE,"PWD=%s",temp);putenv(pwd);}else if(strcmp("export",argv[0]) == 0){ret = 1;if(argv[1]){strcpy(env,argv[1]);putenv(env);}}else if(strcmp("echo",argv[0]) == 0){ret = 1;if(argv[1] == NULL){printf("\n");}else{if(argv[1][0] == '$'){if(argv[1][1] == '?'){printf("%d\n",exitcode);exitcode = 0;}else {char* e = getenv(argv[1]+1);if(e) printf("%s\n",e);}}else {printf("%s\n",argv[1]);}}}return ret;
}int main()
{while(1){//用户输入的指令需要有数组来进行一个接收char commandline[SIZE];//第一步,打印命令行提示符,等待用户输入指令int n = Interactive(commandline,SIZE);if(n == 0) continue;//第二步,将接收到的指令拆解开Split(commandline);//第三步,处理内建命令n = BuildinCmd();//如果是内建命令,就不需要执行下一句代码了,因为已经在BuildinCwd()执行了。if(n) continue;//第四步,执行不是内建命令的命令Execute();}return 0;
}
增加的重定向功能的代码:
#define STREND '\0'
//重定向类型标记
#define NoneRedir -1//没有重定向
#define StdinRedir 0//输入重定向
#define StdoutRedir 1//输出重定向
#define AppendRedir 2//追加重定向//一个跳过字符串中空格字符的小函数,调用了isspace().
//写的是do while 因为常规的while循环后面跟不了';' 所以用do while
//当然了,你也可以单独写一个函数,不用宏定义.
//其实我个人也是更趋向于写函数定义而不是宏定义,看起来怪怪的
#define IgnSpace(buf,pos) do{ while(isspace(buf[pos])) pos++; }while(0)int redir_type = NoneRedir;//重定向类型,默认是没有重定向 //全局变量
char *filename = NULL;//存储文件名字符串 //全局变量//判断指令中是否有重定向指令
void CheckRedir(char in[])
{// ls -a -l 没有重定向的指令// ls -a -l > log.txt 输出重定向// ls -a -l >> log.txt 追加重定向// cat < log.txt 输入重定向redir_type = NoneRedir;//默认没有重定向指令filename = NULL;int pos = strlen(in) - 1;//从指令字符串尾部开始遍历while(pos >= 0){if(in[pos] == '>'){//分两种情况,一种是输出重定向,一种是追加重定向//因为我们是从后向前遍历,所以如果前一个字符也是'>',则为追加重定向if(in[pos - 1] == '>'){redir_type = AppendRedir;//将前面的指令和后面的内容分开//就好比如完整指令是ls -a -l >> log.txt ,我们找到重定向符号后//就ls -a -l \0 >> log.txt 这样后续执行指令的时候就不会受到后面内容的影响in[pos-2] = STREND;//确定好重定向类型后,pos++到下一个字符,我们要越过空格去找重定向的文件//比如ls -a -l >> log.txt ,pos本来在">>"中的第二个'>'位置处//pos++后,就来到了">>"后面的空格处,然后开始跳过空格找log.txtpos++;IgnSpace(in,pos);filename = in + pos;break;}else//输出重定向{//解析和上面同理redir_type = StdoutRedir;in[pos - 1] = STREND;pos++;IgnSpace(in,pos);filename = in + pos;break;}}else if(in[pos] == '<'){redir_type = StdinRedir;in[pos - 1] = STREND;pos++;IgnSpace(in,pos);filename = in + pos;break;}else{pos--;}}
}void Split(char in[])
{//在分离指令之前先判断一下指令中是否有重定向的指令CheckRedir(in);int i = 0;argv[i++] = strtok(in,SEP);while(argv[i++] = strtok(NULL,SEP));if(strcmp(argv[0],"ls") == 0){argv[i-1] = (char*)"--color";argv[i] = NULL;}
}void Execute()
{pid_t id = fork();if(id == 0){int fd = -1;if(redir_type == StdinRedir){fd = open(filename,O_RDONLY);dup2(fd,0);//这里原则上是要判断一下是否执行成功的,不过这里只是测试学习,就没写}else if(redir_type == StdoutRedir){fd = open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);dup2(fd,1);}else if(redir_type == AppendRedir){fd = open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);dup2(fd,1);}else{// nothing to do;}//子进程替换程序execvp(argv[0],argv);//程序替换失败就退出exit(1);}//父进程后续工作,回收子进程资源int status = 0;pid_t rid = waitpid(id, &status, 0);//阻塞等待//处理一下进程退出码if(rid == id) exitcode = WEXITSTATUS(status);}
逻辑讲解:
这里相比于第一版多的逻辑就是:首先我们要在分离指令之前,判断一下这个指令中是否有重定向的指令,也就是CheckRedir()
函数。判断完成后和原来一样分离指令。
分离完指令后(分离指令之前就已经判断好重定向类型了),就开始执行指令。我们创建子进程,根据重定向类型,使用传了不同参数的open()
函数打开重定向的目标文件,然后使用dup2()
函数,让目标文件使用标准文件流的文件描述符。
这样一来当后续执行指令输出的时候就不会输出到原来的标准输出(默认是屏幕),指令需要获取内容的时候也不是从原来的标准输入(默认是键盘)获取,而是目标文件。
例子讲解:
接下来我使用这个指令来给大家说明一下流程:ls -a -l > log.txt
首先,分离指令之前判断指令中是否有重定向的指令,发现有,并且类型为输出重定向。在CheckRedir()
函数中,确定重定向类型,并且将原来的字符串设置为ls -a -l \0> log.txt
,这样设置是为了防止后续分离指令的时候读到重定向指令以及后面的文件,这是不需要的。并且获取重定向的目标文件是log.txt
(也就是说,后续执行指令输出的内容会输出到log.txt
而不是屏幕)
分离完指令后,开始执行指令,先创建子进程,然后判断到重定向类型,发现是输出重定向,并且目标文件是log.txt
,那么就用fd = open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);
打开log.txt
文件。
并且使用dup2(fd,1)
,让log.txt
的文件描述符变为1
,此刻log.txt
就是标准输出。
最后使用execp
函数加载指令程序并执行ls -a -l
。可以发现ls -a -l
指令执行返回的内容就在log.txt
里面。
具体逻辑就是这样,底层原理请看下文。
底层原理
观察现象:
我们运行下面这一段代码,观察一下代码运行的结果:
#include <stdio.h>int main() {//以写方式打开文件FILE *file = fopen("example.txt", "w");if (file == NULL) {printf("文件打开失败!\n");return 1;}fclose(file);return 0;
}
接着我们使用一下输出重定向的指令:>
,观察一下现象:
发现两者的行为是一致的。
接着我们运行下面这一段代码,观察一下运行结果:
#include <stdio.h>int main() {//依旧是以写方式打开文件FILE *file = fopen("example.txt", "w");if (file == NULL) {printf("文件打开失败!\n");return 1;}//使用fprintf函数向文件写入内容fprintf(file, "aaaaaa\n");fclose(file);return 0;
}
接着我们使用一下输出重定向的指令:>
,观察一下现象:
发现两者的行为是一致的。
接着我们运行下面这一段代码,观察一下运行结果:
#include <stdio.h>int main() {//以追加模式“a”打开文件FILE *file = fopen("example.txt", "a");if (file == NULL) {printf("文件打开失败!\n");return 1;}if(fprintf(file, "aaaaaa\n") < 0) {perror("写入失败");fclose(file);return 1;}fclose(file);return 0;
}
接着我们使用一下追加重定向的指令:>>
,观察一下现象:
发现两者的行为是一致的。
同理:
#include <stdio.h>int main() {FILE *fp = fopen("example.txt", "r"); // 只读模式,对应 <if (fp == NULL) {perror("打开文件失败");return 1;}char buffer[100];while (fgets(buffer, sizeof(buffer), fp) != NULL) {printf("%s", buffer); // 相当于 cat 命令的输出}fclose(fp);return 0;
}
前面我们以w或者a方式打开一个文件,都有一个特点就是:文件不存在的话就会在当前工作目录下创建。这是为什么呢?这主要是因为进程的信息里记录了当前的工作目录,也就是cwd
。关于cwd
的详细内容在进程的创建_进程创建实验-CSDN博客里面的fopen函数模块有讲解。
得出结论:
重定向的效果,和,使用不同模式打开fopen
函数的效果,是一样的。
核心对应关系表
fopen 模式 | Linux 重定向符号 | 等效 Shell 命令示例 | 操作特性 |
---|---|---|---|
"r" | < | program < input.txt | 只读输入(文件必须存在) |
"w" | > | program > output.txt | 截断写入(创建/清空文件) |
"a" | >> | program >> log.txt | 追加写入(保留原有内容) |
输入重定向<
,实际上就是读文件。输出重定向>
和追加重定向>>
,实际上就是向文件写入,只不过写入方式不同。
所以,我们可以不严谨的认为重定向的原理和fopen
的原理是一样的。于是我们想弄清楚重定向是怎么实现的就得弄清楚fopen
的底层原理。
这里大概是这样的:
可以这样理解,fopen
函数和重定向指令,两者的底层几乎一模一样,所以了解fopen
函数的底层,就是了解重定向指令的底层。
这里我直接给大家剧透一下,具体还是要去看下面提供的两个链接。
FILE *file = fopen("log.txt", "w");
//封装着:
int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
//所以输出重定向>的底层也是调用这个open函数,传入相同的函数
FILE *file = fopen("log.txt", "a");
//封装着:
int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
//所以追加重定向>>的底层也是调用这个open函数,传入相同的函数
链接1:(相关内容就在链接内容中的: fopen
的底层原理 和 重定向的底层原理):Linux(操作系统)文件系统–对打开文件的管理-CSDN博客
链接2:(dup2()
函数的讲解在这里面):Linux系统中与操作文件相关的系统调用-CSDN博客