mybash:简易 Shell 实现的设计思路与核心模块解析

前言

Shell 是用户与操作系统内核之间的接口,为了更深入理解 Linux 下进程管理、文件描述符、信号处理等核心机制,我基于 C 语言从零实现了一个简易命令行 Shell ------ mybash。

整个项目不依赖第三方库,全部通过原生系统调用完成, 实现了 cd、pwd、ls、kill、cp、cat 核心 Shell 命令,覆盖文件 IO、目录操作等核心场景,彻底理解了 Shell 命令的底层执行逻辑。

一、命令行提示符

1. 系统提示符分析

当我们打开 Linux 终端时,会看到类似这样的提示符:

根据图片,分析标准的 Bash 提示符,可以得知其构成要素为:用户名、主机名、当前路径、特殊用户标识等,从而明确了 mybash 提示符的设计目标。

2. 核心函数

实现提示符所需的核心函数有:

  • 获取登录用户名:getlogin()
  • 获取主机名:uname()
  • 获取当前路径:getcwd()

3. 代码实现与优化

利用上面的三个函数,就可以实现基础的系统提示符的输出,编译链接后的运行结果

参照标准的提示符,还可以做出以下的优化改进

  1. 直接显示当前目录,而非完整路径
  2. 用户名改用环境变量获取,避免 getlogin() 的限制
  3. root 用户显示 # 而非 $
  4. 处理特殊路径:~/
  5. 提示符高亮显示,提升可读性:优化后提示符格式可以为 " 用户名 @主机名:当前路径 $ ",root 用户自动替换为 #

4. 代码

(此处仅展示核心片段,不贴全量代码)

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/utsname.h>
#include <string.h>
#include <sys/types.h>
#include <pwd.h>

// 核心:输出自定义命令行提示符
void PrintPrompt()
{
    char ch = '$';
    uid_t uid = getuid();
    struct passwd *p = getpwuid(uid);
    if (p == NULL) {
        perror("get passwd info faild");
        printf("mybash> ");
        fflush(stdout);
        return;
    }
    // root用户提示符为#
    if (uid == 0) ch = '#';

    // 获取主机名
    struct utsname sysinfo;
    if (uname(&sysinfo) == -1) {
        perror("get host info failed");
        printf("mybash> ");
        fflush(stdout);
        return;
    }

    // 获取并简化当前路径
    char buf[128] = {0};
    if (getcwd(buf, 127) == NULL) {
        perror("get current working directory failed");
        printf("mybash> ");
        fflush(stdout);
        return;
    }
    // 家目录替换为~,非家目录只显示最后一级
    char *s = (strcmp(buf, p->pw_dir) == 0) ? "~" : buf;
    if (s == buf) {
        s = strrchr(buf, '/');
        if (strlen(s) != 1) s++;
    }

    // 带颜色输出提示符
    printf("mybash> \033[1;32m%s@%s\033[0m\033[1;34m:%s\033[0m%c ", 
           p->pw_name, sysinfo.nodename, s, ch);
    fflush(stdout);
}

int main()
{
    while (1) {
        // 输出提示符
        PrintPrompt();
        // 获取用户命令
        char cmd[128] = {0};
        if (fgets(cmd, 127, stdin) == NULL) continue;
        // 执行用户命令
    }
}

二、分离用户命令和参数

系统终端通过获取用户输入,针对输入的命令以及参数来实现功能,所以我们也需要将命令和参数分离开来,命令用来对应需要实现的功能,参数作为命令的补充实现。

1. 核心函数 strtok()

  • 函数作用:按指定分隔符拆分字符串
  • 核心用法规则 :首次调用传入原始字符串,后续调用传入 NULL
  • 返回值:指向拆分后子串的指针
  • 内部机制 :修改原始字符串,用 \0 替换分隔符

2. 代码实现

实现命令行输入的参数拆分逻辑,处理连续空格、制表符等边界情况。通过循环调用 strtok () 拆分输入,用数组存储拆分后的命令和参数

cpp 复制代码
// 命令和参数总数
#define CMD_NUM 10

