【Linux系統】從零開始構建簡易 Shell:從輸入處理到命令執行的深度剖析

在這里插入圖片描述

文章目錄

  • 前言
    • 一、打印命令行提示符
      • 代碼功能概述
    • 二、讀取鍵盤輸入的指令
      • 2.1 為什么不繼續使用`scanf()`而換成了`fgets()`?
      • 2.2 調試輸出的意義
      • 2.3 為什么需要去掉換行符?
    • 三、指令切割
      • 補充知識: `strtok` 的函數原型
    • 四、普通命令的執行
      • 代碼功能概述
    • 五、內建指令執行
      • 代碼功能概述
  • 結語


前言

在操作系統的世界里,Shell 作為用戶與系統交互的橋梁,扮演著至關重要的角色。無論是資深的開發者,還是對系統運維感興趣的新手,了解 Shell 的工作原理和實現機制都能極大地加深對操作系統底層運行邏輯的理解。

本文將帶領大家深入探索簡易 Shell 的實現過程,從最基礎的打印命令行提示符開始,逐步實現讀取用戶輸入、切割指令、執行普通命令以及處理內建指令等核心功能。每一步都配有詳細的代碼解析和關鍵知識點說明,不僅讓你知其然,更能知其所以然。通過閱讀本文,你將掌握 Shell 實現的關鍵技術,理解其中涉及的編程技巧和系統調用原理,同時也能體會到從無到有構建一個實用工具的樂趣與成就感。


為了方便理解我把源代碼發給大家。(單擊藍色字體)

一、打印命令行提示符

#define MARK ":"
#define LABLE "$"
#define LINE_SIZE 1024char commandline[LINE_SIZE]; // 用于存儲輸入的命令行
char pwd[LINE_SIZE]; // 當前工作目錄// 獲取用戶名
const char* getusername()
{return getenv("USER");
}// 獲取主機名
const char* gethostname()
{return getenv("HOSTNAME");
}// 獲取當前工作目錄并進行格式化
void getpwd()
{getcwd(pwd, sizeof(pwd)); // 獲取當前工作目錄const char* home = getenv("HOME"); // 獲取用戶主目錄if (home != NULL && strcmp(pwd, home) == 0){// 如果當前目錄等于用戶主目錄,則返回 "~"strcpy(pwd, "~");}else if (home != NULL && strncmp(pwd, home, strlen(home)) == 0){// 如果當前目錄是用戶主目錄的子目錄,則替換主目錄部分為 "~"static char relative_path[LINE_SIZE];snprintf(relative_path, sizeof(relative_path), "~%s", pwd + strlen(home));strcpy(pwd, relative_path);}
}int main()
{getpwd();printf("%s@%s"MARK"%s"LABLE" ", getusername(), gethostname(), pwd);scanf("%s", commandline);                                                                               return 0;
}

代碼功能概述

  1. 定義宏和變量:

    • MARKLABLE 定義了提示符中的分隔符 :$,用于提示符的格式化輸出。
    • LINE_SIZE 設置了緩沖區大小(1024),用于存儲命令行輸入和當前工作目錄。
    • 全局變量 commandlinepwd
      • commandline: 存儲從用戶輸入讀取的命令行。
      • pwd: 存儲當前工作目錄。
  2. 獲取環境變量:

    • 函數 getusername()gethostname() 分別通過調用 getenv() 獲取環境變量 USERHOSTNAME,用于表示用戶名和主機名。
  3. 格式化當前工作目錄:

    • 函數 getpwd()

      獲取當前工作目錄并將其格式化:

      • 如果當前目錄是用戶的主目錄(HOME),用 ~ 替代完整路徑。
      • 如果當前目錄是主目錄的子目錄,則用 ~ 替代主目錄部分。
      • 否則顯示完整路徑。
  4. 打印命令行提示符:

    • main() 函數中調用 getpwd() 獲取當前工作目錄,然后通過 printf()

      按以下格式打印提示符:

      在這里插入圖片描述

  5. 等待用戶輸入:

    • 程序通過 scanf() 等待用戶輸入命令,將輸入存儲到 commandline 中。

二、讀取鍵盤輸入的指令

