本文主要是介绍自定义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()
函数,程序能够检索系统环境变量USER
和HOSTNAME
的值,这些信息随后被用来构建命令行提示符。 - 当前工作目录:同样使用
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程序(内附源码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!