// 分割用户命令和参数
int SplitCmd(char *cmd, char *cmdArr[])
{
    // 首次分割:传递字符串
    // 以空格为标准进行分割
    char *token = strtok(cmd, " ");
    int count = 0; // 数组索引
    while (token != NULL)
    {
        // 将分割得到的字符串保存在cmdArr数字中
        cmdArr[count++] = token;
        // 继续分割,传递的字符串为NULL
        token = strtok(NULL, " ");
    }
    // 返回字符串个数
    return
}

3. 运行结果

三、执行用户命令

内置命令 VS 外置命令

首先需要了解一下系统命令分为两种:内置bash的命令和外置命令。外置命令可以通过 which + 命令查到命令所在的文件/目录,如 ls 命令,而内置bash命令不行,如cd。

外置命令一般放在bin目录内,所以我们自己实现时写可以创建一个 mybin 目录装外置命令实现函数。内置命令直接放在 mybash.c 中即可。

具体区别

维度 内置命令(Built-in) 外置命令(External Command)
本质 Shell 程序本身的一部分(函数 / 指令) 独立的可执行文件(二进制 / 脚本)
执行方式 由 Shell 直接执行,不创建子进程 Shell 会 fork 子进程,再 exec 加载执行
速度 极快(无需磁盘 IO、无进程创建开销) 较慢(需要加载文件、创建子进程)
作用域 影响当前 Shell 进程(如 cd 仅影响子进程(无法改变父 Shell 状态)
查找方式 无需 PATH,Shell 内部直接识别 依赖 PATH 环境变量查找可执行文件

1. 运行命令函数 + exit 命令的实现

编写一个函数,在函数中根据用户输入的命令进行不同的操作

在主函数中已经获取到了用户输入,且对输入进行了分割,cmdArr[0]就是命令,将cmdArr[0]和exit等命令对比,就能针对不同情况进行处理。

可以先得出一个大致框架如下:exit命令直接调用系统exit函数即可。

cpp 复制代码
#include<stdlib.h> //exit需要的头文件

// 执行系统命令
void RunCmd(char *cmdArr[], int count)
{
    char* cmd = cmdArr[0];
    if (strcmp(cmd, "cd") == 0)
    {
        printf("cd->%s\n", cmd);
    }
    else if (strcmp(cmd, "exit") == 0)
    {
        exit(0);
    }
    else
    {
        printf("other cmd:%s\n", cmd);
    }
}

主函数调用该函数

cpp 复制代码
int main(){
    while(1){
        ......
        //3.执行用户命令
        RunCmd(cmdArr,count);
    }
    return 0;
}

2. cd 命令的实现

核心函数: chdir()

代码实现

1、情况分析:

  • 一般情况:切换到指定路径
  • 参数为 ~:解析为用户主目录
  • 无参数:切换到用户主目录

2、代码与运行结果:

核心难点是:解析~为环境变量 HOME 的路径,通过 getenv ("HOME") 获取用户主目录,解决了无参数时的默认切换逻辑"

cpp 复制代码
// cd命令的实现
void RunCd(char *path)//参数为传递的参数
{
    char buf[128]={0};
    struct passwd* p=getpwuid(getuid());
    if(p==NULL){
        printf("get pwuid info failed\n");
        return;
    }
    //没有参数
    if(path==NULL){
        path=p->pw_dir;//目的路径为家目录
    }
    //参数为~ + ~后面还有参数
    else if(strncmp(path,"~",1)==0){
        strcpy(buf,p->pw_dir);//令buf为家目录
        strcat(buf,path+1);//把剩余参数连接到buf上
        path=buf;//令path保存目的路径
    }
    //一般情况,直接切换路径
    //切换路径
    if (chdir(path) == -1)
    {
        perror("cd failed\n");
        return;
    }
}
......
if (strcmp(cmd, "cd") == 0)
    {
        RunCd(cmdArr[1]);//将参数传递给函数
    }
......

3. 退格键功能的实现

核心结构体: termios

书籍《Linux程序设计》中写到:

核心函数:tcgetattr()、tcsetattr()

  1. tcgetattr():读取终端属性
  2. tcsetattr():设置终端属性

代码实现

修改终端属性,支持退格键编辑输入,通过修改 termios 的 c_lflag 位,关闭 ICANON 模式实现逐字符输入,支持退格键删除已输入字符

cpp 复制代码
#include <termios.h>
#include <stdio.h>
#include <unistd.h>

// 核心:手动获取用户输入,支持退格键编辑
void GetCmd(char *cmd)
{
    struct termios oldAttr, newAttr;
    // 获取并修改终端属性:关闭规范模式+回显,支持逐字符输入
    if (tcgetattr(STDIN_FILENO, &oldAttr) == -1) {
        perror("tcgetattr failed");
        return;
    }
    newAttr = oldAttr;
    newAttr.c_lflag &= ~(ICANON | ECHO); // 核心:关闭规范模式、取消回显
    if (tcsetattr(STDIN_FILENO, TCSANOW, &newAttr) == -1) {
        perror("tcsetattr failed");
        return;
    }

    int index = 0;
    char ch;
    while ((ch = getchar()) != '\n') {
        if (ch == 8 && index >= 1) { // 退格键处理
            cmd[--index] = 0;
            printf("\033[1D\033[K"); // 光标左移+删除光标后内容
            fflush(stdout);
        } else if (ch != 8) { // 普通字符输入
            cmd[index++] = ch;
            printf("%c", ch); // 手动实现回显
            fflush(stdout);
        }
    }

    // 恢复终端原始属性
    tcsetattr(STDIN_FILENO, TCSANOW, &oldAttr);
    printf("\n");
}

// 主函数仅保留核心调用框架
int main()
{
    while (1) {
        char cmd[128] = {0};
        GetCmd(cmd); // 调用自定义输入函数,支持退格编辑
        // 后续命令解析逻辑
    }
}

4. pwd 命令的实现

下面的命令都是外置命令,为了和系统终端类似,我们需要自建一个 mybin 目录,用来存储外置命令函数及可执行文件(一般把自定义命令和 mybash 主程序放在同一项目目录下)

(1) 准备工作

在上文提到过外置命令的实现是:先 fork 子进程,再 exec 加载执行,所以需要在 RunCmd() 函数中创建子进程来执行 pwd 的命令。

(2) 命令分析 + 核心函数:getcwd()

pwd命令的作用是:打印当前工作目录的绝对路径

其功能实现也比较简单,直接使用到 **getcwd()**函数即可。

需要头文件 <unistd.h>

**(3)**代码实现和执行命令 :集成到主程序中。pwd 命令通过 getcwd () 获取当前工作目录,封装为独立模块,通过 execv () 调用,符合 Shell 命令的执行逻辑

注意:执行前,需要先将 pwd.c 编译链接为可执行文件 pwd

5. ls 命令的实现

ls命令的核心功能是:列出指定目录下的文件 / 目录信息

(1) 核心函数与结构

opendir()

目录流(directory stream):这是一个抽象的概念,本质上是一个 DIR* 类型的指针,代表了对某个目录的 "读取会话"。它内部维护了读取位置、文件信息等状态。

DIR *opendir(const char *name); //name 是要打开的目录路径(可以是相对路径或绝对路径)

函数返回值 :成功时返回一个非空的 DIR* 指针;失败时返回 NULL,并设置 errno(如目录不存在、权限不足等)。

readdir()

readdir() 会从目录流中读取下一个条目,直到没有更多条目为止。

closedir()

dirent 结构

(2) 基础功能实现

ShowPathFile()函数

  • 首先实现 ls 不带参数(如-l、-a等)的功能
  • 若指定多个路径,先打印路径名,再列出对应文件

主函数

  • 统计用户输入的非参数路径个数:过滤掉以 - 开头的参数
  • 遍历每个路径,调用 ShowPathFile() 函数,列出该目录下的非隐藏文件
  • 如果用户未指定任何路径,默认列出当前目录下的文件

(3) 增加对不同参数的处理

整体操作还是只针对与 ls.c 文件,在这里先实现两个常见参数 -a 和 -i

参数 核心作用
-a 显示目录下所有文件 / 目录 (包括以 . 开头的隐藏文件 / 目录,如 .bashrc..
-i 显示文件 / 目录对应的 inode 编号(inode 是文件系统中标识文件的唯一数字 ID)

可以定义全局bool类型变量来表示某个参数是否存在

函数 OptionAns():统计参数个数,并判断以-开头的参数是否存在

(4) 不同类型文件不同颜色输出:目录:蓝色;可执行文件:绿色

使用 lstat()函数 获取文件属性(包括类型、权限),存入 struct stat 结构体;

(5) 核心代码与运行结果

核心难点 1 是:解析 - l 参数时获取文件权限、大小、修改时间等元信息,通过 stat 结构体的 st_mode 解析权限位;

核心难点 2 是:区分普通文件 / 目录 / 链接文件,通过 ANSI 颜色码实现不同类型文件不同颜色输出

cpp 复制代码
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>

// 核心标识:控制显示逻辑
static bool showHidden = false; // -a参数:显示隐藏文件
static bool showInode = false;  // -i参数:显示inode

// 核心:遍历目录并按规则显示文件
void ShowFiles(const char *path) {
    DIR *dir = opendir(path);
    if (!dir) return;

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        // 过滤隐藏文件(-a参数控制)
        if (!showHidden && entry->d_name[0] == '.') continue;

        // 显示inode(-i参数控制)
        if (showInode) printf("%ld ", entry->d_ino);

        // 核心:文件类型判断 + 颜色输出
        struct stat st;
        char fullPath[1024] = {0};
        snprintf(fullPath, sizeof(fullPath), "%s/%s", path, entry->d_name);
        lstat(fullPath, &st);

        // 目录(蓝)、可执行文件(绿)、普通文件(默认)
        if (S_ISDIR(st.st_mode))
            printf("\033[1;34m%s\033[0m   ", entry->d_name);
        else if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
            printf("\033[1;32m%s\033[0m   ", entry->d_name);
        else
            printf("%s    ", entry->d_name);
    }
    closedir(dir);
    printf("\n");
}

// 核心:解析命令行参数
void ParseOptions(int argc, char *argv[]) {
    for (int i = 1; i < argc; i++) {
        if (argv[i][0] == '-') { // 解析选项
            showHidden = (strstr(argv[i], "a") != NULL);
            showInode = (strstr(argv[i], "i") != NULL);
        }
    }
}

// 主函数核心逻辑
int main(int argc, char *argv[]) {
    ParseOptions(argc, argv);
    // 无路径参数则显示当前目录,否则遍历指定路径
    int pathCount = 0;
    for (int i = 1; i < argc; i++) {
        if (argv[i][0] != '-') {
            ShowFiles(argv[i]);
            pathCount++;
        }
    }
    if (pathCount == 0) ShowFiles(".");
    return 0;
}

6. kill 命令的实现

kill 命令的核心是通过 kill() 系统调用向指定进程发送信号,是 Linux 进程管理的基础接口。自定义 mybash 的 kill 命令,核心就是封装这个系统调用,适配原生 kill 命令的使用习惯。

(1) 基础实现:kill PID1 PID2 ...

核心逻辑:接收多个进程 ID(PID),调用 kill() 发送默认终止信号(SIGTERM,编号 15),实现进程终止的核心功能。

易错点提醒

  • 未校验 PID 合法性(如负数、非数字 PID),导致 kill() 调用失败;
  • 忽略 kill() 返回值,进程无权限 / 不存在时未给出错误提示;
  • 未处理参数个数异常(如无参数直接执行 kill)。

(2) 优化:支持指定信号

优化点分析 :基础版仅支持默认信号,优化后解析 -9/-2 等信号参数,适配原生 kill 用法(如 -9 强制杀死进程),核心是解析信号编号并转换为合法的信号值。

(3) 核心代码

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc < 2) { // 参数合法性校验
        printf("argument error\n");
        return -1;
    }

    int sig = SIGTERM; // 默认终止信号
    int start = 1;
    // 解析指定信号(如 -9、-2)
    if (argv[1][0] == '-') {
        sig = abs(atoi(argv[1]+1)); // 提取信号编号并取绝对值
        start = 2;
    }

    // 遍历PID并发送信号
    for (int i = start; i < argc; i++) {
        int pid = atoi(argv[i]);
        if (pid <= 0 || kill(pid, sig) == -1) { // PID校验 + 信号发送
            perror("kill failed");
            return -1;
        }
    }
    return 0;
}

