自行編寫一個簡單的shell!

本文旨在編寫一個簡單的shell外殼程序!功能類似于shell的一些基本操作!雖然不能全部實現shell的一些功能!但是通過此文章,自己寫一個簡單的shell程序也是不成問題!并且通過此文章,可以讓讀者對linux中一些環境變量等基本概念有更深的理解!希望讀完本篇文章能對讀者有一定的收獲!文末會附帶自己編寫shell的源碼!


好的廢話少說,正文開始!

首先我們先來看一下linux中的shell長什么樣子!

這是其shell剛啟動的時候的樣子!其外貌就是一個中括號內部加上一系列的東西!其實當我們認真觀察,不難發現,里面包括的就是“用戶名“+“@”+“主機名字”+“當前工作路徑!”那么發現了此規律之后我們不難實現此描述框!那么接下來我們就著手與這些描述框的實現!

linux描述框的實現!

其中要想獲得我們的用戶名!我們其實可以通過環境變量進行獲取,那么該如何獲取環境變量的值呢?這里就不得不引進一個獲得環境變量的值的函數了!

getenv(“USER”)

通過查詢man手冊,我們可以發現getenv()函數只需要傳遞一個參數即可!那么此參數是什么呢?其實此參數就是我們想要獲得環境變量的值的名字!所以要想獲得用戶名,我們可以直接使用getenv(USER),即可獲得我們想要的用戶名!我們可以驗證一下USER對應的環境變量是否真的是我們所要的環境變量名!我們可以通過echo $USER? 此命令來判斷是否真的是我們想要的用戶名!

不難看出,USER對應的環境變量確實是我們的用戶名!


getenv(“HOSTNAME”)

既然有了用戶名,那么我們的主機名如何獲得呢?思路還是調用getenv(HOSTNAME)操作!獲取主機名!同樣的也可以通過echo命令進行驗證!這里就不再累贅了!


getenv(“PWD”)

最后再來獲取我們的當前工作目錄!也是調用getenv函數!同樣的可以通過echo命令進行驗證!


那么這些基本的環境變量都出來了,我們是否可以通過上述思路來創建一個簡單的描述框呢?

代碼如下:

其中這里為了方便起見,直接將各個函數進行封裝!保證代碼的健壯性!

其中還需要擴充的幾點有:

為了區別與系統的shell,我們在描述框后面加上一個#以區分系統的$ !這樣我們的描述框已經基本實現了!

獲取用戶指令以及將其分割!

那么基本的描述框已經實現了!我們還需要做的一點就是獲取用戶輸入的指令!那么如何獲取用戶的指令呢?思路很簡單:定義一個數組,然后將用戶輸入的字符放到數組中即可!!那么能否用scanf函數呢?答案是肯定不行!因為用戶輸入的指令一般都是指令+選項!其中指令和選項之間都是有著空格來間隔區分的!那么應該如何獲取用戶的輸入呢?答案很簡單,用fgets函數即可,那么接下來我們就來介紹一下fgets函數的用法!!

fgets函數

?通過查詢man手冊可以看出,其中fgets函數中有三個參數,第一個是就是緩沖區即(將要被寫到哪里的地址!)第二個參數表示此緩沖區的大小!第三個參數是用哪些流進行寫入!一般第三個參數我們都選擇(stdin標準輸入流)進行寫入!

既然介紹了fgets函數的用法,那么我們就知道我們需要創建一個數組來存放即將要寫入的數據!數組的大小自己來定義即可!

那么用戶的指令獲取成功之后,我們需要將用戶的指令進行打散然后利用execvp進行替換即可!那么如何進行打散這段字符串呢?這里就不得不引進我們C語言中的strtok函數了!

strtok()函數!

查詢man手冊可以得知,strtok有兩個參數!其中第一個參數是將要打散的原字符串,第二個字符串指的是用于打散的標記符都有哪些。

返回值:第一次調用,返回標記符第一次出現的位置,然后并將標記符轉化為\0,此時會記住此位置!然后再次使用的使用第一個參數只需要傳NULL指針即可!如果最終不可再進行分割的時候,返回值就會返回NULL!這樣就可以將原字符串進行打散!我們的目的是想要將其打散放在一個數組中,方便之后使用!所以我們還需要自己再定義一個指針數組用于存放分割后的各個字符串!

通過以上的思路,我們就可以將用戶命令和將命令打散此功能進行實現了!

代碼如下:

