【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
当前局限性(可作为练习方向)
- 不支持引号(” ” ‘ ‘)
- 不支持多级管道(cmd1 | cmd2 | cmd3)
- 不支持
&&||命令列表 - 不支持变量赋值(
VAR=val cmd) - 不支持
~路径展开 - 重定向和管道不能同时复杂组合
- 后台任务没有 jobs/fg/bg 管理
- 错误提示非常原始
如果你想继续扩展,可以按以下优先级尝试:
- 支持双引号/单引号解析
- 支持多级管道(动态创建 pipe 数组)
- 实现 jobs / fg / bg 功能
- 支持环境变量赋值与展开(完整版)
- 更好的错误处理与提示
祝你实现一个更有趣的 mini-shell!