7. cp 命令的实现

cp 是 Linux 核心文件操作命令,核心功能是将源文件 / 目录复制到目标位置。自定义实现的核心是:通过文件 IO 操作完成内容拷贝,结合lstat()判断文件类型,适配不同使用场景。

(1) 基础功能:cp 源文件 目标文件

核心目标 :实现「源文件 → 目标文件」的内容复制(目标文件不存在则创建,存在则覆盖)。实现核心步骤

  1. 参数校验:至少传入 2 个参数,源文件必须是普通文件;
  2. 文件类型判断:通过lstat() + struct stat判断文件类型(S_ISREG校验普通文件);
  3. 内容拷贝:4KB 缓冲区读写,平衡效率与内存占用;
  4. 异常处理:文件不存在(errno==ENOENT)可创建,其他错误需终止。

关键注意点

  • 目标文件不存在时,fopen("w")会自动创建,无需额外处理;
  • 源文件为空时,以 "w" 打开目标文件已自动清空内容,无需额外写入。

核心代码

cpp 复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>

int main(int argc, char *argv[]) {
    // 参数校验:至少传入源文件+目标文件
    if (argc < 3) {
        printf("用法:cp 源文件 目标文件\n");
        return -1;
    }

    // 校验源文件:必须是普通文件
    struct stat st1;
    if (lstat(argv[1], &st1) == -1 || !S_ISREG(st1.st_mode)) {
        perror("源文件必须为普通文件");
        return -1;
    }

    // 目标文件:不存在则创建,存在则覆盖
    FILE *fr = fopen(argv[1], "r");
    FILE *fw = fopen(argv[2], "w");
    if (!fr || !fw) {
        perror("文件打开失败");
        fr && fclose(fr);
        fw && fclose(fw);
        return -1;
    }

    // 4KB缓冲区拷贝内容(核心逻辑)
    char buff[4096] = {0};
    size_t rsize;
    while ((rsize = fread(buff, 1, sizeof(buff), fr)) > 0) {
        fwrite(buff, 1, rsize, fw);
    }

    fclose(fr);
    fclose(fw);
    return 0;
}

