简易 Shell 实现指南

目录

前言:

一、代码中的核心功能

1. 环境变量获取

2. 当前路径处理

3. 用户输入处理

4. 命令解析

5. 内建命令处理

6. 外部命令执行

7. 错误处理

二、代码中涉及的关键知识点

1. 系统调用

2. 环境变量

3. 字符串处理

4. 文件操作

5. 进程管理

三、代码的运行过程

1. 初始化

2. 用户输入

3. 命令解析

4. 内建命令检测

5. 外部命令执行

6. 循环继续

四、编译与测试

1. 编译

2. 运行

3. 测试

五、源码


前言:

        大家好,今天带大家了解一个简易 Shell 的实现!这个 Shell 是用 C 语言编写的,功能虽然简单,但涵盖了命令行工具的基本要素,如获取用户输入、执行命令、管理环境变量等。接下来,我会详细讲解代码中的各个部分,帮助你理解它是如何工作的。

        我们用下图的时间轴来表示事件的发生次序,其中时间从左向右。shell由标识为bash的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"起建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序并等待这个进程结束。

所以要写一个shell,需要循环以下过程:

  • 获取命令行
  • 解析命令行
  • 建立一个子进程(fork)
  • 替换子进程(execvp)
  • 父进程等待子进程退出(wait)

一、代码中的核心功能

1. 环境变量获取

        Shell 需要知道一些系统信息,比如用户名、主机名、当前路径等,这些信息都存储在环境变量中。代码中的 GetUserNameGetHostName GetCwd 函数分别用于获取这些信息:

// 获取当前用户名
const char *GetUserName()
{
    const char *name = getenv("USER");
    return name ? name : "None";
}

// 获取主机名
const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    return hostname ? hostname : "None";
}

// 获取当前工作路径
const char *GetCwd()
{
    const char *cwd = getenv("PWD");
    return cwd ? cwd : "None";
}
  • getenv 是一个标准库函数,用于获取环境变量的值。

  • 如果 USER 环境变量不存在,返回默认值 "None"

2. 当前路径处理

Shell 会频繁处理路径信息。代码中定义了 SkipPath 宏和 getcwd 函数:

#define SkipPath(p) do{ (p += strlen(p) - 1); while(*p != '/') p--; }while(0); // 逆向查找路径分隔符'\'

