?????爆笑教程????《看表情包學(xué)Linux》???猛戳訂閱????
?? 寫在前面:本章是個(gè) "插敘",前幾章我們學(xué)了程序替換,現(xiàn)在我們可以嘗試動(dòng)手做一個(gè) "會(huì)創(chuàng)建,會(huì)終止,會(huì)等待,會(huì)程序替換" 的簡(jiǎn)易?shell 了。通過本章的內(nèi)容,可以進(jìn)一步鞏固進(jìn)程替換,學(xué)習(xí)內(nèi)建命令的概念以實(shí)現(xiàn)路徑切換,并再次理解環(huán)境變量。
?? 本章目錄:
0x00 補(bǔ)充:Vim 小技巧之文本替換
0x01 顯示提示符和獲取用戶輸入
0x02 將接收到的字符串拆開
0x03 創(chuàng)建進(jìn)程 & 程序替換
0x04 給命令帶顏色
0x05?內(nèi)建命令:實(shí)現(xiàn)路徑切換
0x06 再次理解環(huán)境變量
?? 本篇博客全站熱榜排名:未上榜?
0x00 補(bǔ)充:Vim 小技巧之文本替換
?在開始之前,我們先補(bǔ)充一個(gè)??使用小技巧:?
:%s///g
0x01 顯示提示符和獲取用戶輸入
?shell 本質(zhì)就是個(gè)死循環(huán),我們不關(guān)心獲取這些屬性的接口,如果要實(shí)現(xiàn) shell:
- Step1:顯示提示符 →??#
- Step2:獲取用戶輸入 → fgets
- Step3:將接收到的字符串拆開? →??把 "ls -a -l"?轉(zhuǎn)換成??"ls"? "-a"? "-l"?
- ……
?我們先從簡(jiǎn)單的入手,先來實(shí)現(xiàn)前兩步,顯示提示符 和 獲取用戶輸入:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define NUM 1024
char command_line[NUM]; // 用來接收命令行內(nèi)容
int main(void)
{
while (1) {
/* Step1:顯示提示符 */
printf("[檸檬葉子@我的主機(jī)名 當(dāng)前目錄] # ");
fflush(stdout);
/* Step2:獲取用戶輸入 */
memset (
command_line,
'\0',
sizeof(command_line) * sizeof(char)
);
fgets(command_line, NUM, stdin); /* 從鍵盤獲取,標(biāo)準(zhǔn)輸入,stdin
獲取到 C 風(fēng)格的字符串,默認(rèn)添加 '\0' */
printf("%s\n", command_line);
}
}
?? 說明:我們利用 fgets 函數(shù)從鍵盤上獲取,標(biāo)準(zhǔn)輸入?stdin,獲取到 C 風(fēng)格的字符串,
注意默認(rèn)會(huì)添加 \0?,我們先把獲取到的結(jié)果 command_line 打印出來看看:
?因?yàn)?command_line 里有一個(gè) \n,我們把它替換成 \0 即可:
command_line[strlen(command_line) - 1] = '\0'; // 消除 '\0'
?? 運(yùn)行結(jié)果如下:
至此,我們已經(jīng)完成了提示用戶輸入,并且也獲取到用戶的輸入了。
0x02 將接收到的字符串拆開
下面我們需要 將接收到的字符串拆開,比如:把 "ls -a -l" 拆成??"ls"? "-a"? "-l"?
?因?yàn)?exec 函數(shù)簇?zé)o論是列表傳參還是數(shù)組傳參,一定是要逐個(gè)傳遞的!
"所以我們不得不拆,我的四十米長(zhǎng)刀早已饑渴難耐!"
?我們可以使用 strtok 函數(shù),將一個(gè)字符串按照特定的分隔符打散,將子串依次返回:
char* strtok(char* str, const char* delim);
?? 代碼演示:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define NUM 1024
#define SEP " "
char command_line[NUM]; // 存儲(chǔ)命令行內(nèi)容
char* command_args[SIZE]; // 命令參數(shù)
int main(void)
{
while (1) {
/* Step1:顯示提示符 */
printf("[檸檬葉子@我的主機(jī)名 當(dāng)前目錄] # ");
fflush(stdout);
/* Step2:獲取用戶輸入 */
memset (
command_line,
'\0',
sizeof(command_line) * sizeof(char)
);
fgets(command_line, NUM, stdin); /* 從鍵盤獲取,標(biāo)準(zhǔn)輸入,stdin
獲取到 C 風(fēng)格的字符串,默認(rèn)添加 '\0' */
command_line[strlen(command_line) - 1] = '\0'; // 消除 '\0'
/* Step3: 將接收到的字符串拆開 - 字符串切分 */
command_args[0] = strtok(command_line, SEP); // 按空格切分
int idx = 1;
/* 這里的 = 是故意這么寫的,因?yàn)?strtok 截取成功返回字符串起始地址
截取失敗,返回 NULL */
while (command_args[idx++] = strtok(NULL, SEP));
// 我們來測(cè)試一下看看
for (int i = 0; i < idx; i++) {
printf("%d : %s\n", command_args[i]);
}
printf("%s\n", command_line);
}
}
??? 運(yùn)行結(jié)果如下:
字符串切分搞定了!
0x03 創(chuàng)建進(jìn)程 & 程序替換
下面我們實(shí)現(xiàn) 創(chuàng)建進(jìn)程,執(zhí)行它。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define NUM 1024
#define SEP " "
#define SIZE 128
char command_line[NUM];
char* command_args[SIZE];
int main(void)
{
while (1) {
/* Step1:顯示提示符 */
printf("[檸檬葉子@我的主機(jī)名 當(dāng)前目錄] # ");
fflush(stdout);
/* Step2:獲取用戶輸入 */
memset (
command_line,
'\0',
sizeof(command_line) * sizeof(char)
);
fgets(command_line, NUM, stdin); /* 從鍵盤獲取,標(biāo)準(zhǔn)輸入,stdin
獲取到 C 風(fēng)格的字符串,默認(rèn)添加 '\0' */
command_line[strlen(command_line) - 1] = '\0'; // 消除 '\0'
/* Step3: 將接收到的字符串拆開 - 字符串切分 */
command_args[0] = strtok(command_line, SEP);
int idx = 1;
/* 這里的 = 是故意這么寫的,因?yàn)?strtok 截取成功返回字符串起始地址
截取失敗,返回 NULL */
while (command_args[idx++] = strtok(NULL, SEP));
//我們來測(cè)試一下看看
// for (int i = 0; i < idx; i++) {
// printf("%d : %s\n", i, command_args[i]);
// }
// printf("%s\n", command_line);
/* Step4. TODO */
/* Step5. 創(chuàng)建進(jìn)程,執(zhí)行 */
pid_t id = fork();
if (id == 0) {
/* child */
/* Step6: 程序替換 */
execvp (
command_args[0], // 保存的是我們要執(zhí)行的程序名字
command_args
);
exit(1); // 只要執(zhí)行到這里,子進(jìn)程一定是替換失敗了,直接退出。
}
/* Father */
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) { // 等待成功
printf("等待成功!sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
}
} // end while
}
?? 運(yùn)行結(jié)果如下:
0x04 給命令帶顏色
?還有很多地方不完美,比如:如何讓我們的命令帶顏色呢?
?? 代碼演示:給 ls 命令添加顏色
/* Step3: 將接收到的字符串拆開 - 字符串切分 */
command_args[0] = strtok(command_line, SEP);
int idx = 1;
// 顏色的添加 -> 提出程序名,如果名師輸入 ls,在 command 里添加 --color
if (strcmp(command_args[0] /* 程序名 */, "ls") == 0) {
command_args[idx++] = (char*)"--color=auto";
}
?? 運(yùn)行結(jié)果如下:
0x05?內(nèi)建命令:實(shí)現(xiàn)路徑切換
目前還有一個(gè)問題,我們 cd..?回退到上級(jí)目錄時(shí),我們的路徑是不發(fā)生變化的:
真相:雖然系統(tǒng)中存在 cd 命令,但我們寫的 shell 腳本中用的根本就不是這個(gè) cd 命令。
當(dāng)你在執(zhí)行 cd 命令時(shí),調(diào)用 execvp 執(zhí)行的實(shí)際上是系統(tǒng)特定路徑下的 cd:
if (id == 0) {
/* child */
/* Step6: 程序替換 */
execvp (
command_args[0], // 保存的是我們要執(zhí)行的程序名字
command_args
);
exit(1); // 只要執(zhí)行到這里,子進(jìn)程一定是替換失敗了,直接退出。
}
它只影響了子進(jìn)程,如果我們直接 exec* 執(zhí)行 cd,那么最多只是讓子進(jìn)程進(jìn)行路徑切換。
但是請(qǐng)不要忘了:子進(jìn)程是一運(yùn)行就完畢的進(jìn)程!運(yùn)行完了你切換它的路徑,毫無意義。
所以,我們?cè)?shell 中,更希望誰的路徑發(fā)生變化呢?父進(jìn)程?。╯hell 本身)
父進(jìn)程對(duì)應(yīng)的路徑發(fā)生變化,這一塊稍微有一點(diǎn)繞:
只要讓我執(zhí)行 cd,按照之前的代碼就是進(jìn)程替換,和父進(jìn)程有什么關(guān)系,子進(jìn)程一跑就完了,曾經(jīng)的復(fù)出沒有任何意義了實(shí)際上是想讓父進(jìn)程的路徑發(fā)生變化。那么在我們現(xiàn)有的代碼中能做到讓父進(jìn)程的路徑發(fā)生變化嗎?不可能因?yàn)槲覀儸F(xiàn)有的代碼在進(jìn)行操作的時(shí)候最終的結(jié)果都會(huì)落實(shí)到 fork,然后 exec。這也就意味著,不管是什么命令,最后你都是創(chuàng)建子進(jìn)程,cd 命令也不除外。
所以,對(duì)我們來說我們此時(shí)就有一個(gè)需求了:如果有些行為是必須讓父進(jìn)程 shell 執(zhí)行的,不想讓子進(jìn)程執(zhí)行,這樣的場(chǎng)景下,絕對(duì)不能創(chuàng)建子進(jìn)程!進(jìn)位一旦創(chuàng)建了子進(jìn)程最后執(zhí)行任務(wù)的是子進(jìn)程,和你就沒有任何干系了,只能是父進(jìn)程自實(shí)現(xiàn)對(duì)應(yīng)的代碼。
這部分由 shell 自己執(zhí)行的命令,我們稱之為 內(nèi)建指令?(build-in) 。
?下面我們就來解決路徑切換的問題:
/* Shell 內(nèi)置函數(shù): 路徑跳轉(zhuǎn) */
int ChangeDir(const char* new_path) {
chdir(new_path);
return 0; // 調(diào)用成功
}
int main(void)
{
...
/* Step4. TODO 編寫后面的邏輯,內(nèi)建命令 */
if (strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL) {
ChangeDir(command_args[1]); // 讓調(diào)用方進(jìn)行路徑切換
continue;
}
...
}
?? 運(yùn)行結(jié)果如下:
??? 說明:在上層你看到的是個(gè)命令,但是在 shell 內(nèi)部本質(zhì)上是由父 shell 自己實(shí)現(xiàn)、調(diào)用的一個(gè)函數(shù)(并沒有創(chuàng)建子進(jìn)程),這種就是對(duì)應(yīng)上上層的 內(nèi)建命令。
內(nèi)建命令表現(xiàn)是用用戶層面的一條命令,本質(zhì)就是 Shell 內(nèi)部的一個(gè)函數(shù),由父 Shell 自己執(zhí)行,而不創(chuàng)建子進(jìn)程。
0x06 再次理解環(huán)境變量
我們上一章學(xué)過的 exec 的函數(shù),是可以直接執(zhí)行這指定的命令、環(huán)境變量的。
?獲取環(huán)境變量,直接遍歷環(huán)境變量列表就行:
// 方便測(cè)試,我們創(chuàng)建一個(gè) hello.c 文件
#include <stdio.h>
int main(void)
{
/* 獲取環(huán)境變量列表 */
extern char** environ;
for (int i = 0; environ[i] != NULL; i++) {
printf("[%d]: %s\n", i, environ[i]);
}
return 0;
}
環(huán)境變量具有全局屬性,我們可以在程序中添加環(huán)境變量的聲明:
extern char** environ; // 環(huán)境變量指針聲明
/* Step6: 程序替換 */
execvp (
command_args[0], // 保存的是我們要執(zhí)行的程序名字
command_args,
environ // 添加環(huán)境變量
);
程序替換中,對(duì)于 exec 函數(shù)簇,如果如果函數(shù)名沒?e,所有的環(huán)境變量是會(huì)被繼承的。
不帶 e,環(huán)境變量依舊是可以被繼承的,如果我們自己定一個(gè)環(huán)境變量的指針數(shù)組,
它會(huì)覆蓋我們的環(huán)境變量列表,我現(xiàn)在不想覆蓋,我想新增:
/* 放置環(huán)境變量 */
void PutEnvMyShell(const char* new_env) {
putenv(new_env);
}
if (strcmp(command_args[0], "export") == 0 && command_args[1] != NULL) {
PutEnvMyShell((char*)command_args[1]); // export myval=100
continue;
}
這是為什么呢?因?yàn)楫?dāng)前環(huán)境變量信息存儲(chǔ)在了 command_line 中,會(huì)被清空。
那么環(huán)境變量也會(huì)隨之清空而丟失,所以我么需要一個(gè)專門存儲(chǔ)環(huán)境變量的:
char env_buffer[NUM]; // 保存環(huán)境變量 just for test
if (strcmp(command_args[0], "export") == 0 && command_args[1] != NULL) {
// 目前,環(huán)境變量信息在 command_line,會(huì)被清空,環(huán)境變量也隨之清空
// 此處我們需要自己保存一下環(huán)境變量的內(nèi)容
strcpy(env_buffer, command_args[1]);
PutEnvMyShell(env_buffer); // export myval=100
continue;
}
?? 運(yùn)行結(jié)果如下:?
?? 環(huán)境變量的數(shù)據(jù)在進(jìn)程的上下文中:
① 環(huán)境變量會(huì)被子進(jìn)程繼承下去,所以他會(huì)有全局屬性。
② 當(dāng)我們進(jìn)行程序替換時(shí), 當(dāng)前進(jìn)程的環(huán)境變量非但不會(huì)替換,而且是繼承父進(jìn)程的!
環(huán)境你不傳,默認(rèn)子進(jìn)程全部都會(huì)自動(dòng)繼承。
如果你 exel 函數(shù)簇帶 e,就相當(dāng)于你選擇了自己傳,就會(huì)覆蓋式地把原本的環(huán)境變量弄沒,然后你自己交給子進(jìn)程。如果不帶 e,那么環(huán)境變量就會(huì)自己被子進(jìn)程繼承。
如果既不想覆蓋系統(tǒng),也不想新增,所以我們采用 putEnv 的方式向父 Shell 導(dǎo)入新增一個(gè)它自己的環(huán)境變量,這樣的話原始的環(huán)境變量還在,我們能在 shell 上下文上給它新增環(huán)境變量。
所以,如何理解環(huán)境變量具有全局屬性?
因?yàn)樗械沫h(huán)境變量會(huì)被當(dāng)前進(jìn)程之下的所有子進(jìn)程默認(rèn)繼承下去。
如何在 Shell 內(nèi)部自己導(dǎo)入新增自己的環(huán)境變量?
putEnv,要注意的是,需要一個(gè)獨(dú)立的空間,放置環(huán)境變量的數(shù)據(jù)被改寫。
?? [ 筆者 ]? ?王亦優(yōu)
?? [ 更新 ]? ?2023.3.21
? [ 勘誤 ]?? /* 暫無 */
?? [ 聲明 ]? ?由于作者水平有限,本文有錯(cuò)誤和不準(zhǔn)確之處在所難免,
本人也很想知道這些錯(cuò)誤,懇望讀者批評(píng)指正!
?? 參考資料? C++reference[EB/OL]. []. http://www.cplusplus.com/reference/. Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. . 百度百科[EB/OL]. []. https://baike.baidu.com/.文章來源:http://www.zghlxwxcb.cn/news/detail-835416.html 比特科技. Linux[EB/OL]. 2021[2021.8.31 xi文章來源地址http://www.zghlxwxcb.cn/news/detail-835416.html |
到了這里,關(guān)于【看表情包學(xué)Linux】插敘:實(shí)現(xiàn)簡(jiǎn)易的 Shell | 通過內(nèi)建命令實(shí)現(xiàn)路徑切換 | 再次理解環(huán)境變量的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!