// 交互式模式,顯示提示符并讀取用戶輸入
void interact(char* cline, int size)
{getpwd(); // 獲取當前工作目錄printf("%s@%s"MARK"%s"LABLE" ", getusername(), gethostname(), pwd); // 顯示提示符char* s = fgets(cline, size, stdin); // 讀取用戶輸入assert(s); // 確保輸入不為空printf("echo: %s\n", s); // 測試能否讀取成功cline[strlen(cline)-1] = '\0'; // 去掉末尾的換行符
}int main()
{while(1) // 循環運行直到用戶退出{// 1. 獲取用戶輸入的命令行interact(commandline, sizeof(commandline));}return 0; 
}

2.1 為什么不繼續使用scanf()而換成了fgets()

scanf 通常用于格式化輸入,但它在處理用戶輸入時存在一些顯著的缺點:

  • scanf 默認以空格、制表符或換行符作為輸入的分隔符,因此只能讀取一個單詞(或無空格的字符串)。
  • 在交互式 Shell 中,用戶輸入的命令往往由多個部分組成(如命令和參數),scanf 無法正確讀取整行命令。
  • scanf 不會自動限制輸入長度,如果用戶輸入超出緩沖區大小,就會導致緩沖區溢出,進而引發未定義行為甚至安全漏洞。

2.2 調試輸出的意義

加調試輸出 printf("echo: %s\n", s) 是為了測試程序的輸入功能是否正常運行,具體驗證以下幾點:

  1. 提示符顯示后,用戶是否能輸入數據
    • 如果 s == NULL,則說明 fgets() 未能成功讀取輸入(可能是因為輸入錯誤或用戶直接按下 Ctrl+D)。
  2. 輸入是否被正確存儲到緩沖區 cline
    • 打印輸入內容以確保其正確性。
  3. 緩沖區大小是否足夠
    • 如果輸入過長,可能會導致緩沖區溢出或截斷。

調試結果
在這里插入圖片描述

2.3 為什么需要去掉換行符?

  • 用戶通過鍵盤輸入時,輸入內容會帶有換行符(\n),這是因為按下回車鍵會在輸入的末尾自動添加一個換行符。

  • 例如,用戶輸入 ls -l 后,緩沖區中的數據實際是:

    ls -l\n\0
    
    • \n 是換行符。
    • \0 是字符串的終止符。
  • 在處理命令時,換行符通常是多余的:

    • 它會影響字符串的比較。例如,strcmp(command, "exit") 會返回不匹配,因為字符串實際是 "exit\n"
    • 如果直接打印字符串,換行符會造成多余的空行。
    • 某些函數(如文件名或路徑相關函數)可能會因為換行符導致邏輯錯誤。

三、指令切割

#define DELIM " "	// 分隔符
#define ARGC_SIZE 32char *argv[ARGC_SIZE]; // 用于存儲命令行參數// 將輸入的命令行分割為參數數組
int splitstring(char* cline, char* _argv[])
{int i = 0;argv[i++] = strtok(cline, DELIM); // 分割第一個參數while(_argv[i++] = strtok(NULL, DELIM)); // 分割剩余的參數return i - 1; // 返回參數個數
}int main()
{while(1) // 循環運行直到用戶退出{// 1. 獲取用戶輸入的命令行interact(commandline, sizeof(commandline));// 2. 分割命令行字符串為指令和參數int argc = splitstring(commandline, argv);// 調試代碼for(int i = 0; argv[i]; i++){printf("[%d]->%s\n", i, argv[i]);}// 3. 如果沒有輸入指令(空行),跳過本次循環if(argc == 0) continue;}return 0; 
}

補充知識: strtok 的函數原型

char *strtok(char *str, const char *delim);
  • 參數:
    1. char *str:
      • 第一次調用時傳入需要分割的字符串。
      • 后續調用傳入 NULL 表示繼續上一次的分割。
    2. const char *delim:
      • 一個以 \0 結尾的字符串,表示分割的分隔符集合(例如,空格、逗號等)。
  • 返回值:
    • 成功:返回一個指向分割后的子字符串(token)的指針。
    • 失敗:如果沒有更多的子字符串可以返回,則返回 NULL