(2) 完善 1:支持目标为目录

基础版仅支持 "目标为文件",实际使用中需适配「将文件复制到目录」的场景,核心难点是路径拼接的规范化处理

  • 目标为目录时,需将「目录路径 + 源文件名」拼接为最终目标路径;
  • 处理目录末尾的 /(避免拼接出 //);
  • 源文件路径含目录时,通过 basename() 提取纯文件名(需先拷贝到临时变量,避免修改原参数)。

核心代码

cpp 复制代码
#include <libgen.h>
#include <string.h>
#include <sys/stat.h>

// 目标路径拼接核心逻辑
char dest_path[4096] = {0};
struct stat st2;
if (lstat(argv[2], &st2) == 0 && S_ISDIR(st2.st_mode)) {
    // 处理目录末尾/,拼接源文件名
    char dir[1024] = {0};
    strncpy(dir, argv[2], sizeof(dir)-1);
    if (dir[strlen(dir)-1] == '/') dir[strlen(dir)-1] = '\0';
    
    char src[1024] = {0};
    strncpy(src, argv[1], sizeof(src)-1);
    snprintf(dest_path, sizeof(dest_path), "%s/%s", dir, basename(src));
} else {
    strncpy(dest_path, argv[2], sizeof(dest_path)-1);
}

(3) 完善 2:支持 -r 递归复制目录

-r 是 cp 命令的核心扩展功能,用于递归复制目录(含子文件 / 子目录),核心是递归遍历 + 分类型处理

  • 选项解析:识别 -r 参数,标记递归复制;
  • 目录复制函数 CopyDir():创建目标目录(继承源目录权限)→ 遍历源目录 → 普通文件调用 CopyReg() 复制,子目录递归调用 CopyDir()
  • 兼容普通文件复制:带 -r 时也可复制普通文件,贴合原生 cp 行为。

核心难点

  1. 跳过 ... 避免无限递归;
  2. 目录权限继承(mkdir(des_dir, src_st.st_mode & 0777));
  3. 路径拼接的规范化(同 "目标为目录" 的处理逻辑)。

核心代码

cpp 复制代码
#include <dirent.h>
#include <sys/stat.h>
#include <stdbool.h>

static bool haveR = false;

// 1. 选项解析
void OptionAns(int argc, char *argv[]) {
    haveR = (argc >=4 && strcmp(argv[1], "-r") == 0);
}

// 2. 普通文件复制(核心函数)
void CopyReg(const char *src, const char *dest) {
    FILE *fr = fopen(src, "r"), *fw = fopen(dest, "w");
    if (!fr || !fw) { /* 错误处理 */ }
    char buff[4096] = {0};
    size_t rsize;
    while ((rsize = fread(buff, 1, sizeof(buff), fr)) > 0) {
        fwrite(buff, 1, rsize, fw);
    }
    fclose(fr); fclose(fw);
}

// 3. 递归复制目录(核心函数)
int CopyDir(const char *src_dir, const char *des_dir) {
    // 创建目标目录(继承权限)
    struct stat src_st;
    lstat(src_dir, &src_st);
    mkdir(des_dir, src_st.st_mode & 0777);

    // 遍历源目录
    DIR *dir = opendir(src_dir);
    struct dirent *entry;
    char src_path[4096], des_path[4096];
    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
        
        // 路径拼接
        snprintf(src_path, sizeof(src_path), "%s/%s", src_dir, entry->d_name);
        snprintf(des_path, sizeof(des_path), "%s/%s", des_dir, entry->d_name);

        // 分类型处理:文件复制,目录递归
        struct stat st;
        lstat(src_path, &st);
        if (S_ISREG(st.st_mode)) CopyReg(src_path, des_path);
        else if (S_ISDIR(st.st_mode)) CopyDir(src_path, des_path);
    }
    closedir(dir);
    return 0;
}