// 获取当前工作路径
const char *GetCwd()
{
    const char *cwd = getenv("PWD");
    return cwd ? cwd : "None";
}
  • 作用:将指针移动到路径字符串的最后一个'/'位置,从而提取出当前工作目录的名称。

  • 示例"/home/user/demo" → 指针定位到/demo前的'/'

  • 实现原理

    1. 将指针移动到字符串末尾(strlen(p)-1

    2. 逆向查找直到遇到'/'字符

    3. 使用do-while确保至少执行一次

3. 用户输入处理

        Shell 需要从用户那里获取输入命令。代码中的 MakeCommandLineAndPrint GetUserCommand 函数负责这个过程:

// 构造并打印命令行提示符
void MakeCommandLineAndPrint()
{
    char line[SIZE];
    const char *username = GetUserName();
    const char *hostname = GetHostName();
    const char *cwd = GetCwd();

    SkipPath(cwd);
    snprintf(line, sizeof(line), "[%s@%s %s]~ ", 
            username, hostname, 
            strlen(cwd) == 1 ? "/" : cwd+1);
    printf("%s", line);
    fflush(stdout);
}

// 获取用户输入命令
int GetUserCommand(char command[], size_t n)
{
    char *s = fgets(command, n, stdin);
    if(s == NULL) return -1;
    command[strlen(command)-1] = ZERO;
    return strlen(command);
}
  • MakeCommandLineAndPrint 函数生成一个格式化的提示符,显示用户名、主机名和当前路径。

  • GetUserCommand 函数从标准输入读取用户输入,并将其存储在 command 数组中。

4. 命令解析

        用户输入的命令需要被解析成可执行的格式。代码中的 SplitCommand 函数负责将命令字符串分割成命令和参数:

// 命令分割函数
void SplitCommand(char command[], size_t n)
{
    gArgv[0] = strtok(command, SEP);
    int index = 1;
    while(gArgv[index++] = strtok(NULL, SEP));
}
  • strtok 是一个标准库函数,用于按指定分隔符分割字符串。

  • 分割后的命令和参数存储在全局数组 gArgv 中,其中 gArgv[0] 是命令名称,后面的元素是参数。

5. 内建命令处理

        Shell 通常包含一些内建命令,如 cd、echo $?等。代码中的 CheckBuildin Cd 函数实现了这些功能:

// 获取用户HOME目录
const char *GetHome()
{
    const char *home = getenv("HOME");
    return home ? home : "/";
}

// 切换目录实现
void Cd()
{
    const char *path = gArgv[1];
    if(path == NULL) path = GetHome();
    chdir(path);

    // 更新环境变量
    char temp[SIZE*2];
    getcwd(temp, sizeof(temp));
    snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
    putenv(cwd);
}

// 检查内建命令
int CheckBuildin()
{
    int yes = 0;
    const char *enter_cmd = gArgv[0];
    
    if(strcmp(enter_cmd, "cd") == 0) {
        yes = 1;
        Cd();
    }
    /* 可扩展其他内建命令
    else if(strcmp(enter_cmd, "echo") == 0 && 
            strcmp(gArgv[1], "$?") == 0) {
        yes = 1;
        printf("%d\n", lastcode);
        lastcode = 0;
    }
    */
    return yes;
}
  • CheckBuildin 函数检查用户输入的命令是否是内建命令。

  • Cd 函数实现了 cd 命令,用于切换当前工作目录。

6. 外部命令执行

Shell 还需要支持执行外部命令。代码中的 ExecuteCommand 函数使用了 fork execvp

// 执行外部命令
void ExecuteCommand()
{
    pid_t id = fork();
    if(id < 0) Die();
    
    if(id == 0) { // 子进程
        execvp(gArgv[0], gArgv);
        exit(errno);
    }
    else { // 父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0) {
            lastcode = WEXITSTATUS(status);
            if(lastcode != 0)
                printf("%s:%s:%d\n", 
                      gArgv[0], strerror(lastcode), lastcode);
        }
    }
}
  • fork 创建一个子进程,子进程通过 execvp 执行用户指定的外部命令。

  • 父进程通过 waitpid 等待子进程结束,并检查其退出状态。

  • 执行流程图

7. 错误处理

代码中的 Die 函数用于简单地终止程序:

// 异常退出处理
void Die()
{
    exit(1);
}

二、代码中涉及的关键知识点

1. 系统调用

  • fork:创建子进程。

  • execvp:执行外部命令。

  • waitpid:等待子进程结束。

2. 环境变量

  • getenv:获取环境变量的值。

  • putenv:设置环境变量的值。

3. 字符串处理

  • strtok:按分隔符分割字符串。

  • snprintf:格式化字符串。

4. 文件操作

  • fgets:从标准输入读取字符串。

5. 进程管理

  • exit:终止进程。

三、代码的运行过程

1. 初始化

  • main 函数启动时,程序进入一个无限循环,等待用户输入。

2. 用户输入

  • MakeCommandLineAndPrint 输出一个格式化的提示符。
  • GetUserCommand 从标准输入读取用户输入。

3. 命令解析

  • SplitCommand 将用户输入的字符串分割成命令和参数。

4. 内建命令检测

  • CheckBuildin 检查用户输入的命令是否是内建命令(如 cd)并执行相应的操作。

5. 外部命令执行

  • 如果命令不是内建命令,ExecuteCommand 通过 forkexecvp 执行外部命令。

6. 循环继续

  • 重复以上步骤,直到用户输入 exit 或程序被强制终止。

四、编译与测试

1. 编译

使用以下命令编译代码:

gcc myshell.c -o myshell

2. 运行

运行生成的可执行文件:

./myshell

