从零手搓实现 Linux 简易 Shell:内建命令 + 环境变量 + 程序替换全解析

从零手搓实现 Linux 简易 Shell
(内建命令 + 环境变量 + 程序替换完整版)

下面我们用 C 语言一步步实现一个极简但功能相对完整的 shell,支持:

  • 读取用户输入、解析命令行(支持参数)
  • 内建命令(cd、exit、pwd、echo、export)
  • 环境变量的读取与修改(支持 $PATH、$HOME 等)
  • 通过 fork + execvp 执行外部程序
  • 管道(|)的简单支持(单级管道)
  • 前后台进程(&)
  • 基本的信号处理(Ctrl+C 不退出 shell)

目标最终效果(类似 bash 的极简版)

myshell$ pwd
/home/user
myshell$ cd /tmp
myshell$ echo $HOME
/home/user
myshell$ export MYVAR=hello
myshell$ echo $MYVAR
hello
myshell$ ls -l | grep txt
-rw-r--r-- 1 user user 1234 Feb  4  txtfile.txt
myshell$ sleep 10 &
[1] 12345
myshell$ 

完整实现代码(约 300 行)

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <errno.h>
#include <limits.h>

#define MAX_INPUT 1024
#define MAX_ARGS  64
#define MAX_PIPES 2   // 支持一级管道

// 全局环境变量表(我们自己维护一份副本)
extern char **environ;
char **my_environ = NULL;

// 前台进程组 pid
pid_t fg_pgid = 0;

// ---------------------- 辅助函数 ----------------------

// 字符串 trim 两端空白
void trim(char *str) {
    char *start = str;
    char *end;

    // 跳过开头空白
    while (*start && (*start == ' ' || *start == '\t' || *start == '\n')) start++;

    if (*start == 0) {
        *str = '\0';
        return;
    }

    // 找到结尾
    end = start + strlen(start) - 1;
    while (end > start && (*end == ' ' || *end == '\t' || *end == '\n')) end--;

    *(end + 1) = '\0';

    // 移动到开头
    if (start != str) memmove(str, start, end - start + 2);
}

// 解析一行输入为参数数组
int parse_command(char *input, char **args) {
    int argc = 0;
    char *token;

    trim(input);

    if (strlen(input) == 0) return 0;

    token = strtok(input, " \t\n");
    while (token && argc < MAX_ARGS - 1) {
        args[argc++] = token;
        token = strtok(NULL, " \t\n");
    }
    args[argc] = NULL;
    return argc;
}

// 查找命令是否为内建
int is_builtin(const char *cmd) {
    return strcmp(cmd, "cd") == 0 ||
           strcmp(cmd, "exit") == 0 ||
           strcmp(cmd, "pwd") == 0 ||
           strcmp(cmd, "echo") == 0 ||
           strcmp(cmd, "export") == 0 ||
           strcmp(cmd, "env") == 0;
}

// 内建命令执行
int builtin_execute(char **args) {
    if (!args[0]) return 1;

    if (strcmp(args[0], "cd") == 0) {
        if (!args[1]) {
            chdir(getenv("HOME"));
        } else {
            if (chdir(args[1]) != 0) {
                perror("cd");
            }
        }
        return 1;
    }

    if (strcmp(args[0], "exit") == 0) {
        exit(0);
    }

    if (strcmp(args[0], "pwd") == 0) {
        char cwd[PATH_MAX];
        if (getcwd(cwd, sizeof(cwd)) != NULL) {
            printf("%s\n", cwd);
        } else {
            perror("pwd");
        }
        return 1;
    }

    if (strcmp(args[0], "echo") == 0) {
        int i = 1;
        while (args[i]) {
            printf("%s", args[i]);
            if (args[i+1]) printf(" ");
            i++;
        }
        printf("\n");
        return 1;
    }

    if (strcmp(args[0], "export") == 0) {
        if (args[1]) {
            // 支持 export VAR=value 或 export VAR
            char *eq = strchr(args[1], '=');
            if (eq) {
                *eq = '\0';
                setenv(args[1], eq+1, 1);
            } else {
                setenv(args[1], "", 1);
            }
        }
        return 1;
    }

    if (strcmp(args[0], "env") == 0) {
        char **env = environ;
        while (*env) {
            printf("%s\n", *env);
            env++;
        }
        return 1;
    }

    return 0;
}

