自定义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

相关文章

shell脚本快速检查192.168.1网段ip是否在用的方法

《shell脚本快速检查192.168.1网段ip是否在用的方法》该Shell脚本通过并发ping命令检查192.168.1网段中哪些IP地址正在使用,脚本定义了网络段、超时时间和并行扫描数量,并使用... 目录脚本:检查 192.168.1 网段 IP 是否在用脚本说明使用方法示例输出优化建议总结检查 1

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

SpringBoot 自定义消息转换器使用详解

《SpringBoot自定义消息转换器使用详解》本文详细介绍了SpringBoot消息转换器的知识,并通过案例操作演示了如何进行自定义消息转换器的定制开发和使用,感兴趣的朋友一起看看吧... 目录一、前言二、SpringBoot 内容协商介绍2.1 什么是内容协商2.2 内容协商机制深入理解2.2.1 内容

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

EMLOG程序单页友链和标签增加美化

单页友联效果图: 标签页面效果图: 源码介绍 EMLOG单页友情链接和TAG标签,友链单页文件代码main{width: 58%;是设置宽度 自己把设置成与您的网站宽度一样,如果自适应就填写100%,TAG文件不用修改 安装方法:把Links.php和tag.php上传到网站根目录即可,访问 域名/Links.php、域名/tag.php 所有模板适用,代码就不粘贴出来,已经打