Files
C-exp-collection/exp0.5/task52_readable.c
2026-05-15 21:25:01 +08:00

295 lines
9.6 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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;
}