目录
前言:
大家好,今天带大家了解一个简易 Shell 的实现!这个 Shell 是用 C 语言编写的,功能虽然简单,但涵盖了命令行工具的基本要素,如获取用户输入、执行命令、管理环境变量等。接下来,我会详细讲解代码中的各个部分,帮助你理解它是如何工作的。
我们用下图的时间轴来表示事件的发生次序,其中时间从左向右。shell由标识为bash的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"起建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序并等待这个进程结束。
所以要写一个shell,需要循环以下过程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
- 父进程等待子进程退出(wait)
一、代码中的核心功能
1. 环境变量获取
Shell 需要知道一些系统信息,比如用户名、主机名、当前路径等,这些信息都存储在环境变量中。代码中的 GetUserName
、GetHostName
和 GetCwd
函数分别用于获取这些信息:
// 获取当前用户名
const char *GetUserName()
{
const char *name = getenv("USER");
return name ? name : "None";
}
// 获取主机名
const char *GetHostName()
{
const char *hostname = getenv("HOSTNAME");
return hostname ? hostname : "None";
}
// 获取当前工作路径
const char *GetCwd()
{
const char *cwd = getenv("PWD");
return cwd ? cwd : "None";
}
-
getenv
是一个标准库函数,用于获取环境变量的值。 -
如果
USER
环境变量不存在,返回默认值"None"
。
2. 当前路径处理
Shell 会频繁处理路径信息。代码中定义了 SkipPath
宏和 getcwd
函数:
#define SkipPath(p) do{ (p += strlen(p) - 1); while(*p != '/') p--; }while(0); // 逆向查找路径分隔符'\'
// 获取当前工作路径
const char *GetCwd()
{
const char *cwd = getenv("PWD");
return cwd ? cwd : "None";
}
作用:将指针移动到路径字符串的最后一个'/'位置,从而提取出当前工作目录的名称。
示例:
"/home/user/demo"
→ 指针定位到/demo
前的'/'实现原理:
将指针移动到字符串末尾(
strlen(p)-1
)逆向查找直到遇到'/'字符
使用
do-while
确保至少执行一次
3. 用户输入处理
Shell 需要从用户那里获取输入命令。代码中的 MakeCommandLineAndPrint
和 GetUserCommand
函数负责这个过程:
// 构造并打印命令行提示符
void MakeCommandLineAndPrint()
{
char line[SIZE];
const char *username = GetUserName();
const char *hostname = GetHostName();
const char *cwd = GetCwd();
SkipPath(cwd);
snprintf(line, sizeof(line), "[%s@%s %s]~ ",
username, hostname,
strlen(cwd) == 1 ? "/" : cwd+1);
printf("%s", line);
fflush(stdout);
}
// 获取用户输入命令
int GetUserCommand(char command[], size_t n)
{
char *s = fgets(command, n, stdin);
if(s == NULL) return -1;
command[strlen(command)-1] = ZERO;
return strlen(command);
}
-
MakeCommandLineAndPrint
函数生成一个格式化的提示符,显示用户名、主机名和当前路径。 -
GetUserCommand
函数从标准输入读取用户输入,并将其存储在command
数组中。
4. 命令解析
用户输入的命令需要被解析成可执行的格式。代码中的 SplitCommand
函数负责将命令字符串分割成命令和参数:
// 命令分割函数
void SplitCommand(char command[], size_t n)
{
gArgv[0] = strtok(command, SEP);
int index = 1;
while(gArgv[index++] = strtok(NULL, SEP));
}
-
strtok
是一个标准库函数,用于按指定分隔符分割字符串。 -
分割后的命令和参数存储在全局数组
gArgv
中,其中gArgv[0]
是命令名称,后面的元素是参数。
5. 内建命令处理
Shell 通常包含一些内建命令,如 cd、echo $?等
。代码中的 CheckBuildin
和 Cd
函数实现了这些功能:
// 获取用户HOME目录
const char *GetHome()
{
const char *home = getenv("HOME");
return home ? home : "/";
}
// 切换目录实现
void Cd()
{
const char *path = gArgv[1];
if(path == NULL) path = GetHome();
chdir(path);
// 更新环境变量
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_cmd = gArgv[0];
if(strcmp(enter_cmd, "cd") == 0) {
yes = 1;
Cd();
}
/* 可扩展其他内建命令
else if(strcmp(enter_cmd, "echo") == 0 &&
strcmp(gArgv[1], "$?") == 0) {
yes = 1;
printf("%d\n", lastcode);
lastcode = 0;
}
*/
return yes;
}
-
CheckBuildin
函数检查用户输入的命令是否是内建命令。 -
Cd
函数实现了cd
命令,用于切换当前工作目录。
6. 外部命令执行
Shell 还需要支持执行外部命令。代码中的 ExecuteCommand
函数使用了 fork
和 execvp
:
// 执行外部命令
void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
if(id == 0) { // 子进程
execvp(gArgv[0], gArgv);
exit(errno);
}
else { // 父进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0) {
lastcode = WEXITSTATUS(status);
if(lastcode != 0)
printf("%s:%s:%d\n",
gArgv[0], strerror(lastcode), lastcode);
}
}
}
-
fork
创建一个子进程,子进程通过execvp
执行用户指定的外部命令。 -
父进程通过
waitpid
等待子进程结束,并检查其退出状态。 -
执行流程图:
7. 错误处理
代码中的 Die
函数用于简单地终止程序:
// 异常退出处理
void Die()
{
exit(1);
}
二、代码中涉及的关键知识点
1. 系统调用
fork
:创建子进程。
execvp
:执行外部命令。
waitpid
:等待子进程结束。
2. 环境变量
getenv
:获取环境变量的值。
putenv
:设置环境变量的值。
3. 字符串处理
strtok
:按分隔符分割字符串。
snprintf
:格式化字符串。
4. 文件操作
fgets
:从标准输入读取字符串。
5. 进程管理
exit
:终止进程。
三、代码的运行过程
1. 初始化
main
函数启动时,程序进入一个无限循环,等待用户输入。
2. 用户输入
MakeCommandLineAndPrint
输出一个格式化的提示符。GetUserCommand
从标准输入读取用户输入。
3. 命令解析
SplitCommand
将用户输入的字符串分割成命令和参数。
4. 内建命令检测
CheckBuildin
检查用户输入的命令是否是内建命令(如cd
)并执行相应的操作。
5. 外部命令执行
- 如果命令不是内建命令,
ExecuteCommand
通过fork
和execvp
执行外部命令。
6. 循环继续
- 重复以上步骤,直到用户输入
exit
或程序被强制终止。
四、编译与测试
1. 编译
使用以下命令编译代码:
gcc myshell.c -o myshell
2. 运行
运行生成的可执行文件:
./myshell
3. 测试
五、源码
/***********************************************
* 简易Shell实现(C语言版)
* 功能:基础命令执行、路径切换、状态码保持
* 编译:gcc -o myshell myshell.c -Wall
***********************************************/
#include <stdio.h> // 标准输入输出
#include <unistd.h> // 系统调用接口(fork, exec等)
#include <stdlib.h> // 内存管理、环境变量
#include <errno.h> // 错误码处理
#include <string.h> // 字符串操作
#include <sys/types.h> // 进程类型定义
#include <sys/wait.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);
/*----------------------------------------------
* 全局变量声明区
----------------------------------------------*/
char cwd[SIZE*3]; // 当前路径环境变量缓冲区
char *gArgv[NUM]; // 命令参数数组(用于execvp)
int lastcode = 0; // 记录上条命令的退出状态码
/*----------------------------------------------
* 函数声明区(按调用顺序排列)
----------------------------------------------*/
void Die(); // 异常终止函数
const char *GetHome(); // 获取用户主目录
const char *GetUserName(); // 获取当前用户名
const char *GetHostName(); // 获取主机名
const char *GetCwd(); // 获取当前工作目录
void MakeCommandLineAndPrint(); // 构造提示符
int GetUserCommand(char cmd[], size_t n); // 获取输入
void SplitCommand(char cmd[], size_t n); // 分割命令
void ExecuteCommand(); // 执行外部命令
void Cd(); // 切换目录实现
int CheckBuildin(); // 内建命令检查
/*----------------------------------------------
* [函数实现] 系统信息获取模块
----------------------------------------------*/
// 异常退出处理(简化版)
void Die() {
exit(1); // 直接退出并返回状态码1
}
// 获取用户主目录路径
const char *GetHome() {
// 通过HOME环境变量获取
const char *home = getenv("HOME");
return home ? home : "/"; // 保底返回根目录
}
// 获取当前用户名(来自环境变量)
const char *GetUserName() {
const char *name = getenv("USER");
return name ? name : "None"; // 默认值处理
}
// 获取主机名(环境变量方式)
const char *GetHostName() {
const char *hostname = getenv("HOSTNAME");
return hostname ? hostname : "None";
}
// 获取当前工作目录(环境变量缓存值)
const char *GetCwd() {
const char *cwd = getenv("PWD");
return cwd ? cwd : "None";
}
/*----------------------------------------------
* [功能模块] 命令行界面处理
----------------------------------------------*/
// 构造并显示提示符
void MakeCommandLineAndPrint() {
char line[SIZE]; // 行缓冲区
// 获取系统信息三元组
const char *username = GetUserName();
const char *hostname = GetHostName();
const char *cwd = GetCwd();
// 路径处理:定位到最后一个'/'后的目录名
// 示例:/home/user → user
SkipPath(cwd);
// 构造提示符格式:[user@host dir]~
snprintf(line, sizeof(line), "[%s@%s %s]~ ",
username,
hostname,
// 处理根目录特殊情况
(strlen(cwd) == 1) ? "/" : cwd+1);
printf("%s", line); // 输出提示符
fflush(stdout); // 强制刷新确保立即显示
}
// 获取用户输入命令
int GetUserCommand(char command[], size_t n) {
// 使用fgets获取整行输入(包含换行符)
char *s = fgets(command, n, stdin);
if(s == NULL) return -1; // 读取失败处理
// 替换换行符为字符串终止符
// 示例:"ls -l\n" → "ls -l\0"
command[strlen(command)-1] = ZERO;
return strlen(command); // 返回有效长度
}
// 命令分割:将字符串命令解析为参数数组
void SplitCommand(char command[], size_t n) {
// 使用strtok进行分割(破坏性操作)
gArgv[0] = strtok(command, SEP); // 首次调用
int index = 1;
// 循环分割直到返回NULL(自动添加NULL结尾)
// 示例:"ls -l" → ["ls", "-l", NULL]
while((gArgv[index++] = strtok(NULL, SEP)));
}
/*----------------------------------------------
* [核心功能] 命令执行模块
----------------------------------------------*/
// 执行外部命令(非内建命令)
void ExecuteCommand() {
pid_t id = fork(); // 创建子进程
if(id < 0) Die(); // fork失败则终止
if(id == 0) { // 子进程执行流
// 使用execvp执行命令(自动搜索PATH)
// 参数格式要求:数组以NULL结尾
execvp(gArgv[0], gArgv);
// 只有exec失败才会执行到这里
exit(errno); // 返回错误码
}
else { // 父进程执行流
int status = 0;
// 等待子进程结束(阻塞方式)
pid_t rid = waitpid(id, &status, 0);
if(rid > 0) {
// 解析子进程退出状态
lastcode = WEXITSTATUS(status);
// 非零状态码表示异常退出
if(lastcode != 0) {
printf("%s:%s:%d\n",
gArgv[0], // 程序名
strerror(lastcode),// 错误描述
lastcode); // 错误码
}
}
}
}
// 实现cd命令(内建命令)
void Cd() {
// 获取目标路径(支持无参数)
const char *path = gArgv[1];
if(path == NULL) path = GetHome(); // 默认主目录
// 系统调用切换目录
chdir(path);
// 更新环境变量(使PWD与真实路径同步)
char temp[SIZE*2];
getcwd(temp, sizeof(temp)); // 获取实际路径
// 构造环境变量字符串(格式:PWD=...)
snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
putenv(cwd); // 修改环境变量
}
// 检查并执行内建命令
int CheckBuildin() {
int yes = 0;
const char *enter_cmd = gArgv[0];
// cd命令处理
if(strcmp(enter_cmd, "cd") == 0) {
yes = 1; // 标记为已处理
Cd(); // 调用cd实现
}
/* 可扩展区域:其他内建命令示例
else if(strcmp(enter_cmd, "echo") == 0 &&
strcmp(gArgv[1], "$?") == 0) {
yes = 1;
printf("%d\n", lastcode); // 输出上条命令状态码
lastcode = 0; // 重置状态码
}
*/
return yes; // 返回是否处理了内建命令
}
/*----------------------------------------------
* 主程序入口
----------------------------------------------*/
int main() {
int quit = 0; // 退出标志(未实现退出命令)
// REPL循环(Read-Eval-Print Loop)
while(!quit) {
// 步骤1:显示提示符
MakeCommandLineAndPrint();
// 步骤2:获取用户输入
char usercommand[SIZE]; // 输入缓冲区
int n = GetUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) return 1; // 输入错误处理
// 步骤3:分割命令参数
SplitCommand(usercommand, sizeof(usercommand));
// 步骤4:处理内建命令
if(CheckBuildin()) continue;
// 步骤5:执行外部命令
ExecuteCommand();
}
return 0;
}