295 lines
9.6 KiB
C
295 lines
9.6 KiB
C
|
|
#include <stdio.h>
|
|||
|
|
#include <stdlib.h>
|
|||
|
|
#include <string.h>
|
|||
|
|
#include <unistd.h>
|
|||
|
|
#include <sys/types.h>
|
|||
|
|
#include <sys/wait.h>
|
|||
|
|
#include <fcntl.h>
|
|||
|
|
#include <ctype.h>
|
|||
|
|
|
|||
|
|
#define MAX_CMD 1024
|
|||
|
|
#define MAX_ARGS 64
|
|||
|
|
#define MAX_PIPES 32
|
|||
|
|
|
|||
|
|
/* =========================================================
|
|||
|
|
* 工具函数
|
|||
|
|
* ========================================================= */
|
|||
|
|
|
|||
|
|
/* 去除字符串首尾空白字符(在原字符串上修改) */
|
|||
|
|
char *trim(char *s) {
|
|||
|
|
while (isspace((unsigned char)*s)) s++;
|
|||
|
|
if (*s == '\0') return s;
|
|||
|
|
char *end = s + strlen(s) - 1;
|
|||
|
|
while (end > s && isspace((unsigned char)*end)) end--;
|
|||
|
|
*(end + 1) = '\0';
|
|||
|
|
return s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 把命令字符串按空格拆成 args[],返回参数个数 */
|
|||
|
|
int parse_args(char *cmd, char **args) {
|
|||
|
|
int argc = 0;
|
|||
|
|
char *token = strtok(cmd, " \t");
|
|||
|
|
while (token != NULL && argc < MAX_ARGS - 1) {
|
|||
|
|
args[argc++] = token;
|
|||
|
|
token = strtok(NULL, " \t");
|
|||
|
|
}
|
|||
|
|
args[argc] = NULL;
|
|||
|
|
return argc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* =========================================================
|
|||
|
|
* 第一步辅助函数:split_pipeline
|
|||
|
|
* 把 "cmd1 | cmd2 | cmd3" 按 '|' 拆成若干段,
|
|||
|
|
* 存入 segments[],返回段数。
|
|||
|
|
* 注意:会修改 buf 的内容(把 '|' 替换为 '\0')。
|
|||
|
|
* ========================================================= */
|
|||
|
|
int split_pipeline(char *buf, char *segments[], int max_segs) {
|
|||
|
|
int nseg = 0;
|
|||
|
|
char *seg_start = buf; /* 当前段的起始位置 */
|
|||
|
|
char *p = buf;
|
|||
|
|
|
|||
|
|
while (*p != '\0') {
|
|||
|
|
if (*p == '|') {
|
|||
|
|
/* 找到管道符:截断当前段,保存去空格后的指针 */
|
|||
|
|
*p = '\0';
|
|||
|
|
char *seg = trim(seg_start);
|
|||
|
|
if (seg[0] != '\0') { /* 跳过空段 */
|
|||
|
|
if (nseg >= max_segs) {
|
|||
|
|
fprintf(stderr, "shell: too many pipes (max %d)\n", max_segs);
|
|||
|
|
return -1;
|
|||
|
|
}
|
|||
|
|
segments[nseg++] = seg;
|
|||
|
|
}
|
|||
|
|
seg_start = p + 1; /* 下一段从 '|' 后面开始 */
|
|||
|
|
}
|
|||
|
|
p++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 保存最后一段('|' 后面,或整个命令没有 '|' 时) */
|
|||
|
|
char *last = trim(seg_start);
|
|||
|
|
if (last[0] != '\0') {
|
|||
|
|
segments[nseg++] = last;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nseg;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* =========================================================
|
|||
|
|
* 第二步辅助函数:exec_cmd_in_child
|
|||
|
|
* 在子进程里执行一条命令,处理 < 和 > 重定向。
|
|||
|
|
* in_fd / out_fd 来自管道(或 STDIN/STDOUT_FILENO)。
|
|||
|
|
* 此函数只能在 fork() 后的子进程里调用,内部会 exit()。
|
|||
|
|
* ========================================================= */
|
|||
|
|
static void exec_cmd_in_child(char **args, int argc, int in_fd, int out_fd) {
|
|||
|
|
/* --- 解析重定向符,把 < file 和 > file 从参数里剥离 --- */
|
|||
|
|
char *infile = NULL;
|
|||
|
|
char *outfile = NULL;
|
|||
|
|
char *clean_args[MAX_ARGS];
|
|||
|
|
int clean_argc = 0;
|
|||
|
|
|
|||
|
|
for (int i = 0; i < argc; i++) {
|
|||
|
|
if (strcmp(args[i], "<") == 0 && i + 1 < argc) {
|
|||
|
|
infile = args[++i]; /* 记录输入文件,跳过文件名 */
|
|||
|
|
} else if (strcmp(args[i], ">") == 0 && i + 1 < argc) {
|
|||
|
|
outfile = args[++i]; /* 记录输出文件,跳过文件名 */
|
|||
|
|
} else {
|
|||
|
|
clean_args[clean_argc++] = args[i]; /* 普通参数保留 */
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
clean_args[clean_argc] = NULL;
|
|||
|
|
|
|||
|
|
if (clean_argc == 0) exit(0);
|
|||
|
|
|
|||
|
|
/* --- 设置标准输入 ---
|
|||
|
|
* 优先级:< 文件重定向 > 管道读端 > 默认终端 stdin */
|
|||
|
|
if (infile) {
|
|||
|
|
if (in_fd != STDIN_FILENO) close(in_fd); /* 管道读端不再需要,关掉 */
|
|||
|
|
int fd = open(infile, O_RDONLY);
|
|||
|
|
if (fd < 0) { perror("open"); exit(1); }
|
|||
|
|
dup2(fd, STDIN_FILENO);
|
|||
|
|
close(fd);
|
|||
|
|
} else if (in_fd != STDIN_FILENO) {
|
|||
|
|
dup2(in_fd, STDIN_FILENO);
|
|||
|
|
close(in_fd);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* --- 设置标准输出 ---
|
|||
|
|
* 优先级:> 文件重定向 > 管道写端 > 默认终端 stdout */
|
|||
|
|
if (outfile) {
|
|||
|
|
if (out_fd != STDOUT_FILENO) close(out_fd);
|
|||
|
|
int fd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|||
|
|
if (fd < 0) { perror("open"); exit(1); }
|
|||
|
|
dup2(fd, STDOUT_FILENO);
|
|||
|
|
close(fd);
|
|||
|
|
} else if (out_fd != STDOUT_FILENO) {
|
|||
|
|
dup2(out_fd, STDOUT_FILENO);
|
|||
|
|
close(out_fd);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
execvp(clean_args[0], clean_args);
|
|||
|
|
fprintf(stderr, "shell: command not found: %s\n", clean_args[0]);
|
|||
|
|
exit(127);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* =========================================================
|
|||
|
|
* 第三步辅助函数:run_single
|
|||
|
|
* 执行单条命令(无管道),fork 一个子进程并等待它结束。
|
|||
|
|
* ========================================================= */
|
|||
|
|
static void run_single(char *seg) {
|
|||
|
|
char buf[MAX_CMD];
|
|||
|
|
strncpy(buf, seg, MAX_CMD - 1);
|
|||
|
|
buf[MAX_CMD - 1] = '\0';
|
|||
|
|
|
|||
|
|
char *args[MAX_ARGS];
|
|||
|
|
int argc = parse_args(buf, args);
|
|||
|
|
if (argc == 0) return;
|
|||
|
|
|
|||
|
|
pid_t pid = fork();
|
|||
|
|
if (pid < 0) { perror("fork"); return; }
|
|||
|
|
if (pid == 0) {
|
|||
|
|
/* 子进程:从终端 stdin 读,输出到终端 stdout */
|
|||
|
|
exec_cmd_in_child(args, argc, STDIN_FILENO, STDOUT_FILENO);
|
|||
|
|
}
|
|||
|
|
/* 父进程:等待子进程结束 */
|
|||
|
|
waitpid(pid, NULL, 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* =========================================================
|
|||
|
|
* 第四步辅助函数:run_pipeline
|
|||
|
|
* 执行多段管道命令。
|
|||
|
|
*
|
|||
|
|
* 核心思路(以 "A | B | C" 为例):
|
|||
|
|
*
|
|||
|
|
* 终端stdin ──> [A] ──pipe0──> [B] ──pipe1──> [C] ──> 终端stdout
|
|||
|
|
*
|
|||
|
|
* 1. 创建 nseg-1 个管道(pipe0, pipe1, ...)
|
|||
|
|
* 2. 依次 fork 每段命令的子进程,子进程拿到自己的 in_fd/out_fd
|
|||
|
|
* 3. 父进程关闭所有管道端,然后等待全部子进程结束
|
|||
|
|
* ========================================================= */
|
|||
|
|
static void run_pipeline(char *segments[], int nseg) {
|
|||
|
|
int npipes = nseg - 1;
|
|||
|
|
|
|||
|
|
/* --- 1. 一次性创建所有管道 --- */
|
|||
|
|
int pipes[MAX_PIPES][2];
|
|||
|
|
for (int i = 0; i < npipes; i++) {
|
|||
|
|
if (pipe(pipes[i]) < 0) {
|
|||
|
|
perror("pipe");
|
|||
|
|
/* 出错:关闭已创建的管道后返回 */
|
|||
|
|
for (int j = 0; j < i; j++) {
|
|||
|
|
close(pipes[j][0]);
|
|||
|
|
close(pipes[j][1]);
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* --- 2. 依次 fork 每段命令的子进程 --- */
|
|||
|
|
pid_t pids[MAX_PIPES + 1];
|
|||
|
|
|
|||
|
|
for (int i = 0; i < nseg; i++) {
|
|||
|
|
|
|||
|
|
/* 确定本段从哪里读、往哪里写:
|
|||
|
|
* 第一段:从终端 stdin 读
|
|||
|
|
* 中间段:从上一个管道的读端读,向下一个管道的写端写
|
|||
|
|
* 最后段:向终端 stdout 写 */
|
|||
|
|
int in_fd = (i == 0) ? STDIN_FILENO : pipes[i - 1][0];
|
|||
|
|
int out_fd = (i == nseg - 1) ? STDOUT_FILENO : pipes[i][1];
|
|||
|
|
|
|||
|
|
/* 解析本段命令的参数 */
|
|||
|
|
char cbuf[MAX_CMD];
|
|||
|
|
strncpy(cbuf, segments[i], MAX_CMD - 1);
|
|||
|
|
cbuf[MAX_CMD - 1] = '\0';
|
|||
|
|
char *args[MAX_ARGS];
|
|||
|
|
int argc = parse_args(cbuf, args);
|
|||
|
|
|
|||
|
|
pid_t pid = fork();
|
|||
|
|
if (pid < 0) {
|
|||
|
|
perror("fork");
|
|||
|
|
/* fork 失败:关闭所有管道,等待已启动的子进程 */
|
|||
|
|
for (int j = 0; j < npipes; j++) {
|
|||
|
|
close(pipes[j][0]);
|
|||
|
|
close(pipes[j][1]);
|
|||
|
|
}
|
|||
|
|
for (int j = 0; j < i; j++) waitpid(pids[j], NULL, 0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (pid == 0) {
|
|||
|
|
/* 子进程:
|
|||
|
|
* 关闭所有"不是自己用"的管道端,
|
|||
|
|
* 防止 EOF 永远不到达读端。 */
|
|||
|
|
for (int j = 0; j < npipes; j++) {
|
|||
|
|
if (pipes[j][0] != in_fd) close(pipes[j][0]);
|
|||
|
|
if (pipes[j][1] != out_fd) close(pipes[j][1]);
|
|||
|
|
}
|
|||
|
|
if (argc == 0) exit(0);
|
|||
|
|
exec_cmd_in_child(args, argc, in_fd, out_fd);
|
|||
|
|
/* exec_cmd_in_child 内部会 exit,永远不会走到这里 */
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pids[i] = pid; /* 父进程记录子进程 PID */
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* --- 3. 父进程关闭所有管道端,再统一等待子进程 ---
|
|||
|
|
* 必须先全部关闭,否则最后一段命令永远读不到 EOF。 */
|
|||
|
|
for (int i = 0; i < npipes; i++) {
|
|||
|
|
close(pipes[i][0]);
|
|||
|
|
close(pipes[i][1]);
|
|||
|
|
}
|
|||
|
|
for (int i = 0; i < nseg; i++) {
|
|||
|
|
waitpid(pids[i], NULL, 0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* =========================================================
|
|||
|
|
* 顶层入口:execute
|
|||
|
|
* 只做"分流":
|
|||
|
|
* - 把命令行按 '|' 分段
|
|||
|
|
* - 1 段 → run_single
|
|||
|
|
* - 多段 → run_pipeline
|
|||
|
|
* ========================================================= */
|
|||
|
|
void execute(char *cmdline) {
|
|||
|
|
char buf[MAX_CMD];
|
|||
|
|
strncpy(buf, cmdline, MAX_CMD - 1);
|
|||
|
|
buf[MAX_CMD - 1] = '\0';
|
|||
|
|
|
|||
|
|
char *segments[MAX_PIPES + 1];
|
|||
|
|
int nseg = split_pipeline(buf, segments, MAX_PIPES);
|
|||
|
|
|
|||
|
|
if (nseg <= 0) return; /* 空命令或出错 */
|
|||
|
|
|
|||
|
|
if (nseg == 1) {
|
|||
|
|
run_single(segments[0]); /* 普通命令,无管道 */
|
|||
|
|
} else {
|
|||
|
|
run_pipeline(segments, nseg); /* 多段管道命令 */
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* =========================================================
|
|||
|
|
* main:读取命令行,循环执行
|
|||
|
|
* ========================================================= */
|
|||
|
|
int main() {
|
|||
|
|
char cmdline[MAX_CMD];
|
|||
|
|
|
|||
|
|
while (1) {
|
|||
|
|
printf("%% ");
|
|||
|
|
fflush(stdout);
|
|||
|
|
|
|||
|
|
if (fgets(cmdline, MAX_CMD, stdin) == NULL) {
|
|||
|
|
printf("\n");
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
char *cmd = trim(cmdline);
|
|||
|
|
if (strlen(cmd) == 0) continue;
|
|||
|
|
|
|||
|
|
if (strcmp(cmd, "exit") == 0 || strcmp(cmd, "logout") == 0) {
|
|||
|
|
printf("Goodbye!\n");
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
execute(cmd);
|
|||
|
|
}
|
|||
|
|
return 0;
|
|||
|
|
}
|