Linux實(shí)現(xiàn)簡易版Shell的代碼詳解
一、程序流程分析
我們?nèi)粘J褂肂ash時,通過輸入命令執(zhí)行相應(yīng)的操作,比如:

那么,Bash是如何進(jìn)行工作的呢?觀察一下,就會發(fā)現(xiàn),首先Bash會打印命令行提示符,包括當(dāng)前用戶、主機(jī)名以及路徑。之后會等待我們輸入相關(guān)命令,然后根據(jù)命令執(zhí)行相應(yīng)程序。程序執(zhí)行結(jié)束后,就會再次打印命令行提示符,等待我們再次輸入指令…很明顯是一個死循環(huán)。
總結(jié)一下,Bash的大體工作流程是:
1.打印命令行提示符(包括當(dāng)前用戶名、主機(jī)名、路徑)
2.獲取用戶輸入的命令行
3.解析命令行
4.執(zhí)行命令
5.繼續(xù)打印命令行提示符…
注意:當(dāng)Bash執(zhí)行非內(nèi)建命令時,會創(chuàng)建一個子進(jìn)程,由子進(jìn)程完成相應(yīng)的工作,Bash自己等待子進(jìn)程工作結(jié)束。而對于內(nèi)建命令(如cd,echo),需要Bash自己執(zhí)行任務(wù)。
![![[Pasted image 20250518164821.png]]](http://img.jbzj.com/file_images/article/202505/202505211125213.png)
二、代碼實(shí)現(xiàn)
接下來,我們開始按照上述工作流程,一步步實(shí)現(xiàn)我們的簡易Shell。
1. 打印命令行提示符
CentOS的命令行提示符主要包含三個內(nèi)容:當(dāng)前用戶名、主機(jī)名和當(dāng)前所在路徑。 之前學(xué)習(xí)Linux環(huán)境變量時,我們了解到環(huán)境變量(USER、HOSTNAME、PWD)中存儲著這些內(nèi)容。所以我們使用getenv函數(shù)獲取環(huán)境變量相應(yīng)的值。
代碼實(shí)現(xiàn):
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//獲取當(dāng)前用戶名
const char* GetUserName()
{
const char* name = getenv("USER");
return name == nullptr ? "none" : name;
}
//獲取當(dāng)前主機(jī)名
const char* GetHostName()
{
const char* name = getenv("HOSTNAME");
return name == nullptr ? "none" : name;
}
//獲取當(dāng)前工作路徑
const char* GetPwd()
{
const char* pwd = getenv("PWD");
return pwd == nullptr ? "none" : pwd;
}
接下來,調(diào)用這些函數(shù),形成一串命令行提示符并打?。?/p>
//創(chuàng)建并打印命令行提示符
void PrintCommandPrompt()
{
char CommandPrompt[1024];
//這里為了區(qū)分Bash,用大括號
snprintf(CommandPrompt, sizeof CommandPrompt,"{%s@%s %s}@ " , GetUserName(), GetHostName(), GetPwd());
std::cout << CommandPrompt << std::flush; // 打印并刷新緩沖區(qū)
}
注意這里打印結(jié)束后,由于沒有換行,所以緩沖區(qū)可能不會刷新,導(dǎo)致命令行提示符沒有出現(xiàn)在屏幕上。因此這里我們需要主動刷新緩沖區(qū)。
進(jìn)行測試:
int main()
{
PrintCommandPrompt();
return 0;
}
運(yùn)行結(jié)果:

可以看到,用戶名和主機(jī)名都正常打印,但在我們寫的shell中,當(dāng)前所在路徑是絕對路徑,太過冗長,所以可以對生成的路徑進(jìn)行一些處理:
//創(chuàng)建并打印命令行提示符
void PrintCommandPrompt()
{
char CommandPrompt[1024];
//處理當(dāng)前工作路徑
std::string pwd = GetPwd();
if(pwd != "/") // 如果是根目錄,則直接輸出
pwd = pwd.substr(pwd.rfind('/') + 1); // 查找最后一個"/",并從下一個位置開始分割
//這里為了區(qū)分Bash,用大括號
snprintf(CommandPrompt, sizeof CommandPrompt,"{%s@%s %s}@ " , GetUserName(), GetHostName(), pwd.c_str());
std::cout << CommandPrompt << std::flush; // 打印并刷新緩沖區(qū)
}
運(yùn)行結(jié)果:
![![[Pasted image 20250518161526.png]]](http://img.jbzj.com/file_images/article/202505/202505211125215.png)
2. 獲取用戶輸入的命令行
在Bash輸入命令時,往往會帶上一些選項(xiàng),并用空格隔開。因此,使用scanf或cin讀入時會以空格作為分隔符,達(dá)不到想要的效果。這里我們選擇用fgets進(jìn)行讀取。
另外,主函數(shù)當(dāng)中,命令的全部處理應(yīng)該放在一個死循環(huán)當(dāng)中,這樣才能完成用戶多次派發(fā)的任務(wù)。
//獲取用戶輸入的命令行
bool GetCommandLine(char* command, int size)
{
//從鍵盤讀取命令
if(fgets(command, size, stdin) == nullptr)
{
return false;
}
//注意清理末尾的'\n'
if(strlen(command) == 0) return false;
command[strlen(command) - 1] = '\0';
return true;
}
int main()
{
while(true)
{
//打印命令行提示符
PrintCommandPrompt();
//讀入命令
char command[1024];
if(!GetCommandLine(command, sizeof command)) // 若讀取錯誤,就continue重新讀取
{
std::cout << "讀取錯誤" << std::endl;
continue;
}
//打印輸入的命令行
std::cout << command << std::endl;
}
return 0;
}
注意:使用fgets從鍵盤讀取字符串時會附帶末尾’\n’,需要進(jìn)行處理。
運(yùn)行測試:
![![[Pasted image 20250518165317.png]]](http://img.jbzj.com/file_images/article/202505/202505211125226.png)
可以看到,程序成功讀入了我們的命令(包括空格),并且將命令回顯出來。
3. 命令行解析
命令行解析的過程中,需要將用戶讀入的命令行進(jìn)行分割,提取出要執(zhí)行的程序名以及選項(xiàng)。這里我們創(chuàng)建兩個全局變量g_argc和g_argv,分別存儲解析到的命令行參數(shù)以及參數(shù)個數(shù),方便后續(xù)指令的執(zhí)行。
注:這里的命令行分割操作由strtok函數(shù)完成。
代碼實(shí)現(xiàn):
//全局變量存儲命令行參數(shù)及其個數(shù)
int g_argc = 0;
char* g_argv[128];
//命令行解析
bool CommandParse(char* command)
{
g_argc = 0;
for(char* p = strtok(command, " "); p != nullptr; p = strtok(nullptr, " "))
{
g_argv[g_argc++] = p;
}
return g_argc == 0 ? false : true;
}
int main()
{
while(true)
{
//打印命令行提示符
PrintCommandPrompt();
//讀入命令
char command[1024];
if(!GetCommandLine(command, sizeof command)) // 若讀取錯誤,就continue重新讀取
{
std::cout << "輸入錯誤" << std::endl;
continue;
}
// //打印輸入的命令行
// std::cout << command << std::endl;
//命令行解析
if(!CommandParse(command))
{
std::cout << "命令行解析失敗" << std::endl;
continue;
}
//打印解析結(jié)果
for(int i = 0; i < g_argc; i++)
{
std::cout << g_argv[i] << std::endl;
}
}
return 0;
}
測試結(jié)果:
![![[Pasted image 20250518173801.png]]](http://img.jbzj.com/file_images/article/202505/202505211125237.png)
程序成功地按照空格將我們輸入的命令行參數(shù)提取了出來。接下來,根據(jù)提取到的參數(shù),就可以執(zhí)行相關(guān)指令了。
4. 執(zhí)行命令
對于非內(nèi)建命令,Bash會創(chuàng)建子進(jìn)程,并讓子進(jìn)程執(zhí)行;而對于內(nèi)建命令,則是由Bash自己執(zhí)行。因此,執(zhí)行命令之前,需要先判斷該命令是否是內(nèi)建命令,然后進(jìn)行相應(yīng)的操作。
為什么會有內(nèi)建命令?
- 效率: 執(zhí)行內(nèi)建命令通常比執(zhí)行外部命令更快,因?yàn)楸苊饬藙?chuàng)建新進(jìn)程的開銷。
- Shell 功能: 許多內(nèi)建命令直接操作 Shell 的內(nèi)部狀態(tài),例如改變當(dāng)前工作目錄 (cd)、設(shè)置環(huán)境變量 (export)、控制 Shell 行為等,這些功能如果作為外部命令實(shí)現(xiàn)會更加復(fù)雜或不可能。
- 基本操作: 一些非?;A(chǔ)和常用的操作需要作為內(nèi)建命令提供,以確保 Shell 的基本功能可用
內(nèi)建命令的處理
在Bash當(dāng)中,可以使用type命令判斷一個命令是否是內(nèi)建命令,例如:
![![[Pasted image 20250518174929.png]]](http://img.jbzj.com/file_images/article/202505/202505211125238.png)
當(dāng)然,除了cd和echo命令,還有printf、help等內(nèi)建命令。本次實(shí)現(xiàn)中,為了能夠讓大家深刻理解shell運(yùn)行原理,同時降低實(shí)現(xiàn)難度,博主就只針對cd和echo這兩個內(nèi)建命令進(jìn)行簡易實(shí)現(xiàn)。
內(nèi)建命令的檢查和處理:
//內(nèi)建命令處理
bool CheckAndExecBuiltin()
{
//取出命令行參數(shù)表的首元素,判斷是否為內(nèi)建命令,如果是,則直接執(zhí)行
std::string str = g_argv[0];
if(str == "cd")
{
Cd();
return true;
}
else if(str == "echo")
{
Echo();
return true;
}
//else if(...)
return false; // 不是內(nèi)建命令,直接返回
}
int main()
{
while(true)
{
//打印命令行提示符
PrintCommandPrompt();
//讀入命令
char command[1024];
if(!GetCommandLine(command, sizeof command)) // 若讀取錯誤,就continue重新讀取
{
std::cout << "輸入錯誤" << std::endl;
continue;
}
// //打印輸入的命令行
// std::cout << command << std::endl;
//命令行解析
if(!CommandParse(command))
{
std::cout << "命令行解析失敗" << std::endl;
continue;
}
// //打印解析結(jié)果
// for(int i = 0; i < g_argc; i++)
// {
// std::cout << g_argv[i] << std::endl;
// }
//內(nèi)建命令的處理
if(CheckAndExecBuiltin())
{
continue; // 是內(nèi)建命令,執(zhí)行完畢就回去重新打印提示符
}
//不是內(nèi)建命令,由子進(jìn)程處理
}
return 0;
}
cd的簡易實(shí)現(xiàn)
cd的功能是改變當(dāng)前工作路徑。如果創(chuàng)建子進(jìn)程,其只能修改它自己的工作路徑,而無法修改Bash的工作路徑。因此cd操作需要Bash親自完成。 我們獲取到命令行參數(shù)后,可以通過調(diào)用chdir函數(shù)實(shí)現(xiàn):
void Cd()
{
std::string dst;
if(g_argc == 1 || g_argv[1] == std::string("~")) // 處理進(jìn)入家目錄的情況
{
dst = GetHome();
if(dst == "") return;
}
else
{
dst = g_argv[1];
}
chdir(dst.c_str());
}
測試結(jié)果:
![![[Pasted image 20250518190207.png]]](http://img.jbzj.com/file_images/article/202505/202505211125249.png)
可以看到,使用cd后,當(dāng)前路徑貌似并沒有發(fā)生改變。為什么呢?實(shí)際上chdir確實(shí)起到了效果,但是我們的命令行提示符中,當(dāng)前工作路徑是從環(huán)境變量中獲取的,環(huán)境變量中的PWD并沒有發(fā)生改變。 因此,修改當(dāng)前工作路徑之后,要順帶著修改環(huán)境變量PWD的值。其次,當(dāng)前工作路徑改變后,修改環(huán)境變量之前,要獲取到當(dāng)前工作路徑,就需要使用getcwd函數(shù)。進(jìn)行鍵值處理后,使用putenv修改環(huán)境變量。
另外要注意:許多putenv函數(shù)的實(shí)現(xiàn)(特別是 POSIX 標(biāo)準(zhǔn)的實(shí)現(xiàn))不會復(fù)制我們傳遞給它的字符串,而會直接使用傳入的指針來表示新的環(huán)境變量,因此要創(chuàng)建一個全局變量存儲PWD鍵值對,確保其不會被銷毀。
![![[Pasted image 20250518193953.png]]](http://img.jbzj.com/file_images/article/202505/2025052111252510.jpg)
代碼實(shí)現(xiàn):
//全局的當(dāng)前工作路徑
char cwd[256];
void Cd()
{
std::string dst;
if(g_argc == 1 || g_argv[1] == "~") // 處理進(jìn)入家目錄的情況
{
dst = GetHome();
if(dst == "") return;
}
else
{
dst = g_argv[1];
}
chdir(dst.c_str());
//同時修改環(huán)境變量中的當(dāng)前工作路徑
char tmp[128];
if(getcwd(tmp, sizeof tmp))
{
snprintf(cwd, sizeof cwd, "PWD=%s", tmp); // 鍵值處理
putenv(cwd);
}
}
測試結(jié)果:
![![[Pasted image 20250518200749.png]]](http://img.jbzj.com/file_images/article/202505/2025052111252511.png)
echo的簡易實(shí)現(xiàn)
echo不僅可以向屏幕打印字符串,還可以打印上一個執(zhí)行的程序的退出碼、以及環(huán)境變量等信息。為了確保效率以及執(zhí)行的可靠性,該命令也是一個內(nèi)建命令,由Bash親自執(zhí)行。
//全局變量記錄上一個程序的退出碼
int exit_code;
void Echo()
{
std::string op = g_argv[1];
if(op == "$?") // 打印上一個程序的退出碼
{
std::cout << exit_code << std::endl;
exit_code = 0;
}
else if(op[0] == '$') // 打印環(huán)境變量的值
{
//從"$"后的第一個字符開始,獲取環(huán)境變量名
std::string name = op.substr(1);
//用getenv獲取環(huán)境變量值
const char* value = getenv(name.c_str());
if(value)
std::cout << value << std::endl;
}
else // 打印字符串
{
std::cout << op << std::endl;
}
}
測試結(jié)果:
![![[Pasted image 20250518200451.png]]](http://img.jbzj.com/file_images/article/202505/2025052111252612.png)
非內(nèi)建命令的處理
針對非內(nèi)建命令,需要創(chuàng)建子進(jìn)程,然后由子進(jìn)程找到系統(tǒng)的對應(yīng)指令程序的位置,直接進(jìn)行進(jìn)程程序替換,完成相關(guān)任務(wù)。這樣我們就無需一個個實(shí)現(xiàn)相關(guān)指令操作了。
為了讓子進(jìn)程自動查找對應(yīng)程序的位置,減少我們的工作量,且使程序替換的參數(shù)與我們創(chuàng)建的全局命令行參數(shù)表一一對應(yīng),這里我們直接選用execvp進(jìn)行程序替換。
#include <unistd.h> int execvp(const char *file, char *const argv[])
代碼實(shí)現(xiàn):
//執(zhí)行非內(nèi)建命令
void Execute()
{
//創(chuàng)建子進(jìn)程
pid_t id = fork();
if(id < 0)
{
//創(chuàng)建子進(jìn)程失敗,直接退出
exit(1);
}
else if(id == 0)
{
//子進(jìn)程 -- 通過程序替換執(zhí)行指令
execvp(g_argv[0], g_argv);
}
else
{
//父進(jìn)程 -- 等待子進(jìn)程,并獲取子進(jìn)程的退出信息
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0) // 等待成功
{
//將退出信息存入全局exit_code
exit_code = WEXITSTATUS(status);
}
}
}
int main()
{
while(true)
{
//打印命令行提示符
PrintCommandPrompt();
//讀入命令
char command[1024];
if(!GetCommandLine(command, sizeof command)) // 若讀取錯誤,就continue重新讀取
{
std::cout << "輸入錯誤" << std::endl;
continue;
}
// //打印輸入的命令行
// std::cout << command << std::endl;
//命令行解析
if(!CommandParse(command))
{
std::cout << "命令行解析失敗" << std::endl;
continue;
}
// //打印解析結(jié)果
// for(int i = 0; i < g_argc; i++)
// {
// std::cout << g_argv[i] << std::endl;
// }
//內(nèi)建命令的處理
if(CheckAndExecBuiltin())
{
continue; // 是內(nèi)建命令,執(zhí)行完畢就回去重新打印提示符
}
//不是內(nèi)建命令,由子進(jìn)程處理
Execute();
}
return 0;
}
三、測試
接下來,我們輸入一些指令進(jìn)行測試,看看我們實(shí)現(xiàn)的shell能否達(dá)到預(yù)期效果:
![![[Pasted image 20250518203838.png]]](http://img.jbzj.com/file_images/article/202505/2025052111253013.png)
perfect!
四、程序全部代碼
自實(shí)現(xiàn)shell的全部代碼如下:
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//全局變量存儲命令行參數(shù)及其個數(shù)
int g_argc = 0;
char* g_argv[128];
//全局的當(dāng)前工作路徑
char cwd[256];
//全局變量記錄上一個程序的退出碼
int exit_code;
//獲取當(dāng)前用戶名
const char* GetUserName()
{
const char* name = getenv("USER");
return name == nullptr ? "none" : name;
}
//獲取當(dāng)前主機(jī)名
const char* GetHostName()
{
const char* name = getenv("HOSTNAME");
return name == nullptr ? "none" : name;
}
//獲取當(dāng)前工作路徑
const char* GetPwd()
{
const char* pwd = getenv("PWD");
return pwd == nullptr ? "none" : pwd;
}
//獲取用戶的家目錄
const char* GetHome()
{
const char* home = getenv("HOME");
return home == nullptr ? "" : home;
}
//創(chuàng)建并打印命令行提示符
void PrintCommandPrompt()
{
char CommandPrompt[1024];
//處理當(dāng)前工作路徑
std::string pwd = GetPwd();
if(pwd != "/") // 如果是根目錄,則直接輸出
pwd = pwd.substr(pwd.rfind('/') + 1); // 查找最后一個"/",并從下一個位置開始分割
//這里為了區(qū)分Bash,用大括號
snprintf(CommandPrompt, sizeof CommandPrompt,"{%s@%s %s}@ " , GetUserName(), GetHostName(), pwd.c_str());
std::cout << CommandPrompt << std::flush; // 打印并刷新緩沖區(qū)
}
//獲取用戶輸入的命令行
bool GetCommandLine(char* command, int size)
{
//從鍵盤讀取命令
if(fgets(command, size, stdin) == nullptr)
{
return false;
}
//注意清理末尾的'\n'
if(strlen(command) == 0) return false;
command[strlen(command) - 1] = '\0';
return true;
}
//命令行解析
bool CommandParse(char* command)
{
g_argc = 0;
for(char* p = strtok(command, " "); p != nullptr; p = strtok(nullptr, " "))
{
g_argv[g_argc++] = p;
}
return g_argc == 0 ? false : true;
}
void Cd()
{
std::string dst;
if(g_argc == 1 || g_argv[1] == std::string("~")) // 處理進(jìn)入家目錄的情況
{
dst = GetHome();
if(dst == "") return;
}
else
{
dst = g_argv[1];
}
chdir(dst.c_str());
//同時修改環(huán)境變量中的當(dāng)前工作路徑
char tmp[128];
if(getcwd(tmp, sizeof tmp))
{
snprintf(cwd, sizeof cwd, "PWD=%s", tmp);
putenv(cwd);
}
}
void Echo()
{
std::string op = g_argv[1];
if(op == "$?") // 打印上一個程序的退出碼
{
std::cout << exit_code << std::endl;
exit_code = 0;
}
else if(op[0] == '$') // 打印環(huán)境變量的值
{
//從"$"后的第一個字符開始,獲取環(huán)境變量名
std::string name = op.substr(1);
//用getenv獲取環(huán)境變量值
const char* value = getenv(name.c_str());
if(value)
std::cout << value << std::endl;
}
else // 打印字符串
{
std::cout << op << std::endl;
}
}
//內(nèi)建命令處理
bool CheckAndExecBuiltin()
{
//取出命令行參數(shù)表的首元素,判斷是否為內(nèi)建命令,如果是,則直接執(zhí)行
std::string str = g_argv[0];
if(str == "cd")
{
Cd();
return true;
}
else if(str == "echo")
{
Echo();
return true;
}
//else if(...)
return false; // 不是內(nèi)建命令,直接返回
}
//執(zhí)行非內(nèi)建命令
void Execute()
{
//創(chuàng)建子進(jìn)程
pid_t id = fork();
if(id < 0)
{
//創(chuàng)建子進(jìn)程失敗,直接退出
exit(1);
}
else if(id == 0)
{
//子進(jìn)程 -- 通過程序替換執(zhí)行指令
execvp(g_argv[0], g_argv);
}
else
{
//父進(jìn)程 -- 等待子進(jìn)程,并獲取子進(jìn)程的退出信息
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0) // 等待成功
{
//將退出信息存入全局exit_code
exit_code = WEXITSTATUS(status);
}
}
}
int main()
{
while(true)
{
//打印命令行提示符
PrintCommandPrompt();
//讀入命令
char command[1024];
if(!GetCommandLine(command, sizeof command)) // 若讀取錯誤,就continue重新讀取
{
std::cout << "輸入錯誤" << std::endl;
continue;
}
// //打印輸入的命令行
// std::cout << command << std::endl;
//命令行解析
if(!CommandParse(command))
{
std::cout << "命令行解析失敗" << std::endl;
continue;
}
// //打印解析結(jié)果
// for(int i = 0; i < g_argc; i++)
// {
// std::cout << g_argv[i] << std::endl;
// }
//內(nèi)建命令的處理
if(CheckAndExecBuiltin())
{
continue; // 是內(nèi)建命令,執(zhí)行完畢就回去重新打印提示符
}
//不是內(nèi)建命令,由子進(jìn)程處理
Execute();
}
return 0;
}
總結(jié)
本篇文章,我們基于之前學(xué)習(xí)的Linux進(jìn)程相關(guān)概念以及進(jìn)程控制接口,實(shí)現(xiàn)了一個簡易版的shell。本次實(shí)現(xiàn)讓我們對Shell的運(yùn)行原理有了更深刻的理解。
以上就是Linux實(shí)現(xiàn)簡易版Shell的代碼詳解的詳細(xì)內(nèi)容,更多關(guān)于Linux簡易版Shell的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Shell實(shí)現(xiàn)強(qiáng)制釋放內(nèi)存腳本分享
這篇文章主要介紹了Shell實(shí)現(xiàn)強(qiáng)制釋放內(nèi)存腳本分享,本文直接給出實(shí)現(xiàn)代碼,并對每一句代碼都做了講解了,需要的朋友可以參考下2015-02-02
學(xué)習(xí)shell腳本之前的基礎(chǔ)知識[圖文]
在學(xué)習(xí)shell腳本之前,需要你了解很多關(guān)于shell的知識,這些知識是編寫shell腳本的基礎(chǔ),所以希望你能夠熟練的掌握2013-03-03
shell腳本實(shí)現(xiàn)服務(wù)器進(jìn)程監(jiān)控的方法
這篇文章主要介紹了shell腳本實(shí)現(xiàn)服務(wù)器進(jìn)程監(jiān)控的方法,非常不錯,具有參考借鑒價值,需要的朋友參考下吧2018-04-04
Linux下使用tcpdump抓包的實(shí)現(xiàn)方法
tcpdump是Linux下面的一個開源的抓包工具,和Windows下面的wireshark抓包工具一樣, 支持抓取指定網(wǎng)口、指定目的地址、指定源地址、指定端口、指定協(xié)議的數(shù)據(jù)。這篇文章主要介紹了Linux下使用tcpdump抓包的實(shí)現(xiàn)方法,需要的朋友可以參考下2015-10-10
linux上配置jdk時,java命令提示沒有此文件或文件夾的解決方法
下面小編就為大家?guī)硪黄猯inux上配置jdk時,java命令提示沒有此文件或文件夾的解決方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05

