Linux从0到1——自定义shell2.0(添加重定向功能)
0. 前言
学习本篇之前,需要大家掌握简单的自定义shell
:
1. 宏+全局变量的设计
// redir
#define NoneRedir 0
#define OutPutRedir 1
#define AppendRedir 2
#define InputRedir 3
int redir = NoneRedir; // 默认是没有重定向
char *filename = NULL; // 重定向的文件名
- 定义了四个宏,表示不同的重定向类型;
- 变量
redir
用来记录当前进程的重定向类型; filename
用来记录重定向符号后面跟的文件名。
2. 主函数设计
int main()
{
while(1)
{
// 每次循环对这两个重定向指标赋初始值
redir = NoneRedir;
filename = NULL;
char usercommand[NUM]; // 用户输入的命令字符串
char *argv[SIZE]; // 命令行参数表
// 1. 打印提示符+获取用户命令字符串
int n = getUserCommand(usercommand, sizeof(usercommand));
if (n <= 0) continue; // 如果getUserCommand函数出错了或者得到的字符串是空串,就不要往后走了
// 1.1 检查是否发生重定向
// "ls -a -l > log.txt" -> "ls -a -l" redir_type "log.txt"
checkRedir(usercommand, strlen(usercommand));
// 2. 分割字符串
// "ls -a -l" -> "ls" "-a" "-l"
commandSplit(usercommand, argv);
// 3. check build-in command,先检查是否是内建命令
n = doBuildin(argv);
if(n) continue;
// 4. 创建子进程,执行对应的命令
execute(argv);
}
return 0;
}
- 在分割字符串之前,先进行是否发生重定向的检查。
3. checkRedir函数的设计
#define SkipSpace(filename) do{ while(isspace(*filename)) filename++; }while(0)
void checkRedir(char usercommand[], int len)
{
char *end = usercommand + len - 1;
char *start = usercommand;
while(end > start)
{
if (*end == '>')
{
if (*(end - 1) == '>')
{
redir = AppendRedir;
*(end - 1) = '\0';
filename = end + 1;
SkipSpace(filename);
break;
}
else
{
redir = OutPutRedir;
*end = '\0';
filename = end + 1;
SkipSpace(filename);
break;
}
}
else if (*end == '<')
{
*end = '\0';
filename = end + 1;
SkipSpace(filename); // 如果有空格就跳过
redir = InputRedir;
break;
}
else
{
end--;
}
}
}
- 从后向前查找有没有重定向符号。如果是
<
输入重定向,直接执行<
的逻辑;如果是>
,还要判断是否是追加重定向>>
,向前再看一步。 - 这个函数的功能就是将,命令,重定向符号,文件名,三个元素拆开。基本的思路就是:
- 用
\0
将命令和文件名分隔开,这样进行字符串分割时,就只需要分割命令的部分了; - 用
filename
将文件名存起来,用redir
记录当前的重定向类别; - 获取文件名时还设计了一个宏
SkipSpace
,用来跳过空格。
- 用
4. 改造execute函数
// 执行一个命令
int execute(char *argv[])
{
pid_t id = fork();
if (id < 0) return -1;
else if (id == 0)
{
// child
int fd = 0;
if (redir == InputRedir)
{
fd = open(filename, O_RDONLY); // 差错处理暂时不做
dup2(fd, 0);
}
else if (redir == OutPutRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if (redir == AppendRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{
// do nothing
}
// 程序替换不会影响重定向
execvp(argv[0], argv);
// 走到下面说明替换出错了
exit(1);
}
else
{
// father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
5. 完整代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<ctype.h>
#include<fcntl.h>
// redir
#define NoneRedir 0
#define OutPutRedir 1
#define AppendRedir 2
#define InputRedir 3
int redir = NoneRedir; // 默认是没有重定向
char *filename = NULL; // 重定向的文件名
#define NUM 1024 // 用户在命令行能最大输入的字符串长度
#define SIZE 64 // 命令行参数表的大小
#define SEP " " // 分割符
//#define Debug 1
char cwd[1024]; // 当前路径必须全局有效
char enval[1024]; // 全局环境变量
int lastcode = 0; // 最近一次进程退出码
// 获取用户名
const char *getUsername()
{
const char *name = getenv("USER");
if (name) return name;
else return "none";
}
// 获取主机名
const char *getHostname()
{
const char *name = getenv("HOSTNAME");
if (name) return name;
else return "none";
}
// 获取当前路径
const char *getCwd()
{
const char *cwd = getenv("PWD");
if (cwd) return cwd;
else return "none";
}
// 获取家目录
char *homepath()
{
char *home = getenv("HOME");
if (home) return home;
else return (char*)".";
}
// 获取用户命令字符串
int getUserCommand(char *command, int num)
{
printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
char *r = fgets(command, num, stdin); // 无论你输入了什么,最终我们还是会传入\n,为usercommand的最后一个有效字符
if (r == NULL) return -1; // 出错了
command[strlen(command) - 1] = '\0'; // 去掉最终输入的\n(不会越界,因为usercommand中至少有一个\n)
return strlen(command);
}
// 打散字符串
void commandSplit(char *in, char *out[])
{
int argc = 0;
out[argc++] = strtok(in, SEP);
while(out[argc++] = strtok(NULL, SEP));
// 测试
#ifdef Debug
for(int i = 0; out[i]; i++)
{
printf("%d:%s\n", i, out[i]);
}
#endif
}
// 执行一个命令
int execute(char *argv[])
{
pid_t id = fork();
if (id < 0) return -1;
else if (id == 0)
{
// child
int fd = 0;
if (redir == InputRedir)
{
fd = open(filename, O_RDONLY); // 差错处理暂时不做
dup2(fd, 0);
}
else if (redir == OutPutRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if (redir == AppendRedir)
{
fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else
{
// do nothing
}
// 程序替换不会影响重定向
execvp(argv[0], argv);
// 走到下面说明替换出错了
exit(1);
}
else
{
// father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
void cd(const char *path)
{
chdir(path);
char tmp[1024];
getcwd(tmp, sizeof(tmp));
sprintf(cwd, "PWD=%s", tmp);
putenv(cwd); // 由于cwd中的字符串开头是PWD,所以会直接覆盖环境变量PWD中的内容
}
// 什么是内建命令?内建命令就是必须由bash自己执行的命令,类似于自己内部的一个函数
// 检查是否是内建命令,是内建命令就顺便执行一下,返回1,不是就返回0
int doBuildin(char *argv[])
{
if (strcmp(argv[0], "cd") == 0)
{
char *path = NULL;
if (argv[1] == NULL) path = homepath(); // 如果cd后内容为空,默认切换至家目录
else path = argv[1];
cd(path);
return 1;
}
else if (strcmp(argv[0], "export") == 0)
{
if (argv[1] == NULL) return 1;
strcpy(enval, argv[1]);
putenv(enval);
return 1;
}
else if (strcmp(argv[0], "echo") == 0)
{
if (argv[1] == NULL)
{
printf("\n");
return 1;
}
else if (*(argv[1]) == '$' && strlen(argv[1]) > 1)
{
char *val = argv[1] + 1; // 跳过$,指向$后的字符串
if (strcmp(val, "?") == 0)
{
printf("%d\n", lastcode);
lastcode = 0;
}
else
{
const char *enval = getenv(val);
if (enval) printf("%s\n", getenv(val));
else printf("\n");
}
return 1;
}
else
{
printf("%s\n", argv[1]);
}
return 1;
}
else if (0)
{
// 穷举其他内建命令,不再写了
}
return 0;
}
#define SkipSpace(filename) do{ while(isspace(*filename)) filename++; }while(0)
void checkRedir(char usercommand[], int len)
{
char *end = usercommand + len - 1;
char *start = usercommand;
while(end > start)
{
if (*end == '>')
{
if (*(end - 1) == '>')
{
redir = AppendRedir;
*(end - 1) = '\0';
filename = end + 1;
SkipSpace(filename);
break;
}
else
{
redir = OutPutRedir;
*end = '\0';
filename = end + 1;
SkipSpace(filename);
break;
}
}
else if (*end == '<')
{
*end = '\0';
filename = end + 1;
SkipSpace(filename); // 如果有空格就跳过
redir = InputRedir;
break;
}
else
{
end--;
}
}
}
int main()
{
while(1)
{
// 每次循环对这两个重定向指标赋初始值
redir = NoneRedir;
filename = NULL;
char usercommand[NUM]; // 用户输入的命令字符串
char *argv[SIZE]; // 命令行参数表
// 1. 打印提示符+获取用户命令字符串
int n = getUserCommand(usercommand, sizeof(usercommand));
if (n <= 0) continue; // 如果getUserCommand函数出错了或者得到的字符串是空串,就不要往后走了
// 1.1 检查是否发生重定向
// "ls -a -l > log.txt" -> "ls -a -l" redir_type "log.txt"
checkRedir(usercommand, strlen(usercommand));
// 2. 分割字符串
// "ls -a -l" -> "ls" "-a" "-l"
commandSplit(usercommand, argv);
// 3. check build-in command,先检查是否是内建命令
n = doBuildin(argv);
if(n) continue;
// 4. 创建子进程,执行对应的命令
execute(argv);
}
return 0;
}