实现shell

实现shell

打造一个绝无伦比的 xxx-super-shell (xxx 是你的名字)
要求
它能实现下面这些功能:
实现管道(也就是 |)
实现输入输出重定向(也就是 < > >>)
要求在管道组合命令的两端实现重定向运算符
例如cat < 1.txt | grep -C 10 abc | grep -Lefd | tac >> 2.txt
实现后台运行(也就是 &)
实现 cd,要求支持能切换到绝对路径、相对路径和支持 cd -
屏蔽一些信号(如 ctrl + c 不能终止)
界面美观

思路

先从简单命令入手,实现最基本的shell,如ls,再实现cd,然后再实现管道以及重定向,由于要实现的功能较多,采用函数封装的方式实现。
命令参数解析的方式是很重要的,这个决定了实现这些功能的难易程度以及整体的架构。
采用结构体对数据进行存储

  1. 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
  1. 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);

主要用途包括:

  1. 重定向标准输入、输出或错误:在子进程中,dup2 可以用来将标准输入(STDIN_FILENO,通常为 0)、标准输出(STDOUT_FILENO,通常为 1)或标准错误(STDERR_FILENO,通常为 2)重定向到文件或其他文件描述符。
  2. 保存和恢复文件描述符:在执行某些操作之前,可以使用 dup2 保存当前文件描述符的状态,以便在操作完成后恢复。
  3. 实现管道通信:在进程间通信中,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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值