其中第60行是將最后的\n轉化為\0,防止其進行跳行!!其中在commandSplit函數中,我們還設置了條件宏!用于檢查我們的代碼是否將原字符串進行正確的打斷!如果最后不想要打印出分割后的字符串,可以將宏定義取消即可!其中char*out[]表示打散后的數組!!char *in 表示的是原字符串!spint是一個宏定義用來標明分割字符串都有哪些,這里分割符只要空格!

至此,描述框和獲取用戶指令都已經實現了!

完成進程替換!

那么我們如何將用戶的指令轉化為shell的操作呢?這里就得引進進程替換的概念!我們需要將進程進行替換來讓他執行我們想讓他執行的代碼!

那么進程替換有很多中調用方式?我們應該選擇那種呢?其實很簡單!我們已經將用戶的命令行進行打斷分散處理了,所以我們完全可以根據v的特性來進行選擇,又因為我們并不知道用戶以后需要輸入的指令,所以我們也不知道其指令所在路徑,所以我們就可以使用execvp這個系統調用來進行進程替換!其中v我們已經有了!p默認為我們提供了路徑,所以用戶的指令肯定是存放在v[0]上的!所以我們的進程替換就可以寫出來了!

需要注意的是,我們要進行進程替換的時候,一定不要讓我們的父進程進行替換!因為一旦父進程進行替換的時候,如果進程掛掉了,那么我們的shell不就是結束了么,所以我們可以使用fork來創建子進程來進行進程替換,而父進程只需要等待子進程退出,回收其資源即可!

下面來看一下進程替換的代碼!

至此,進程替換的指令也可以實現了,我們自定義的shell程序也能實現ls? top pwd 等操作了!但是對于其他命令我們自定義的shell程序卻不能正確的執行了!例如cd命令,還有export命令!這是為什么呢?這就不得不引進內建命令了!

內建命令

何為內建命令呢?內建命令就是這些命令只能由bash自己執行!而不能讓子進程進行執行!那么我們常見的linux中有哪些命令是內建命令呢,下面就來簡單的介紹幾個內建命令,并且在我們自定義的shell中實現這些內建命令!!

cd命令!!

其中cd是一種常見的內建命令!這個指令只能交付給父進程自己執行,而不能交付給子進程讓子進程執行!因為cd指的就是改變當前的路徑,如果交給子進程進行執行,那么父進程的路徑將不會修改!那么該如何進行編寫我們shell中的cd命令呢?

代碼如下:

其中cd主要進行的操作就是將當前的工作目錄進行修改!那么如何修改當前的工作目錄呢?這里就不得不引進chdir這個系統調用了!

chdir()

其中chdir函數只有一個參數,這個參數代表的是將要修改的路徑!我們只需要定義一個字符數組,然后將我們要修改的路徑存放到此數組中,然后將此數組就進行傳遞即可完成改變當前的路徑!其中還需要將當前的環境變量PWD也進行修改!創建一個臨時數組和全局數組,全局用于存放環境變量的值!然后將修改后的環境變量的值寫入到全局數組中!最后再將環境變量進行同步!只需要調用putenv就可以將環境變量進行修改!

export命令!

還有一個常見的內建命令就是export,那么什么是export呢?export命令就是將我們定義的變量導入到環境變量之中!下面來看一下如何實現我們自己shell的export命令!

代碼如下:

其中我們需要定義一個全局變量的數組用于存放我們的環境變量的值!如果我們使用的是局部變量的話!就會導致每次用戶輸入命令的時候,我們不更新環境變量,其環境變量就會自動消失!這是因為局部變量的局部性!所以定義一個全局變量是最為合適的!但是此代碼也有一個小bug,就是當再導入一個新的環境變量的時候,之前的那個環境變量就會消失!

既然環境變量也能導出了,那么我們總得知道是否真正的將其導出了,這里就得引出了echo命令了,因為此命令也是內建命令,所以也得交給我們的父進程自己執行!下面就來寫一下關于echo命令的代碼!

ehco命令!

其中echo命令簡單分為三個功能!第一個是回顯出退出碼!第二個是顯示出環境變量的值!第三個就是普通的回顯字符串!

對于第一個回顯錯誤碼:我們只需要判斷其分割后的第二個字符串是否是“$”即可!然后根據$后面跟的字符即可判斷出來,如果$后面跟的是"?"字符的話,那就是顯示出退出碼的信息!如果“$”后面跟的不是"?"而是一個字符串!那么就是顯示出其環境變量的值!最后如果連"$"字符都沒有的話,那就是簡單的回顯字符串了!

這就是echo命令實現的簡單邏輯了!