3. 测试

五、源码

/***********************************************
* 简易Shell实现(C语言版)
* 功能:基础命令执行、路径切换、状态码保持
* 编译:gcc -o myshell myshell.c -Wall
***********************************************/

#include <stdio.h>      // 标准输入输出
#include <unistd.h>     // 系统调用接口(fork, exec等)
#include <stdlib.h>     // 内存管理、环境变量
#include <errno.h>      // 错误码处理
#include <string.h>     // 字符串操作
#include <sys/types.h>  // 进程类型定义
#include <sys/wait.h>   // 进程等待相关

/*----------------------------------------------
* 宏定义区(程序关键参数配置)
----------------------------------------------*/
#define SIZE 512        // 输入缓冲区大小
#define ZERO '\0'       // 字符串终止符
#define SEP " "         // 命令分割符(空格)
#define NUM 32          // 最大参数个数
// 路径处理宏:逆向查找路径中的最后一个'/'
#define SkipPath(p) do{ (p += strlen(p) - 1); \
    while(*p != '/') p--; }while(0); 

/*----------------------------------------------
* 全局变量声明区
----------------------------------------------*/
char cwd[SIZE*3];       // 当前路径环境变量缓冲区
char *gArgv[NUM];       // 命令参数数组(用于execvp)
int lastcode = 0;       // 记录上条命令的退出状态码

/*----------------------------------------------
* 函数声明区(按调用顺序排列)
----------------------------------------------*/
void Die(); // 异常终止函数
const char *GetHome(); // 获取用户主目录
const char *GetUserName(); // 获取当前用户名
const char *GetHostName(); // 获取主机名
const char *GetCwd(); // 获取当前工作目录
void MakeCommandLineAndPrint(); // 构造提示符
int GetUserCommand(char cmd[], size_t n); // 获取输入
void SplitCommand(char cmd[], size_t n); // 分割命令
void ExecuteCommand(); // 执行外部命令
void Cd(); // 切换目录实现
int CheckBuildin(); // 内建命令检查

/*----------------------------------------------
* [函数实现] 系统信息获取模块
----------------------------------------------*/

// 异常退出处理(简化版)
void Die() {
    exit(1); // 直接退出并返回状态码1
}

// 获取用户主目录路径
const char *GetHome() {
    // 通过HOME环境变量获取
    const char *home = getenv("HOME");
    return home ? home : "/"; // 保底返回根目录
}

// 获取当前用户名(来自环境变量)
const char *GetUserName() {
    const char *name = getenv("USER");
    return name ? name : "None"; // 默认值处理
}

// 获取主机名(环境变量方式)
const char *GetHostName() {
    const char *hostname = getenv("HOSTNAME");
    return hostname ? hostname : "None";
}

// 获取当前工作目录(环境变量缓存值)
const char *GetCwd() {
    const char *cwd = getenv("PWD");
    return cwd ? cwd : "None"; 
}

/*----------------------------------------------
* [功能模块] 命令行界面处理
----------------------------------------------*/

// 构造并显示提示符
void MakeCommandLineAndPrint() {
    char line[SIZE]; // 行缓冲区
    
    // 获取系统信息三元组
    const char *username = GetUserName();
    const char *hostname = GetHostName();
    const char *cwd = GetCwd();

    // 路径处理:定位到最后一个'/'后的目录名
    // 示例:/home/user → user
    SkipPath(cwd); 

    // 构造提示符格式:[user@host dir]~ 
    snprintf(line, sizeof(line), "[%s@%s %s]~ ", 
            username, 
            hostname, 
            // 处理根目录特殊情况
            (strlen(cwd) == 1) ? "/" : cwd+1); 
    
    printf("%s", line);    // 输出提示符
    fflush(stdout);       // 强制刷新确保立即显示
}