注意點:循環結束時,i 的值比實際的參數個數多 1(因為最后一次分割返回 NULL)。

因此,用 i - 1 表示參數的實際個數。

調試結果

在這里插入圖片描述

四、普通命令的執行

#define EXIT_CODE 44extern char **environ; // 環境變量
int lastcode = 0; // 上次命令的退出狀態
int quit = 0; // 是否退出// 執行外部命令
void normalExcute(char* _argv[])
{pid_t id = fork(); // 創建子進程if(id < 0){perror("fork"); // 創建失敗,打印錯誤信息return;}if(id == 0) // 子進程{execvpe(_argv[0], _argv, environ);exit(EXIT_CODE);}else // 父進程{int status = 0;waitpid(id, &status, 0); // 等待子進程完成if (WIFEXITED(status)) // 檢查子進程是否正常退出{lastcode = WEXITSTATUS(status); // 獲取子進程的退出狀態}}
}int main()
{while(!quit) // 循環運行直到用戶退出{// 1. 獲取用戶輸入的命令行interact(commandline, sizeof(commandline));// 2. 分割命令行字符串為指令和參數int argc = splitstring(commandline, argv);// 3. 如果沒有輸入指令(空行),跳過本次循環if(argc == 0) continue;// 4.執行普通外部指令normalExcute(argv);}return 0; 
}

代碼功能概述

功能:執行外部命令并處理子進程的退出狀態。

關鍵點

  1. fork 創建子進程
  2. execvpe 替換子進程執行映像
  3. waitpid 等待子進程結束并處理退出狀態

結果:命令執行結果的退出碼存儲在 lastcode 中供后續使用。

調試結果

在這里插入圖片描述

五、內建指令執行

char myenv[LINE_SIZE]; // 用于存儲導出的環境變量// 構建內置命令
int buildCommand(char* _argv[], int _argc)
{// 內置命令:cdif(_argc == 2 && strcmp(_argv[0], "cd") == 0){getpwd();chdir(_argv[1]); // 改變工作目錄sprintf(getenv("PWD"), "%s", pwd); // 更新環境變量PWDreturn 1; // 返回已處理標志}// 內置命令:exportelse if(_argc == 2 && strcmp(_argv[0], "export") == 0){strcpy(myenv, _argv[1]); // 保存環境變量putenv(myenv); // 設置環境變量return 1; // 返回已處理標志}// 內置命令:echoelse if(_argc == 2 && strcmp(_argv[0], "echo") == 0){if(strcmp(_argv[1], "$?") == 0) // 顯示上一個命令的退出狀態{printf("%d\n", lastcode);lastcode = 0; // 重置退出狀態}else if(*_argv[1] == '$') // 顯示環境變量的值{char* val = getenv(_argv[1] + 1);if(val) printf("%s\n", val);}else printf("%s\n", _argv[1]); // 直接打印參數return 1; // 返回已處理標志}// 自動為 ls 添加 --color 參數if(strcmp(_argv[0], "ls") == 0){_argv[_argc++] = "--color";_argv[_argc] = NULL; // 確保參數數組以 NULL 結束}return 0; // 返回未處理標志
}int main()
{while(!quit) // 循環運行直到用戶退出{// 1. 獲取用戶輸入的命令行interact(commandline, sizeof(commandline));// 2. 分割命令行字符串為指令和參數int argc = splitstring(commandline, argv);// 3. 如果沒有輸入指令(空行),跳過本次循環if(argc == 0) continue;// 4. 嘗試執行內置命令int n = buildCommand(argv, argc);// 5. 如果不是內置命令,執行普通外部指令if(!n) normalExcute(argv);}return 0; 
}

代碼功能概述

