【Linux指南】进程控制系列(五)实战 —— 微型 Shell 命令行解释器实现

【Linux指南】进程控制系列(五)实战 —— 微型 Shell 命令行解释器实现

下面是一个功能相对完整但代码量控制在 300 行左右的微型 shell(称为 mini-sh),它实现了以下核心功能:

  • 读取一行命令
  • 简单的词法分割(支持空格分隔,不支持引号嵌套)
  • 支持后台运行(命令末尾 &)
  • 支持前台进程的 wait
  • 支持管道(单级管道 cmd1 | cmd2
  • 支持重定向(> >> <)
  • 支持 cd、exit、pwd 内置命令
  • 支持环境变量展开(仅 $HOME、$PATH、$USER 简单实现)
  • 支持 SIGINT(Ctrl+C)传递给前台进程组

mini-sh.c (带详细注释)

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <pwd.h>

#define MAX_LINE    1024
#define MAX_ARGS    64
#define MAX_PIPES   2   // 目前只支持一级管道(两个进程)

static pid_t foreground_pgid = 0;  // 前台进程组

// 简单信号处理:Ctrl+C 发给前台进程组
static void sigint_handler(int sig) {
    if (foreground_pgid > 0) {
        kill(-foreground_pgid, SIGINT);
    }
}

void setup_signals() {
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sa.sa_flags = SA_RESTART;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);
    // 忽略后台进程的 SIGCHLD(防止僵尸进程累积)
    signal(SIGCHLD, SIG_IGN);
}

// 去除首尾空白
static char *trim(char *s) {
    while (*s && (*s == ' ' || *s == '\t' || *s == '\n')) s++;
    char *end = s + strlen(s) - 1;
    while (end >= s && (*end == ' ' || *end == '\t' || *end == '\n')) *end-- = '\0';
    return s;
}

// 非常简单的 $VAR 展开(仅支持 $HOME $USER $PATH)
static void expand_vars(char *buf) {
    char *p = buf;
    char tmp[MAX_LINE];
    char *out = tmp;

    while (*p) {
        if (*p == '$') {
            p++;
            if (strncmp(p, "HOME", 4) == 0) {
                char *home = getenv("HOME");
                if (home) { strcpy(out, home); out += strlen(home); }
                p += 4;
            } else if (strncmp(p, "USER", 4) == 0) {
                char *user = getenv("USER");
                if (user) { strcpy(out, user); out += strlen(user); }
                p += 4;
            } else if (strncmp(p, "PATH", 4) == 0) {
                char *path = getenv("PATH");
                if (path) { strcpy(out, path); out += strlen(path); }
                p += 4;
            } else {
                *out++ = '$';  // 未知变量原样保留
            }
        } else {
            *out++ = *p++;
        }
    }
    *out = '\0';
    strcpy(buf, tmp);
}

// 解析一行命令,返回参数数组,设置是否后台、输入/输出重定向
int parse_command(char *line,
                  char **argv, int *argc,
                  int *bg,
                  char **infile, char **outfile, int *append) {
    *argc = 0;
    *bg = 0;
    *infile = NULL;
    *outfile = NULL;
    *append = 0;

    expand_vars(line);
    line = trim(line);
    if (!*line) return 0;

    char *token = strtok(line, " \t");
    while (token) {
        if (strcmp(token, "&") == 0) {
            *bg = 1;
            break;
        }
        else if (strcmp(token, "<") == 0) {
            token = strtok(NULL, " \t");
            if (token) *infile = token;
        }
        else if (strcmp(token, ">") == 0 || strcmp(token, ">>") == 0) {
            *append = (token[1] == '>');
            token = strtok(NULL, " \t");
            if (token) *outfile = token;
        }
        else if (*argc < MAX_ARGS - 1) {
            argv[(*argc)++] = token;
        }
        token = strtok(NULL, " \t");
    }
    argv[*argc] = NULL;
    return *argc > 0;
}

// 内置命令
int builtin(char **argv, int argc) {
    if (argc == 0) return 0;

    if (strcmp(argv[0], "exit") == 0) {
        exit(0);
    }
    else if (strcmp(argv[0], "cd") == 0) {
        if (argc < 2) {
            chdir(getenv("HOME"));
        } else {
            if (chdir(argv[1]) != 0) {
                perror("cd");
            }
        }
        return 1;
    }
    else if (strcmp(argv[0], "pwd") == 0) {
        char cwd[1024];
        if (getcwd(cwd, sizeof(cwd))) {
            puts(cwd);
        }
        return 1;
    }
    return 0;
}

