[OS]Linux 游侠终端
模拟终端实验
目标:实现一个简单的shell,它能够解析和执行用户输入的命令。这个shell支持基本的命令,如ls和cp,内部命令,如cd,以及输入/输出重定向和管道。
parse函数:
解析用户输入的命令,并将命令和参数分开存储在argv数组中。如果用户输入的是内部命令,如exit、help或cd,则直接在这个函数中处理
void parse(char *word, char **argv) {
int count = 0;
memset(argv, 0, sizeof(char *) * (64));
char *lefts = NULL;
const char *split = " "; //置分隔符
while (1) {
char *p = strtok_r(word, split, &lefts);
if (p == NULL) {
break;
}
argv[count] = p;//将分割后的字符串存入argv数组
word = lefts;
count++;
}
if (strcmp(argv[0], "exit") == 0){
printf("Exiting...Bye\n");
exit(0);
}
else if (strcmp(argv[0], "help") == 0) {
printf("--------\n");
printf("God helps those who help themselves\n");
printf("--------\n");
}
else if (strcmp(argv[0], "cd") == 0) { //cd命令
int ch = chdir(argv[1]); //改变当前目录
f = 1;
//则改变当前目录
}
// else if (strcmp(argv[0], "history") == 0) { //打印历史命令
// int i = 0;
// for (i = 0; i < hist_size; i++) {
// if (hist[i] != NULL) {
// printf("%d %s\n", i, hist[i]);
// }
// }
// }
}
trim
解析,去掉多余的空格
char *trim(char *string)
{
int i = 0;
int j = 0;
char *ptr = malloc(sizeof(char *) * strlen(string)); //用于分配指定长度的内存块
for (i = 0; string[i] != '\0'; i++)
if (string[i] != ' ') { //如果不是空格
ptr[j] = string[i];
j++;
}
ptr[j] = '\0';
string = ptr;
return string;
}
execute函数:
创建一个子进程来执行用户输入的命令。父进程会等待子进程执行完毕。
void execute(char **argv) {
pid_t pid;
int status;
if ((pid = fork()) < 0) {
printf("error:fork failed.\n");
exit(1);
} else if (pid == 0) {
// 关闭 stderr
close(2);
if (execvp(argv[0], argv) >= 0 || !strcmp(argv[0], "cd")) {} else printf("error:invalid command.\n");
exit(0);
} else {
while (wait(&status) != pid)
;
}
}
execute_file函数:
执行输出重定向。它创建一个子进程来执行命令,并将命令的输出重定向到指定的文件。
void execute_file(char **argv, char *output) {
pid_t pid;
int status, flag;
char *file = NULL;
// 创建子进程
if ((pid = fork()) < 0) {
printf("error:fork failed.\n");
exit(1);
} else if (pid == 0) { // 子进程
// 检查是否有输出重定向符号 ">"
if (strstr(output, ">") > 0) {
char *p = strtok_r(output, ">", &file);
output += 1;
file = trim(file);
flag = 1;
// 保存当前标准输出
int old_stdout = dup(1);
// 将标准输出重定向到指定文件
FILE *fp1 = freopen(output, "w+", stdout);
// 递归调用 execute_file 执行命令
execute_file(argv, file);
// 关闭重定向的文件
fclose(stdout);
// 恢复原来的标准输出
FILE *fp2 = fdopen(old_stdout, "w");
*stdout = *fp2;
exit(0);
}
// 检查是否有输入重定向符号 "<"
if (strstr(output, "<") > 0) {
char *p = strtok_r(output, "<", &file);
file = trim(file);
flag = 1;
// 打开输入文件
int fd = open(file, O_RDONLY);
if (fd < 0) {
printf("No such file or directory.");
exit(0);
}
// 将标准输入重定向到指定文件
dup2(fd, 0);
close(fd);
}
// 检查是否有管道符号 "|"
if (strstr(output, "|") > 0) {
fflush(stdout);
printf("here");
fflush(stdout);
char *p = strtok_r(output, "|", &file);
file = trim(file);
flag = 1;
// 解析管道后面的命令
char *args[64];
parse(file, args);
execute(args); // 执行管道后的命令
}
// 如果没有重定向或管道,直接执行命令
int old_stdout = dup(1);
FILE *fp1 = freopen(output, "w+", stdout);
if (execvp(argv[0], argv) < 0)
printf("error:in exec");
fclose(stdout);
FILE *fp2 = fdopen(old_stdout, "w");
*stdout = *fp2;
exit(0);
} else { // 父进程
// 等待子进程结束
while (wait(&status) != pid);
}
}
execute_input函数:
执行输入重定向。它创建一个子进程来执行命令,并将指定文件的内容作为命令的输入。
void execute_input(char **argv, char *output) {
pid_t pid;
int fd;
char *file;
int flag = 0;
int status;
// 创建子进程
if ((pid = fork()) < 0) {
printf("error:fork failed\n");
exit(1);
} else if (pid == 0) { // 子进程
// 检查是否有输入重定向符号 "<"
if (strstr(output, "<") > 0) {
char *p = strtok_r(output, "<", &file);
file = trim(file);
flag = 1;
fd = open(output, O_RDONLY);
if (fd < 0) {
printf("No such file or directory.");
exit(0);
}
output = file;
}
// 检查是否有输出重定向符号 ">"
if (strstr(output, ">") > 0) {
char *p = strtok_r(output, ">", &file);
file = trim(file);
flag = 1;
fflush(stdout);
int old_stdout = dup(1);
// 将标准输出重定向到指定文件
FILE *fp1 = freopen(file, "w+", stdout);
execute_input(argv, output);
fclose(stdout);
FILE *fp2 = fdopen(old_stdout, "w");
*stdout = *fp2;
exit(0);
}
// 检查是否有管道符号 "|" 看后面的管道
if (strstr(output, "|") > 0) {
char *p = strtok_r(output, "|", &file);
file = trim(file);
flag = 1;
char *args[64];
parse(file, args);
int pfds[2];
pid_t pid, pid2;
int status, status2;
pipe(pfds);
int fl = 0;
if ((pid = fork()) < 0) {
printf("error:fork failed\n");
exit(1);
}
if ((pid2 = fork()) < 0) {
printf("error:fork failed\n");
exit(1);
}
if (pid == 0 && pid2 != 0) {
close(1);
dup(pfds[1]);
close(pfds[0]);
close(pfds[1]);
fd = open(output, O_RDONLY);
close(0);
dup(fd);
if (execvp(argv[0], argv) < 0) {
close(pfds[0]);
close(pfds[1]);
printf("error:in exec");
fl = 1;
exit(0);
}
close(fd);
exit(0);
} else if (pid2 == 0 && pid != 0 && fl != 1) {
close(0);
dup(pfds[0]);
close(pfds[1]);
close(pfds[0]);
if (execvp(args[0], args) < 0) {
close(pfds[0]);
close(pfds[1]);
printf("error:in exec");
exit(0);
}
} else {
close(pfds[0]);
close(pfds[1]);
while (wait(&status) != pid);
while (wait(&status2) != pid2);
}
exit(0);
}
// 打开输入文件并将其重定向到标准输入
fd = open(output, O_RDONLY);
close(0);
dup(fd);
if (execvp(argv[0], argv) < 0) {
printf("error:in exec");
}
close(fd);
exit(0);
} else { // 父进程
// 等待子进程结束
while (wait(&status) != pid);
}
}
execute_pipe函数:
执行管道操作。(多管道同理)
管道只能一端写入,另一端读出,这种模式容易造成混乱,因为父进程和子进程都可以同时写 入,也都可以读出。那么,为了避免这种情况,通常的做法是:
父进程关闭读取的 fd[0],只保留写入的 fd[1]; 子进程关闭写入的 fd[1],只保留读取的 fd[0];
创建两个子进程,一个执行管道左边的命令,一个执行右边的命令。左边命令的输出会被重定向到右边命令的输入。
第一个子进程:
- 关闭标准输出 (close(1)) 并将其重定向到管道写端 (dup(pfds[1]))。
- 执行命令 execvp(argv[0], argv),如果失败,打印错误信息并终止进程。
第二个子进程:
- 检查输入重定向符号 (<) 和输出重定向符号 (>),分别处理输入和输出重定向。
- 关闭标准输入 (close(0)) 并将其重定向到管道读端 (dup(pfds[0]))。
- 如果有输出重定向,将标准输出重定向到目标文件。
- 执行命令 execvp(args[0], args),如果失败,打印错误信息并终止进程。
父进程:
- 关闭管道的读写端 (close(pfds[0]) 和 close(pfds[1]))。
- 等待两个子进程结束。
void execute_pipe(char **argv, char *output) {
int pfds[2], flag, old_stdout;
char *file;
pid_t pid, pid2;
int status, status2;
char *args[64];
int fl = 0;
pipe(pfds); // 创建管道
// 创建第一个子进程
if ((pid = fork()) < 0) exit(1);
// 创建第二个子进程
if ((pid2 = fork()) < 0) exit(1);
// 第一个子进程
if (pid == 0 && pid2 != 0) {
close(1); // 关闭标准输出
dup(pfds[1]); // 将管道写端复制到标准输出
close(pfds[0]); // 关闭管道读端
close(pfds[1]); // 关闭管道写端
// 执行第一个命令
if (execvp(argv[0], argv) < 0) {
close(pfds[0]);
close(pfds[1]);
printf("error:in exec");
fl = 1;
kill(pid2, SIGUSR1); // 通知第二个子进程
exit(0);
}
}
// 第二个子进程
else if (pid2 == 0 && pid != 0) {
if (fl == 1) exit(0);
// 处理输入重定向
if (strstr(output, "<") > 0) {
char *p = strtok_r(output, "<", &file);
file = trim(file);
flag = 1;
parse(output, args); // 将 output 分割到 args 数组
execute_input(args, file);
close(pfds[0]);
close(pfds[1]);
exit(0);
}
// 处理输出重定向
int blah = 0;
if (strstr(output, ">") > 0) {
char *p = strtok_r(output, ">", &file);
file = trim(file);
flag = 1;
parse(output, args);
blah = 1;
} else {
parse(output, args);
}
close(0); // 关闭标准输入
dup(pfds[0]); // 将管道读端复制到标准输入
close(pfds[1]); // 关闭管道写端
close(pfds[0]); // 关闭管道读端
// 如果有输出重定向,将标准输出重定向到目标文件
if (blah == 1) {
old_stdout = dup(1);
FILE *fp1 = freopen(file, "w+", stdout);
}
// 执行第二个命令
if (execvp(args[0], args) < 0) {
fflush(stdout);
printf("error:in exec %d", pid);
kill(pid, SIGUSR1);
close(pfds[0]);
close(pfds[1]);
}
fflush(stdout);
// 如果输出重定向,恢复标准输出
if (blah == 1) {
fclose(stdout);
FILE *fp2 = fdopen(old_stdout, "w");
*stdout = *fp2;
}
}
// 父进程
else {
close(pfds[0]); // 关闭管道读端
close(pfds[1]); // 关闭管道写端
while (wait(&status) != pid);
while (wait(&status2) != pid2);
}
}
main函数:
主循环,不断地读取用户输入的命令,解析命令,然后执行命令
int main() {
char line[1024]; //用于存储用户输入的命令
char *argv[64]; //用于存储解析后的命令
char *args[64];
char *left; //用于存储管道符左边的命令
size_t size = 0; //用于存储用户输入的命令的长度
char ch; //用于存储用户输入的命令
int count = 0; //用于存储命令的个数
char *tri; //用于存储去掉空格后的命令
char *second; //用于存储管道符右边的命令
char *file; //用于存储重定向的文件
for (int i = 0; i < hist_size; i++) {
hist[i] = (char *) malloc(150);
}
while (1) {
count = 0;
int flag = 0;
char *word = NULL;
char *dire[] = {"pwd"};
fflush(stdout);
char hostname[1024];
hostname[1023] = '\0';
gethostname(hostname, 1023);
struct passwd *pw;
pw = getpwuid(geteuid());
char* username = pw->pw_name;
char cwd[1024];
getcwd(cwd, sizeof(cwd));
printf("\n\033[34m(%s@%s:YoSHELL)\033[0m %s \n", username, hostname, cwd);
fflush(stdout);
printf(">>");
int len = getline(&word, &size, stdin);
if (*word == '\n')
continue;
word[len - 1] = '\0';
char *file = NULL;
int i = 0;
char *temp = (char *) malloc(150);
strcpy(temp, word);
parse(temp, argv);
strcpy(hist[(head + 1) % hist_size], word); //存储历史记录
head = (head + 1) % hist_size;
filled = filled + 1;
for (i = 0; word[i] != '\0'; i++) {
if (word[i] == '>') {
char *p = strtok_r(word, ">", &file);
file = trim(file);
flag = 1;
break;
} else if (word[i] == '<') {
char *p = strtok_r(word, "<", &file);
file = trim(file);
flag = 2;
break;
} else if (word[i] == '|') {
char *p = strtok_r(word, "|", &left);
flag = 3;
break;
}
}
if (strcmp(word, "exit") == 0) {
exit(0);
}
if (flag == 1) {
parse(word, argv);
execute_file(argv, file);
} else if (flag == 2) {
parse(word, argv);
execute_input(argv, file);
} else if (flag == 3) {
char *argp[64];
char *output, *file;
if (strstr(left, "|") > 0) { //多个管道
char *p = strtok_r(left, "|", &file);
parse(word, argv);
parse(left, args);
parse(file, argp);
execute_pipe2(argv, args, argp);
} else {
parse(word, argv);
execute_pipe(argv, left);
}
} else {
// 单独处理内部命令,使它们只执行一次
if (strcmp(argv[0], "cd") == 0) {
} else if (strcmp(argv[0], "help") == 0) {
} else {
parse(word, argv);
execute(argv);
}
}
}
}
笺評 (issue)