☰
1024手写Shell:拼写建议、信号处理与执行边界
2026/10/25 2:37:53 网站建设 项目流程

终端里把grep敲成gerp,程序可以猜一个相似单词,却不能据此断定用户想做什么。这个差别很小,也很容易被“智能Shell”四个字盖过去。

这次只做一个窄原型:按空白拆参数,启动一个前台子进程,等待它退出;找不到程序时给出静态词典中的拼写建议。它不支持管道、引号、变量展开、重定向和后台作业。遇到这些语法直接拒绝,避免把半套解析器交给用户误用。

先把Ctrl+C的归属讲清楚

在交互终端中,Ctrl+C通常向前台进程组发送SIGINT。本原型不实现完整作业控制,父Shell与命令仍在同一前台组:父进程忽略SIGINT,子进程在execvp前恢复默认处置。子进程还显式清空信号屏蔽字。

这里有两个不同动作:sigaction设置处置方式,sigprocmask设置哪些信号暂时不递送。SIG_DFL是默认处置,不是“默认屏蔽字”。忽略信号的处置可能跨exec保留,因此不能指望启动新程序后自动恢复。Linux signal(7)

图中waitpid对EINTR重试是一般防御;如果某个信号被忽略,它本身并不必然造成EINTR。完整Shell还需要处理进程组、终端控制权、停止与继续,这里没有实现。

一个可以编译的原型

保存成minish.c:

#define _POSIX_C_SOURCE 200809L #include <errno.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> static int min3(int a, int b, int c) { int m = a < b ? a : b; return m < c ? m : c; } /* Optimal string alignment distance: not unrestricted Damerau-Levenshtein. */ static int osa(const char *a, const char *b) { size_t n = strlen(a), m = strlen(b); if (n > 63 || m > 63) return 64; int d[64][64]; for (size_t i = 0; i <= n; ++i) d[i][0] = (int)i; for (size_t j = 0; j <= m; ++j) d[0][j] = (int)j; for (size_t i = 1; i <= n; ++i) { for (size_t j = 1; j <= m; ++j) { d[i][j] = min3(d[i-1][j] + 1, d[i][j-1] + 1, d[i-1][j-1] + (a[i-1] != b[j-1])); if (i > 1 && j > 1 && a[i-1] == b[j-2] && a[i-2] == b[j-1] && d[i-2][j-2] + 1 < d[i][j]) d[i][j] = d[i-2][j-2] + 1; } } return d[n][m]; } static void suggest(const char *cmd) { const char *dict[] = {"grep", "cat", "ls", "pwd", NULL}; for (size_t i = 0; dict[i]; ++i) if (osa(cmd, dict[i]) <= 1) fprintf(stderr, "suggest only: %s\n", dict[i]); } static int disposition(int sig, void (*handler)(int)) { struct sigaction sa; memset(&sa, 0, sizeof sa); sa.sa_handler = handler; sigemptyset(&sa.sa_mask); return sigaction(sig, &sa, NULL); } int main(void) { if (disposition(SIGINT, SIG_IGN) || disposition(SIGQUIT, SIG_IGN)) return 1; char line[4096]; int last = 0; for (;;) { if (isatty(STDIN_FILENO)) { fputs("mini> ", stdout); fflush(stdout); } if (!fgets(line, sizeof line, stdin)) break; if (!strchr(line, '\n') && !feof(stdin)) { int ch; while ((ch = getchar()) != '\n' && ch != EOF) {} fputs("line too long\n", stderr); last = 2; continue; } if (strpbrk(line, "|;&$><\\\"'`()")) { fputs("unsupported syntax\n", stderr); last = 2; continue; } char *argv[64], *save = NULL; size_t n = 0; int overflow = 0; for (char *t = strtok_r(line, " \t\r\n", &save); t; t = strtok_r(NULL, " \t\r\n", &save)) { if (n == 63) { overflow = 1; break; } argv[n++] = t; } if (overflow) { fputs("too many arguments\n", stderr); last = 2; continue; } if (!n) continue; argv[n] = NULL; if (!strcmp(argv[0], "exit")) { if (n != 1) { fputs("exit takes no arguments\n", stderr); last = 2; continue; } return last; } pid_t pid = fork(); if (pid < 0) { perror("fork"); last = 1; continue; } if (!pid) { sigset_t empty; sigemptyset(&empty); if (disposition(SIGINT, SIG_DFL) || disposition(SIGQUIT, SIG_DFL) || sigprocmask(SIG_SETMASK, &empty, NULL)) _exit(126); execvp(argv[0], argv); int e = errno; if (e == ENOENT) suggest(argv[0]); fprintf(stderr, "%s: %s\n", argv[0], strerror(e)); _exit(e == ENOENT ? 127 : 126); } int st; pid_t w; do { w = waitpid(pid, &st, 0); } while (w < 0 && errno == EINTR); if (w < 0) { perror("waitpid"); last = 1; } else if (WIFEXITED(st)) last = WEXITSTATUS(st); else if (WIFSIGNALED(st)) last = 128 + WTERMSIG(st); else last = 1; } return last; }

编译命令:

cc -std=c11 -Wall -Wextra -Werror -O2 minish.c -o minish ./minish

纠错函数使用的是optimal string alignment distance(OSA),支持受限的相邻换位。它不是完整的Damerau–Levenshtein算法,更不是语义理解。距离近的命令可能完全不同,所以程序只向stderr输出候选,不会执行候选。

fork后的地址空间、文件描述符和信号存在明确继承规则;多线程程序在子进程执行exec前能安全做的操作更受限制。此例是单线程教学程序,不能直接塞入已有多线程终端宿主。fork(2)

用失败输入检查边界

示例在macOS arm64、Apple Clang 21.0.0上以以上严格编译选项通过。运行记录包括:

输入或动作观察到的结果
true、false、exit最后返回1
gerp、exitstderr给出grep建议,返回127
echo a|cat拒绝未支持语法,返回2
超长行、过多参数明确拒绝,返回2
PATH中不可执行的测试文件返回126,没有拼写建议
子进程执行sleep时向测试进程组发SIGINT子进程终止,父进程继续读exit,最终返回130

这些是本机用户态结果,不是Linux发行版兼容性证明。SIGINT测试使用独立测试进程组,没有给日常终端或其他进程发信号。

还要小心ENOENT:它也可能来自可执行文件依赖的解释器或加载器缺失,不总意味着命令名字拼错。建议前缀因此写成suggest only。静态词典也可能建议一个本机没有安装的程序;若未来扫描PATH,缓存更新、权限与同名程序选择都得重新设计。

这个原型的价值在于能说清自己做了什么:启动命令、等待退出、展示候选。它没有“自愈成功”的承诺,也不应该获得替用户执行修正命令的默认权力。2026年10月9日Anthropic披露的内部评测越界案例可以作为授权边界的背景,不能拿来证明本程序已经安全。原始报告

资料于2026年10月23日核对。封面和流程图为教学示意,图中的终端不是实测截图。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询