Appearance
[English version](/posts/c教程/en-US/07-Working Close to the System/070-Practice Mini Shell in C)
跟练交付物
- 已具备状态:完成第 061-069 课,能重新编译上一章示例。
- 工作目录:
~/c-course-labs/070-mini-shell。 - 第一条命令:macOS / Linux 运行
mkdir -p ~/c-course-labs/070-mini-shell && cd ~/c-course-labs/070-mini-shell;Windows PowerShell 运行New-Item -ItemType Directory -Force "$HOME\c-course-labs\070-mini-shell"; Set-Location "$HOME\c-course-labs\070-mini-shell"。 - 成功证据:保留源码、可执行文件、
evidence.md,并记录pwd/echo/ 一条管道或重定向命令的运行记录。 - 本章边界:本章做一个可观察的 Shell 原型;暂不要求 job control、完整脚本语言或生产级安全沙箱。
- 重置方式:删除本章生成的可执行文件、临时数据和输出文件,保留源码与
evidence.md方便复盘。
是时候将您迄今为止学到的所有内容(系统调用、进程创建、管道、重定向和信号处理)整合到一个有凝聚力的项目中了。
在本节中,您将构建一个最小的交互式 shell,就像bash或者zsh,但精简到必需品。它将运行命令、处理输入/输出重定向,甚至支持管道。
第 1 步:您将构建什么
您的迷你外壳将:
- 显示如下提示
$ - 读取用户输入(例如,
ls -l,cat file.txt) - 将其解析为命令和参数
- 使用创建一个新进程
fork() - 使用请求的命令替换过程映像
execvp()6.等待孩子说完
可选扩展:
- 优雅地处理信号 (Ctrl+C)
- 将输出重定向到文件(
>重定向) - 使用管道链接命令(
|)
步骤 2. 核心循环骨架
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX 1024
int main(void) {
char input[MAX];
while (1) {
printf("$ ");
fflush(stdout);
if (!fgets(input, sizeof(input), stdin))
break;
// Remove newline
input[strcspn(input, "\n")] = 0;
// Exit command
if (strcmp(input, "exit") == 0)
break;
// Tokenize input
char *args[64];
int i = 0;
char *token = strtok(input, " ");
while (token) {
args[i++] = token;
token = strtok(NULL, " ");
}
args[i] = NULL;
// Fork and execute
pid_t pid = fork();
if (pid == 0) {
execvp(args[0], args);
perror("execvp");
exit(1);
} else if (pid > 0) {
wait(NULL);
} else {
perror("fork");
}
}
printf("Goodbye!\n");
return 0;
}编译并运行:
gcc mini_shell.c -o mini_shell
./mini_shell尝试命令:
$ ls
$ pwd
$ echo hello world
$ exit步骤 3. 优雅地处理错误
如果输入错误的命令:
$ xyz输出:
execvp: No such file or directory发生这种情况是因为程序处理execvp()正确失败perror(),正如您在第 69 节中学到的那样。
步骤 4. 添加信号处理
让我们让 Ctrl+C 停止正在运行的命令,但不终止 shell 本身。
#include <signal.h>
void sigint_handler(int sig) {
printf("\nType 'exit' to quit.\n$ ");
fflush(stdout);
}
int main(void) {
signal(SIGINT, sigint_handler);
...
}现在,shell 在等待输入时会忽略 Ctrl+C,而不是终止。
步骤 5. 支持输出重定向
我们将添加>重定向如:
$ echo Hello > out.txt在之前添加此内容execvp()把孩子叫进来:
#include <fcntl.h>
for (int j = 0; args[j]; j++) {
if (strcmp(args[j], ">") == 0) {
args[j] = NULL;
int fd = open(args[j + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
close(fd);
break;
}
}现在stdout命令的内容将转到文件而不是屏幕。
步骤 6. 支持输入重定向
同样,对于<重定向:
$ cat < in.txt添加:
if (strcmp(args[j], "<") == 0) {
args[j] = NULL;
int fd = open(args[j + 1], O_RDONLY);
dup2(fd, STDIN_FILENO);
close(fd);
break;
}步骤 7. 添加管道支撑
处理如下命令:
$ ls | wc -l我们创建两个通过管道连接的进程。
int pipefd[2];
pipe(pipefd);
pid_t p1 = fork();
if (p1 == 0) {
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[0]);
execlp("ls", "ls", NULL);
}
pid_t p2 = fork();
if (p2 == 0) {
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[1]);
execlp("wc", "wc", "-l", NULL);
}
close(pipefd[0]);
close(pipefd[1]);
wait(NULL);
wait(NULL);这与您在第 64 节中看到的模式相同,ls | wc -l.
步骤 8. 组合所有功能
现在你的外壳:
- 解析用户输入
- 产生子进程
- 处理 I/O 重定向
- 支持Ctrl+C中断
- 运行简单的管道
只需大约 150 行代码,您就拥有了一个可用的 Unix shell 原型。
步骤 9. 小代码:完整的迷你 Shell
这是干净、最小的版本:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
void sigint_handler(int sig) {
printf("\n$ ");
fflush(stdout);
}
int main(void) {
signal(SIGINT, sigint_handler);
char input[1024];
while (1) {
printf("$ ");
fflush(stdout);
if (!fgets(input, sizeof(input), stdin)) break;
input[strcspn(input, "\n")] = 0;
if (strcmp(input, "exit") == 0) break;
char *args[64];
int i = 0;
char *token = strtok(input, " ");
while (token) {
args[i++] = token;
token = strtok(NULL, " ");
}
args[i] = NULL;
pid_t pid = fork();
if (pid == 0) {
for (int j = 0; args[j]; j++) {
if (strcmp(args[j], ">") == 0) {
int fd = open(args[j + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
close(fd);
args[j] = NULL;
} else if (strcmp(args[j], "<") == 0) {
int fd = open(args[j + 1], O_RDONLY);
dup2(fd, STDIN_FILENO);
close(fd);
args[j] = NULL;
}
}
execvp(args[0], args);
perror("execvp");
exit(1);
} else if (pid > 0) {
wait(NULL);
}
}
printf("Exiting shell.\n");
return 0;
}尝试一下,这是一个真正的交互式 shell。
第 10 步:为什么它很重要
本练习结合了您在第 7 章中学到的所有内容:
- 系统调用(
fork,exec,wait,pipe,dup2) - 信号(
SIGINT) - 文件描述符和重定向
- 错误处理
errno - 环境继承
您刚刚构建了一个为每个 Unix shell 提供支持的核心的简化版本,从bash到zsh到fish.
自己尝试一下
1.添加对管道的支持(|)通过链接多个命令。 2. 实现后台进程&. 3.添加cd和pwd作为内置命令。 4. 在每个命令后显示退出代码。 5. 处理多个空格和带引号的参数。
接下来,我们将进入第 8 章:调试、测试和分析,从 gdb 开始,它是您理解和修复 C 程序最强大的盟友。