// 4. 主逻辑
int main(int argc, char *argv[]) {
    OptionAns(argc, argv);
    if (haveR) {
        struct stat st;
        lstat(argv[2], &st);
        S_ISDIR(st.st_mode) ? CopyDir(argv[2], argv[3]) : CopyReg(argv[2], argv[3]);
    } else {
        // 普通文件复制 + 目标为目录的路径拼接逻辑
    }
    return 0;
}

8. cat 命令的实现

cat(concatenate)的核心作用是读取文件内容并输出到终端,自定义实现围绕「按行读取 + 多场景适配」展开,覆盖原生 cat 的核心使用场景。

(1) 基础功能:查看单个文件内容

核心逻辑 :通过fgets()按行读取文件内容,输出到终端;仅处理普通文件,过滤目录等非普通文件类型。关键优化

  • S_ISREG判断普通文件,避免读取目录等无效类型;
  • 替换printffputs,直接输出到标准输出,更贴近系统原生实现;
  • fgets()返回值作为循环条件,简化逻辑。

核心代码

cpp 复制代码
#include <stdio.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    if (argc < 2) { printf("用法:cat 文件名\n"); return -1; }

    // 仅处理普通文件
    struct stat st;
    if (lstat(argv[1], &st) == -1 || !S_ISREG(st.st_mode)) {
        printf("cat:%s 非普通文件/目录\n", argv[1]);
        return -1;
    }

    FILE *fp = fopen(argv[1], "r");
    char buff[4096];
    while (fgets(buff, sizeof(buff)-1, fp) != NULL) {
        fputs(buff, stdout); // 直接输出到终端
    }
    fclose(fp);
    return 0;
}

