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

news/2024/9/19 0:39:14/ 标签: linux, 运维, 服务器

在这篇博客中,我们将深入探讨如何自行编写一个简单的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()仅在失败时返回,此处的错误处理对于诊断问题很有帮助。

http://www.ppmy.cn/news/1519834.html

相关文章

Python 全栈系列265 使用ORM、Kafka、Apscheduler实现任务的并发处理

说明 这次的尝试&#xff0c;从框架来说是比较成功的。但是不太走运的是&#xff0c;有一个小的磁盘回收没有写&#xff0c;结果在我外出旅游的时候磁盘打满&#xff0c;导致任务没有按预期执行完&#xff0c;这点比较遗憾。 这里快速把实现的框架梳理一下&#xff0c;后续可…

差旅游记|绵阳:生活的意义在于体验

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 几年前在微博上有个段子广为流传&#xff0c;说是梁朝伟哪天烦闷了&#xff0c;就去机场&#xff0c;赶上哪班就搭哪班&#xff0c;比如去伦敦广场晒太阳&#xff0c;发呆&#xff0c;喂鸽子&#xff0c;完了再搭最近…

Azure Data Factory 多选选项集不受支持

在用ADF往外部推数据时&#xff0c;会碰到CRM的一种数据类型&#xff0c;多选下拉框&#xff0c;如下图中的 如果我们把多选字段输入源字段中&#xff0c;会得到如下的提示 查询官方文档&#xff0c;则有如下的说法 所以把值往外推就需要变通下&#xff0c;例如使用一个文本字段…

爬虫:爬取MDPI杂志中国作者单位和邮箱

Python爬虫&#xff0c;简单来说&#xff0c;就是使用Python编程语言编写的一种自动化获取网页内容的程序。它们能够模拟人类浏览网页的行为&#xff0c;如访问网页、解析网页内容、甚至填写表单和点击链接等&#xff0c;从而帮助我们从互联网上大量收集和处理数据。Python爬虫…

dart 字符串截取

截取 String str "500001"; String lastThreeDigits str.substring(str.length - 3);在这个例子中&#xff0c;str.length - 3计算的是开始截取的索引位置&#xff0c;它从字符串的倒数第三个字符开始截取&#xff0c;一直到字符串的末尾。因此&#xff0c;lastTh…

Nginx学习(第二天)

一.Nginx高级配置 1.1 Nginx状态页 基于nginx 模块 ngx_http_stub_status_module 实现&#xff0c; 在编译安装nginx的时候需要添加编译参数 --with-http_stub_status_module 否则配置完成之后监测会是提示法错误 注意: 状态页显示的是整个服务器的状态,而非虚拟主机的状态 …

运维问题0001:MM模块-MIGO收货报错“消息号 M7036 对于采购订单********无收货可能”

1、问题解析&#xff1a; 该报错为SAP标准报错类型,针对公司不同配置/业务设计/校验逻辑&#xff0c;导致该问题原因比较多。 常见的问题总结如下&#xff1a; 1&#xff09;输入的PO信息有问题&#xff08;例如&#xff1a;PO输入错误/PO删除状态/PO冻结状态/PO已完成收货等…

【Next】3. 开发规范

笔记来源&#xff1a;编程导航 1、约定式路由 Next.js 使用 约定式路由&#xff0c;根据文件夹的结构和名称&#xff0c;自动将对应的 URL 地址映射到页面文件。 常见的几种路由规则如下&#xff1a; 1&#xff09;基础规则&#xff1a;以 app 目录作为根路径&#xff0c;根…

企微获客链接 中文乱码问题处理

企微获客链接 中文乱码问题处理 问题背景问题处理补充内容 问题背景 为了推广产品&#xff0c;同时更好的服务客户&#xff0c;公司在接入企业微信后&#xff0c;需要用到企微获客链接相关推广操作&#xff0c;那么通过API 接口创建企微获客链接时&#xff0c;出现了中文乱码问…

MYSQL————联合查询

