目录
前言:
一、代码中的核心功能
1. 环境变量获取
2. 当前路径处理
3. 用户输入处理
4. 命令解析
5. 内建命令处理
6. 外部命令执行
7. 错误处理
二、代码中涉及的关键知识点
1. 系统调用
2. 环境变量
3. 字符串处理
4. 文件操作
5. 进程管理
三、代码的运行过程
1. 初始化
2. 用户输入
3. 命令解析
4. 内建命令检测
5. 外部命令执行
6. 循环继续
四、编译与测试
1. 编译
2. 运行
3. 测试
五、源码
前言:
大家好,今天带大家了解一个简易 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;
}