但是我們寫的shell還有大多數功能沒有實現,比如本地變量的存儲,以及重定向的操作!對于本地變量的存儲,我們可以用malloc在堆內申請空間來存儲變量中的值,對于重定向!我們可以利用dup函數進行重定向的操作!下面來看一下簡單的檢查是否有重定向的函數吧!

重定向:

其中SkipSpace是一個宏,其作用就是跳過空格的!我們檢查是否有重定向,順便也能將文件名與命令相分割,然后在execu函數體內進行重定向的操作!

這樣我們的shell也能支持重定向的功能了!

至此我們自己的shell已經初步完成了,它能完成一些簡單的操作!!希望讀完本文,讀者也嘗試寫一下shell的實現!

下面將源碼附在下面!

源碼

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
#define Size 50
#define NUM 1024
#define spint " "
//#define debug 1#define NOredir 0
#define AppendRedir 3
#define InputRedir  1
#define OutputRedir 2#define SkipSpace(pos) do{ while(isspace(*pos)) pos++; }while(0)char *filename=NULL;
int redir=NOredir;
int lastcode=0;
char enval[1024];
char cwd[1024];
// char eni[1024];
const char* getUser()
{char* user=getenv("USER");if(user){return user;}else{return "none";}}const char*getHost()
{char *host=getenv("HOSTNAME");if(host){return host;}else{return "none";}
}char*gethome()
{char *pwd=getenv("PWD");if(pwd){return pwd;}else{return "none";}
}int getcommand(char*command,int n)
{printf("[%s@%s %s]#",getUser(),getHost(),gethome());char*r=fgets(command,n,stdin);if(r==NULL) return 0 ;command[strlen(command)-1]='\0';return 1;
}void commandSplit(char *in,char *out[])
{int argc=0;out[argc++] =strtok(in,spint);while(out[argc++]=strtok(NULL,spint));
#ifdef debug int i=0;for(i=0;out[i];i++){// printf("%d:%s\n",i,out[i]);printf("%s\n",out[i]);}// printf("\n");
#endif
}//只需要將用戶的命令行數組指令傳遞過來即可!
int execute(char* argv[])
{pid_t rit=fork();if(rit==0){int fd=0;if(redir==InputRedir){fd = open(filename, O_RDONLY); // 差錯處理我們不做了dup2(fd, 0);}else if(redir==OutputRedir){fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);dup2(fd, 1);}else if(redir==AppendRedir){fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);dup2(fd, 1);}else{//do nothing}//子進程!用于進程切換!而不是讓父進程bash直接自己運行!//其中進程替換直接用execvp函數即可,因為我們有了用戶的命令行了!execvp(argv[0],argv);exit(0);//如果替換失敗就會退出!負責代表進程替換成功!}else{int status=0;//父進程!只需要等待子進程退出即可!pid_t ret=waitpid(rit,&status,0);if(ret==rit){// printf("wait success\n");lastcode = WEXITSTATUS(status);// printf("%d",lastcode);//  return 0;}}return 0;
}void cd(const char*path)
{chdir(path);char tem[1024];getcwd(tem,sizeof(tem));sprintf(cwd,"PWD=%s",tem);putenv(cwd);}
//檢查是否為內建命令并執行!
int dobuildin(char*argv[])
{//cd命令!if(strcmp(argv[0],"cd")==0){char *path=NULL;if(argv[1]==NULL)  {path=gethome(); }else path=argv[1];cd(path);return 1;}else if(strcmp(argv[0],"export")==0){ if(argv[1]==NULL) return 1; strcpy(enval,argv[1]);//  strcpy(envir,argv[1]);// putenv(envir);//此處需要用全局變量數組來存儲env 因為一旦使用局部變量的時候,會隨著用戶輸入的指令putenv(enval);//此處需要用全局變量數組來存儲env 因為一旦使用局部變量的時候,會隨著用戶輸入的指令//環境變量會消失!return 1;}else if(strcmp(argv[0],"echo")==0){//與系統中的echo保持一致!if(argv[1]==NULL){printf("\n");return 1;}if(*(argv[1])=='$'&&strlen(argv[1])>1){char *val=argv[1]+1;if(strcmp(val,"?")==0){printf("%d\n",lastcode);lastcode=0;}else{char *enval=getenv(val);if(enval) printf("%s\n",enval);else{printf("\n");}// return 1;}return 1;}else{printf("%s\n",argv[1]);return 1;}// return 1;}else if(0){}return 0;
}void checkRedir(char usercommand[], int len)
{// ls -a -l > log.txt// ls -a -l >> log.txtchar *end = usercommand + len - 1;char *start = usercommand;while(end>start){if((*end) == '>'){if(*(end-1) == '>'){*(end-1) = '\0';filename = end+1;SkipSpace(filename);redir = AppendRedir;break;}else{*end = '\0';filename = end+1;SkipSpace(filename);redir = OutputRedir;break;}}else if(*end == '<'){*end = '\0';filename = end+1;SkipSpace(filename); // 如果有空格,就跳過redir = InputRedir;break;}else{end--;}}
}int main()
{while(1){char userCommand[NUM];char* argv[Size];//顯示框架!獲取用戶輸入的指令!int n= getcommand(userCommand,sizeof(userCommand));// if(n==0) continue;// printf("%s")//將用戶的命令進行切割!checkRedir(userCommand,strlen(userCommand));commandSplit(userCommand,argv);//判斷命令是否為內建命令1!int k=dobuildin(argv);if(k) continue;//創建子進程用于進行進程替換!execute(argv);}// return 0;
}

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

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

相關文章

C#基礎面試題集

C#基礎 1. 簡述值類型和引用類型有什么區別2. C# String類型比 stringBuilder 類型的優勢是什么?3.面向對象的三大特點4.請簡述private&#xff0c;public&#xff0c;protected&#xff0c;internal的區別5.結構體和類6.請描述Interface與抽象類之間的不同7.在類的構造函數前…

藍橋杯:貨物擺放--因數存到數組里的技巧--減少運算量的方法

小藍有一個超大的倉庫&#xff0c;可以擺放很多貨物。 現在&#xff0c;小藍有 n 箱貨物要擺放在倉庫&#xff0c;每箱貨物都是規則的正方體。小藍規定了長、寬、高三個互相垂直的方向&#xff0c;每箱貨物的邊都必須嚴格平行于長、寬、高。 小藍希望所有的貨物最終擺成一個大…

go自帶rpc框架生產環境使用demo

基礎使用 序列化使用自帶gob協議 server package mainimport ("net""net/rpc" )// 定義一個handler結構體 type HelloService struct { }// 定義handler方法,大小寫&#xff0c;參數&#xff0c;返回值都是固定的&#xff0c;否則無法注冊 func (receiv…

數據庫事務:保障數據一致性的基石

目錄 1. 什么是數據庫事務&#xff1f; 1.1 ACID特性解析 2. 事務的實現與控制 2.1 事務的開始和結束 2.2 事務的隔離級別 3. 并發控制與事務管理 3.1 并發控制的挑戰 3.2 鎖和并發控制算法 4. 最佳實踐與性能優化 4.1 事務的劃分 4.2 批處理操作 5. 事務的未來發展…

Qt OpenCV 學習(文章鏈接匯總)

Qt OpenCV 學習&#xff08;一&#xff09;&#xff1a;環境搭建 Qt OpenCV 學習&#xff08;二&#xff09;&#xff1a;兩個簡單圖片識別案例 Qt OpenCV 學習&#xff08;三&#xff09;&#xff1a;跟蹤視頻中的運動物體 Qt OpenCV 學習&#xff08;四&#xff09;&#xff…

SpringSecurity6 | 自定義登錄頁面

?作者簡介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;熱愛Java后端開發者&#xff0c;一個想要與大家共同進步的男人&#x1f609;&#x1f609; &#x1f34e;個人主頁&#xff1a;Leo的博客 &#x1f49e;當前專欄&#xff1a; Java從入門到精通 ?特色專欄&#xf…

高工氫電年會 | 未勢能源解超朋博士受邀出席并做主題演講

12月4日&#xff0c;以“戰略重構 商業覺醒”為主題的2023高工氫電年會在深圳舉辦&#xff0c;未勢能源副總裁解超朋博士受邀出席開幕式論壇&#xff0c;以《把握機遇、直面挑戰&#xff0c;迎接氫車規模化推廣時代》為主題發表演講&#xff0c;并參與圓桌論壇研討。 氫勢已來&…

【Linux】resolv.conf 文件

resolv.conf resolv.conf 文件 是 DNS 的 client 端使用的文件&#xff0c;用于設置 DNS 服務器的 ip 地址以及 DNS 域名&#xff0c;還可以配置域名搜索順序等等。主要包含如下關鍵字&#xff1a;nameserver、domain、search、sortlist、options。設置的格式都是 關鍵字空格 …

管理類聯考——數學——真題篇——按知識分類——數據

文章目錄 排列組合2023真題&#xff08;2023-05&#xff09;-數據分析-排列組合-組合-C運算-至少-需反面思考真題&#xff08;2023-08&#xff09;-數據分析-排列組合-相鄰不相鄰-捆綁法插空法-插空法注意空位比座位多1個&#xff0c;是用A&#xff1b;捆綁法內部排序用A&#…

Linux(centos, ubuntu) 快速安裝anaconda;5秒安裝anaconda

1.下載Anaconda安裝腳本: 首先&#xff0c;訪問Anaconda的官方下載頁面&#xff1a;https://www.anaconda.com/products/distribution 在頁面上&#xff0c;選擇適用于Linux的Python 3.x版本的Anaconda安裝腳本。也可以使用wget或curl命令從終端下載。示例&#xff1a; wget …

2023中國(海南)國際高爾夫旅游文化博覽會 暨國際商界峰層·全球華人高爾夫精英巡回賽 全國潁商自貿港行盛大啟幕

2023中國&#xff08;海南&#xff09;國際高爾夫旅游文化博覽會&#xff08;以下簡稱“海高博”&#xff09;暨全國潁商走進海南自貿港于12月7-9日在海口觀瀾湖盛大開幕。該活動由中國國際貿易促進委員會海南省委員會、海南省旅游和文化廣電體育廳主辦&#xff0c;中國國際商會…

C語言中getchar函數

在 C 語言中&#xff0c;getchar() 是一個標準庫函數&#xff0c;用于從標準輸入&#xff08;通常是鍵盤&#xff09;讀取單個字符。它的函數原型如下&#xff1a; int getchar(void);getchar() 函數的工作原理如下&#xff1a; 當調用 getchar() 函數時&#xff0c;它會等待…

最新版本11.17的YOLOv8加入注意力方法

本文基于11.17版本,以往版本略有不同,可查看改進YOLOv8,教你YOLOv8如何添加20多種注意力機制進行參考 放入注意力代碼,以biformer注意力為例 import torch import torch.nn as nn import torch.nn.functional as Fdef position(H, W, is_cuda=

探索 Python 中鏈表的實現:從基礎到高級

# 更多資料獲取 &#x1f4da; 個人網站&#xff1a;ipengtao.com 鏈表是一種基礎的數據結構&#xff0c;它由一系列節點組成&#xff0c;每個節點都包含數據和指向下一個節點的引用。在Python中&#xff0c;可以使用類來實現鏈表&#xff0c;本文將介紹如何實現鏈表&#xff…

c語言編程題經典100例——(90~95例)

1,寫一個函數&#xff0c;實現數字的加密和解密。 下面是一個簡單的C語言函數&#xff0c;可以實現數字的加密和解密。這個函數采用簡單的加密算法&#xff0c;將輸入的數字乘以一個固定的密鑰&#xff0c;然后加上一個固定的偏移量。解密過程就是將加密后的數字減去偏移量&am…

《C++新經典設計模式》之第18章 備忘錄模式

《C新經典設計模式》之第18章 備忘錄模式 備忘錄模式.cpp 備忘錄模式.cpp #include <iostream> #include <vector> #include <memory> using namespace std;// 保存對象內部狀態&#xff0c;必要時恢復 // 在不破壞封裝性的前提下&#xff0c;捕獲對象的內部…

(C)一些題11

1. #include<stdio.h> #include<string.h> void main() { char *s1"ABCDEF"&#xff0c;*s2"aB"&#xff1b; s1; s2; puts(s1)&#xff1b; puts(s2)&#xff1b; printf("%d\n",strcmp(s1,s2))&#xff1b; } 答案&#xff1…

【密碼學引論】認證

認證是許多應用系統中安全保護的第一道設防認證和加密的區別&#xff1a;加密用來確保數據的保密性&#xff0c;而認證用來確保報文發送者和接受者的真實性和報文的完整性。認證和數字簽名的區別&#xff1a; 認證總是基于某種收發雙方共享的保密數據來認證被鑒別對象的真實性&…

關于linux開機自啟動

1、系統啟動流程 2、 init、 inittab、 init.d、 rcx.d /etc/inittab是Linux系統中的一個配置文件&#xff0c;用于定義系統的運行級別和相應的操作。其語法格式如下&#xff1a; 標簽&#xff1a;運行級別&#xff1a;操作&#xff1a;進程 label:runlevel:action:process下面…

每天一點python——day90

#每天一點Python——90 #類的創建 創建類的語法&#xff1a; class 類名&#xff1a;pass【縮進之后寫類里面的內容】 [類里面寫什么沒有想好之前&#xff0c;可以用pass進行占位.可以不報錯]#演示&#xff1a; class Lei:pass #以上就上一個類被創建的樣例注意事項&#xff1a…