北京 順義 網(wǎng)站制作培訓(xùn)網(wǎng)站制作
個人主頁:chian-ocean
文章專欄-Linux
前言:
Shell(外殼)是一個操作系統(tǒng)的用戶界面,它提供了一種方式,使得用戶能夠與操作系統(tǒng)進行交互。Shell 是用戶與操作系統(tǒng)之間的橋梁,允許用戶通過命令行輸入來執(zhí)行各種操作,例如文件管理、程序執(zhí)行、進程控制、系統(tǒng)監(jiān)控等
常見的 Shell 類型:
Bash(Bourne Again Shell)
:- 是 Linux 和 macOS 等類 Unix 系統(tǒng)中常見的默認 Shell。它是 Bourne Shell 的增強版,支持豐富的特性,如命令補全、歷史命令、數(shù)組等。
Zsh(Z Shell)
:- 是一個功能強大的 Shell,支持更豐富的自動化、命令補全、插件系統(tǒng)等特性。Zsh 常常被認為是最為用戶友好的 Shell 之一。
Fish(Friendly Interactive Shell
):- 是一個具有用戶友好界面和豐富特性(如自動提示、自動補全等)的現(xiàn)代 Shell。其設(shè)計注重簡潔和易用性。
C Shell(csh)
:- 基于 C 語言語法的 Shell,主要用于早期的 Unix 系統(tǒng)。C Shell 提供了較強的腳本編程功能。
Korn Shell(ksh)
:- 是一個功能強大的 Shell,結(jié)合了 Bourne Shell 和 C Shell 的特性,并且提供了很多增強的功能。
shell外殼的實現(xiàn)
引入頭文件
#include<string>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<assert.h>
-
#include<string>
:引入 C++ 的string
庫,用于字符串處理。 -
#include<unistd.h>
:提供訪問系統(tǒng)調(diào)用的接口,例如fork()
、execvp()
、getcwd()
等。 -
#include<sys/wait.h>
:包含等待子進程退出的函數(shù)。 -
#include<sys/types.h>
:包含系統(tǒng)數(shù)據(jù)類型的定義,如pid_t
(進程 ID 類型)。 -
#include<stdlib.h>
:提供一些標(biāo)準(zhǔn)庫函數(shù),如exit()
、getenv()
和putenv()
等。 -
#include<stdio.h>
:提供輸入輸出函數(shù),如printf()
。 -
#include<string.h>
:提供字符串操作函數(shù),如strtok()
、strcmp()
等。 -
#include<assert.h>
:提供調(diào)試宏assert()
,用于檢測程序中的錯誤
宏定義
#define DELIM " \t"
#define LEFT "["
#define RIGHT "]"
#define LABLE "$"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 4
-
DELIM
:命令行參數(shù)的分隔符,包含空格和制表符。 -
LEFT
,RIGHT
,LABLE
:格式化命令行提示符的符號,用于顯示用戶、主機和當(dāng)前工作目錄。 -
LINE_SIZE
:最大命令行字符長度,設(shè)置為1024。 -
ARGC_SIZE
:最大命令行參數(shù)數(shù)量,設(shè)置為32。 -
EXIT_CODE
:用于退出的錯誤代碼。
全局變量
int quit = 0;
int LASTCODE = 0;
char* argv[ARGC_SIZE];
char commondline[LINE_SIZE];
char pwd[ARGC_SIZE];
char myenv[ARGC_SIZE];
-
quit
:控制程序是否退出的標(biāo)志。 -
LASTCODE
:記錄上一個命令的退出狀態(tài)碼。 -
argv
:存儲命令行解析后的參數(shù)。 -
commondline
:存儲輸入的命令行字符串。 -
pwd
:存儲當(dāng)前工作目錄路徑。 -
myenv
:存儲環(huán)境變量。
const char* getusr()
{return getenv("USER");
}const char* gethostname()
{return getenv("HOSTNAME");
}
getusr
:返回當(dāng)前用戶的用戶名。gethostname
:返回當(dāng)前計算機的主機名。
獲取當(dāng)前工作目錄
void getpwd()
{getcwd(pwd, sizeof(pwd));
}
getpwd
:調(diào)用getcwd
獲取當(dāng)前工作目錄,并將結(jié)果存儲在pwd
中。
交互式輸入處理
void ineract(char* cline, int size)
{getpwd();printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusr(), gethostname(), pwd);char* s = fgets(cline, size, stdin);assert(s);(void)s;cline[strlen(cline) - 1] = '\0';
}
該 ineract
函數(shù)是命令行交互的核心部分,用于顯示提示符并獲取用戶輸入。以下是對代碼逐行的解析:
函數(shù)定義
void ineract(char* cline, int size)
cline
:指向存儲用戶輸入命令的字符數(shù)組的指針。size
:輸入緩沖區(qū)的大小,表示cline
數(shù)組的最大容量。
獲取當(dāng)前工作目錄并顯示提示符
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusr(), gethostname(), pwd);
-
getpwd()
:調(diào)用getpwd
函數(shù)來獲取當(dāng)前工作目錄并存儲到全局變量pwd
中。 -
printf
:顯示命令行提示符。格式為[user@hostname pwd]$
,其中:getusr()
:獲取當(dāng)前用戶名(通過環(huán)境變量USER
)。gethostname()
:獲取當(dāng)前主機名(通過環(huán)境變量HOSTNAME
)。pwd
:顯示當(dāng)前工作目錄。
提示符通過格式化字符串顯示,
LEFT
和RIGHT
用于添加方括號([
和]
)包圍信息,而LABLE
是一個$
字符,表示命令行提示符。
獲取用戶輸入
char* s = fgets(cline, size, stdin);
assert(s);
(void)s;
fgets(cline, size, stdin)
:從標(biāo)準(zhǔn)輸入(鍵盤)讀取用戶輸入,存儲在cline
數(shù)組中,最多讀取size-1
個字符。fgets
會自動在輸入末尾添加一個\0
來終止字符串。assert(s)
:如果fgets
返回NULL
,程序?qū)⒔K止并輸出錯誤信息。assert
是一種調(diào)試檢查,確保輸入讀取成功。如果s
為NULL
,說明讀取輸入失敗。(void)s
:(void)s
的作用是消除未使用變量s
的編譯器警告,實際上這里并沒有做任何事情。
去除輸入末尾的換行符
cline[strlen(cline) - 1] = '\0';
strlen(cline) - 1
:計算輸入字符串的長度,并將其最后一個字符(換行符\n
)替換為字符串結(jié)束符\0
。這一步去除fgets
讀取時可能留下的換行符。
命令行解析
int AnalyzeCommandLine(char* cline)
{int i = 0;argv[i++] = strtok(cline, DELIM);while (argv[i++] = strtok(NULL, DELIM));return i - 1;
}
AnalyzeCommandLine
函數(shù)用于解析輸入的命令行字符串,并將解析出的各個命令參數(shù)存儲在 argv
數(shù)組中。以下是對該函數(shù)的逐行解析:
函數(shù)定義
int AnalyzeCommandLine(char* cline)
cline
:輸入的命令行字符串(用戶在命令行輸入的完整命令)。該字符串將會被解析為多個命令和參數(shù)。
初始化參數(shù)索引
int i = 0;
i
:定義一個整數(shù)變量i
,用于跟蹤argv
數(shù)組的索引位置,表示當(dāng)前解析的命令參數(shù)的位置。
使用 strtok
解析命令行
argv[i++] = strtok(cline, DELIM);
strtok(cline, DELIM)
:strtok
是一個字符串分割函數(shù),它通過指定的分隔符(DELIM
)將 cline
字符串分割成多個子字符串。DELIM
在此代碼中定義為 " \t"
,即空格和制表符。
- 第一次調(diào)用
strtok()
時,它會返回cline
字符串中的第一個子字符串(即命令或第一個參數(shù))。返回值會存儲在argv[i]
中。 - 然后
i++
使得i
增加 1,指向下一個位置
繼續(xù)解析命令行參數(shù)
while (argv[i++] = strtok(NULL, DELIM));
strtok(NULL, DELIM)
:在第一次調(diào)用strtok()
后,后續(xù)調(diào)用需要傳入NULL
作為第一個參數(shù),表示繼續(xù)從上次分割的位置開始。strtok()
會繼續(xù)根據(jù)分隔符分割剩余的命令行字符串,并返回下一個子字符串。- 這段代碼通過
while
循環(huán)逐個提取命令行中的每個子字符串,并將其存儲到argv[i]
中。每次調(diào)用strtok()
后,i++
將i
指向下一個數(shù)組位置。
返回參數(shù)的數(shù)量
return i - 1;
i - 1
:由于最后一次i++
會多加一次,因此函數(shù)返回i - 1
,即存儲在argv
數(shù)組中的參數(shù)個數(shù)(命令行中的參數(shù)數(shù)量)。
執(zhí)行常規(guī)命令
void NormalExecl(char* _argv[])
{pid_t id = fork();if (id < 0){perror("fork");return;}else if (id == 0){execvp(_argv[0], argv);exit(EXIT_CODE);}else{int status = 0;pid_t rid = waitpid(id, &status, 0);if (id){LASTCODE = WEXITSTATUS(status);}}
}
函數(shù)定義
void NormalExecl(char* _argv[])
_argv[]
:這是一個參數(shù)數(shù)組,用于傳遞命令及其參數(shù)。例如,_argv[0]
是命令,_argv[1]
是命令的第一個參數(shù),依此類推。
創(chuàng)建子進程
pid_t id = fork();
fork()
:fork()
函數(shù)用于創(chuàng)建一個新進程。它將當(dāng)前進程復(fù)制一份。新進程被稱為子進程,原始進程是父進程。
- 如果
fork()
成功,它會返回兩次:- 父進程:返回子進程的進程 ID(PID)。
- 子進程:返回 0。
- 如果
fork()
失敗,它返回負值。
錯誤處理
if (id < 0)
{perror("fork");return;
}
id < 0
:如果fork()
返回負值,表示創(chuàng)建子進程失敗。此時打印錯誤信息并返回。perror("fork")
:輸出錯誤信息,說明fork()
失敗的原因。
子進程執(zhí)行命令
else if (id == 0)
{execvp(_argv[0], argv);exit(EXIT_CODE);
}
id == 0
:這是子進程中的代碼塊。如果 fork()
返回 0,表示當(dāng)前代碼在子進程中執(zhí)行。
execvp(_argv[0], argv)
:子進程調(diào)用 execvp()
函數(shù)來執(zhí)行命令。execvp()
會用指定的命令替換當(dāng)前進程的映像。具體來說:
_argv[0]
是命令(例如ls
)。argv
是命令的參數(shù)數(shù)組,其中包含命令和它的所有參數(shù)(例如ls -l
)。
exit(EXIT_CODE)
:如果 execvp()
失敗,子進程會退出,返回 EXIT_CODE
。如果 execvp()
成功,當(dāng)前進程會被新的命令替代,exit()
不會被執(zhí)行
父進程等待子進程結(jié)束
else
{int status = 0;pid_t rid = waitpid(id, &status, 0);if (id){LASTCODE = WEXITSTATUS(status);}
}
-
else
:這是父進程中的代碼塊,父進程需要等待子進程結(jié)束并獲取其退出狀態(tài)。 -
int status = 0;
:定義一個變量status
用來存儲子進程的退出狀態(tài)。 -
waitpid(id, &status, 0)
:父進程使用
waitpid()
函數(shù)等待子進程的結(jié)束。waitpid()
會阻塞父進程,直到指定的子進程結(jié)束,并返回子進程的退出狀態(tài)。id
:是子進程的進程 ID,表示父進程等待這個子進程。&status
:存儲子進程退出時的狀態(tài)信息。0
:表示父進程等待子進程的退出,不對其狀態(tài)做其他操作。
-
LASTCODE = WEXITSTATUS(status)
:獲取子進程的退出狀態(tài)碼并存儲在LASTCODE
中。WEXITSTATUS(status)
提取的是子進程的退出代碼。
內(nèi)建命令執(zhí)行
int BuildExec(char* _argv[], int _argc)
{if (_argc == 2 && strcmp(_argv[0], "cd") == 0){chdir(_argv[1]);getpwd();sprintf(getenv("PWD"), "%s", pwd);return 1;}else if (_argc == 2 && strcmp(_argv[0], "export") == 0){strcpy(myenv, _argv[1]);putenv(myenv);return 1;}else if (_argc == 2 && strcmp(_argv[0], "echo") == 0){if (strcmp(_argv[1], "$?")){printf("%d\n", LASTCODE);LASTCODE = 0;}else if (strcmp(_argv[1], "$")){char* val = getenv(_argv[1] + 1);printf("%s\n", val);}else{printf("%s\n", _argv[1]);}}if (strcmp(_argv[0], "ls") == 0){_argv[_argc++] = "--color";_argv[_argc] = NULL;}return 0;
}
函數(shù)定義
int BuildExec(char* _argv[], int _argc)
_argv[]
:命令行解析后參數(shù)的數(shù)組,存儲命令及其參數(shù)。_argc
:命令行參數(shù)的數(shù)量。
處理 cd
命令
if (_argc == 2 && strcmp(_argv[0], "cd") == 0)
{chdir(_argv[1]);getpwd();sprintf(getenv("PWD"), "%s", pwd);return 1;
}
strcmp(_argv[0], "cd") == 0
:檢查命令是否為cd
。如果argv[0]
是"cd"
,則執(zhí)行以下操作。chdir(_argv[1])
:改變當(dāng)前工作目錄到argv[1]
指定的路徑。getpwd()
:調(diào)用getpwd()
獲取新的工作目錄并更新全局變量pwd
。sprintf(getenv("PWD"), "%s", pwd)
:更新環(huán)境變量PWD
,使其反映當(dāng)前工作目錄。return 1;
:表示已經(jīng)處理了cd
命令,因此直接返回,不繼續(xù)處理后面的代碼。
處理 export
命令
else if (_argc == 2 && strcmp(_argv[0], "export") == 0)
{strcpy(myenv, _argv[1]);putenv(myenv);return 1;
}
strcmp(_argv[0], "export") == 0
:檢查命令是否為export
。如果argv[0]
是"export"
,則執(zhí)行以下操作。strcpy(myenv, _argv[1])
:將argv[1]
的值復(fù)制到myenv
字符數(shù)組中。argv[1]
應(yīng)該是一個環(huán)境變量的設(shè)置(例如"VAR=value"
)。putenv(myenv)
:使用putenv()
將myenv
中的環(huán)境變量設(shè)置添加到當(dāng)前環(huán)境中。return 1;
:表示已經(jīng)處理了export
命令,直接返回。
處理 echo
命令
else if (_argc == 2 && strcmp(_argv[0], "echo") == 0)
{if (strcmp(_argv[1], "$?")){printf("%d\n", LASTCODE);LASTCODE = 0;}else if (strcmp(_argv[1], "$")){char* val = getenv(_argv[1] + 1);printf("%s\n", val);}else{printf("%s\n", _argv[1]);}
}
strcmp(_argv[0], "echo") == 0
:檢查命令是否為echo
。如果是,繼續(xù)執(zhí)行以下代碼。strcmp(_argv[1], "$?")
:檢查是否要求輸出上一個命令的退出狀態(tài)碼。如果argv[1]
是"$?"
,則輸出上一個命令的退出代碼LASTCODE
,并將LASTCODE
重置為 0。strcmp(_argv[1], "$")
:檢查是否要求輸出某個環(huán)境變量的值。如果argv[1]
是以$
開頭(例如$HOME
),則獲取該環(huán)境變量的值并打印。printf("%s\n", _argv[1]);
:如果既不是"$?"
也不是以$
開頭,則直接輸出argv[1]
,即用戶傳遞給echo
的字符串。
特殊處理 ls
命令
if (strcmp(_argv[0], "ls") == 0)
{_argv[_argc++] = "--color";_argv[_argc] = NULL;
}
strcmp(_argv[0], "ls") == 0
:檢查命令是否為ls
。如果是ls
命令,執(zhí)行以下操作。_argv[_argc++] = "--color";
:給ls
命令添加--color
參數(shù),這樣ls
命令輸出的文件列表會使用不同的顏色顯示(通常是通過文件類型區(qū)分)。_argv[_argc] = NULL;
:將數(shù)組最后一個元素設(shè)置為NULL
,確保execvp()
在執(zhí)行時能正確處理參數(shù)數(shù)組。
返回值
return 0;
- 如果命令不是內(nèi)建命令(
cd
、export
、echo
)或者沒有進行特殊處理(如ls
),則返回 0,表示該命令需要外部執(zhí)行。
主程序邏輯
int main()
{while (!quit){//命令行提示ineract(commondline, sizeof(commondline));//命令解析int argc = AnalyzeCommandLine(commondline);//指令解析int n = BuildExec(argv, argc);if (!n) NormalExecl(argv);}return 0;
}
main
:主程序循環(huán),不斷提示用戶輸入命令。首先獲取并解析命令行輸入,然后判斷是否為內(nèi)建命令,若不是,則調(diào)用NormalExecl
執(zhí)行外部命令。直到quit
被設(shè)置為 1 時,程序結(jié)束。