简易版:Shell 基础构建
一个 Shell 启动后,先显示提示符,再等用户输入。要把这个动作做出来,最先需要拿到当前环境变量,比如用户名和家目录。这里直接用 getenv() 就够了,头文件是 <stdlib.h>。
命令行输入与解析
Shell 的核心其实就是处理一行字符串。这里用 fgets() 从标准输入读取命令,比如 ls、pwd、touch。读完以后要把末尾的换行去掉,不然后面做比较或者拼接都会别扭。
#define MAX 32
char str[MAX];
fgets(str, sizeof(str) - 1, stdin);
str[strnlen(str, MAX) - 1] = '\0'; // 安全地去除换行符
接下来要把这一整行按空格切成参数数组。strtok() 正好适合干这个活,不过它有个老毛病:会直接改原字符串,把分隔符写成 \0,而且调用方式也有点绕,第一次传原串,后面继续切就传 NULL。
#include <string.h>
char* argv[MAX] = { NULL };
const char* delim = " \t\n"; // 常见的空白符作为分隔符
int i = 0;
argv[i++] = strtok(str, delim);
while (argv[i - 1]) {
argv[i++] = strtok(NULL, delim);
}
进程创建与执行
拿到参数数组后,就可以把命令真正跑起来了。这里的主线很固定:先 fork() 出子进程,再在子进程里用 execvp() 覆盖当前进程映像。
pid_t d = fork();
if (d == 0) {
// 子进程逻辑
if (argv[0] == NULL) return 0;
int count = execvp(argv[0], argv);
if (count < 0) perror("execvp failed");
exit(EXIT_FAILURE);
} else {
// 父进程逻辑
waitpid(-1, NULL, 0); // 阻塞等待子进程结束
}
这里用阻塞等待,交互会更稳:子进程没结束,父进程就不急着进入下一轮循环。做一个简单 Shell,这样最省事,也最不容易把控制流搞乱。真要做成更完整的 Shell,再考虑非阻塞和作业控制。
代码封装
把上面的逻辑拆成几个函数,结构会清楚很多。变量作用域要收好,尤其是全局的 delim 和 argv,别让输入解析和执行过程互相踩。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define LEFT "["
#define RIGHT "]"
#define MAX 32
const char* delim = " \t\n";
char str[MAX];
char* argv[MAX] = { NULL };
// 打印提示符
void command_printf() {
printf(LEFT "%s"@"%s" "#" RIGHT " ", getenv("USER"), getenv("HOME"));
}
// 获取输入
char* command_get(char str[MAX], int size) {
char* pc = fgets(str, size, stdin);
if (pc) {
size_t len = strlen(str);
if (len > 0 && str[len - 1] == '\n') str[len - 1] = '\0';
}
return pc;
}
// 解析参数
void command_extraction(char str[MAX], const char* delim, char* argv[MAX]) {
int i = 0;
argv[i++] = strtok(str, delim);
while (argv[i - 1]) {
argv[i++] = strtok(NULL, delim);
}
}
// 执行命令
void command_use(char* argv[MAX]) {
pid_t d = fork();
if (d == 0) {
if (argv[0] == NULL) return;
int count = execvp(argv[0], argv);
if (count < 0) perror("execvp failed");
exit(EXIT_FAILURE);
} else {
waitpid(-1, NULL, 0);
}
}
int main() {
while (1) {
command_printf();
char* pc = command_get(str, sizeof(str));
if (!pc) break;
command_extraction(str, delim, argv);
command_use(argv);
}
return 0;
}
运行起来后,Shell 能回显并执行外部命令:

挑战版:内置命令处理
基础版做完以后,很快会碰到 cd 和 echo $PATH 这种命令不太对劲。原因也不复杂:Shell 里的命令分两类。
- 外部命令:像
ls、cat,本质上是磁盘里的可执行文件,走fork + exec就行。 - 内置命令:像
cd、exit、export,它们要改的是 Shell 自己的状态,不能扔到子进程里做。
解决 cd 指令
cd 改的是当前工作目录。如果放到子进程里执行,子进程改完就退出了,父进程还是停在原来的目录。这个坑很常见,所以 cd 必须在父进程里直接调用 chdir()。
if (strcmp(argv[0], "cd") == 0 && argc == 2) {
if (strcmp(argv[1], "./") == 0) return; // 不做任何操作
const char* path = argv[1];
if (chdir(path) == -1) {
printf("路径执行错误\n");
return;
}
}
解决 echo 命令
echo $PATH 这类输入,Shell 先要做变量展开,把 $PATH 换成真实值,比如 /usr/local/sbin:...,再交给程序或者直接打印。这里的处理方式比较直接:如果参数以 $ 开头,就拿变量名去 getenv() 查。
if (strcmp(argv[0], "echo") == 0 && argc == 2) {
char* pc = argv[1];
if (pc[0] == '$' && strlen(pc) > 1) {
char* var_name = pc + 1;
char* value = getenv(var_name);
if (value) argv[1] = value;
}
}
这样以后,echo 就能正常输出环境变量内容了。像 export 这类命令也可以照着这个思路继续补,只是边界会多一点,重点还是要分清:哪些东西只是给子进程用,哪些东西真的要改 Shell 自己的环境。


