Linux->自定義shell

目錄

引入:

1:shell是什么?

2:命令行提示符是什么?

3:xshell是什么?

一:命令行提示符 ?

二:?獲取用戶輸入

三:分割字符串

四:執行命令

五:內建處理

1:內建指令-->cd

2:內建指令-->export

3:內建指令-->echo

六:重定向

1:先遍歷查找到重定向符號

2:賦值?redir_type 和 將重定向符號置為'\0'

3:獲取到文件名字

七:上色

八:總代碼

1:makefile

2:myshell.c?

九:Ubuntuhb版本下的差異

1:獲取主機名的方式

2:open函數的權限


引入:

1:shell是什么?

Shell 是操作系統內核(Kernel)與用戶之間的接口,負責接收用戶輸入的命令,解釋并傳遞給操作系統執行,同時返回結果。它是操作系統的“外殼”!既然你是一個接口,就代表你是一個程序!

2:命令行提示符是什么?

是使用 Shell 的文本交互界面,也就是我們一直輸入指令的地方。所以我們不斷的輸入指令,其實本質就是不斷的在調用shell這個程序,來對我們的指令作解析;所以shell內部一定自啟動的同時還是自循環的,否則我們怎么可以不斷的只用寫指令,還從來沒有顯式的調用這個程序?

3:xshell是什么?

Xshell 是一款終端模擬器軟件(Windows 平臺),用于遠程連接和管理服務器;其作用是提供一個圖形化界面,讓用戶通過命令行與遠程服務器交互!本質上是一個“橋梁”,將本地輸入的命令傳遞給遠程服務器的 Shell 執行!

類比說明:

  • Shell?像“翻譯官”:負責將你的命令翻譯成系統能理解的操作。

  • 命令行提示符?像“對話的紙張”:是你寫命令和看結果的媒介。

  • Xshell?像“電話”:讓你(本地)能遠程和另一臺計算機的“翻譯官”(Shell)對話。

Q1:所以自定義shell的本質是什么?

A1:其實就是實現shell外殼罷了,也就是寫一個接口(程序),讓其能夠對指令做出正確的反應,我們不在對自帶的shell外殼發送指令,而是要對我們自己的寫的程序發送指令,其也能做出正確反應!所以我們要知道的是,我們的命令行提示符也是shell程序的一部分,因為shell程序運行了起來,給我們顯示了命令行提示符,我們才知道在哪里輸入指令!

所以現在需要明白的是,即使OS都是Linux,但是在不同的發行版下,命令行提示符會有所差距,所以針對不對的發行版的shell自定義就會有所差距!

Q2:發行版是什么意思?

A2:就像同樣使用汽油發動機,但豐田和寶馬的發動機的設計不同;所以同時是Linux,但是CentOS和Ubuntuh的設計不同,CentOS?更適合企業使用,Ubuntuh更適合個人使用,當然區別不止這一點,我們只用知道二者的命令行提示符有區別,所以其的自定義shell就有區別,所以我們可以先見識一下二者的命令行提示符的區別:

注:二者都是wtt1這個普通用戶在/home/wtt1/dir1下的命令行提示符!

①:CentOS的命令行提示符

[wtt1@hcss-ecs-1a2a dir1]$
部分說明
[wtt1?或?wtt1當前登錄用戶名(wtt1),方括號?[ ]?是 CentOS 的常見風格。
@hcss-ecs-1a2a主機名(假設與之前示例一致)。
dir1當前目錄名(位于?/home/wtt1/dir1?下,~?被替換為實際目錄名)。
]$?或?$提示符符號:$?表示普通用戶,#?表示 root 用戶。

②:Ubuntuh的命令行提示符

wtt1@hcss-ecs-1a2a:~/dir1$
  • wtt1:當前登錄的用戶名(這里是?wtt1)。

  • @:分隔符,表示“在”哪臺主機上。

  • hcss-ecs-1a2a:主機名(Hostname),即這臺計算機的名稱。

  • ::分隔符。

  • ~:當前工作目錄(~?是用戶主目錄的簡寫,完整路徑通常是?/home/wtt1)。

  • $:提示符符號,表示當前是普通用戶身份。如果是?#,則表示超級用戶(root)

?

解釋:可以看出二者的差距不大,我們實現的是CentOS下的shell!當然,自帶的shell程序時非常強大的,其對任何指令都能做出正確反應,我們是肯定做不到的,因為我們連指令都沒有全部見過,所以此篇自定義shell只是對常見的指令做出反應,重點是在實現的過程中體會shell的本質!

注:博客會涉及到很多知識,所以務必先看這兩篇博客:

Linux->進程控制(精講)-CSDN博客(先看)

Linux->基礎IO-CSDN博客

一:命令行提示符 ?

第一步肯定是向用戶打印出命令行提示符,所以我們需要獲取到三個環境變量,USER(用戶名),PWD(當前工作目錄),以及HOSTNAME(主機名),我在實現的時候,不會像引入中標準的命令行提示符那樣省去部分路徑,而是直接打印絕對路徑,一是因為這樣會簡化代碼,二是在后面能夠通過絕對路徑反應出代碼的不足,所以望周知!

代碼如下:

#include <stdio.h>
#include <stdlib.h>//getenv();//獲取環境變量-->主機名
const char* HostName()
{char *hostname = getenv("HostName");if(hostname) return hostname;//對獲取失敗的處理else return "None";
}//獲取環境變量-->用戶名
const char* UserName()
{char *hostname = getenv("USER");if(hostname) return hostname;//對獲取失敗的處理else return "None";
}//獲取環境變量-->當前工作目錄
const char *CurrentWorkDir()
{char *hostname = getenv("PWD");if(hostname) return hostname;//對獲取失敗的處理else return "None";
}int main()
{// 1. 打印命令行提示符printf("[%s@%s %s]$ ",UserName(),HostName(), CurrentWorkDir());printf("\n");return 0;
}

運行效果:

代碼很簡單,獲取三個環境變量然后在通過指定的排版和格式打印出來我們的命令行提示符

二:?獲取用戶輸入

給用戶展示了命令行提示符,那么用戶就開始輸入其指令了,用戶輸入的指令本質是字符串,如果指令帶有選項,則是一個帶有空格字符的字符串,所以我們現在要先獲取到用戶輸入的指令字符串

不能用scanf來獲取,scanf默認讀取到空格就會停止,所以我們采取fgets來獲取,為什么用fgets來獲取,一是因為其能夠讀取到含有空格的整個字符串,而是因為其是C的接口,我們知道Linux的底層代碼99%是C,所以我們不用getline這種C++接口

代碼如下:

#include <stdio.h>
#include <stdlib.h>//getenv();//上方部分代碼省略 .....//交互函數
//打印命令行提示符+獲取用戶輸入的指令字符串
void Interactive(char out[], int size)
{//輸出提示符printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());//獲取用戶輸入的命令字符串"ls -a -l"fgets(out, size, stdin);}int main()
{   // 1. 打印命令行提示符,獲取用戶輸入的命令字符串char commandline[SIZE];Interactive(commandline, SIZE);return 0;
}

解釋:我們創建了一個譯為交互的函數,其把打印命令行提示符和獲取用戶輸入的命令字符串這兩件事情一起做了!

fgets函數:

char *fgets(char *str, int size, FILE *stream);

參數:?

  • str:存儲讀取數據的字符數組(緩沖區)。

  • size:最多讀取的字符數(包括結尾的?\0)。

  • stream:輸入流(如?stdin、文件指針等)。

注:所以size一般就會設定為str的大小,這樣可以防止緩沖區溢出

返回值

  • 成功:返回?str(即傳入的緩沖區地址)。
  • 失敗或到達文件末尾:返回?NULL

fgets的特點:

fgetsh函數始終會在讀取的字符序列末尾,自動添加 '\0',確保其輸出是一個合法的C風格字符串

所以當我們接收到指令后再打印一下接收到的指令,其效果如下:

// 打印命令行提示符+獲取用戶輸入的指令字符串
void Interactive(char out[], int size)
{printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());fgets(out, size, stdin);printf("%s\n",out);
}