(2) 完善 1:打印多个文件

核心思路:通过循环遍历命令行参数,逐个处理文件(打开→读取→输出→关闭),按参数顺序输出内容,无中间存储,贴合原生 cat 行为。

cpp 复制代码
// 主函数核心循环
for (int i = 1; i < argc; i++) {
    cat_files(argv[i]); // 封装单个文件处理逻辑
}

(3) 完善 2:无参数 /'-' 处理

核心需求:无参数时读取终端输入(按 Enter 回显),贴合原生 cat 交互逻辑。

核心代码

cpp 复制代码
void cat_stdin() {
    char buff[1024];
    while (fgets(buff, sizeof(buff), stdin) != NULL) {
        fputs(buff, stdout); // 读取终端输入并回显
    }
}

// 主函数判断
if (argc == 1) cat_stdin(); // 无参数时读取终端输入

(4) 完善 3:支持 -n 行号选项

核心功能 :显示内容时添加行号(空行也保留行号,多文件行号不重置)。核心难点

  • 行号变量需全局 / 传参,保证多文件连续自增;
  • -n但无文件时,终端输入也需显示行号。

核心代码

cpp 复制代码
#include <stdbool.h>

static bool showLineNum = false;

// 选项解析
void ParseOption(char *argv[]) {
    showLineNum = (strcmp(argv[1], "-n") == 0);
}