該函數 buildCommand 的功能是處理內置命令,包括 cdexportecho,并對特定外部命令(如 ls)添加額外參數。如果輸入的命令屬于內置命令范圍,函數會執行相應邏輯并返回已處理標志;否則返回未處理標志,交由其他部分(如外部命令執行器)處理。

  1. 處理內置命令:cd

    if (_argc == 2 && strcmp(_argv[0], "cd") == 0)
    
    • 檢查用戶輸入的命令是否是 cd,且參數個數為 2(命令名 + 目標目錄)。
    • 功能:
      • 使用 chdir 改變當前工作目錄為用戶指定的路徑(_argv[1])。
      • 更新環境變量 PWD,同步當前工作目錄的變化。
    • 實現思路:要注意 cd 命令是由 bash 本身去做,而不是創建一個子進程去做,故而需要改變的是當前可執行程序的工作目錄,并且需要將環境變量中的 PWD 改變。
    • 測試:
      在這里插入圖片描述
  2. 處理內置命令:export

    else if (_argc == 2 && strcmp(_argv[0], "export") == 0)
    
    • 檢查用戶輸入的命令是否是 export,且參數個數為 2。
    • 功能:
      • 設置環境變量,將用戶輸入的 變量=值 格式字符串存儲到全局變量 myenv
      • 調用 putenv 將該變量添加到環境變量中。
    • 實現思路:我們輸入的環境變量實際上是保存在commandline當中,只要當下一次輸入指令,上一次定義的環境變量就會被清空。putenv 添加環境變量,并不是把對應的字符串深拷貝到系統的環境變量表當中,而是把該字符串的地址保存在系統的環境變量表中(淺拷貝)。因此我們要確保保存環境變量字符串的那個地址里的環境變量不會被修改,所以我們需要為用戶輸入的環境變量,也就是那一串字符串單獨開辟一塊空間進行存儲,保證在內次重新輸入指令的時候,不會影響到之前用戶添加的環境變量。所以我們需要定義一個二維數組用于存儲導出的環境變量(這里只簡單地分配了一維數組)。
    • 測試:
      在這里插入圖片描述
      在這里插入圖片描述
      注意看,連續兩次的寫入導致第一次的定義的環境變量被覆蓋了。
  3. 處理內置命令:echo

    else if (_argc == 2 && strcmp(_argv[0], "echo") == 0)
    
    • 檢查用戶輸入的命令是否是 echo,且參數個數為 2。
    • 功能:
      • 如果參數為 "$?",顯示上一個命令的退出狀態(從全局變量 lastcode 獲取)。
      • 如果參數以 $ 開頭,顯示對應環境變量的值。
      • 否則,直接打印參數內容。
    • 測試:
      在這里插入圖片描述
      在這里插入圖片描述
      故意寫成 ll (沒有定義的),導致子進程退出,退出碼剛好是44。
  4. 處理外部命令:自動為 ls 添加 --color 參數

    if (strcmp(_argv[0], "ls") == 0)c
    
    • 檢查用戶輸入的命令是否是 ls
    • 功能:
      • 自動為命令添加 --color 參數,用于增強可讀性(適用于 Linux 的 ls 命令)。
      • 確保參數數組以 NULL 結束。
    • 測試:
      在這里插入圖片描述
      總結:說了這么久的環境變量,那么請問我們登錄的時候,系統中的 shell 的環境變量又是從哪里來的呢?答案是 Bash。那么 Bash 的環境變量又是從何而來?當然是系統自帶的目錄文件中寫入的。

結語

通過以上對簡易 Shell 實現過程的詳細講解,相信大家對 Shell 的工作流程和實現細節已經有了較為全面的認識。從命令行提示符的設計,到輸入指令的處理,再到不同類型命令的執行,每一個環節都凝聚著操作系統與編程的智慧。

雖然本文實現的 Shell 只是一個簡化版本,但其中涉及的技術和思想為進一步探索更復雜、功能更強大的 Shell,乃至深入理解操作系統的運行機制奠定了堅實的基礎。希望大家能將所學應用到實際開發或探索中,不斷挖掘操作系統的奧秘。如果在閱讀過程中有任何疑問或想法,歡迎在評論區交流分享,也別忘了點贊、收藏并持續關注后續更多精彩的技術內容!

天的分享到這里就結束啦!如果覺得文章還不錯的話,可以三連支持一下,17的主頁還有很多有趣的文章,歡迎小伙伴們前去點評,您的支持就是17前進的動力!

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/82452.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/82452.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/82452.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

湖倉一體架構在金融典型數據分析場景中的實踐

