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

相关文章

CentOS和Ubuntu系统使用shell脚本创建用户和设置密码

《CentOS和Ubuntu系统使用shell脚本创建用户和设置密码》在Linux系统中,你可以使用useradd命令来创建新用户,使用echo和chpasswd命令来设置密码,本文写了一个shell... 在linux系统中,你可以使用useradd命令来创建新用户,使用echo和chpasswd命令来设

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依

将Java程序打包成EXE文件的实现方式

《将Java程序打包成EXE文件的实现方式》:本文主要介绍将Java程序打包成EXE文件的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录如何将Java程序编程打包成EXE文件1.准备Java程序2.生成JAR包3.选择并安装打包工具4.配置Launch4

Java程序进程起来了但是不打印日志的原因分析

《Java程序进程起来了但是不打印日志的原因分析》:本文主要介绍Java程序进程起来了但是不打印日志的原因分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java程序进程起来了但是不打印日志的原因1、日志配置问题2、日志文件权限问题3、日志文件路径问题4、程序

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思

shell编程之函数与数组的使用详解

《shell编程之函数与数组的使用详解》:本文主要介绍shell编程之函数与数组的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录shell函数函数的用法俩个数求和系统资源监控并报警函数函数变量的作用范围函数的参数递归函数shell数组获取数组的长度读取某下的

SpringShell命令行之交互式Shell应用开发方式

《SpringShell命令行之交互式Shell应用开发方式》本文将深入探讨SpringShell的核心特性、实现方式及应用场景,帮助开发者掌握这一强大工具,具有很好的参考价值,希望对大家有所帮助,如... 目录引言一、Spring Shell概述二、创建命令类三、命令参数处理四、命令分组与帮助系统五、自定

Spring Shell 命令行实现交互式Shell应用开发

《SpringShell命令行实现交互式Shell应用开发》本文主要介绍了SpringShell命令行实现交互式Shell应用开发,能够帮助开发者快速构建功能丰富的命令行应用程序,具有一定的参考价... 目录引言一、Spring Shell概述二、创建命令类三、命令参数处理四、命令分组与帮助系统五、自定义S

SpringBoot实现微信小程序支付功能

《SpringBoot实现微信小程序支付功能》小程序支付功能已成为众多应用的核心需求之一,本文主要介绍了SpringBoot实现微信小程序支付功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作... 目录一、引言二、准备工作(一)微信支付商户平台配置(二)Spring Boot项目搭建(三)配置文件

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++