// 带行号的终端输入处理
void cat_stdin() {
    unsigned long line_num = 1;
    char buff[1024];
    while (fgets(buff, sizeof(buff), stdin) != NULL) {
        if (showLineNum) fprintf(stdout, "%6lu  ", line_num++);
        fputs(buff, stdout);
    }
}

// 带行号的文件处理
void cat_files(char *filename, unsigned long *line_num) {
    // 普通文件判断 + 打开文件
    FILE *fp = fopen(filename, "r");
    char buff[4096];
    while (fgets(buff, sizeof(buff)-1, fp) != NULL) {
        if (showLineNum) fprintf(stdout, "%6lu  ", (*line_num)++);
        fputs(buff, stdout);
    }
    fclose(fp);
}

四、项目总结

1. 项目结构(核心文件说明)

整个项目做了模块化拆分,便于维护和扩展,核心文件如下:

  • mybash.c:主程序入口,包含命令行提示符、输入读取、命令解析/执行循环,以及cd、exit等内置命令实现;
  • pwd.c/ls.c/kill.c/cp.c/cat.c:各外置命令独立实现,通过fork+exec调用,符合Linux Shell命令执行逻辑;
  • Makefile:项目构建脚本,指定-g调试选项、链接必要的系统库(如-lc),一键编译所有模块,简化构建流程。

2.可扩展方向

  • 命令交互优化:支持历史记录回溯(上下箭头)、Tab自动补全
  • 核心功能扩展:实现管道|、重定向>/>><、后台运行&等Shell核心特性
  • 鲁棒性提升:优化输入解析,支持引号、转义字符,完善参数合法性校验
  • 功能补充:增加环境变量操作(export/echo $VAR)、作业管理(jobs/fg/bg)

3.核心收获

通过从零实现mybash,彻底理解了Linux Shell的底层逻辑:

  • 内置命令与外置命令的执行差异(是否创建子进程)
  • 终端属性修改、文件IO、进程信号、目录遍历等系统调用的实际应用
  • 命令解析、参数处理的鲁棒性设计思路(如路径拼接、权限继承、异常处理)
相关推荐
程序员爱德华1 小时前
Linux中的 源 和 Channels
linux·channels·
2501_918126912 小时前
stm32核心板是什么属性?
linux·c语言·stm32·嵌入式硬件·个人开发
500佰2 小时前
Hive常见故障多案例FAQ宝典 --项目总结(宝典一)
大数据·linux·数据仓库·hive·hadoop·云计算·运维开发
滕青山2 小时前
URL编码/解码 核心JS实现
前端·javascript·vue.js
henry1010102 小时前
Ansible自动化运维全攻略(AI生成)
linux·运维·python·ansible·devops
vortex52 小时前
APT软件包管理从入门到精通
linux·运维·服务器·kali
古译汉书2 小时前
RTOS:ISR与互斥量的关系
运维·服务器·stm32·嵌入式硬件
feng68_2 小时前
Keepalived基础实现
运维·服务器·keepalived
暴力求解2 小时前
Linux---基础IO详解
linux·运维·服务器