// 获取用户输入命令
int GetUserCommand(char command[], size_t n) {
    // 使用fgets获取整行输入(包含换行符)
    char *s = fgets(command, n, stdin);
    if(s == NULL) return -1; // 读取失败处理
    
    // 替换换行符为字符串终止符
    // 示例:"ls -l\n" → "ls -l\0"
    command[strlen(command)-1] = ZERO; 
    
    return strlen(command); // 返回有效长度
}

// 命令分割:将字符串命令解析为参数数组
void SplitCommand(char command[], size_t n) {
    // 使用strtok进行分割(破坏性操作)
    gArgv[0] = strtok(command, SEP); // 首次调用
    
    int index = 1;
    // 循环分割直到返回NULL(自动添加NULL结尾)
    // 示例:"ls -l" → ["ls", "-l", NULL]
    while((gArgv[index++] = strtok(NULL, SEP))); 
}

/*----------------------------------------------
* [核心功能] 命令执行模块
----------------------------------------------*/

// 执行外部命令(非内建命令)
void ExecuteCommand() {
    pid_t id = fork(); // 创建子进程
    if(id < 0) Die();  // fork失败则终止
    
    if(id == 0) { // 子进程执行流
        // 使用execvp执行命令(自动搜索PATH)
        // 参数格式要求:数组以NULL结尾
        execvp(gArgv[0], gArgv);
        
        // 只有exec失败才会执行到这里
        exit(errno); // 返回错误码
    } 
    else { // 父进程执行流
        int status = 0;
        // 等待子进程结束(阻塞方式)
        pid_t rid = waitpid(id, &status, 0);
        
        if(rid > 0) {
            // 解析子进程退出状态
            lastcode = WEXITSTATUS(status);
            
            // 非零状态码表示异常退出
            if(lastcode != 0) {
                printf("%s:%s:%d\n", 
                      gArgv[0],         // 程序名
                      strerror(lastcode),// 错误描述
                      lastcode);        // 错误码
            }
        }
    }
}

// 实现cd命令(内建命令)
void Cd() {
    // 获取目标路径(支持无参数)
    const char *path = gArgv[1];
    if(path == NULL) path = GetHome(); // 默认主目录
    
    // 系统调用切换目录
    chdir(path);
    
    // 更新环境变量(使PWD与真实路径同步)
    char temp[SIZE*2];
    getcwd(temp, sizeof(temp)); // 获取实际路径
    // 构造环境变量字符串(格式:PWD=...)
    snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
    putenv(cwd); // 修改环境变量
}

// 检查并执行内建命令
int CheckBuildin() {
    int yes = 0;
    const char *enter_cmd = gArgv[0];
    
    // cd命令处理
    if(strcmp(enter_cmd, "cd") == 0) {
        yes = 1;    // 标记为已处理
        Cd();       // 调用cd实现
    }
    /* 可扩展区域:其他内建命令示例
    else if(strcmp(enter_cmd, "echo") == 0 && 
            strcmp(gArgv[1], "$?") == 0) {
        yes = 1;
        printf("%d\n", lastcode); // 输出上条命令状态码
        lastcode = 0;            // 重置状态码
    }
    */
    return yes; // 返回是否处理了内建命令
}