// 简单信号处理:Ctrl+C 只影响前台进程
void sigint_handler(int sig) {
    if (fg_pgid > 0) {
        kill(-fg_pgid, SIGINT);
    }
    // 不退出 shell
}

// 执行外部命令(支持单级管道)
void execute_external(char **args, int background) {
    int pipefd[2];
    int has_pipe = 0;

    // 检测是否有管道
    for (int i = 0; args[i]; i++) {
        if (strcmp(args[i], "|") == 0) {
            has_pipe = 1;
            args[i] = NULL;  // 分割成两个命令
            char **cmd2 = &args[i+1];
            pipe(pipefd);

            pid_t pid1 = fork();
            if (pid1 == 0) {
                // 左命令
                dup2(pipefd[1], STDOUT_FILENO);
                close(pipefd[0]);
                close(pipefd[1]);
                execvp(args[0], args);
                perror("execvp left");
                _exit(1);
            }

            pid_t pid2 = fork();
            if (pid2 == 0) {
                // 右命令
                dup2(pipefd[0], STDIN_FILENO);
                close(pipefd[0]);
                close(pipefd[1]);
                execvp(cmd2[0], cmd2);
                perror("execvp right");
                _exit(1);
            }

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

            if (!background) {
                fg_pgid = pid1;
                waitpid(pid1, NULL, 0);
                waitpid(pid2, NULL, 0);
                fg_pgid = 0;
            } else {
                printf("[%d] %d\n", 1, pid1);
            }
            return;
        }
    }

    // 无管道,普通执行
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        execvp(args[0], args);
        perror("execvp");
        _exit(1);
    } else if (pid > 0) {
        if (!background) {
            fg_pgid = pid;
            waitpid(pid, NULL, 0);
            fg_pgid = 0;
        } else {
            printf("[%d] %d\n", 1, pid);
        }
    }
}

// 主循环
int main() {
    char input[MAX_INPUT];
    char *args[MAX_ARGS];
    int argc;

    // 初始化环境变量副本(可选,setenv/putenv 实际操作的是 environ)
    // my_environ = environ; // 如果需要自己维护可复制

    signal(SIGINT, sigint_handler);
    signal(SIGTSTP, SIG_IGN); // 忽略 Ctrl+Z

    printf("欢迎使用 myshell (简易版)\n");
    printf("支持命令:cd, pwd, echo, export, exit, env, 外部程序, 简单管道, & 后台\n\n");

    while (1) {
        char cwd[PATH_MAX];
        getcwd(cwd, sizeof(cwd));
        printf("myshell:%s$ ", strrchr(cwd, '/') ? strrchr(cwd, '/') : cwd);

        if (!fgets(input, MAX_INPUT, stdin)) {
            printf("\n退出\n");
            break;
        }

        // 去掉换行
        input[strcspn(input, "\n")] = 0;

        // 解析
        argc = parse_command(input, args);
        if (argc == 0) continue;

        int background = 0;
        if (argc > 1 && strcmp(args[argc-1], "&") == 0) {
            background = 1;
            args[--argc] = NULL;
        }

        // 内建命令
        if (is_builtin(args[0])) {
            builtin_execute(args);
            continue;
        }

        // 外部命令或管道
        execute_external(args, background);
    }

    return 0;
}

编译 & 运行

gcc -o myshell myshell.c
./myshell

功能说明与扩展点

已实现:

  • 内建:cd、exit、pwd、echo、export、env
  • 环境变量读取($PATH 等通过 getenv)
  • 外部程序执行(execvp)
  • 简单单级管道(ls | grep)
  • 后台执行(sleep 100 &)
  • Ctrl+C 只杀前台进程

可以继续扩展的方向

  1. 支持多级管道(需要多个 pipefd 数组 + 循环 fork)
  2. 支持重定向(< > >>)→ 解析时识别符号,dup2
  3. 支持环境变量展开($VAR)→ 在 parse 前替换
  4. 支持历史命令(上下箭头)→ readline 库
  5. 支持 job control(jobs、fg、bg)→ 记录后台进程列表
  6. 支持 alias

小结

这个版本大约 300 行代码,已经包含了 shell 最核心的三大能力:

  • 内建命令(直接执行)
  • 环境变量(getenv / setenv)
  • 程序替换(fork + execvp)

如果你想继续深入某个部分(例如:实现多级管道、重定向、变量展开、job control),可以告诉我,我可以继续补充对应代码和解析。

文章已创建 4391

发表回复

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

相关文章

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

返回顶部