乍一看沒有錯,其實已經錯了,printf只有一次換行,為什么結果卻有兩次?

Q:首先是肯定已經讀取到了,但是為什么中間會有一個空行呢?

A:是因為我們輸入指令的時候 末尾字符a的后面 我們還會輸入一個'\n',也就是回車,fgets會保留讀取到整個字符串,包括回車'\n',然后再在其最后面加上'\0',所以才會顯示一個空行,那么現在我們就要手動的將'\n'位置直接置為'\0'即可解決問題!

代碼如下:


//上方部分代碼省略 .....//打印命令行提示符+獲取用戶輸入的指令字符串
void Interactive(char out[], int size)
{printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());// 輸出提示符并獲取用戶輸入的命令字符串"ls -a -l"fgets(out, size, stdin);out[strlen(out)-1] = 0; //將'\n'置為'\0'printf("%s\n",out);}int main()
{   // 1. 打印命令行提示符,獲取用戶輸入的命令字符串char commandline[SIZE];Interactive(commandline, SIZE);return 0;
}

解釋:字符串有5個字符,則strlen返回5,所以4就是'\n'所處的位置,將其置為'\0'即可!

效果如下:

這就對了,只有一次printf里面的換行!

三:分割字符串

現在我們獲取到了一整個字符串,所以我們下一步要做的就是分割字符串,為什么要分割,本質就是你分割了,你才能夠再下一步執行指令的時候,直到哪部分是指令本身,哪部分是指令的選項,比如"ls -a",就應該分割成"ls"和"-a"才對!

現在要明白的是,假設已經分割完成了,我們要去執行指令,其實本質一定是要進行進程替換的,不然你怎么能執行指令對應的程序呢,既然是進程替換,那么就一定要使用進程替換的接口,而在之前的博客中已經介紹過了,程序替換的接口不管你是采用數組還是列表的形式傳參,你的最后一個元素一定是NULL

舉個例子:

所以切割字符串我們采取的C接口是strtok

char *strtok(char *str, const char *delim);
  • 功能:將字符串?str?按分隔符?delim?切割成多個子字符串(token)。

  • 首次調用:傳入待切割的字符串?str

  • 后續調用:傳入?NULL,繼續切割同一字符串。

  • 返回值

    • 成功:返回下一個子字符串的指針。

    • 失敗/無更多子字符串:返回?NULL

解釋:

所以這個函數挺有意思的,第二次往后你再想對同一個字符串調用,你的第一次參數必須為NULL!其次當切割完最后一次的時候,再往下切割,其就會失敗,會返回NULL,這個NULL正好可以被利用起來,作為我們數組的最后一個元素,這樣調用進程替換的接口,我們直接可以將數組傳過去,方便又省事!

代碼如下:

//省略部分代碼....#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "char *argv[MAX_ARGC];//設置一個上限,指令最多被切割成64份 已經完全夠用了//分割字符串
void Split(char in[])
{int i = 0;argv[i++] = strtok(in, SEP); // "ls -a -l"while(argv[i++] = strtok(NULL, SEP)); // 故意將== 寫成 =}int main()
{// 1. 打印命令行提示符,獲取用戶輸入的命令字符串char commandline[SIZE];Interactive(commandline, SIZE);// 2. 對命令行字符串進行切割Split(commandline);return 0;
}

解釋:

由于strtok函數的特性,所以其第一次一定是在while循環外面的,另外while條件的這種寫法,可以簡單完美的完成切割且賦值給數組,并且最后一次strtok函數返回NULL的時候,也會賦給數組,最后整個表達式的結果為NULL,while循環退出,這代碼才叫優雅~

下面我們在Split函數里面打印一下數組內容,看下是否正確完成了切割:

完美~

其次我們在main中加入一個while死循環,因為自帶的shell就是一直循環等待輸入的

int main()
{while (1){// 1. 打印命令行提示符,獲取用戶輸入的命令字符串char commandline[SIZE];Interactive(commandline, SIZE);// 2. 對命令行字符串進行切割Split(commandline);}return 0;
}

目前的總代碼:

#include <stdio.h>
#include <stdlib.h> //getenv();
#include <string.h> //strlen();#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "char *argv[MAX_ARGC]; // 設置一個上限,指令最多被切割成64份 已經完全夠用了//獲取環境變量-->主機名const char* HostName(){char *hostname = getenv("HostName");if(hostname) return hostname;else return "None";}// 獲取環境變量-->用戶名
const char *UserName()
{char *hostname = getenv("USER");if (hostname)return hostname;elsereturn "None";
}// 獲取環境變量-->當前工作目錄
const char *CurrentWorkDir()
{char *hostname = getenv("PWD");if (hostname)return hostname;elsereturn "None";
}// 打印命令行提示符+獲取用戶輸入的指令字符串
void Interactive(char out[], int size)
{printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());fgets(out, size, stdin);out[strlen(out) - 1] = 0; // 將'\n'置為'\0'}// 分割字符串
void Split(char in[])
{int i = 0;argv[i++] = strtok(in, SEP); // "ls -a -l"while (argv[i++] = strtok(NULL, SEP));//打印一下切割字符串for (int i = 0; argv[i]; i++){printf("argv[%d]: %s\n", i, argv[i]);}
}int main()
{while (1){// 1. 打印命令行提示符,獲取用戶輸入的命令字符串char commandline[SIZE];Interactive(commandline, SIZE);// 2. 對命令行字符串進行切割Split(commandline);}return 0;
}

效果:

?

解釋:循環起來才是對的,符合自帶的shell,雖然現在還沒有開始執行命令,但是有點那味了

四:執行命令

已經完成了對字符串的切割,下一步就是用進程替換接口執行命令, 但是現在有一個問題,直接在當前進程下進行進程替換嗎?那是肯定不可以的,一是因為我們自帶的shell,其實可以一直循環使用的,二是因為執行指令不是一個安全的操作,所以我們肯定是fork創建子進程,讓子進程去進行進程替換的!

Q:可是上面不是已經在mian中加入了死循環嗎?為什么在當前進程直接進程替換會不循環了?

A:進程替換的特點就是,會直接忽略掉當前進程開始替換那一行的后面的所有代碼,所以不要被main中死循環誤導了,進程都替換了,代碼和數組全部都替換了,誰還管這個死循環?

所以執行命令這個接口代碼如下:

void Execute()
{pid_t id = fork();if(id == 0){execvp(argv[0], argv);exit(1);}waitpid(id, NULL, 0);
}

解釋:非常簡答,創建子進程后,在子進程中進行進程替換,而我們為什么選擇進程替換眾多接口中的execvp接口呢,因為我們已經有了數組,所以選擇這個接口是傳參最輕松的!

效果如下:

解釋:循環的執行我們的指令,和shell類似了!但是其還是有很多的問題!我們知道:

[wtt1@hcss-ecs-1a2a /home/wtt1/lesson2]$

這行代碼中當前工作目錄部分,是會隨著我們pwd而改變的,那我們cd到另一個路徑,其回想我們預期的那樣改變嗎?

先看下Ubuntuh下cd對命令行提示符的影響:

再看我們的:

很明顯,自帶shell的cd能夠影響到命令行提示符,但是我們的自定義shell影響不到!先想一下,我們每次循環的時候,都會先去獲取環境變量去打印命令行提示符,所以不是因為環境變量是之前的環境變量這個原因!其次我們的cd壓根沒有影響到pwd!也就是說當前進程的pwd是沒變的!

所以原因是因為我們的所有指令目前都是子進程去進行進程替換執行的,這一步當然沒問題,但是如果cd這種指令也是子進程去執行,那么改變的只會是子進程的路徑和其的環境變量!!而我們的命令行提示符中的環境變量是父進程的,所以根據父進程的環境變量打印出來的命令行提示符當然不會改變了,其次連環境變量都沒變,pwd指令得到的路徑當然也不會變了!

所以有些指令是不能讓子進程去進行進程替換執行的,這種指令叫作內建指令

所以對于內建指令,我們需要一個單獨的函數 --->內建函數來進行處理!

五:內建處理

所以我們在第四步執行命令之前,應該去判斷一個指令是否屬于內建指令,如果是,則在內建函數中直接進行處理了,而不會再有執行命令函數來執行!

所以main中的邏輯如下:

//內建指令處理
int BuildinCmd()
{int ret = 0;//檢測是否是內建命令, 是 1, 否 0//處理指令......//返回retreturn ret;
}int main()
{while(1){char commandline[SIZE];// 1. 打印命令行提示符,獲取用戶輸入的命令字符串Interactive(commandline, SIZE);// 2. 對命令行字符串進行切割Split(commandline);// 3. 處理內建命令n = BuildinCmd();if(n) continue;// 4. 執行這個命令Execute();}return 0;
}

解釋:當?BuildinCmd 中檢測到指令是內建指令,則會返回1,main中接收到1,則continue會跳過第4步,回到循環最開始

至于如何檢測一個指令是不是內建指令,很簡單的方法,窮舉即可,是的,我們只能判斷,這就證明了我們的自定義shell注定了不完整,因為我們不可能將所有的內建指令都寫進去,一是沒學過,二是太麻煩,我們只是為了感受shell的本質

1:內建指令-->cd

代碼如下:

//新增的全局變量
char pwd[SIZE];int BuildinCmd()
{int ret = 0;// 1. 檢測是否是內建命令, 是 1, 否 0if(strcmp("cd", argv[0]) == 0){// 2. 執行ret = 1;char *target = argv[1]; //獲取到cd后的路徑if(!target) target = Home();//單獨cd指令的特判chdir(target);//用cd后面的路徑更改當前目錄}return ret;
}

解釋:

用strcmp函數來對比一下數組中的第一個字符串是否和"cd"一致,是則進入if,先置ret為1,這樣main中就會continue跳過Execute()!然后既然是cd指令 則需要先取到數組中的第二個元素,此時需注意 如果第二個元素為空 則代表指令僅僅為cd,cd后面不跟路徑也是一個指令,其會直接回到家目錄,所以我們相當于特判了一下

那現在會成功運行嗎?效果如下:

解釋:

仍未正確,這就是shell對于cd指令的本質了,當我們以為我們用chdir函數來修改了當前工作目錄的時候,會直接影響到當前進程的環境變量,但實際根本影響不了,不是我們的方法不對,而是shell對于cd指令也會這樣,本質是因為沒有我們想的這么多自動,其實shell跟我們一樣,也是chdir了當前工作目錄,但是其緊接著會用一個putenv接口,來更新當前進程的環境變量,所以我們需要跟它一樣,手動的更新環境變量!

代碼如下:

int BuildinCmd()
{int ret = 0;// 1. 檢測是否是內建命令, 是 1, 否 0if(strcmp("cd", argv[0]) == 0){// 2. 執行ret = 1;char *target = argv[1]; //獲取到cd后的路徑if(!target) target = Home();//單獨cd指令的特判chdir(target);//用cd后面的路徑更改當前目錄snprintf(pwd, SIZE, "PWD=%s", target);//拼接字符串為kv型putenv(pwd);}return ret;
}

效果如下:

解釋:

現在不僅pwd正確了,命令行也正確了;其次為什么使用snprintf函數,因為我們雖然有了target變量,里面存放就是cd后面的路徑,但是要想更新環境變量,需要putenv接口的同時,參數還要是環境變量對應的kv類型,所以我們使用snprintfp拼接一下!結果放在了pwd!

但是此時還有問題,當我們試圖cd ..的時候,會出現以下效果

解釋:

從未見過cd .. 后,命令行里面的路徑直接變成了..,但是為什么會這樣呢,不正是因為我們的函數里面就是這樣的邏輯嗎,我們的target變量就是"..",所以"PWD=".."" 。只能說,我們的代碼不夠完善,shell里面是怎么做的呢?很簡單其不會直接的把target作為環境變量,而是只將target用來chdir,之后再用getcwd函數來獲取被chdir(target)后的新路徑,這樣就是你是cd .. ,那么你getcwd函數獲取到的就會是一個絕對路徑,而不是..,然后再將getcwd的結果用spprintf函數拼裝后,給putenv函數更新正確的環境變量!

代碼如下:

int BuildinCmd()
{int ret = 0;// 1. 檢測是否是內建命令, 是 1, 否 0if(strcmp("cd", argv[0]) == 0){// 2. 執行ret = 1;char *target = argv[1]; //獲取到cd后的路徑if(!target) target = Home();//單獨cd指令的特判chdir(target);//用cd后面的路徑更改當前目錄char temp[1024];getcwd(temp, 1024); // 獲取正確的當前路徑snprintf(pwd, SIZE, "PWD=%s", temp);putenv(pwd);}return ret;
}

效果如下:

完美~

但是現在有一個很小的報錯,當我們直接輸入回車的時候,會報錯段錯誤

很有意思的是,當你沒實現BuildinCmd()函數的時候,你只輸入回車,并不會報錯!因為在我們沒有實現BuildinCmd()函數之前,此時輸入回車,唯一會影響的就是Execute()函數中的execvp(NULL, argv),但是會被忽略,因為execvp接口對?NULL?檢查不嚴格,絕對不要依賴這種行為!這是未定義行為!所以即使沒有報錯,我們也要知道有這種錯誤!

而實現BuildinCmd()函數后會報錯的原因如下:先執行的BuildinCmd()函數中的strcmp(NULL, "cd"),必然會崩潰報錯,因為:strcmp 的規范:該函數要求兩個參數均為有效字符串指針(以 \0 結尾)。傳入 NULL 會嘗試訪問非法內存地址(地址 0x0),觸發段錯誤。

所以,我們要修復這個報錯,很簡單,我們只需要在Interactivej接口中檢測一下用戶輸入的字符串長度,如果為0,直接在main中continue即可,這樣就會回到循環一開始!

代碼改動如下:

// 打印命令行提示符+獲取用戶輸入的指令字符串
int Interactive(char out[], int size)
{printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());fgets(out, size, stdin);out[strlen(out)-1] = 0; // 將'\n'置為'\0'return strlen(out);//新增代碼  返回字符串字符數
}int main()
{while (1){char commandline[SIZE];// 1. 打印命令行提示符,獲取用戶輸入的命令字符串int n = Interactive(commandline, SIZE);if(n == 0) continue;//空串 則continue// 2. 對命令行字符串進行切割Split(commandline);// 3. 處理內建命令n = BuildinCmd();if (n)continue;// 4. 執行這個命令Execute();}return 0;
}

效果:

現在一致只輸入回車也沒事了!

2:內建指令-->export

emport指令使用來?設置已有的環境變量或導入新的環境變量的指令,那為什么其要被作為內建指令呢?

當我們在原生shell中輸入指令:

export myval=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

此時我們通過env指令可以找到這個指令:

但是在我們自定義的shell中執行了一樣的指令,但是也找不到,為什么?

原因就是我們是子進程去進行了export ,影響到的是子進程的環境變量,而子進程的環境變量怎么可能影響到父進程呢?所以export應該作為內建指令來處理!

所以我們在BuildinCmd()函數中又要新增一個if分支了,我們判斷到指令是export之后,我們就要獲取argv數組中的第二個元素了,也就是kv類型的環境變量的定義 ,比如我們執行指令 :export myval=aaa ,則數組的第二個元素就是myval=aaa

所以代碼應該如下:

int BuildinCmd()
{int ret = 0;// 1. 檢測是否是內建命令cd, 是 1, 否 0if (strcmp("cd", argv[0]) == 0){// 2. 執行ret = 1;char *target = argv[1]; // cd XXX or cdif (!target)target = Home();chdir(target);char temp[1024];getcwd(temp, 1024); // 獲取正確的當前路徑snprintf(pwd, SIZE, "PWD=%s", temp);putenv(pwd);}//2.檢測是否是內建命令export, 是 1, 否 0else if(strcmp("export", argv[0]) == 0){ret = 1;if(argv[1]){putenv(argv[1]);}}return ret;
}

此時我們再執行export指令試試:

再通過env指令查看是否找得到:

解釋:符合預期,我們自定義的shell也能正確執行export指令了!

但是我們知道,環境變量更改后,只有你手動刪除環境變量,或者重啟客戶端,其才會消失;但是現在我們導入環境變量之后,我們去隨便執行一個帶選項的指令,比如 ls -l,會發現效果如下:

執行完ls -l指令,我們執行env發現找不到我們的myval環境變量了:

這是為什么呢?這是因為你的argv數組是一個字符指針數組,我們的環境變量是其中的argv[1];但同時別忘了,argv數組是用來存放分割后的指令的數組,所以這代表著當你下次輸入指令的時候,比如"ls -l",你的argv[1]會變成 "-l"? ,也就是說你的環境變量的內容是argv[1]中的地址指向的內容,但是現在argv[1]中地址指向的被下一次指令切割后的第二個元素覆蓋了!所以你找不到原有的環境變量了!其次,你還能在env指令下找到"-l"這個環境變量!

如下:

那如何解決呢?方法很簡單,我把當前的環境變量的內容拷貝到一個簡單的數組里面不就行了,然后把這個數組進行putenv即可,普通數組根本不會被修改,所以環境變量不可能被覆蓋!

代碼如下:

char env[SIZE];//存儲導入的環境變量 避免使用argv[1],然后被覆蓋else if(strcmp("export", argv[0]) == 0){ret = 1;if(argv[1]){strcpy(env, argv[1]);//將其拷貝到一個普通的數組中putenv(env);//然后再導入}}

此時我們再執行之前的代碼,會發現依然能夠找得到了!

截圖如下:

執行任何指令之后:?

解釋:不再影響到我們導入的環境變量

3:內建指令-->echo

為什么echo也是一個內建指令?因為echo $PATH 這種用法,就需要獲取到環境變量,而我們在原生的shell輸入這種指令的時候,我們肯定是想獲取到當前進程的環境變量,而不是創建子進程,去獲取到子進程的環境變量,所以echo是一個內建指令。

但是有一個比較特殊的指令echo $? ,我們只知道其是獲取最近一個進程的退出碼的指令,那這個指令是內建指令嗎?

我們在原生shell中執行echo $?指令:

再在我們自定義shell中執行echo $?指令:

很顯然,上面是正確的,而下面卻是打印出了"$?",這就側面說明了echo $?指令沒這么簡單,其實echo $?指令中的$?是 Shell 進程內部的臨時變量,存儲上一條命令的退出狀態碼,當 Shell 啟動子進程時,子進程會繼承父進程的環境變量副本,但?$??不屬于環境變量,這就是為什么其要當做內建指令來執行!

所以想要保存一個退出碼,很簡單,但是要理解為什么子進程無法獲取父進程的$?,因為其不是環境變量,所以無法繼承,所以我們執行echo $?要內建執行

所以代碼如下:

int lastcode = 0;//新增全局變量else if(strcmp("echo", argv[0]) == 0){ret = 1;if(argv[1] == NULL) //echo{printf("\n");}else{if(argv[1][0] == '$'){if(argv[1][1] == '?')//echo $?{printf("%d\n", lastcode);//打印上次退出的進程的退出碼lastcode = 0;//更新退出碼為0}else//echo $環境變量{char *e = getenv(argv[1]+1);if(e) printf("%s\n", e);}}else//echo+打印的內容{printf("%s\n", argv[1]);}}}

解釋:邏輯如下:

值得注意的是,當你沒把echo當做內建指令來寫的時候,你在自定義shell中執行echo指令,不帶其他選項的時候,此時效果如下:

首先,這是正確的現象,因為原生shell也是這樣:

但是打印換行不是因為我們輸入了echo和'\n',其識別到換行,所以才打印換行,而是此時子進程進行進程替換之后,去調用echo程序,此時echo程序的源碼有一點類似如下:

if (無參數) {輸出換行符 "\n";  // 這是 POSIX 標準規定的行為
}

所以其才會有換行的效果,切記不是因為我們輸入的回車!

所以問題來了,為什么我們內建函數對于echo的代碼,中argv[1]為NULL的時候,此時我們要手動的打印換行?因為我們壓根就沒有去程序替換調用echo程序,而是我們單獨的將echo判斷為了內建指令,所以任何的效果都要我們自己實現,所以我們才要手動的打印換行!

其余if分支已經很清晰明了,不再贅述....

總結:博主只手動寫了三個內建指令,但是真正的shell有更多的內建指令,并且博主寫的echo內建邏輯也是簡化版的,因為解釋即使你索引到[1][0],但不一定能索引到[1][1],所以還是那句話,在實現的過程中體會shell的本質即可!

而關于echo $?獲取最新退出進程的退出碼,我們除了在內建函數中要賦值給lastcode,我們還要在Execute(),也就是執行函數中,我們要把waitpid函數利用起來啦!我們要獲取到退出碼!所以當當我們執行任何指令的時候,子進程去進程進程替換后,進程執行完成后,我們能夠回收到子進程的退出碼,將其賦給了lastcode,這意味著當你下次調用echo $?指令的時候,就會直接打印出上次進程的退出碼了!邏輯緊扣!

void Execute()
{pid_t id = fork();if(id == 0){// 讓子進程執行命令execvp(argv[0], argv);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id) lastcode = WEXITSTATUS(status); 
}

截止目前,總代碼如下:

#include <stdio.h>     //C頭文件 snprintf();
#include <stdlib.h>    //getenv();putenv();
#include <string.h>    //strlen();
#include <sys/types.h> //fork();waitid();
#include <sys/wait.h>  //waitpid();
#include <unistd.h>    //fork();getcwd();chdir();#define SIZE 1050
#define MAX_ARGC 64
#define SEP " "char pwd[SIZE];
char *argv[MAX_ARGC]; // 設置一個上限,指令最多被切割成64份 已經完全夠用了
char env[SIZE];//存儲導入的環境變量 避免使用argv[1],然后被覆蓋int lastcode = 0;//存儲退出碼//獲取環境變量-->主機名// centos環境下
//  const char* HostName()
//  {
//      char *hostname = getenv("HostName");
//      if(hostname) return hostname;
//      else return "None";
//  }// Ubuntuh環境下
const char *HostName()
{FILE *fp = popen("hostname", "r"); // 執行 hostname 命令if (fp == NULL)return "None";static char buf[256];if (fgets(buf, sizeof(buf), fp) != NULL){                                   // 修復:補全括號并檢查返回值buf[strcspn(buf, "\n")] = '\0'; // 去除換行符pclose(fp);return buf;}pclose(fp);return "None";
}// 獲取環境變量-->用戶名
const char *UserName()
{char *hostname = getenv("USER");if (hostname)return hostname;elsereturn "None";
}// 獲取環境變量-->當前工作目錄
const char *CurrentWorkDir()
{char *hostname = getenv("PWD");if (hostname)return hostname;elsereturn "None";
}char *Home()
{return getenv("HOME");
}// 打印命令行提示符+獲取用戶輸入的指令字符串
int Interactive(char out[], int size)
{printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());fgets(out, size, stdin);out[strlen(out)-1] = 0; // 將'\n'置為'\0'return strlen(out);
}// 分割字符串
void Split(char in[])
{int i = 0;argv[i++] = strtok(in, SEP); // "ls -a -l"while (argv[i++] = strtok(NULL, SEP));// 打印一下切割字符串// for (int i = 0; argv[i]; i++)// {//     printf("argv[%d]: %s\n", i, argv[i]);// }
}
void Execute()
{pid_t id = fork();if (id == 0){execvp(argv[0], argv);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id) lastcode = WEXITSTATUS(status); }int BuildinCmd()
{int ret = 0;// 1. 檢測是否是內建命令cd, 是 1, 否 0if (strcmp("cd", argv[0]) == 0){// 2. 執行ret = 1;char *target = argv[1]; // cd XXX or cdif (!target)target = Home();chdir(target);char temp[1024];getcwd(temp, 1024); // 獲取正確的當前路徑snprintf(pwd, SIZE, "PWD=%s", temp);putenv(pwd);}//2.檢測是否是內建命令export, 是 1, 否 0else if(strcmp("export", argv[0]) == 0){ret = 1;if(argv[1]){strcpy(env, argv[1]);putenv(env);}}else if(strcmp("echo", argv[0]) == 0){ret = 1;if(argv[1] == NULL) {//printf("\n");}else{if(argv[1][0] == '$'){if(argv[1][1] == '?'){printf("%d\n", lastcode);//打印上次退出的進程的退出碼lastcode = 0;//更新退出碼為0}}else{char *e = getenv(argv[1]+1);if(e) printf("%s\n", e);}}else{printf("%s\n", argv[1]);}}}return ret;
}int main()
{while (1){char commandline[SIZE]; // export myval=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb// 1. 打印命令行提示符,獲取用戶輸入的命令字符串int n = Interactive(commandline, SIZE);if(n == 0) continue;// 2. 對命令行字符串進行切割Split(commandline);// 3. 處理內建命令n = BuildinCmd();if (n)continue;// 4. 執行這個命令Execute();}return 0;
}

六:重定向

但是我們還有一個非常重要的功能沒有實現,那就是重定向!我們的自定義shell根本不認識
> ,< ,>>,這三個符號!

而重定向肯定不是內建指令,其往往是更改默認輸出流或者默認輸入流,所以其單獨在一個函數里面,其次第六大點是要先看此篇博客:Linux->基礎IO-CSDN博客,就能理解重定向相關知識

所以現在如果輸入了一個指令:

ls -l > file.txt

我們肯定是無法執行的,所以我們要新增一個函數CheckRedir,來檢測是哪種重定向,比如此指令就是輸出重定向,然后我們要先把>置為'\0',此時這個字符串就變成了"ls -l '\0' file.txt",為什么要將其置為'\0'呢?因為此時我們就可以再把置為'\0'之后的字符串傳給分割函數了,此時分割函數就只會過去到重定向符號的前面的干凈的指令!

所以如果是沒有重定向的指令,則我們CheckRedir函數什么都不做,反之有的話,CheckRedir則需要判斷出是哪種重定向的同時,還要將重定向符號置為'\0'!

首先我們直接宏定義出幾種重定向和文件名和重定向類型:

#define NoneRedir  -1 //無重定向
#define StdinRedir  0 //輸入重定向
#define StdoutRedir 1 //輸出重定向
#define AppendRedir 2 //追加重定向int redir_type = NoneRedir; //重定向類型(初始化為無重定向)
char *filename = NULL; //文件名(初始化為NULL)

然后在用戶輸入的字符串數組在被分割函數分割前,我們要先讓其進行CheckRedir函數!

void Split(char in[])
{CheckRedir(in);//先判斷是否有重定向int i = 0;argv[i++] = strtok(in, SEP); while(argv[i++] = strtok(NULL, SEP)); // 間接寫法}

然后我們的?CheckRedir函數代碼如下:

1:先遍歷查找到重定向符號

// ls -a -l
// ls -a -l > log.txt
// ls -a -l >> log.txt
// cat < log.txt
void CheckRedir(char in[])
{int pos = strlen(in) - 1;//倒著遍歷用戶輸入的字符串 從'\0'的前一個開始向前遍歷while( pos >= 0 ){if(in[pos] == '>')//可能是輸出重定向或追加重定向{if(in[pos-1] == '>')//追加重定向{}else//輸出重定向{}}else if(in[pos] == '<')//輸入重定向{}else{pos--;//pos遍歷}}
}

解釋:倒著遍歷和正著遍歷都可以,只是倒著遍歷在后面會稍微簡單一點

2:賦值?redir_type 和 將重定向符號置為'\0'

再說一遍為什么要把重定向符號置為'\0',這樣才能在后面的分割函數中,讓分割函數得到干凈的指令,而不包括重定向符合和文件名,否則放進argv數組中,是執行不了的!

代碼如下:

#define STREND '\0'//新增的宏// ls -a -l
// ls -a -l > log.txt
// ls -a -l >> log.txt
// cat < log.txt
void CheckRedir(char in[])
{int pos = strlen(in) - 1;//倒著遍歷用戶輸入的字符串 從'\0'的前一個開始向前遍歷while( pos >= 0 ){if(in[pos] == '>')//可能是輸出重定向或追加重定向{if(in[pos-1] == '>')//追加重定向{redir_type = AppendRedir;//根據重定向符號 賦值對應的宏給redir_typein[pos-1] = STREND;//將重定向符號置為'\0'break;}else//輸出重定向{redir_type = StdoutRedir;in[pos] = STREND;break;}}else if(in[pos] == '<')//輸入重定向{redir_type = StdinRedir;in[pos] = STREND;break;}else{pos--;//pos遍歷}}
}

解釋:唯一需要注意的是 追加重定向我們要把從左往右數的第一個>置為'\0',如果置的是第二個>,則分割函數或獲取到一個>作為argv數組的元素,所以我們要倒退回去(pos-1)置為'\0'!

3:獲取到文件名字

現在我們做完了以上的兩步,我們下一步就是要獲取到指令中的文件名字了,所以我們需要跳過宮格去找到文件名字,然后把文件名字賦給全局定義的filename

代碼如下:

#include <ctype.h>     //isspace();#define IgnSpace(buf,pos) do{ while(isspace(buf[pos])) pos++; }while(0)void CheckRedir(char in[])
{// ls -a -l// ls -a -l > log.txt// ls -a -l >> log.txt// cat < log.txtredir_type = NoneRedir;filename = NULL;int pos = strlen(in) - 1;while( pos >= 0 ){if(in[pos] == '>'){if(in[pos-1] == '>'){redir_type = AppendRedir;in[pos-1] = STREND;pos++;IgnSpace(in, pos);//跳過空格filename = in+pos;//數組名+偏移量獲取到起始字符地址給filenamebreak;}else{redir_type = StdoutRedir;in[pos++] = STREND;IgnSpace(in, pos);//同理filename = in+pos;//同理break;}}else if(in[pos] == '<'){redir_type = StdinRedir;in[pos++] = STREND;IgnSpace(in, pos);//同理filename = in+pos;//同理break;}else{pos--;}}
}

解釋:在使用IgnSpaceh函數之前,需要++跳過我們手動置的'\0',因為IgnSpace函數里面的isspace函數遇到'\0'就會停止判斷;其次IgnSpace函數的寫法:

#define IgnSpace(buf,pos) do{ while(isspace(buf[pos])) pos++; }while(0)
  • 功能:從?buf[pos]?開始,向右移動?pos?直到遇到非空白字符。

  • 組成部分

    • isspace(buf[pos]):檢測當前字符是否為空格

    • pos++:位置指針右移

    • do{...}while(0):將多語句組合成宏的標準技巧

這是一個裝逼的寫法昂,說白了等效下面這種寫法:

            if(in[pos-1] == '>'){redir_type = AppendRedir;in[pos-1] = STREND;pos++;do{ while(isspace(in[pos])) pos++; }while(0);filename = in+pos;break;}

但其的確可以讓我們的if內部 不那么的冗余 !

開頭加的那兩句很重要,我們要覆蓋掉上次判斷帶來的影響,所以要賦值一下!

redir_type = NoneRedir;
filename = NULL;

所以至此我們的CheckRedir函數就完成了,現在我們知道了是哪種重定向了,并且不影響分割函數的正常運行,同時還獲取到了文件名字,所以下一班,我們的執行函數的邏輯也要做處修改了,當我們需要重定向的時候,會有具體的做法了

代碼如下:

/執行命令函數
//負責創建子進程 去進程進程替換
void Execute()
{pid_t id = fork();if(id == 0){//重定向的處理int fd = -1;//先定義一個fdif(redir_type == StdinRedir)//輸入重定向{fd = open(filename, O_RDONLY);dup2(fd, 0);//將fd的內容拷貝到0下標的元素中}else if(redir_type == StdoutRedir)//輸出重定向{fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC);dup2(fd, 1);//將fd的內容拷貝到1下標的元素中}else if(redir_type == AppendRedir)//追加重定向{fd = open(filename, O_CREAT | O_WRONLY | O_APPEND);dup2(fd, 1);//將fd的內容拷貝到1下標的元素中}else{// do nothing}// 讓子進程執行命名execvp(argv[0], argv);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id) lastcode = WEXITSTATUS(status); }

解釋:dump2函數,以及open函數都是在Linux->基礎IO-CSDN博客中詳細講過!

展示重定向的效果:

解釋:我們的重定向正常工作了,但是如果你使用echo和重定向結合在一起,則不行,因為我們沒寫echo的重定向的分支

七:上色

現在我們的自定義shell只差最后一步了,就是上色,我們原生shell展示的內容是有顏色的:

但是我們的自定義shell:

其實原生的shell上色的本質就是ls指令自帶了color選項的,所以我們只需在分割函數中,判斷一下argv[0]是否是ls,是的話,我們就手動的把數組的最后一個元素NULL置為color選項,然后再在后面加一個NULL元素即可

代碼如下:

void Split(char in[])
{CheckRedir(in);int i = 0;argv[i++] = strtok(in, SEP); // "ls -a -l"while(argv[i++] = strtok(NULL, SEP)); // 故意將== 寫成 =if(strcmp(argv[0], "ls") ==0){argv[i-1] = (char*)"--color";argv[i] = NULL;}
}

效果:

八:總代碼

由于代碼眾多,在之前的很多地方都是只展示了相關的代碼,所以下面匯總一份代碼!

1:makefile

myshell:myshell.cgcc -o $@ $^
.PHONY:clean
clean:rm -f myshell

2:myshell.c?

#include <stdio.h>//C頭文件 snprintf();
#include <stdlib.h>//getenv();putenv();
#include <string.h>//strlen();
#include <unistd.h>//fork();getcwd();chdir();
#include <ctype.h>//isspace();
#include <sys/types.h>//fork();waitid();//open();
#include <sys/stat.h>//open();
#include <fcntl.h>//open();
#include <sys/wait.h>//waitpid();//宏的定義
#define SIZE 1064//一般作為數組的大小
#define MAX_ARGC 64//作為argv數組的大小 已經完全夠用
#define SEP " "//作為strtok函數的第二個參數 遇見空格則分割
#define STREND '\0'//作為將重定向置為'\0' 中'\0'的宏// 下面的都和重定向有關
#define NoneRedir  -1 //無重定向
#define StdinRedir  0 //輸入重定向
#define StdoutRedir 1 //輸出重定向
#define AppendRedir 2 //追加重定向//跳過空格的代碼的宏定義
#define IgnSpace(buf,pos) do{ while(isspace(buf[pos])) pos++; }while(0)int redir_type = NoneRedir;//redir_type來接收重定向的類型 初始化為無重定向
char *filename = NULL;//filename用來接收文件名 可能打開該文件 也可能創建該文件char *argv[MAX_ARGC];//定義一個argv數組 其存放的指令分割后的各個元素 作為execvp函數的參數 進行進程替換
char pwd[SIZE];//存放cd指令中導入的kv類型的環境變量
char env[SIZE]; //臨時數組來存儲導入的環境變量,避免環境變量被下一次的argv[1]所覆蓋 
int lastcode = 0;//退出碼的保存//CenTos版本下獲取主機名
const char* HostName()
{char *hostname = getenv("HOSTNAME");if(hostname) return hostname;else return "None";
}// Ubuntuh環境下獲取主機名
// const char *HostName()
// {
//     FILE *fp = popen("hostname", "r"); // 執行 hostname 命令
//     if (fp == NULL)
//         return "None";//     static char buf[256];
//     if (fgets(buf, sizeof(buf), fp) != NULL)
//     {                                   // 修復:補全括號并檢查返回值
//         buf[strcspn(buf, "\n")] = '\0'; // 去除換行符
//         pclose(fp);
//         return buf;
//     }
//     pclose(fp);
//     return "None";
// }//獲取用戶名
const char* UserName()
{char *hostname = getenv("USER");if(hostname) return hostname;else return "None";
}//獲取當前工作目錄
const char *CurrentWorkDir()
{char *hostname = getenv("PWD");if(hostname) return hostname;else return "None";
}//獲取用戶主目錄路徑
char *Home()
{return getenv("HOME");
}//交互函數
//打印命令行提示符+獲取用戶輸入的指令字符串
int Interactive(char out[], int size)
{printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());fgets(out, size, stdin);out[strlen(out)-1] = 0; //將用戶輸入的回車置為'\0'return strlen(out);//防止用戶僅輸入回車
}//檢查重定向函數
//確定重定向類型+把重定向符號置為'\0'+獲取到文件名
void CheckRedir(char in[])
{// ls -a -l// ls -a -l > log.txt// ls -a -l >> log.txt// cat < log.txt//更新兩個量 不要被之前影響redir_type = NoneRedir;filename = NULL;int pos = strlen(in) - 1;while( pos >= 0 ){if(in[pos] == '>'){if(in[pos-1] == '>'){redir_type = AppendRedir;in[pos-1] = STREND;pos++;//跳過'\0'IgnSpace(in, pos);filename = in+pos;break;}else{redir_type = StdoutRedir;in[pos++] = STREND;//跳過'\0'IgnSpace(in, pos);filename = in+pos;//printf("debug: %s, %d\n", filename, redir_type);break;}}else if(in[pos] == '<'){redir_type = StdinRedir;in[pos++] = STREND;//跳過'\0'IgnSpace(in, pos);filename = in+pos;//printf("debug: %s, %d\n", filename, redir_type);break;}else{pos--;}}
}//分割函數
//將一整個字符串分割為多個子串放進字符指針數組argv數組中
void Split(char in[])
{CheckRedir(in);int i = 0;argv[i++] = strtok(in, SEP); // "ls -a -l"while(argv[i++] = strtok(NULL, SEP)); // 故意將== 寫成 =//對ls指令進行上色if(strcmp(argv[0], "ls") ==0){argv[i-1] = (char*)"--color";//原先的NULL被換成了上色選項argv[i] = NULL;//所以在后面新增一個NULL  才符合argv的性質}
}//執行命令函數
//判斷是否重定向+創建子進程進行進程替換+等待子進程獲取其退出碼去更新lastcode
void Execute()
{pid_t id = fork();//創建子進程if(id == 0){int fd = -1;if(redir_type == StdinRedir)//輸入重定向{fd = open(filename, O_RDONLY);dup2(fd, 0);}else if(redir_type == StdoutRedir)//輸出重定向{fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC,0666);dup2(fd, 1);}else if(redir_type == AppendRedir)//追加重定向{fd = open(filename, O_CREAT | O_WRONLY | O_APPEND,0666);dup2(fd, 1);}else{// do nothing}// 讓子進程執行命令execvp(argv[0], argv);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id) lastcode = WEXITSTATUS(status); //更新退出碼//printf("run done, rid: %d\n", rid);
}//內建函數
//cd + export + echo 的實現
int BuildinCmd()
{int ret = 0;// 1. cd指令的實現if(strcmp("cd", argv[0]) == 0){// 2. 執行ret = 1;char *target = argv[1]; //獲取到路徑if(!target) target = Home();//路徑為空 代表為echo指令 則直接回到用戶家目錄chdir(target);//改變父進程目錄char temp[1024];getcwd(temp, 1024);//獲取當前目錄放進temp數組中snprintf(pwd, SIZE, "PWD=%s", temp);//拼接成一個kv類型 方便導入環境變量putenv(pwd);//導入環境變量}//2.export指令的實現else if(strcmp("export", argv[0]) == 0){ret = 1;if(argv[1]){strcpy(env, argv[1]);//環境變量要存放到臨時數組中 避免被下一次的argv[1]直接覆蓋putenv(env);//導入環境變量}}//3.echo指令的實現else if(strcmp("echo", argv[0]) == 0){ret = 1;if(argv[1] == NULL) {printf("\n");//模擬echo指令的動作 其會換行 所以手動換}else{if(argv[1][0] == '$')//兩種可能{if(argv[1][1] == '?')//echo $?{printf("%d\n", lastcode);lastcode = 0;}else{//echo $環境變量char *e = getenv(argv[1]+1);if(e) printf("%s\n", e);}}else{//echo+打印的內容printf("%s\n", argv[1]);}}}return ret;
}//主函數
//代碼邏輯框架
int main()
{while(1){char commandline[SIZE];// 1. 打印命令行提示符,獲取用戶輸入的命令字符串int n = Interactive(commandline, SIZE);if(n == 0) continue;// 2. 對命令行字符串進行切割Split(commandline);// 3. 處理內建命令n = BuildinCmd();if(n) continue;// 4. 執行這個命令Execute();}return 0;
}

九:Ubuntuhb版本下的差異

博主是在Ubuntuh版本去實現centos版本的shell,總結出了幾個值得注意的差異之處:

1:獲取主機名的方式

//Ubuntuh環境下
const char* HostName() {FILE *fp = popen("hostname", "r");  // 執行 hostname 命令if (fp == NULL) return "None";static char buf[256];if (fgets(buf, sizeof(buf), fp) != NULL) {  // 修復:補全括號并檢查返回值buf[strcspn(buf, "\n")] = '\0';  // 去除換行符pclose(fp);return buf;}pclose(fp);return "None";
}

解釋:如果你是?Ubuntuhb版本則你需要像這樣獲取到主機名

2:open函數的權限

void Execute()
{pid_t id = fork();//創建子進程if(id == 0){int fd = -1;if(redir_type == StdinRedir)//輸入重定向{fd = open(filename, O_RDONLY);dup2(fd, 0);}else if(redir_type == StdoutRedir)//輸出重定向{fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC);dup2(fd, 1);}else if(redir_type == AppendRedir)//追加重定向{fd = open(filename, O_CREAT | O_WRONLY | O_APPEND);dup2(fd, 1);}else{// do nothing}// 讓子進程執行命令execvp(argv[0], argv);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id) lastcode = WEXITSTATUS(status); //更新退出碼//printf("run done, rid: %d\n", rid);
}

如果你是Centos,你可以像上面這樣不傳open函數的第三個參數,這和你的權限掩碼有關,但是Ubuntuhb版本下的vscode必須寫第三個參數,否則會像下面這樣:

至此,結束~

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

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

相關文章

js中出現-8.881784197001252e-16這種(一個極其接近 0 的極小負數)的浮點數精度計數異常問題解決思路

你的代碼中出現 -8.881784197001252e-16&#xff08;一個極其接近 0 的極小負數&#xff09;的原因是 JavaScript 浮點數精度問題。具體來說&#xff0c;當你反復進行 加法 和 減法 時&#xff0c;由于浮點數在計算機中的存儲方式&#xff0c;可能會引入微小的誤差。一、問題情…

超詳細的 RustDesk 自建中繼節點教程

厭倦了商業遠程控制軟件的會員限制和功能閹割&#xff1f;渴望擁有一個自由掌控、安全可靠的遠程連接方案&#xff1f;開源軟件 RustDesk 正是你需要的答案&#xff01; 相信從事互聯網工作的你&#xff0c;一定對向日葵和ToDesk等商業遠程控制軟件并不陌生。然而&#xff0c;…

Spring Boot 2整合Druid的兩種方式

一、自定義整合Druid&#xff08;非Starter方式&#xff09;適用于需要完全手動控制配置的場景添加依賴&#xff08;pom.xml&#xff09;<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.8</ve…

鴻蒙Next-開發版本升級,API升級(例如API12升API16)

鴻蒙更新換代很快的&#xff0c;2025年1月&#xff0c;截至4月就有 DevEco Studio 5.0.2 Release 升級到 DevEco Studio 5.0.3 Release 升級到 DevEco Studio 5.0.4 Release&#xff0c;三次大版本更新。 作者也想在年前創建的項目中體驗一下新版本的特性&#xff0c;于是查看了…

樹莓派設置時區

查看當前時間 piraspberrypi-CM5:~ $ date Mon 28 Jul 09:22:38 BST 2025BST 指的是 British Summer Time&#xff0c;即英國夏令時&#xff08;UTC1&#xff09;。 所以這是英國&#xff08;倫敦等地&#xff09;在夏令時期間的本地時間&#xff0c;比標準的 UTC 時間快 1 小時…

C Primer Plus 第6版 編程練習——第13章(下)

8.編寫一個程序&#xff0c;以一個字符和任意文件名作為命令行參數。如果字符后面沒有參數&#xff0c;該程序讀取標 淮輸入;否則&#xff0c;程序依次打開每個文件并報告每個文件中該字符出現的次數。文件名和字符本身也要一同報告。程序應包含錯誤檢查&#xff0c;以確定參數…

王樹森推薦系統公開課

github&#xff1a;https://github.com/wangshusen/RecommenderSystem b站&#xff1a;推薦系統公開課——8小時完整版&#xff0c;講解工業界真實的推薦系統_嗶哩嗶哩_bilibili 知乎上一個比較全面的筆記&#xff1a;https://zhuanlan.zhihu.com/p/678664853 我的筆記&…

<RT1176系列11>DMAMUX解讀

1、概述DMA&#xff08;直接內存訪問&#xff0c;DIrect Memory Access&#xff09;工作原理&#xff1a;DMA控制器直接在內存和外設之間傳輸數據&#xff0c;而不需要CPU的干預。優點&#xff1a;極大地提高了數據傳輸效率&#xff0c;釋放CPU資源。適合大批量數據傳輸&#x…

【MySQL】MySQL 緩存方案

一、MySQL主從同步 1.1 主從同步是什么 MySQL 主從同步是一種數據復制機制&#xff0c;通過該機制可以實現將主數據庫&#xff08;Master&#xff09;的 DDL&#xff08;數據定義語言&#xff09;和 DML&#xff08;數據操縱語言&#xff0c;如 update、insert、delete&#x…

base64.b64encode(f.read()).decode(‘utf-8‘)作用

base64.b64encode(f.read()).decode(utf-8) 的作用是將文件內容&#xff08;通常是二進制文件&#xff09;編碼為一個 UTF-8 格式的字符串。下面逐步解釋這個過程&#xff1a;f.read()&#xff1a;讀取文件 f 中的內容。這將返回文件的二進制數據。base64.b64encode()&#xff…

集合框架學習

目錄 集合體系結構 Collection的常用方法 Collection的遍歷方式 迭代器 增強for Lambda表達式 集合框架概述 集合體系結構 單列集合 Collection代表單列集合,每個元素(數據)只包含一個值 雙列集合 Map代表雙列集合,每個元素包含兩個值(鍵值對) Collection集合特點 Li…

經典算法題解析:從思路到實現,掌握核心編程思維

算法是編程的靈魂&#xff0c;也是面試中的重點考察內容。本文精選了幾道經典算法題&#xff0c;涵蓋字符串處理、鏈表操作、樹遍歷等常見場景&#xff0c;通過詳細解析幫助你理解算法設計思路與實現細節&#xff0c;提升解題能力。一、無重復字符的最長子串題目描述給定一個字…

【Unity游戲】——1.俄羅斯方塊

搭建場景 使用任意方塊、純色瓦片或者其他圖形作為背景&#xff0c;設置其大小與目標大小一致或者更大&#xff0c;設置左下角為場景頂點&#xff0c;并放置在&#xff08;0&#xff0c;0&#xff09;處。調整攝像機至合適位置。 制作游戲預制體 每個方塊預制體包含有4個小方…

【C++進階】---- 二叉搜索樹

1.二叉搜索樹的概念 ?叉搜索樹?稱?叉排序樹&#xff0c;它或者是?棵空樹&#xff0c;或者是具有以下性質的?叉樹: ? 若它的左?樹不為空&#xff0c;則左?樹上所有結點的值都?于等于根結點的值 ? 若它的右?樹不為空&#xff0c;則右?樹上所有結點的值都?于等于根結…

基于 OpenCV 與 sklearn 的數字識別:KNN 算法實踐

在計算機視覺領域&#xff0c;數字識別是一個經典問題&#xff0c;廣泛應用于郵政編碼識別、車牌識別等場景。本文將介紹如何使用 OpenCV 進行圖像處理&#xff0c;并結合 KNN&#xff08;K 近鄰&#xff09;算法實現數字識別&#xff0c;同時對比 OpenCV 內置 KNN 與 scikit-l…

利用徑向條形圖探索華盛頓的徒步旅行

利用徑向條形圖探索華盛頓的徒步旅行 import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np import pandas as pdfrom matplotlib.cm import ScalarMappable from matplotlib.lines import Line2D from mpl_toolkits.axes_grid1.inset_locator impor…

火狐瀏覽器中國特供版關閉,如何下載 Firefox 國際版?如何備份數據?

火狐瀏覽器中國特供版關閉&#xff0c;如何下載 Firefox 國際版&#xff1f;如何備份數據&#xff1f;各位火狐老用戶注意了&#xff01;7 月 27 日北京謀智火狐正式發布公告&#xff1a;2025 年 9 月 29 日 24:00 起&#xff0c;中國特供版賬戶服務將徹底關閉&#xff0c;所有…

C語言操作符詳解:從基礎到進階

在C語言中&#xff0c;操作符是構建表達式的基礎&#xff0c;掌握各類操作符的用法、優先級及特性&#xff0c;對寫出高效且正確的代碼至關重要。本文將系統梳理C語言操作符的核心知識點&#xff0c;包含實例代碼與詳細解析&#xff0c;助你徹底搞懂操作符。 1. 操作符的分類 C…

鴻蒙平臺運行Lua腳本

1. 目標 使用 rust 在移動端實現 Lua 腳本的運行。 2. 核心步驟 [Rust Host App]│├── [mLua VM] (通過 mlua 或 rlua 庫嵌入)│ ├── 獨立Lua狀態&#xff08;隔離執行&#xff09;│ ├── 受限標準庫&#xff08;禁用危險函數&#xff09;│ └── 內存/CPU限…

【Ubuntu】發展歷程

Ubuntu 是一個基于 Debian 的 Linux 發行版&#xff0c;由 Canonical 公司開發和維護。它以其易用性、穩定性和強大的社區支持而著稱。以下是 Ubuntu 從發布以來的主要版本和發展歷程&#xff1a;1. Ubuntu 4.10 "Warty Warthog" (2004)發布日期&#xff1a;2004年10…