自定义Shell程序(内附源码)

2024-09-03 00:44

本文主要是介绍自定义Shell程序(内附源码),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这篇博客中,我们将深入探讨如何自行编写一个简单的Shell程序,我们的示例程序是一个用C语言编写的名为myshell的小型命令行界面。这个项目不仅是对操作系统如何通过命令行与用户互动的一个实用介绍,同时也展示了环境变量、进程创建和命令解析等底层操作的基础应用。首先,话不多说,先上源码,内附超全注释!

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <errno.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)extern int putenv(char *string);  // 声明外部putenv函数char cwd[SIZE*2];  // 当前工作目录的缓冲区// 获取环境变量USER的值,即当前用户名
const char* getusername() {const char* name = getenv("USER");if(name == NULL) return "NONE";return name;
}// 获取环境变量HOSTNAME的值,即当前主机名
const char* Gethostname() {const char* hostname = getenv("HOSTNAME");if(hostname == NULL) return "NONE";return hostname;
}// 获取环境变量PWD的值,即当前工作目录
const char* getpwdname() {const char* name = getenv("PWD");if(name == NULL){return "NONE";}return name;
}// 构建并打印命令行提示符
void makecommandlineandPrint(char line[], size_t size) {const char* username = getusername();const char* hostname = Gethostname();const char* cwdname = getpwdname();SkipPath(cwdname);  // 调用宏处理cwdnamesnprintf(line, size, "[%s@%s %s]* ", username, hostname, cwdname);printf("%s", line);fflush(stdout);sleep(5);
}// 从标准输入读取用户输入的命令
int GetUserCommand(char line[], int n) {char* s = fgets(line, n, stdin);if(s == NULL) return -1;line[strlen(line)-1] = ZERO;return strlen(line);
}char* gArav[NUM];  // 全局数组,存储命令及其参数// 将用户输入的命令行分割为命令和参数
void SplitCommand(char command[], size_t n) {gArav[0] = strtok(command, SEP);int index = 1;while((gArav[index++] = strtok(NULL, SEP)));
}// 退出程序的函数,使用errno值作为退出状态
void Die() {exit(errno);
}// 获取HOME环境变量的值,通常是用户的主目录
const char* Home() {const char* home = getenv("HOME");return home;
}// 执行用户输入的命令
void ExcuteCommand() {pid_t id = fork();  // 创建新进程if(id < 0) Die();  // 如果fork失败,调用Die函数退出else if(id == 0) {execvp(gArav[0], gArav);  // 在子进程中执行命令exit(errno);  // 如果execvp返回,使用errno作为退出状态} else {int status = 0;waitpid(id, &status, 0);  // 父进程等待子进程结束}
}// 处理cd命令,更改当前工作目录
void Cd() {const char* path = gArav[1];if(path == NULL){path = Home();  // 如果未指定路径,使用HOME目录}chdir(path);  // 更改目录// 更新PWD环境变量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 = gArav[0];if(strcmp("cd", enter) == 0){yes = 1;Cd();}return yes;  // 返回是否执行了内建命令
}// 主函数,循环读取和执行命令
int main() {while (1) {char commandline[SIZE];makecommandlineandPrint(commandline, sizeof(commandline));  // 打印提示符char usercommand[SIZE];int n = GetUserCommand(usercommand, sizeof(usercommand));  // 读取命令if (n <= 0) continue;  // 如果读取失败,继续下一轮循环if (strcmp(usercommand, "exit") == 0) {  // 检查退出命令break;}SplitCommand(usercommand, sizeof(usercommand));  // 解析命令if (!CheckBUildin()) {  // 检查并执行内建命令ExcuteCommand();  // 执行外部命令}}return 0;  // 正常退出
}

核心功能

在核心功能部分,myshell实现了一系列功能,旨在模拟Shell环境的关键行为。以下是详细说明,展开已有的功能描述,并添加一些更细节的说明:

1. 环境设置与命令提示

myshell首先通过环境变量获取用户名、主机名和当前工作目录。这些信息是用户交互的核心部分,因为它们提供了当前会话的上下文。

  • 获取用户名和主机名:通过调用getenv()函数,程序能够检索系统环境变量USERHOSTNAME的值,这些信息随后被用来构建命令行提示符。
  • 当前工作目录:同样使用getenv()检索PWD,如果该环境变量不存在,则尝试使用系统调用getcwd()直接从操作系统获取当前工作目录。
  • 命令提示符的构建与显示:使用snprintf()根据获取的信息格式化字符串,并通过printf()打印到控制台。此外,还包括一个简单的动态效果——通过sleep(5)函数延迟提示符的更新,这虽然在实际应用中不常见,但为示例程序增添了互动性。

2. 命令读取与解析

myshell接收用户输入的命令行字符串,并将其拆分为可执行命令和相应的参数,这对于执行任何Shell命令是必需的。

  • 命令读取fgets()从标准输入读取一行文本,包括命令和参数。为确保字符串正确处理,将字符串末尾的换行符替换为字符串终结符\0
  • 命令解析:使用strtok()函数,它利用空格作为分隔符来分解命令字符串。这一解析过程填充全局数组gArav,其中每个元素都是命令行中的一个词,例如命令本身和跟随的参数。

3. 命令执行

解析后的命令通过不同的函数进行执行,根据命令的类型(内建命令或外部命令)采取不同的处理方式。

  • 内建命令的处理:例如cd(更改目录),是直接在myshell进程中执行的。这类命令不会创建新的进程,而是直接影响myshell的状态或者环境。
  • 外部命令的执行:使用fork()创建新的进程,然后在子进程中通过execvp()执行命令。父进程等待子进程结束,确保命令序列化执行。

4. 环境管理

myshell能够管理和修改环境变量,这对于很多命令来说是必须的。

  • 环境变量更新:在执行如cd这样的内建命令后,必须更新PWD环境变量以反映新的目录位置。这通过putenv()或者更安全的setenv()实现,后者在处理时可以避免一些与内存管理相关的问题。

这些核心功能共同构成了myshell的基础框架,使其不仅能够执行基本的命令行交互,还能够处理更

技术详解

1. 环境变量的获取与处理

环境变量在Unix和类Unix系统中是传递配置信息给运行的程序的一种方式。在myshell中,环境变量的处理是通过标准C库函数实现的:

通过以上详细的技术解析,我们不仅了解了myshell如何实现其功能,还看到了如何在C语言中处理字符串、环境变量、进程及错误,这些都是Unix系统编程的基础。这样的练习项目不仅帮助学习者深入理解操作系统的工作原理,还提供了实际操作系统调用的实践机会。

  • 获取环境变量getenv()函数用于获取特定的环境变量值,如USER, HOSTNAME, 和PWD。这些值用于配置myshell的行为,比如生成用户提示符。
    const char* getusername() {const char* name = getenv("USER");if(name == NULL) return "NONE";return name;
    }
    

    上述函数尝试获取用户名,如果环境变量USER不存在,则返回"NONE"

  • 更新环境变量:当用户使用cd命令更改目录时,必须更新PWD环境变量以反映当前的工作目录。这通常是通过putenv()setenv()完成的。putenv()接受一个形式为"NAME=value"的字符串,直接修改环境;而setenv()则提供了分离的名称和值参数,更为安全和直观。
    void Cd() {char temp[SIZE*2];getcwd(temp, sizeof(temp));  // 获取当前目录setenv("PWD", temp, 1);  // 更新环境变量,允许覆盖
    }
    

    2. 命令解析与执行

    命令的解析和执行是myshell的核心功能之一,它涉及到字符串处理和进程控制的多个层面:

  • 命令解析:用户输入的字符串首先被fgets()读取,然后使用strtok()根据空格进行分词,将命令和其参数分开
    void SplitCommand(char command[], size_t n) {gArav[0] = strtok(command, SEP);  // 第一部分是命令int index = 1;while((gArav[index++] = strtok(NULL, SEP)));  // 后续是参数
    }
    

    这种方式简单但有效,能够处理基本的命令行输入。

  • 命令执行myshell使用fork()execvp()来执行外部命令。fork()创建一个新进程,execvp()在子进程中执行一个新程序。
    void ExcuteCommand() {pid_t id = fork();  // 创建新进程if(id < 0) {perror("fork failed");exit(EXIT_FAILURE);} else if(id == 0) {execvp(gArav[0], gArav);  // 在子进程中执行命令perror("execvp failed");exit(EXIT_FAILURE);} else {waitpid(id, NULL, 0);  // 父进程等待子进程结束}
    }
    

    这段代码体现了Unix编程中的典型模式:fork-exec-wait,是处理外部命令的标准方法。

    3. 错误处理

    鲁棒的错误处理对于任何涉及系统级调用的程序都至关重要。myshell在多个地方实现了基本的错误处理:

  • 进程创建失败:如果fork()失败,程序会通过perror()输出错误信息并退出。
  • 命令执行失败:如果execvp()失败,同样使用perror()输出错误信息。由于execvp()仅在失败时返回,此处的错误处理对于诊断问题很有帮助。

这篇关于自定义Shell程序(内附源码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1131537

相关文章

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

如何自定义Nginx JSON日志格式配置

《如何自定义NginxJSON日志格式配置》Nginx作为最流行的Web服务器之一,其灵活的日志配置能力允许我们根据需求定制日志格式,本文将详细介绍如何配置Nginx以JSON格式记录访问日志,这种... 目录前言为什么选择jsON格式日志?配置步骤详解1. 安装Nginx服务2. 自定义JSON日志格式各

Android自定义Scrollbar的两种实现方式

《Android自定义Scrollbar的两种实现方式》本文介绍两种实现自定义滚动条的方法,分别通过ItemDecoration方案和独立View方案实现滚动条定制化,文章通过代码示例讲解的非常详细,... 目录方案一:ItemDecoration实现(推荐用于RecyclerView)实现原理完整代码实现

基于Spring实现自定义错误信息返回详解

《基于Spring实现自定义错误信息返回详解》这篇文章主要为大家详细介绍了如何基于Spring实现自定义错误信息返回效果,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录背景目标实现产出背景Spring 提供了 @RestConChina编程trollerAdvice 用来实现 HTT

SpringSecurity 认证、注销、权限控制功能(注销、记住密码、自定义登入页)

《SpringSecurity认证、注销、权限控制功能(注销、记住密码、自定义登入页)》SpringSecurity是一个强大的Java框架,用于保护应用程序的安全性,它提供了一套全面的安全解决方案... 目录简介认识Spring Security“认证”(Authentication)“授权” (Auth

Spring 中 BeanFactoryPostProcessor 的作用和示例源码分析

《Spring中BeanFactoryPostProcessor的作用和示例源码分析》Spring的BeanFactoryPostProcessor是容器初始化的扩展接口,允许在Bean实例化前... 目录一、概览1. 核心定位2. 核心功能详解3. 关键特性二、Spring 内置的 BeanFactory

如何用java对接微信小程序下单后的发货接口

《如何用java对接微信小程序下单后的发货接口》:本文主要介绍在微信小程序后台实现发货通知的步骤,包括获取Access_token、使用RestTemplate调用发货接口、处理AccessTok... 目录配置参数 调用代码获取Access_token调用发货的接口类注意点总结配置参数 首先需要获取Ac

SpringBoot自定义注解如何解决公共字段填充问题

《SpringBoot自定义注解如何解决公共字段填充问题》本文介绍了在系统开发中,如何使用AOP切面编程实现公共字段自动填充的功能,从而简化代码,通过自定义注解和切面类,可以统一处理创建时间和修改时间... 目录1.1 问题分析1.2 实现思路1.3 代码开发1.3.1 步骤一1.3.2 步骤二1.3.3

基于Python开发PDF转Doc格式小程序

《基于Python开发PDF转Doc格式小程序》这篇文章主要为大家详细介绍了如何基于Python开发PDF转Doc格式小程序,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用python实现PDF转Doc格式小程序以下是一个使用Python实现PDF转DOC格式的GUI程序,采用T