// 执行单条命令(可能带重定向)
void exec_single(char **argv, char *infile, char *outfile, int append) {
    pid_t pid = fork();
    if (pid == 0) {  // 子进程
        // 重定向
        if (infile) {
            int fd = open(infile, O_RDONLY);
            if (fd < 0) { perror("open input"); _exit(1); }
            dup2(fd, STDIN_FILENO);
            close(fd);
        }
        if (outfile) {
            int flags = O_WRONLY | O_CREAT;
            if (append) flags |= O_APPEND;
            else        flags |= O_TRUNC;
            int fd = open(outfile, flags, 0644);
            if (fd < 0) { perror("open output"); _exit(1); }
            dup2(fd, STDOUT_FILENO);
            close(fd);
        }

        execvp(argv[0], argv);
        perror("execvp");
        _exit(127);
    }
    else if (pid > 0) {
        foreground_pgid = pid;
        waitpid(pid, NULL, 0);
        foreground_pgid = 0;
    }
}

// 执行带管道的命令(目前只支持 cmd1 | cmd2)
void exec_pipe(char **left, char **right,
               char *infile, char *outfile, int append) {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return;
    }

    pid_t pid1 = fork();
    if (pid1 == 0) {  // 左进程
        dup2(pipefd[1], STDOUT_FILENO);
        close(pipefd[0]);
        close(pipefd[1]);

        if (infile) {
            int fd = open(infile, O_RDONLY);
            if (fd >= 0) { dup2(fd, STDIN_FILENO); close(fd); }
        }

        execvp(left[0], left);
        perror("exec left");
        _exit(1);
    }

    pid_t pid2 = fork();
    if (pid2 == 0) {  // 右进程
        dup2(pipefd[0], STDIN_FILENO);
        close(pipefd[0]);
        close(pipefd[1]);

        if (outfile) {
            int flags = O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC);
            int fd = open(outfile, flags, 0644);
            if (fd >= 0) { dup2(fd, STDOUT_FILENO); close(fd); }
        }

        execvp(right[0], right);
        perror("exec right");
        _exit(1);
    }

    close(pipefd[0]);
    close(pipefd[1]);

    foreground_pgid = pid1;  // 简单起见,把整个管道视为一个前台组
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);
    foreground_pgid = 0;
}

int main() {
    setup_signals();

    char line[MAX_LINE];
    char *argv[MAX_ARGS];
    int argc;
    int bg;
    char *infile, *outfile;
    int append;

    while (1) {
        printf("mini-sh$ ");
        fflush(stdout);

        if (!fgets(line, sizeof(line), stdin)) {
            putchar('\n');
            break;
        }

        if (parse_command(line, argv, &argc, &bg, &infile, &outfile, &append)) {
            if (builtin(argv, argc)) {
                continue;
            }

            // 简单判断是否有管道(只支持一级)
            char **left = argv;
            char **right = NULL;

            for (int i = 0; i < argc; i++) {
                if (strcmp(argv[i], "|") == 0) {
                    argv[i] = NULL;
                    right = argv + i + 1;
                    break;
                }
            }

            if (right) {
                // 有管道
                if (bg) {
                    puts("mini-sh: 暂不支持后台管道");
                } else {
                    exec_pipe(left, right, infile, outfile, append);
                }
            } else {
                // 无管道
                if (bg) {
                    pid_t pid = fork();
                    if (pid == 0) {
                        // 后台进程脱离控制终端
                        setsid();
                        execvp(argv[0], argv);
                        perror("execvp bg");
                        _exit(1);
                    }
                    printf("[%d] %d\n", 1, pid);
                } else {
                    exec_single(argv, infile, outfile, append);
                }
            }
        }
    }

    return 0;
}

编译与运行

gcc -Wall -std=c11 -o mini-sh mini-sh.c
./mini-sh

支持的示例命令

ls -l
ls -l | grep txt
cat file.txt
echo hello > out.txt
echo world >> out.txt
cat < in.txt
sleep 10 &
cd /tmp
pwd
exit

当前局限性(可作为练习方向)

  1. 不支持引号(” ” ‘ ‘)
  2. 不支持多级管道(cmd1 | cmd2 | cmd3)
  3. 不支持 && || 命令列表
  4. 不支持变量赋值(VAR=val cmd
  5. 不支持 ~ 路径展开
  6. 重定向和管道不能同时复杂组合
  7. 后台任务没有 jobs/fg/bg 管理
  8. 错误提示非常原始

如果你想继续扩展,可以按以下优先级尝试:

  1. 支持双引号/单引号解析
  2. 支持多级管道(动态创建 pipe 数组)
  3. 实现 jobs / fg / bg 功能
  4. 支持环境变量赋值与展开(完整版)
  5. 更好的错误处理与提示

祝你实现一个更有趣的 mini-shell!

文章已创建 4298

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部