在數字經濟與金融科技深度融合的今天&#xff0c;數據已成為金融機構的核心戰略資產。然而&#xff0c;傳統數據架構面臨著三大困局&#xff0c;制約著金融機構數據價值的充分釋放。 一、需求驅動更多銀行數據分析場景 金融機構&#xff0c;特別是銀行業&#xff0c;面臨著雙重…

基于Llama3的開發應用(一):Llama模型的簡單部署

Llama模型的簡單部署 0 前言1 環境準備1.1 硬件環境1.2 軟件環境 2 Meta-Llama-3-8B-Instruct 模型簡介2.1 Instruct含義2.2 模型下載 3 簡單調用4 FastAPI 部署4.1 通過FastAPI簡單部署4.2 測試 5 使用 streamlit 構建簡易聊天界面6 總結 0 前言 本系列文章是基于Meta-Llama-…

模擬太陽系(C#編寫的maui跨平臺項目源碼)

源碼下載地址&#xff1a;https://download.csdn.net/download/wgxds/90789056 本資源為用C#編寫的maui跨平臺項目源碼&#xff0c;使用Visual Studio 2022開發環境&#xff0c;基于.net8.0框架&#xff0c;生成的程序為“模擬太陽系運行”。經測試&#xff0c;生成的程序可運行…

基于人工智能的個性化 MySQL 學習路徑推薦研究

基于人工智能的個性化 MySQL 學習路徑推薦研究 摘要: 隨著信息技術的飛速發展,數據庫在各行業應用廣泛,MySQL 作為主流數據庫之一,學習需求龐大。然而,不同學習者在知識水平、學習進度和目標上存在差異,傳統統一的學習路徑難以滿足個性化需求。本研究通過運用人工智能技…

OSPF綜合應用

? 要求&#xff1a; 1&#xff0c;R5為ISP&#xff0c;其上只能配置IP地址&#xff1b;R4作為企業邊界路由器&#xff0c; 出口公網地址需要通過PPP協議獲取&#xff0c;并進行chap認證 2&#xff0c;整個OSPF環境IP基于172.16.0.0/16劃分&#xff1b; 3&#xff0c;所有設備…

中國古代史1

朝代歌 三皇五帝始&#xff0c;堯舜禹相傳。 夏商與西周&#xff0c;東周分兩段。 春秋和戰國&#xff0c;一統秦兩漢。 三分魏蜀吳&#xff0c;二晉前后延。 南北朝并立&#xff0c;隋唐五代傳。 宋元明清后&#xff0c;皇朝至此完。 原始社會 元謀人&#xff0c;170萬年前…

ensp的華為小實驗

1.先進行子網劃分 2.進行接口的IP地址配置和ospf的簡易配置&#xff0c;先做到全網小通 3.進行ospf優化 對區域所有區域域間路由器進行一個匯總 對區域1進行優化 對區域2.3進行nssa設置 4.對ISP的路由進行協議配置 最后ping通5.5.5.5

華為OD機試真題——荒島求生(2025A卷:200分)Java/python/JavaScript/C/C++/GO最佳實現

2025 A卷 200分 題型 本專欄內全部題目均提供Java、python、JavaScript、C、C、GO六種語言的最佳實現方式&#xff1b; 并且每種語言均涵蓋詳細的問題分析、解題思路、代碼實現、代碼詳解、3個測試用例以及綜合分析&#xff1b; 本文收錄于專欄&#xff1a;《2025華為OD真題目錄…

IOC和Bean

IOC IOC將對象的創建&#xff0c;依賴關系的管理和生命周期的控制從應用程序代碼中解耦出來了 IOC容器的依賴注入(DI) 在程序運行過程中動態的向某個對象中注入他所需要的其他對象 依賴注入是基于反射實現的 Spring IOC 容器使用的是Map&#xff08;concorrentMap&#xff…

vue3: pdf.js 2.16.105 using typescript

npm create vite vuepdfpreview //創建項目npm install vue-pdf-embed npm install vue3-pdfjs npm install pdfjs-dist2.16.105 <!--* |~~~~~~~|* | |* | |…

Java面試全棧解析:Spring Boot、Kafka與Redis實戰揭秘

《Java面試全棧解析&#xff1a;Spring Boot、Kafka與Redis實戰揭秘》 【面試現場】 面試官&#xff1a;&#xff08;推了推眼鏡&#xff09;小張&#xff0c;你簡歷里提到用Spring Boot開發過微服務系統&#xff0c;能說說自動配置的實現原理嗎&#xff1f; 程序員&#xff1…

常見的提示詞攻擊方法 和防御手段——提示詞注入(Prompt Injection)攻擊解析

提示詞注入&#xff08;Prompt Injection&#xff09;攻擊解析 提示詞注入是一種針對大型語言模型&#xff08;LLM&#xff09;的新型攻擊手段&#xff0c;攻擊者通過精心設計的輸入文本&#xff08;提示詞&#xff09;操控AI模型的輸出&#xff0c;使其執行非預期行為或泄露敏…

基于NI-PXI的HIL系統開發

基于NI-PXI平臺的汽車電控單元HIL系統開發全解析 引言&#xff1a;HIL系統如何成為汽車電控開發的“效率倍增器”&#xff1f; 某車企通過基于NI-PXI的HIL系統&#xff0c;將懸架控制器的測試周期從3個月壓縮至2周&#xff0c;故障檢出率提升65%。這背后是硬件在環技術對汽車電…

復合機器人案例啟示:富唯智能如何以模塊化創新引領工業自動化新標桿

在國產工業機器人加速突圍的浪潮中&#xff0c;富唯智能復合機器人案例憑借其高精度焊接與智能控制技術&#xff0c;成為行業標桿。然而&#xff0c;隨著制造業對柔性化、全場景協作需求的升級&#xff0c;復合機器人正從單一功能向多模態協同進化。作為這一領域的創新者&#…

如何使用極狐GitLab 軟件包倉庫功能托管 python?

極狐GitLab 是 GitLab 在中國的發行版&#xff0c;關于中文參考文檔和資料有&#xff1a; 極狐GitLab 中文文檔極狐GitLab 中文論壇極狐GitLab 官網 軟件包庫中的 PyPI 包 (BASIC ALL) 在項目的軟件包庫中發布 PyPI 包。然后在需要將它們用作依賴項時安裝它們。 軟件包庫適用…

K8s中的containerPort與port、targetPort、nodePort的關系:

pod中的containerPort與service中的port、targetPort、nodePort的關系&#xff1a; 1、containerPort為pod的配置&#xff0c;對應pod內部服務監聽的具體端口&#xff0c;例如nginx服務默認監聽80端口&#xff0c;那么nginx的pod的containerPort應該配置為80&#xff0c;例如m…

面試題:QTableView和QTableWidget的異同

目錄 1.QTableView簡介 2.QTableWidget簡介 3.QTableView和QTableWidget不同 4.總結 1.QTableView簡介 QTableView是一個基于模型-視圖架構的表格控件&#xff0c;用于展示表格形式的數據。同樣需要關聯一個QAbstractTableModel或其子類&#xff08;如QStandardItemModel&a…

smbd:快速拉取服務端SMB共享文件腳本工具

地址:https://github.com/MartinxMax/smbd smbd 是一款簡單的 Bash 腳本&#xff0c;用於自動化從共享目錄透過 SMB/CIFS 協議下載檔案的過程。此工具設計用來與遠端 SMB 分享進行互動&#xff0c;並將其內容下載到本地目錄中。 環境需求 $ apt install smbclient 使用方式 …

MiInsertVad函數分析之nt!MMVAD結構

MiInsertVad函數分析之nt!MMVAD結構 1: kd> dt nt!MMVAD 89520270 0x000 u1 : __unnamed 0x004 LeftChild : (null) 0x008 RightChild : (null) 0x00c StartingVpn : 0x2b0 0x010 EndingVpn : 0x2c5 0x014 u …

OSPF不規則區域劃分

1、建立一條虛鏈路 vlink 可以被視為是??區域的?段延伸。 這?條虛擬的鏈路&#xff0c;只能夠跨域?個???區域。 [r2-ospf-1-area-0.0.0.1]vlink-peer 3.3.3.3 [r3-ospf-1-area-0.0.0.1]vlink-peer 2.2.2.2 在沒有建立虛鏈路之前,r1是不能ping r4的。vlink建?的鄰居關…