/*----------------------------------------------
* 主程序入口
----------------------------------------------*/
int main() {
    int quit = 0; // 退出标志(未实现退出命令)
    
    // REPL循环(Read-Eval-Print Loop)
    while(!quit) {
        // 步骤1:显示提示符
        MakeCommandLineAndPrint();
        
        // 步骤2:获取用户输入
        char usercommand[SIZE]; // 输入缓冲区
        int n = GetUserCommand(usercommand, sizeof(usercommand));
        if(n <= 0) return 1; // 输入错误处理
        
        // 步骤3:分割命令参数
        SplitCommand(usercommand, sizeof(usercommand));
        
        // 步骤4:处理内建命令
        if(CheckBuildin()) continue;
        
        // 步骤5:执行外部命令
        ExecuteCommand();
    }
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/969937.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

快速排序

目录 什么是快速排序&#xff1a; 图解&#xff1a; 递归法&#xff1a; 方法一&#xff08;Hoare法&#xff09;&#xff1a; 代码实现&#xff1a; 思路分析&#xff1a; 方法二&#xff08;挖坑法&#xff09;&#xff1a; 代码实现&#xff1a; 思路分析&#xff1a; 非递…

网络安全尹毅 《网络安全》

一 网络安全基本概念 1.网络安全定义 安全在字典中的定义是为了防范间谍活动或蓄意破坏、犯罪、攻击而采取的措施。网络安全就是为了防范计算机网络硬件、软件、数据被偶然或蓄意破坏、篡改、窃听、假冒、泄露、非法访问以及保护网络系统持续有效工作的措施总和。网络安全保护…

6.appender

文章目录 一、前言二、源码解析AppenderUnsynchronizedAppenderBaseOutputStreamAppenderConsoleAppenderFileAppenderRollingFileAppenderFileNamePattern 三、总结 一、前言 前一篇文章介绍了appender、conversionRule、root和logger节点的解析, 为的是为本篇详细介绍它们的…

P9584 「MXOI Round 1」城市

题目描述 小 C 是 F 国的总统&#xff0c;尽管这个国家仅存在于网络游戏中&#xff0c;但他确实是这个国家的总统。 F 国由 n 个城市构成&#xff0c;这 n 个城市之间由 n−1 条双向道路互相连接。保证从任意一个城市出发&#xff0c;都能通过这 n−1 条双向道路&#xff0c;…

什么是Docker多架构容器镜像

什么是Docker多架构容器镜像 在 Docker 中&#xff0c;同一个 Docker 镜像可以在不同的平台上运行&#xff0c;例如在 x86、ARM、PowerPC 等不同的 CPU 架构上。 为了支持这种多平台的镜像构建和管理&#xff0c;Docker 在 17.06 版本时引入了 Manifest 的概念&#xff0c;在…

Baklib知识中台构建企业智能运营核心架构

内容概要 在数字化转型的浪潮中&#xff0c;企业对于知识的系统化管理需求日益迫切。Baklib作为新一代的知识中台&#xff0c;通过构建智能运营核心架构&#xff0c;为企业提供了一套从知识汇聚到场景化落地的完整解决方案。其核心价值在于将分散的知识资源整合为统一的资产池…

深度学习机器学习:常用激活函数(activation function)详解

目录 Sigmoid Function ReLU&#xff08;Rectified Linear Unit&#xff09; LeakyReLU&#xff08;Leaky Rectified Linear Unit&#xff09; ClippedReLU&#xff08;Clipped Rectified Linear Unit&#xff09; PRelu&#xff08;Parametric ReLU&#xff09; Tanh&am…

【面试】网络安全常问150道面试题

1&#xff0c;拿到一个待测网站&#xff0c;你觉得应该先做什么&#xff1f; 信息收集&#xff1a; 服务器相关---&#xff1a;## 系统版本&#xff0c;真实IP&#xff0c;开放端口&#xff0c;使用的中间件 指纹信息---## 有无cdn加速&#xff0c;dns解析记录&#xff0c;是不…

【Linux】--- 基础开发工具之yum/apt、vim、gcc/g++的使用

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; Linux网络编程 本篇博客我们来认识一下Linux中的一些基础开发工具 --- yum,vim,gcc/g。 &#x1f3e0; yum &#x1f3b8; 什么是yum 当用户想下载软…

物联网平台-分布式的设备接入与管理系统

乐吾乐物联网平台是由乐吾乐自主研发的一款分布式的设备接入与管理系统&#xff0c;专为满足不断增长的设备接入和数据处理需求而设计。平台集数据采集、分析、监控、告警和通知等功能于一体&#xff0c;并融合了乐吾乐大屏可视化和乐吾乐3D数字孪生技术&#xff0c;帮助用户快…

Day65_20250213图论part9_dijkstra(堆优化版)|Bellman_ford算法精讲

Day65_20250213图论part9_dijkstra(堆优化版)|Bellman_ford算法精讲 dijkstra(堆优化版) 题目 https://www.programmercarl.com/kamacoder/0047.%E5%8F%82%E4%BC%9Adijkstra%E5%A0%86.html 小明参加科学大会 思路 思路 朴素版的dijkstra&#xff0c;时间复杂度为O(n^2)&am…

动手实现自己的 JVM——Go!(ch01)

动手实现自己的 JVM——Go&#xff01;&#xff08;ch01&#xff09; 参考张秀宏老师的《自己动手写java虚拟机》 为什么需要命令行 在 JMV 中&#xff0c;要运行一个 Java 文件&#xff08;字节码&#xff09;&#xff0c;首先需要找到这个文件。那么&#xff0c;如何找到文件…

IIS部署netcore程序后,出现500.30错误解决方案之一

netcore程序部署到IIS后一直出现错误&#xff0c;访问首页后会跳转到登录页地址&#xff0c;然后看到如下错误 HTTP Error 500.30 - ANCM In-Process Start Failure Common solutions to this issue: The application failed to start The application started but then stopp…

将Docker容器打包成镜像提交

前言 Docker 是一个开源软件&#xff0c;也是一个开放平台&#xff0c;用于开发应用、交付&#xff08;shipping&#xff09;应用、运行应用。 Docker允许用户将基础设施&#xff08;Infrastructure&#xff09;中的应用单独分割出来&#xff0c;形成更小的颗粒&#xff08;容…

【设计模式】【行为型模式】命令模式(Command)

&#x1f44b;hi&#xff0c;我不是一名外包公司的员工&#xff0c;也不会偷吃茶水间的零食&#xff0c;我的梦想是能写高端CRUD &#x1f525; 2025本人正在沉淀中… 博客更新速度 &#x1f44d; 欢迎点赞、收藏、关注&#xff0c;跟上我的更新节奏 &#x1f3b5; 当你的天空突…

【DDD系列-3】DDD战术设计实践分享

DDD 战术设计概念​ ​ ​​ TMF2 中的概念&#xff1a;​ 领域能力&#xff1a;​ 扩展点&#xff1a;​ DDD 战术设计使用场景​ 复杂业务场景​ 复杂来源面对的需求方更加多样化。​ 1 相同场景不同垂直业务方的需求&#xff08;1688&#xff0c;淘宝&#xff0c;天…

基于单片机的仓库安防系统(论文+源码)

2.1 需求分析 仓库由于存有大量物品&#xff0c;因此对仓库的监控非常重要&#xff0c;目前仓库已经普遍装有安防系统&#xff0c;以保证仓库的安全&#xff0c;本次基于单片机的仓库安防系统设计&#xff0c;在功能上设计如下&#xff1a; 用户可通过IC卡进入仓库&#xff1…

使用 AutoMQ 和 Tinybird 分析用户网购行为

前言 在当前竞争激烈的市场环境中&#xff0c;数据分析已成为企业实现差异化和精准营销的关键。通过分析用户行为数据&#xff0c;企业能够深入了解用户的习惯、偏好和行为模式&#xff0c;从而更精准地定位目标市场&#xff0c;制定个性化营销策略&#xff0c;并提供定制化推…

小米 R3G 路由器刷机教程(Pandavan)

小米 R3G 路由器刷机教程&#xff08;Pandavan&#xff09; 一、前言 小米 R3G 路由器以其高性价比和稳定的性能备受用户青睐。然而&#xff0c;原厂固件的功能相对有限&#xff0c;难以满足高级用户的个性化需求。刷机不仅可以解锁路由器的潜能&#xff0c;还能通过第三方固…

Python数据可视化 - Matplotlib教程

文章目录 前言一、Matplotlib简介及安装1. Matplotlib简介2. 安装Matplotlib 二、Matplotlib Pyplot1. Pyplot介绍2. Pyplot中方法介绍2.1 创建和管理图形2.2 绘制图形2.3 设置图形属性2.4 保存和展示 三、Matplotlib绘图标记1. 介绍2. 基本用法3. 标记大小与颜色4. 标记样式列…