实现shell
打造一个绝无伦比的 xxx-super-shell (xxx 是你的名字)
要求
它能实现下面这些功能:
实现管道(也就是 |)
实现输入输出重定向(也就是 < > >>)
要求在管道组合命令的两端实现重定向运算符
例如cat < 1.txt | grep -C 10 abc | grep -Lefd | tac >> 2.txt
实现后台运行(也就是 &)
实现 cd,要求支持能切换到绝对路径、相对路径和支持 cd -
屏蔽一些信号(如 ctrl + c 不能终止)
界面美观
思路
先从简单命令入手,实现最基本的shell,如ls,再实现cd,然后再实现管道以及重定向,由于要实现的功能较多,采用函数封装的方式实现。
命令参数解析的方式是很重要的,这个决定了实现这些功能的难易程度以及整体的架构。
采用结构体对数据进行存储
- Command 结构体
作用:表示 单个命令 的完整信息。
typedef struct {
char* args[MAX_ARGS]; // 命令参数(如 ["ls", "-l", NULL])
char* input; // 输入重定向文件(如 "input.txt")
char* output; // 输出重定向文件(如 "output.txt")
int append; // 输出是否追加模式(1 表示 >>,0 表示 >)
} Command;
示例:
对于命令 grep "error" < log.txt > result.txt:
args: ["grep", "error", NULL]
input: "log.txt"
output: "result.txt"
append: 0
- Pipeline 结构体
作用:表示 一组通过管道连接的多个命令。
typedef struct {
Command cmds[MAX_CMDS]; // 管道中的多个命令
int cmd_count; // 命令数量
int background; // 是否后台运行(1 表示 &,0 表示前台)
} Pipeline;
示例:
对于管道命令 cat < input.txt | grep "error" | wc -l > result.txt &:
cmds[0]: {args: ["cat", NULL], input: "input.txt", output: NULL, append: 0}
cmds[1]: {args: ["grep", "error", NULL], input: NULL, output: NULL, append: 0}
cmds[2]: {args: ["wc", "-l", NULL], input: NULL, output: "result.txt", append: 0}
cmd_count: 3
background: 1
命令解析
解析命令分为两层解析,外层以管道符"|"作为分隔,内层解析单个命令及其参数
首先从输入端读取存入line中
char **parse_line(char *line, int *arg_count) {
char **tokens = NULL;
char *token, **tokens_backup;
int buffer_size = 10, position = 0;
if (line == NULL) return NULL;
tokens = malloc(buffer_size *sizeof(char*));
if (!tokens) {
perror("malloc");
exit(EXIT_FAILURE);
}
token = strtok(line, " \t\r\n\a");
while (token != NULL) {
tokens[position] = token;
position++;
if (position >= buffer_size) {
buffer_size += 10;
tokens_backup = tokens;
tokens = realloc(tokens, buffer_size * sizeof(char*));
if (!tokens) {
perror("realloc");
free(tokens_backup);
exit(EXIT_FAILURE);
}
}
token = strtok(NULL, " \t\r\n\a");
}
tokens[position] = NULL;
*arg_count = position;
return tokens;
}
再解析输入并构建Pipeline结构
// 解析输入并构建Pipeline结构
Pipeline parse_input(char* line) {
Pipeline pipeline = {0};
char *saveptr1, *saveptr2;//保存 strtok_r 的内部状态的变量
char *cmd_str, *token;// 临时存储分割后的字符串
// 处理换行符和后台运行标志
line[strcspn(line, "\n")] = '\0';//用于去除字符串line末尾的换行符
if (strlen(line) > 0 && line[strlen(line)-1] == '&') {
pipeline.background = 1;// 标记为后台运行
line[strlen(line)-1] = '\0';// 移除末尾的 &
}
// 分割管道命令
cmd_str = strtok_r(line, "|", &saveptr1);
while (cmd_str && pipeline.cmd_count < MAX_CMDS) {
Command cmd = {0};
int arg_count = 0;
// 分割命令参数
token = strtok_r(cmd_str, " \t", &saveptr2);
while (token && arg_count < MAX_ARGS-1) {
// 处理重定向符号
if (strcmp(token, "<") == 0) {//处理输入重定向 <
cmd.input = strtok_r(NULL, " \t", &saveptr2);// 获取下一个token作为文件名,Shell中的命令参数通常以空格或制表符分隔
if (!cmd.input) {
fprintf(stderr, "syntax error near unexpected token '<'\n");
break;
}
} else if (strcmp(token, ">") == 0 || strcmp(token, ">>") == 0) {//处理输出重定向 > 或 >>
cmd.append = (strcmp(token, ">>") == 0);
cmd.output = strtok_r(NULL, " \t", &saveptr2);// 获取输出文件名
if (!cmd.output) {
fprintf(stderr, "syntax error near unexpected token '%s'\n", token);
break;
}
} else {
cmd.args[arg_count++] = token;//存储命令及参数
}
token = strtok_r(NULL, " \t", &saveptr2);
}
cmd.args[arg_count] = NULL;// 参数数组以NULL结尾
if (arg_count > 0) {
pipeline.cmds[pipeline.cmd_count++] = cmd;
}
cmd_str = strtok_r(NULL, "|", &saveptr1);// 获取下一个管道命令
}
return pipeline;
}
执行命令
与解析命令参数类似,分为两层,第一层为执行单个命令并实现重定向,第二层是实现执行整个管道
// 执行单个命令(带重定向)
void execute_command(Command* cmd, int input_fd, int output_fd)
// 执行整个管道
void execute_pipeline(Pipeline* pipeline)
知识储备
在解析命令时主要是对函数strtok_r的使用,它和strtok的区别主要在于,strtok_r 是线程安全的,下面示例strtok的用法
char str[] = "Hello, world! This is a test.";
char *token = strtok(str, " .,!");
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, " .,!");
}
掌握进程基本操作,管道的实现主要依靠进程之间的通信,命令的执行依靠于exec函数,下面示例实现ls | wc -l
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<errno.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
int fd[2];
pipe(fd);
pid=fork();//ls | wc -l
if(pid==0)//子进程实现wc -l
{
close(fd[1]);//子进程读管道,关闭写端
dup2(fd[0],STDIN_FILENO);//这一行将管道的写端(fd[1])复制到标准输出(STDOUT_FILENO,通常是文件描述符1)
execlp("wc","wc","-l",NULL);
}
else if(pid>0)
{
close(fd[0]);//父进程写管道,关闭读管道
dup2(fd[1],STDOUT_FILENO);//将写入屏幕ls的结果,写入到管道写端
execlp("ls","ls","-l",NULL);
}
return 0;
}
下面是对dup2函数的解释
int dup2(int oldfd, int newfd);
主要用途包括:
- 重定向标准输入、输出或错误:在子进程中,dup2 可以用来将标准输入(STDIN_FILENO,通常为 0)、标准输出(STDOUT_FILENO,通常为 1)或标准错误(STDERR_FILENO,通常为 2)重定向到文件或其他文件描述符。
- 保存和恢复文件描述符:在执行某些操作之前,可以使用 dup2 保存当前文件描述符的状态,以便在操作完成后恢复。
- 实现管道通信:在进程间通信中,dup2 可以用来将管道的读端或写端重定向到标准输入或输出,从而实现进程间的数据交换。
代码实现
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <limits.h>
#include <errno.h>
#define MAX_LINE 1024
#define MAX_ARGS 64
#define MAX_CMDS 10
typedef struct {
char* args[MAX_ARGS];//命令参数
char* input;// 输入重定向文件
char* output;// 输出重定向文件
int append;// 输出是否追加模式(1 表示 >>,0 表示 >)
} Command;
typedef struct {
Command cmds[MAX_CMDS];// 管道中的多个命令
int cmd_count;// 命令数量
int background;// 是否后台运行(1 表示 &,0 表示前台)
} Pipeline;
static char prev_dir[PATH_MAX] = {0};
// 函数声明
Pipeline parse_input(char* line);
void execute_pipeline(Pipeline* pipeline);
void handle_cd(char **args);
void setup_signals() {
struct sigaction sa;
// 设置SIGINT处理为忽略(父进程)
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
// 自动回收子进程(防止僵尸进程)
sa.sa_handler = SIG_IGN;
sigaction(SIGCHLD, &sa, NULL);
}
// 解析输入并构建Pipeline结构
Pipeline parse_input(char* line) {
Pipeline pipeline = {0};
char *saveptr1, *saveptr2;//保存 strtok_r 的内部状态的变量
char *cmd_str, *token;// 临时存储分割后的字符串
// 处理换行符和后台运行标志
line[strcspn(line, "\n")] = '\0';//用于去除字符串line末尾的换行符
if (strlen(line) > 0 && line[strlen(line)-1] == '&') {
pipeline.background = 1;// 标记为后台运行
line[strlen(line)-1] = '\0';// 移除末尾的 &
}
// 分割管道命令
cmd_str = strtok_r(line, "|", &saveptr1);
while (cmd_str && pipeline.cmd_count < MAX_CMDS) {
Command cmd = {0};
int arg_count = 0;
// 分割命令参数
token = strtok_r(cmd_str, " \t", &saveptr2);
while (token && arg_count < MAX_ARGS-1) {
// 处理重定向符号
if (strcmp(token, "<") == 0) {//处理输入重定向 <
cmd.input = strtok_r(NULL, " \t", &saveptr2);// 获取下一个token作为文件名,Shell中的命令参数通常以空格或制表符分隔
if (!cmd.input) {
fprintf(stderr, "syntax error near unexpected token '<'\n");
break;
}
} else if (strcmp(token, ">") == 0 || strcmp(token, ">>") == 0) {//处理输出重定向 > 或 >>
cmd.append = (strcmp(token, ">>") == 0);
cmd.output = strtok_r(NULL, " \t", &saveptr2);// 获取输出文件名
if (!cmd.output) {
fprintf(stderr, "syntax error near unexpected token '%s'\n", token);
break;
}
} else {
cmd.args[arg_count++] = token;//存储命令及参数
}
token = strtok_r(NULL, " \t", &saveptr2);
}
cmd.args[arg_count] = NULL;// 参数数组以NULL结尾
if (arg_count > 0) {
pipeline.cmds[pipeline.cmd_count++] = cmd;
}
cmd_str = strtok_r(NULL, "|", &saveptr1);// 获取下一个管道命令
}
return pipeline;
}
// 执行单个命令(带重定向)
void execute_command(Command* cmd, int input_fd, int output_fd) {
signal(SIGINT, SIG_DFL);
// 输入重定向
if (cmd->input) {
int fd = open(cmd->input, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
dup2(fd, STDIN_FILENO);
close(fd);
} else if (input_fd != STDIN_FILENO) {
dup2(input_fd, STDIN_FILENO);
close(input_fd);
}
// 输出重定向
if (cmd->output) {
int flags = O_WRONLY | O_CREAT;
flags |= cmd->append ? O_APPEND : O_TRUNC;//根据cmd结构体中的append成员的值,决定是追加到文件末尾(O_APPEND)还是覆盖文件内容(O_TRUNC)
int fd = open(cmd->output, flags, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
dup2(fd, STDOUT_FILENO);
close(fd);
} else if (output_fd != STDOUT_FILENO) {//将标准输出恢复到原来的状态
dup2(output_fd, STDOUT_FILENO);
close(output_fd);
}
execvp(cmd->args[0], cmd->args);
perror("execvp");
exit(EXIT_FAILURE);
}
// 执行整个管道
void execute_pipeline(Pipeline* pipeline) {
int prev_pipe_read = -1;
pid_t pids[MAX_CMDS];
int pid_count = 0;
for (int i = 0; i < pipeline->cmd_count; i++) {
int pipefd[2];
if (i < pipeline->cmd_count - 1) {
if (pipe(pipefd) == -1) {//创建管道
perror("pipe");
exit(EXIT_FAILURE);
}
}
pid_t pid = fork();
if (pid == 0) { // 子进程
// 处理输入重定向
if (i > 0) {
dup2(prev_pipe_read, STDIN_FILENO);
close(prev_pipe_read);
}
// 处理输出重定向
if (i < pipeline->cmd_count - 1) {
close(pipefd[0]); // 关闭不需要的读端
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
}
// 执行命令(包含文件重定向)
execute_command(&pipeline->cmds[i], STDIN_FILENO, STDOUT_FILENO);
exit(EXIT_FAILURE); // exec失败时退出
}
else if (pid > 0) { // 父进程
// 关闭前一个管道的读端(如果有)
if (i > 0) {
close(prev_pipe_read);
}
// 保存当前管道的读端供下一个命令使用
if (i < pipeline->cmd_count - 1) {
prev_pipe_read = pipefd[0];
close(pipefd[1]); // 关闭父进程不需要的写端
}
pids[pid_count++] = pid;
}
else {
perror("fork");
exit(EXIT_FAILURE);
}
}
// 等待前台进程
if (!pipeline->background) {
for (int i = 0; i < pid_count; i++) {
waitpid(pids[i], NULL, 0);
}
}
}
char **parse_line(char *line, int *arg_count) {
char **tokens = NULL;
char *token, **tokens_backup;
int buffer_size = 10, position = 0;
if (line == NULL) return NULL;
tokens = malloc(buffer_size *sizeof(char*));
if (!tokens) {
perror("malloc");
exit(EXIT_FAILURE);
}
token = strtok(line, " \t\r\n\a");
while (token != NULL) {
tokens[position] = token;
position++;
if (position >= buffer_size) {
buffer_size += 10;
tokens_backup = tokens;
tokens = realloc(tokens, buffer_size * sizeof(char*));
if (!tokens) {
perror("realloc");
free(tokens_backup);
exit(EXIT_FAILURE);
}
}
token = strtok(NULL, " \t\r\n\a");
}
tokens[position] = NULL;
*arg_count = position;
return tokens;
}
void handle_cd(char **args) {
char *target = args[1];
char cwd[PATH_MAX];
// 保存当前目录
if (getcwd(cwd, sizeof(cwd)) == NULL) {
perror("getcwd");
return;
}
// 确定目标目录
if (!target) {
target = getenv("HOME");
if (!target) {
fprintf(stderr, "cd: HOME environment variable not set\n");
return;
}
}
else if (strcmp(target,"~")==0) {
target = getenv("HOME");
if (!target) {
fprintf(stderr, "cd: HOME environment variable not set\n");
return;
}
}
else if (strcmp(target, "-") == 0) {
if (prev_dir[0] == '\0') {
fprintf(stderr, "cd: no previous directory\n");
return;
}
target = prev_dir;
}
// 执行目录切换
if (chdir(target) == -1) {
perror("cd");
} else {
// 更新prev_dir为切换前的目录
strncpy(prev_dir, cwd, sizeof(prev_dir));
prev_dir[sizeof(prev_dir)-1] = '\0';
}
}
// 修改后的命令执行函数
// 新增:初始化prev_dir
void init_shell() {
setup_signals(); // 初始化信号处理
if (getcwd(prev_dir, sizeof(prev_dir)) == NULL) {
perror("getcwd");
exit(EXIT_FAILURE);
}
}
int main() {
char *line = NULL;
size_t bufsize = 0;
char *prompt = "shell> ";
int arg_count=0;
init_shell();
while (1) {
printf("%s", prompt);
char cwd[PATH_MAX];
// 获取当前工作目录,存储在cwd数组中
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("%s>", cwd);
} else {
perror("getcwd() 错误");
// 处理错误,例如分配更多内存或退出程序
exit(EXIT_FAILURE);
}
fflush(stdout);//用于清空(刷新)标准输出缓冲区(stdout),确保文本被立即显示
if (getline(&line, &bufsize, stdin) == -1) {//用于存储读取的行
break;
}
// 处理内建命令
if (strncmp(line, "cd", 2) == 0) {
char **args = parse_line(line, &arg_count);//解析参数
handle_cd(args);
free(args);
continue;
}
if (strcmp(line, "exit\n") == 0) {
break;
}
Pipeline pipeline = parse_input(line);
if (pipeline.cmd_count == 0) {
continue;
}
execute_pipeline(&pipeline);
}
free(line);
// free(s);
return 0;
}