联合多个表进行查询 设计数据时把表进行拆分&#xff0c;为了消除表中字段的依赖关系&#xff0c;比如部分函数依赖&#xff0c;传递依赖&#xff0c;这时会导致一条SQL查出来的数据&#xff0c;对于业务来说是不完整的&#xff0c;我们就可以使用联合查询把关系中的数据全部查…

Java 入门指南:Java 并发编程 —— Condition 灵活管理线程间的同步

Condition Condition 是 Java 并发编程中的一种高级同步工具&#xff0c;它可以协助线程之间进行等待和通信。提供了一种比传统的 wait() 和 notify() 更加灵活的方式来管理线程间的同步。Condition 接口通常与 Lock 接口一起使用&#xff0c;允许更细粒度的控制线程的等待和唤…

DNS服务器的配置(服务名named,端口53)

目录 前言 配置文件 DNS服务器的配置 主配置文件 扩展配置文件 区域配置文件 重启服务 配置防火墙 配置客户端dns 前言 DNS服务器的主要作用是将人类可读的域名转换为机器可读的IP地址&#xff0c;从而方便用户访问互联网资源。 在互联网中&#xff0c;设备需要通过I…

前端性能优化:使用Vue3+TS+Canvas对图片进行压缩后再上传,优化带宽,减小服务器存储成本,减少流量损耗

在上传图片之前&#xff0c;对图片进行压缩。看到这里是不是有点懵&#xff0c;前端怎么压缩图片呢&#xff0c;这不应该是后端做的吗&#xff1f; 但是我在开发的时候接到了这样一个需求&#xff0c;要求对用户上传的图片进行一定的压缩&#xff0c;而且并且尽量还原图片的清…

vue项目中scss文件导出,js文件引入scss文件时为空{}

解决办法一&#xff1a; 将scss文件重命名为 ‘原名.module.scss’ 解决办法二&#xff1a;降低vue脚手架的版本 "vue/cli-plugin-babel": "~4.5.0", "vue/cli-plugin-eslint": "~4.5.0", "vue/cli-service": "~4.5.0…

二十五、go语言的通道

目录 一、收发通信 二、将通道作为参数传递&#xff08;读、写、读写&#xff09; 三、select 1、先收到消息的先执行 2、一直没有收到消息退出通道 3、不知道何时退出情况下退出通道 go语言中的goroutine可以看成线程&#xff0c;但是又不能看成和其它语言一样的线程&am…

【Kubernetes知识点问答题】Namespace(命名空间)

目录 1. 什么是 K8s 的 namespace&#xff1f; 2. 系统默认创建了哪几个 namespace&#xff1f; 1. 什么是 K8s 的 namespace&#xff1f; 在 K8s 中&#xff0c; Namespace&#xff08;命名空间&#xff09;提供了一种机制&#xff0c;将同一集群中的资源划分为相互隔离的组…

c# net8调用vc写的dll

dll程序&#xff08;vc,x86) 头文件 extern "C" int __declspec(dllexport) WINAPI add(int a, int b);实现 int WINAPI add(int a, int b) {return a b; }c#/net8 函数声明&#xff1a; [DllImport("dll/Dll1.dll", CallingConvention CallingCo…

Git 提交代码注释信息规范

在团队协作开发过程中&#xff0c;规范的 Git 提交信息不仅能提高代码维护的效率&#xff0c;还能让其他开发者更容易理解每次提交的目的和内容。下面是常用的 Git 提交信息类型及其详细说明。此外&#xff0c;还包括一些额外的提交类型&#xff0c;以便更全面地覆盖开发过程中…

线程池相关知识点

线程池是什么相信大家都是知道的&#xff0c;所以这里就不做解释了&#xff0c;直接看相关知识点吧。 初始化线程池方法 继承ThreadPool 实现Runnable 实现Callable 接口 FutureTask &#xff08;可以拿到返回结果&#xff0c;可以处理异常&#xff09; 核心参数 corePoo…

将string类中能够实现的操作都封装在MyString类中

包括&#xff1a; 构造函数 析构函数 重载 &#xff0c;[]&#xff0c;,,,!,<,>,<,>,<<,>>; at&#xff1b; data&#xff1b; c_str; empty; length; capasityacity; clear; push_back; pop_back; append; 程序中我封装了大部分&#…