【linux】自定義shell——bash命令行解釋器小程序

小編個人主頁詳情<—請點擊
小編個人gitee代碼倉庫<—請點擊
linux系列專欄<—請點擊
倘若命中無此運,孤身亦可登昆侖,送給屏幕面前的讀者朋友們和小編自己!
在這里插入圖片描述


目錄

    • 前言
    • 一、交互問題,獲取命令行
    • 二、字串的分隔問題,解析命令行
    • 三、普通命令的執行
    • 四、內建命令
    • 五、源代碼
      • makefile
      • myshell.c
    • 總結


前言

【linux】linux進程控制(三)(進程程序替換,exec系列函數)——書接上文 詳情請點擊<——
本文由小編為大家介紹——【linux】自定義shell——bash命令行解釋器小程序

本文會基于進程控制中的進程創建,進程終止,進程等待,進程替換的知識去模擬實現bash命令行解釋器小程序,建議對于進程控制的知識不熟悉的讀者友友,可以點擊后方藍字進行學習后再來閱讀本文

  1. 進程創建,進程終止詳情請點擊<——
  2. 進程等待詳情請點擊<——
  3. 進程替換詳情請點擊<——

shell是一個外殼程序,shell是操作系統層面命令行解釋器,在linux中的命令行解釋器是bash(shell的范圍更大,bash僅限于linux),shell/bash的本質也就是一個進程,執行指令的時候,也就是通過創建子進程執行的,所以當我們登錄的時候,系統就是要為我們啟動一個shell進程,所以小編可以通過編寫一個程序,在命令行解釋器中啟動,這樣就基于命令行解釋器的基礎上運行起來我們自主實現的shell了

一、交互問題,獲取命令行

在這里插入圖片描述

  1. 我們觀察一下bash命令行,它的格式是[wzx@VM-12-3-centos lesson20]$ 這種形式,所以我們自定義實現的bash也應該類似于這種形式,通常普通用戶使用$,root用戶使用#,但是這里我們為了和小編使用的普通用戶的$進行區分,所以我們使用#作為最后一個字符,即 [用戶@主機名 路徑]# 這種形式
  2. 那么我們就需要獲取當前的用戶名,主機名,以及當前所在的工作路徑,對于這些信息我們都可以使用putenv進行獲取這些環境變量信息,其中用戶名在環境變量中有USER,主機名在環境變量中也有HOSTNAME,當前所在的工作路徑在環境變量中也有PWD進行獲取(注意:后面講到內建命令cd的時候,小編會對當前工作路徑的獲取方式進行修改,這里使用環境變量PWD進行獲取便于理解)
    在這里插入圖片描述
#include <stdio.h>
#include <stdlib.h>char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}char* getpwd()
{return getenv("PWD");
}int main()
{printf("[%s@%s %s]# \n", getusrname(), gethostname(), getpwd());return 0;
}

運行結果如下
在這里插入圖片描述

  1. 我們可以看到當前我們的程序也可以打印出[用戶@主機名 路徑]# 這種形式了,但是注意觀察,bash命令行解釋器打印出[用戶@主機名 路徑]$之后并沒有進行換行,而是等待我們進行輸入,所以小編將我們添加的換行\n去掉
  2. 這個等待其實就是阻塞式等待鍵盤設備就緒,即等待用戶輸入,原理其實很簡單一個sacnf就可以讓我們也是實現這樣的功能,那么我們將用戶的輸入使用字符數組commandline存儲起來便于我們后續的字符串分隔,那么對于這個字符數組的大小通常是1024,并且我們也喜歡使用宏LINE_SIZE進行定義這個1024,便于進行修改
  3. 我們并不喜歡直接將諸如格式之類的直接定義在實現代碼中,我們通常使用一個宏FORMATE定義在開頭便于我們進行修改
#include <stdio.h>
#include <stdlib.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024char commandline[LINE_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}char* getpwd()
{return getenv("PWD");
}int main()
{printf(FORMATE, getusrname(), gethostname(), getpwd());scanf("%s", commandline);printf("echo: %s\n", commandline);//打印測試return 0;
}

運行結果如下
在這里插入圖片描述

  1. 此時我們第一次輸入lllllllllllllllll,scanf可以正常讀取命令行中用戶輸入的內容,并且測試打印也無誤
  2. 可是我們知道指令通常都是帶選項即空格的,那么當我們進行第二次輸入ls -a -l的時候,此時進行打印字符數組commandline中的內容的時候,卻只有一個ls,對于后面的 -a -l沒有進行讀取,這是因為scanf會默認遇到空格或者換行就結束讀取,此時ls和-a中間我們使用了空格進行分隔,所以scanf讀取到這個空格就停止了,所以字符數組commandline中就只會有ls
  1. 那么接下來進行讀取我們應該一次讀取一行,即遇到空格不結束讀取,遇到換行才結束讀取,很多讀者友友心中第一反應應該就是getline這個函數了吧,使用getline可以一次獲取一行,并且遇到空格不結束,遇到換行才結束,符合我們的需求

在這里插入圖片描述

  1. 但是今天小編教大家一個新的玩法同樣也可以實現我們的需求,那么就是fgets,它的作用是從文件的流中讀取內容,并將這個內容輸入到字符數組中,fgets需要傳入一個字符數組,字符數組的大小,流,前兩個參數我們都可以輕松搞定

在這里插入圖片描述

  1. 但是對于第三個參數呢?FILE* stream就是一個流對象,即FILE*就是我們曾經打開的文件,流,對我們來說很陌生,但是這里小編要介紹三個標準流中的stdin,它是一個文件的流對象,在我們的c語言程序啟動的時候,編譯器就會默認幫我們打開一個讀取輸入文件,我們的輸入就會輸入到這個文件中,流對象stdin就是對這個輸入的文件進行管理和讀取寫入等一系列操作的入口,所以我們可以使用stdin作為fgets的第三個參數
#include <stdio.h>
#include <stdlib.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024char commandline[LINE_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}char* getpwd()
{return getenv("PWD");
}int main()
{printf(FORMATE, getusrname(), gethostname(), getpwd());fgets(commandline, sizeof(commandline), stdin);printf("echo: %s\n", commandline);return 0;
}

運行結果如下
在這里插入圖片描述

  1. 我們可以看到這樣小編輸入的ls -a -l就都被fgets讀取寫入到字符數組commandline中了,那么下面我們對比一下下面小編使用scanf的上一次的運行結果

在這里插入圖片描述

  1. 我們可以看到相比較一下,使用fgets獲取的字符串多了一個換行,明明我們打印即printf(“echo: %s\n”, commandline)只有一個\n換行,可是這里卻多了一個換行,即這里有兩個換行,那么這個換行究竟是如何來的呢?
  2. 讀者友友仔細思考一下,我們在bash命令行中輸入完成指令后,例如ls -a -l輸入完成后,是不是都要按一下回車換行,bash命令行才結束讀取執行命令,由于這個回車換行也是一個字符,同樣也被寫入到了stdin這個流中了,所以fgets就會一并將回車換行也進行讀取,所以這里打印的時候才會多出一個換行來
  1. 所以我們還應該將這里給特殊處理一下,將字符數組commandline中的換行調整為\0,同時其實bash命令行解釋器是一個進程,當我們啟動xShell并且登錄的時候,bash命令行解釋器這個進程就啟動了,我們在命令行上輸入,按下回車,bash命令行解釋器創建子進程給我們完成任務,我們接著就是輸入下一個指令,下一個指令……,仔細思考一下,bash命令行解釋器我們如何退出,是不是要按下右上角的?才可以進行退出,類似的,諸如微信,qq,網易云等也要按下右上角的?才可以退出,這些軟件同樣是程序,同樣的都要以進程的方式進行運行,如果我們不按右上角的?這些進程就會一直運行,除非電腦沒電,所以這些進程一旦啟動,都是以死循環的方式進行運行,只有我們按下右上角的?才會終止進程,同樣的bash命令行解釋器也是一個死循環的進程,所以我們模擬bash命令行解釋器的程序整體應該也是死循環
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024char commandline[LINE_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}char* getpwd()
{return getenv("PWD");
}int main()
{while(1){printf(FORMATE, getusrname(), gethostname(), getpwd());fgets(commandline, sizeof(commandline), stdin);//ls -a -l\n\0commandline[strlen(commandline) - 1] = '\0';printf("echo: %s\n", commandline);}return 0;
}

運行結果如下
在這里插入圖片描述

  1. 如上,多出的那一個換行就被小編去掉了,打印無誤,并且也進行了死循環式的等待
  2. 當我們想要退出的時候,按下ctrl+c即可退出
  1. 所以第一個模塊我們就完成了,所以我們去掉用于打印字符數組內容的打印代碼,由于這個模塊是我們的程序和用戶進行交互的區域模塊,所以我們將其放在interact函數中
  2. 并且希望使用傳參的方式進行調用interact()獲取用戶的輸入,寫入到字符數組commandline中,所以字符數組commandline就應該進行傳參,由于fgets要使用字符數組的大小,而字符數組的大小我們又無法在interact函數內求出,所以也應該interact函數外求出進行傳參
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024char commandline[LINE_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}char* getpwd()
{return getenv("PWD");
}void interact(char* cline, int size)
{printf(FORMATE, getusrname(), gethostname(), getpwd());fgets(cline, size, stdin);//ls -a -l\n\0cline[strlen(cline) - 1] = '\0';printf("%s\n", cline);
}int main()
{while(1){//交互interact(commandline, sizeof(commandline));}return 0;
}

二、字串的分隔問題,解析命令行

  1. 上一個模塊,我們可以將用戶的輸入寫入到一個字符數組commandline中了,那么接下來我們就要解析一下用戶的輸入,如果用戶的輸入帶選項的指令,那么選項和指令之間,選項和選項之間都是以空格為分隔符,例如ls -a -l,所以我們應該按照空格為分隔符進行分隔用戶的輸入,即字符數組commandline

在這里插入圖片描述

  1. 那么我們就可以使用c語言的stoke,進行分隔字符數組commandline,strtok的第一個參數傳入需要分隔的字符串,第二個參數輸入需要分隔的字符的合集,對于這個字符的合集我們使用宏DELIM定義一下,便于進行修改,strtok的第一個參數第一次需要傳入傳入進行分隔的字符串,它會在需要分隔的字符的合集中匹配字符,當遇到匹配的字符之后,它就會將匹配分隔的字符對應字符串的位置設置為\0,并且移動到下一個位置,停止,等待第二次調用,所以這個strtok需要進行第一次的預處理,剩下的分隔strtok的第一個參數傳入NULL,它就會將所有匹配分隔字符的字符串位置全部設置為\0了,當移動到最后,沒有字符串可以進行分隔的時候,它會返回NULL,我們可以利用這個NULL退出循環
  2. 我們需要獲取分隔的字符串,并且將這個分隔的字符串放到一個字符串數組argv中,初始化argv的大小的時候,我們使用宏ARGV_SIZE定義一下大小為32,因為一個命令就算選項在這么多,一行中帶的選項一般不會超過32個
  3. 同樣的,我們可以根據分隔的次數-1,去統計argc的次數,即命令加選項的個數
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
#define DELIM " "
#define ARGC_SIZE 32char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}char* getpwd()
{return getenv("PWD");
}void interact(char* cline, int size)
{printf(FORMATE, getusrname(), gethostname(), getpwd());fgets(cline, size, stdin);//ls -a -l\n\0cline[strlen(cline) - 1] = '\0';
}int main()
{while(1){//交互interact(commandline, sizeof(commandline));//解析命令行int i = 0;argv[i++] = strtok(commandline, DELIM);while(argv[i++] = strtok(NULL, DELIM));int argc = i - 1;if(argv[0] == NULL) argc = 0;//當用戶沒有輸入的時候,argc為0,應該特殊處理一下if(argc == 0) continue;//當argc為0的時候,用戶沒有輸入,這時候應該重新與用戶交互for(int j = 0; argv[j]; j++)//打印命令行參數進行測試{printf("argv[%d]: %s\n", j, argv[j]);}printf("argc: %d\n", argc);}return 0;
}

運行結果如下
在這里插入圖片描述

  1. 我們希望將解析命令行放入splitstring這個函數中,會使用到字符數組commandline以及字符串指針數組用來存儲分割后的字符串的起始地址,所以我們將其進行傳參,并且根據字符串分隔的次數,返回argc即命令加選項的個數,如果在函數外接收的argc的個數為0,說明此時用戶僅僅按下回車換行,即沒有有效輸入,我們應該continue重新與用戶進行交互
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
#define DELIM " "
#define ARGC_SIZE 32char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}char* getpwd()
{return getenv("PWD");
}void interact(char* cline, int size)
{printf(FORMATE, getusrname(), gethostname(), getpwd());fgets(cline, size, stdin);//ls -a -l\n\0cline[strlen(cline) - 1] = '\0';
}int splitstring(char* cline, char* _argv[])
{int i = 0;_argv[i++] = strtok(cline, DELIM);while(_argv[i++] = strtok(NULL, DELIM));if(_argv[0] == NULL){return 0;}return i - 1;
}int main()
{while(1){//交互interact(commandline, sizeof(commandline));//解析命令行int argc = splitstring(commandline, argv);if(argc == 0){continue;}}return 0;
}

三、普通命令的執行

  1. 我們上兩個模塊我們已經可以接收用戶輸入,將用戶輸入的字符串解析出來,那么接下來就是根據解析出來的命令和選項去執行命令了,對于普通命令,是由bash創建子進程,子進程去執行普通命令,由于我們有命令就是argv[0],但是我們沒有路徑,我們有命令行參數argv,子進程進行程序替換execvp即可
  2. 接下來就是父進程使用waitpid等待指定的子進程即可,獲取子進程的退出碼即可
  3. 同樣的,我們希望將普通命令的執行放在normalexcute函數中執行,
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
#define DELIM " "
#define ARGC_SIZE 32
#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}char* getpwd()
{return getenv("PWD");
}void interact(char* cline, int size)
{printf(FORMATE, getusrname(), gethostname(), getpwd());fgets(cline, size, stdin);//ls -a -l\n\0cline[strlen(cline) - 1] = '\0';
}int splitstring(char* cline, char* _argv[])
{int i = 0;_argv[i++] = strtok(cline, DELIM);while(_argv[i++] = strtok(NULL, DELIM));if(_argv[0] == NULL){return 0;}return i - 1;
}void normalexcute(char* _argv[])
{int id = fork();if(id < 0) {perror("fork");return;}else if(id == 0){execvp(_argv[0], _argv);exit(EXIT_CODE);}else {//id > 0int status = 0;int ret = waitpid(id, &status, 0);if(ret == id){lastcode = WEXITSTATUS(status);}}
}int main()
{while(1){//交互interact(commandline, sizeof(commandline));//解析命令行int argc = splitstring(commandline, argv);if(argc == 0){continue;}//普通命令的執行normalexcute(argv);  }return 0;
}

運行結果如下
在這里插入圖片描述

  1. 其中小編模擬實現的bash命令行解釋器就可以初步運行起來執行普通命令了,諸如ls,pwd,whoami等普通命令都可以執行
  2. 但是當我們使用cd命令的時候,即cd …想要退回上級目錄發現,退回失敗,并且當前進程的路徑仍然沒有變化,這就很令人困惑,我不是fork子進程,子進程進行程序替換執行這個命令了嗎?為什么當前進程的路徑沒有發生變化?
  3. 恰恰如此,正是由于是子進程執行的這個cd命令,所以變化的是子進程的當前工作路徑,和當前的進程,也就是父進程的工作路徑的無關,所以我們應該讓父進程執行這個cd命令,這樣當前進程的路徑才能切換,這種不創建子進程執行,而是由父進程親自執行的命令我們稱為內建命令,請讀者友友繼續閱讀,由小編進行講解我們的程序如何讓父進程親自執行內建命令

四、內建命令

  1. 其實內建命令很簡單,即使用 if 語句進行判斷即可,既然普通命令都可以通過可執行程序的方式列舉出來,那么內建命令同樣也可以使用 if 語句逐個判斷出來,在bash命令行解釋器中,常見的內建命令有40多個,這里小編模擬實現3個內建命令供大家理解學習bash命令行解釋器
  2. 注意這個內建命令的判斷以及執行的位置應該是在普通命令執行之前進行判斷,因為內建命令我們不期望讓子進程來執行,所以也應該使用一個變量ret進行判斷,執行內建命令就執行普通命令,不執行內建命令就執行普通命令

在這里插入圖片描述
在這里插入圖片描述

  1. 那么首先我們要實現的內建命令是cd命令,cd命令的作用就是修改當前工作路徑,如何修改呢?其實操作系統提供了一個系統調用chdir用于修改當前的工作路徑,傳入路徑即可進行修改當前進程的工作路徑,同時由于與用戶進行交互的字符串,[用戶@主機名 路徑]# 中有對當前工作路徑的打印,并且這個工作路徑是從使用getenv從環境變量PWD中獲取的,所以我們還要對這個環境變量進行更新,所以我們可以使用sprintf對調用獲取的getpwd獲取的字符串(字符串其實是首字符的地址,有了地址就可以對字符串進行寫入)進行格式化寫入路徑即可
  2. 我們期望將內建命令的放在buildcommand這個函數,由于需要對命令行參數中的命令以及選項進行獲取和相應的判斷,所以傳參命令行參數argv,同時還需要命令行參數的個數argc判斷內建命令的命令以及選項個數,因為諸如cd命令,它的執行只能是cd 后面跟路徑,所以命令加選項的個數只能是兩個,我們需要對其進行判斷,所以需要傳參argc命令行參數的個數
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
#define DELIM " "
#define ARGC_SIZE 32
#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}char* getpwd()
{return getenv("PWD");
}void interact(char* cline, int size)
{printf(FORMATE, getusrname(), gethostname(), getpwd());fgets(cline, size, stdin);//ls -a -l\n\0cline[strlen(cline) - 1] = '\0';
}int splitstring(char* cline, char* _argv[])
{int i = 0;_argv[i++] = strtok(cline, DELIM);while(_argv[i++] = strtok(NULL, DELIM));if(_argv[0] == NULL){return 0;}return i - 1;
}void normalexcute(char* _argv[])
{int id = fork();if(id < 0) {perror("fork");return;}else if(id == 0){execvp(_argv[0], _argv);exit(EXIT_CODE);}else {//id > 0int status = 0;int ret = waitpid(id, &status, 0);if(ret == id){lastcode = WEXITSTATUS(status);}}
}int buildcommand(int _argc, char* _argv[])
{if(_argc == 2 && strcmp(_argv[0], "cd") == 0){chdir(_argv[1]);sprintf(getpwd(), "%s", _argv[1]);return 0;}return 1;
}int main()
{while(1){//交互interact(commandline, sizeof(commandline));//解析命令行int argc = splitstring(commandline, argv);if(argc == 0){continue;}//內建命令的執行int ret = buildcommand(argc, argv);//普通命令的執行if(ret) {normalexcute(argv);} }return 0;
}

運行結果如下
在這里插入圖片描述

  1. 經過我們的 if 判斷之后果然我們的路徑發生了改變,但是對于[用戶@主機名 路徑]# 中有對當前工作路徑的打印,卻成為了…這并不是我們期望看到的,因為如果我們使用cd …改變當前路徑,這個…是相對路徑,而不是絕對路徑,所以我們使用argv[1]進行訪問,會將…寫入到環境變量PWD中,我們期望每時每刻getpwd獲取的當前的工作路徑是絕對路徑,所以就不能使用環境變量進行獲取

在這里插入圖片描述

  1. 所以我們需要對getpwd獲取當前工作路徑的方式進行修改,不使用getenv從環境變量PWD中獲取當前的工作路徑,而是采用系統調用getcwd獲取當前進程的工作路徑,同時我們使用一個字符數組pwd將這個路徑進行存儲,字符數組pwd的大小使用宏LINE_SIZE進行定義,那么getpwd這個函數的就不設置返回值了,而是直接對pwd字符數組進行寫入即可,由于pwd字符數組是一個全局變量,所以寫入的結果我們在任何函數都可以進行獲取這個路徑
  2. 同時我們還應使用sprintf對當前進程的環境變量中的PWD對應的工作路徑進行修改為getcwd對應的當前的工作路徑
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
#define DELIM " "
#define ARGC_SIZE 32
#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[LINE_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}void getpwd()
{getcwd(pwd, sizeof(pwd));
}void interact(char* cline, int size)
{getpwd();printf(FORMATE, getusrname(), gethostname(), pwd);fgets(cline, size, stdin);//ls -a -l\n\0cline[strlen(cline) - 1] = '\0';
}int splitstring(char* cline, char* _argv[])
{int i = 0;_argv[i++] = strtok(cline, DELIM);while(_argv[i++] = strtok(NULL, DELIM));if(_argv[0] == NULL){return 0;}return i - 1;
}void normalexcute(char* _argv[])
{int id = fork();if(id < 0) {perror("fork");return;}else if(id == 0){execvp(_argv[0], _argv);exit(EXIT_CODE);}else {//id > 0int status = 0;int ret = waitpid(id, &status, 0);if(ret == id){lastcode = WEXITSTATUS(status);}}
}int buildcommand(int _argc, char* _argv[])
{if(_argc == 2 && strcmp(_argv[0], "cd") == 0){chdir(_argv[1]);getpwd();sprintf(getenv("PWD"), "%s", pwd);return 0;}return 1;
}int main()
{while(1){//交互interact(commandline, sizeof(commandline));//解析命令行int argc = splitstring(commandline, argv);if(argc == 0){continue;}//內建命令的執行int ret = buildcommand(argc, argv);//普通命令的執行if(ret) {normalexcute(argv);} }return 0;
}

運行結果如下
在這里插入圖片描述
使用getcwd獲取絕對路徑之后,對于命令行中,[用戶@主機名 路徑]# 中有對當前工作路徑的打印就是當前進程的絕對路徑了

  1. 那么我們接下來嘗試測試export,export可以添加環境變量,那么我們看一下我們的程序是否可以進行添加當前進程的環境變量

運行結果如下
在這里插入圖片描述
在這里插入圖片描述

  1. 無法添加環境變量,因為export也是內建命令,如果不使用 if 語句進行判斷,那么上述是fork子進程,讓子進程進程程序替換執行export,將MYVALUE添加到子進程的環境變量中
  2. 奇怪,明明是子進程執行了export,那么按道理來講,我們使用env去查看,由于env也沒有經過 if 語句判斷,所以env實際上查看的是子進程的環境變量,那么此時子進程由于export添加環境變量MYVALUE之后,應該可以查看到這個MYVALUE,但是這里子進程的環境變量卻沒有這個MYVALUE,這又是為什么呢?
  3. 其實環境變量的本質是字符串指針數組,真正的環境變量是在內核空間中,是由bash進程啟動的時候,從操作系統的環境變量的配置文件 .bash_profile 中將環境變量字符串讀取拷貝到內核空間的,進程中的環境變量是字符串指針數組,里面存儲著指向環境變量字符串的一個個的指針
  4. 我們讀取用戶的輸入,并且進行分隔,將其放到char* argv[]字符串指針數組中,以上面為例argv[0]就存儲著export,argv[1]就存儲著MYVALUE=1111111111111111111111,當進程執行export的時候,實際上是在環境變量表中找到一個空閑的位置,將要添加的環境變量對應的字符串的指針放到進程的環境變量表中,此時環境變量中就存儲著argv[1]對應的字符串的地址,但是由于我們的程序要不斷的與用戶進行交互,所以argv[1]的內容會進行不斷的替換,但是在環境變量表中還存儲著argv[1]對應的字符串的地址,所以在環境變量中就找不到原來MYVALUE=1111111111111111111111了,取而代之的是新的內容,由于小編使用env進行查看,所以argv[1]位置處就為NULL,所以env查看環境變量的內容就無法查看到MYVALUE=1111111111111111111111了,MYVALUE=1111111111111111111111位置處的內容被替換成NULL
  1. 所以經過上面的分析,我們還應該維護給當前進程維護自己的環境變量表,這里小編就簡單的維護可以存儲一個字符串自己的環境變量表進行演示了,感興趣的讀者友友可以自己嘗試編寫一個二維的環境變量表,可以存儲多個字符串,也就可以維護多個環境變量

在這里插入圖片描述

  1. 同時小編將export也是用 if 語句進行判斷,當argc對一個的命令行參數的個數為兩個(export的作用就是導入環境變量,所以命令是export,選項是要添加的環境變量,所以argc對應的命令行參數的個數必須為兩個)并且是export的時候,我們就將argv[1]的內容寫入到我們維護的自己的環境變量中,并且將我們自己的環境變量使用putenv放到進程的環境變量中
  2. 但是我們現在還無法查看父進程的環境變量,如果使用env,那么查看的是子進程的環境變量,這里小編換一種方式使用echo $環境變量的方式去查看父進程的環境變量,此時面臨著同樣的境遇,如果不采用 if 語句進行判斷,那么此時也就是采用的fork創建子進程,子進程進行程序替換去執行echo $環境變量,那么查看到的仍然為子進程的環境變量,所以這里的echo仍然為內建命令,所以我們需要使用 if 語句對echo進行判斷,當為echo $?的時候,將上一個進程的退出碼打印出來,當為echo $環境變量的時候,此時使用getenv獲取環境變量進行打印即可,但是這里需要注意不可以直接使用getenv(argv[1])進行獲取,因為此時argv[1]中的字符串指針指向的內容實際上是 $環境變量,所以應該跳過$這個字符,即getenv(argv[1] + 1)進行獲取環境變量進行打印,當上述這兩種情況都不是,那么就直接打印argv[1]的字符串內容,因為echo的作用就是打印字符串內容
  3. 所以當我們編寫好內建命令export以及echo之后,我們就可以在當前的父進程中添加并且查看環境變量了
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
#define DELIM " "
#define ARGC_SIZE 32
#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}void getpwd()
{getcwd(pwd, sizeof(pwd));
}void interact(char* cline, int size)
{getpwd();printf(FORMATE, getusrname(), gethostname(), pwd);fgets(cline, size, stdin);//ls -a -l\n\0cline[strlen(cline) - 1] = '\0';
}int splitstring(char* cline, char* _argv[])
{int i = 0;_argv[i++] = strtok(cline, DELIM);while(_argv[i++] = strtok(NULL, DELIM));if(_argv[0] == NULL){return 0;}return i - 1;
}void normalexcute(char* _argv[])
{int id = fork();if(id < 0) {perror("fork");return;}else if(id == 0){execvp(_argv[0], _argv);exit(EXIT_CODE);}else {//id > 0int status = 0;int ret = waitpid(id, &status, 0);if(ret == id){lastcode = WEXITSTATUS(status);}}
}int buildcommand(int _argc, char* _argv[])
{if(_argc == 2 && strcmp(_argv[0], "cd") == 0){chdir(_argv[1]);getpwd();sprintf(getenv("PWD"), "%s", pwd);return 0;}else if(_argc == 2 && strcmp(_argv[0], "export") == 0){sprintf(myenv, "%s", _argv[1]);putenv(myenv);return 0;}else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){if(strcmp(_argv[1], "$?") == 0){printf("%d\n", lastcode);lastcode = 0;}else if(*_argv[1] == '$'){char* ret = getenv(_argv[1] + 1);if(ret){printf("%s\n", ret);}}else {printf("%s\n", _argv[1]);}return 0;}return 1;
}int main()
{while(1){//交互interact(commandline, sizeof(commandline));//解析命令行int argc = splitstring(commandline, argv);if(argc == 0){continue;}//內建命令的執行int ret = buildcommand(argc, argv);//普通命令的執行if(ret) {normalexcute(argv);} }return 0;
}

運行結果如下
在這里插入圖片描述
此時當前進程就可以使用export添加并且使用echo &查看環境變量了

  1. 仔細觀察一下下面ls的運行,bash命令行解釋器有進行配色,而小編編寫的程序去運行的ls命令卻沒有配色,這時候我們在ls的命令后面添加- -color選項就可以使我們的ls命令帶上配色,達到與bash命令行解釋器一樣的效果
    在這里插入圖片描述
  2. 那么我們同樣需要對ls命令進行特殊處理一下,這個特殊處理小編就放在內建命令的模塊進行處理,那么就在命令行參數中添加一個選項- -color即可完成
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
#define DELIM " "
#define ARGC_SIZE 32
#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}void getpwd()
{getcwd(pwd, sizeof(pwd));
}void interact(char* cline, int size)
{getpwd();printf(FORMATE, getusrname(), gethostname(), pwd);fgets(cline, size, stdin);//ls -a -l\n\0cline[strlen(cline) - 1] = '\0';
}int splitstring(char* cline, char* _argv[])
{int i = 0;_argv[i++] = strtok(cline, DELIM);while(_argv[i++] = strtok(NULL, DELIM));if(_argv[0] == NULL){return 0;}return i - 1;
}void normalexcute(char* _argv[])
{int id = fork();if(id < 0) {perror("fork");return;}else if(id == 0){execvp(_argv[0], _argv);exit(EXIT_CODE);}else {//id > 0int status = 0;int ret = waitpid(id, &status, 0);if(ret == id){lastcode = WEXITSTATUS(status);}}
}int buildcommand(int _argc, char* _argv[])
{if(_argc == 2 && strcmp(_argv[0], "cd") == 0){chdir(_argv[1]);getpwd();sprintf(getenv("PWD"), "%s", pwd);return 0;}else if(_argc == 2 && strcmp(_argv[0], "export") == 0){sprintf(myenv, "%s", _argv[1]);putenv(myenv);return 0;}else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){if(strcmp(_argv[1], "$?") == 0){printf("%d\n", lastcode);lastcode = 0;}else if(*_argv[1] == '$'){char* ret = getenv(_argv[1] + 1);if(ret){printf("%s\n", ret);}}else {printf("%s\n", _argv[1]);}return 0;}if(strcmp(_argv[0], "ls") == 0){_argv[_argc++] = "--color";_argv[_argc] = NULL;}return 1;
}int main()
{while(1){//交互interact(commandline, sizeof(commandline));//解析命令行int argc = splitstring(commandline, argv);if(argc == 0){continue;}//內建命令的執行int ret = buildcommand(argc, argv);//普通命令的執行if(ret) {normalexcute(argv);} }return 0;
}

運行結果如下
在這里插入圖片描述

五、源代碼

makefile

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

myshell.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#define FORMATE "[%s@%s %s]# "
#define LINE_SIZE 1024
#define DELIM " "
#define ARGC_SIZE 32
#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];char* getusrname()
{return getenv("USER");
}char* gethostname()
{return getenv("HOSTNAME");
}void getpwd()
{getcwd(pwd, sizeof(pwd));
}void interact(char* cline, int size)
{getpwd();printf(FORMATE, getusrname(), gethostname(), pwd);fgets(cline, size, stdin);//ls -a -l\n\0cline[strlen(cline) - 1] = '\0';
}int splitstring(char* cline, char* _argv[])
{int i = 0;_argv[i++] = strtok(cline, DELIM);while(_argv[i++] = strtok(NULL, DELIM));if(_argv[0] == NULL){return 0;}return i - 1;
}void normalexcute(char* _argv[])
{int id = fork();if(id < 0) {perror("fork");return;}else if(id == 0){execvp(_argv[0], _argv);exit(EXIT_CODE);}else {//id > 0int status = 0;int ret = waitpid(id, &status, 0);if(ret == id){lastcode = WEXITSTATUS(status);}}
}int buildcommand(int _argc, char* _argv[])
{if(_argc == 2 && strcmp(_argv[0], "cd") == 0){chdir(_argv[1]);getpwd();sprintf(getenv("PWD"), "%s", pwd);return 0;}else if(_argc == 2 && strcmp(_argv[0], "export") == 0){sprintf(myenv, "%s", _argv[1]);putenv(myenv);return 0;}else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){if(strcmp(_argv[1], "$?") == 0){printf("%d\n", lastcode);lastcode = 0;}else if(*_argv[1] == '$'){char* ret = getenv(_argv[1] + 1);if(ret){printf("%s\n", ret);}}else {printf("%s\n", _argv[1]);}return 0;}if(strcmp(_argv[0], "ls") == 0){_argv[_argc++] = "--color";_argv[_argc] = NULL;}return 1;
}int main()
{while(1){//交互interact(commandline, sizeof(commandline));//解析命令行int argc = splitstring(commandline, argv);if(argc == 0){continue;}//內建命令的執行int ret = buildcommand(argc, argv);//普通命令的執行if(ret) {normalexcute(argv);} }return 0;
}

總結

以上就是今天的博客內容啦,希望對讀者朋友們有幫助
水滴石穿,堅持就是勝利,讀者朋友們可以點個關注
點贊收藏加關注,找到小編不迷路!

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

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

相關文章

【Python】Python爬蟲學習路線

文章目錄Python爬蟲學習路線&#xff1a;從入門到實戰的全景指南一、地基&#xff1a;Python核心基礎1. 基礎語法與數據結構2. 面向對象編程&#xff08;OOP&#xff09;3. 正則表達式&#xff08;Regex&#xff09;4. 模塊與包管理二、工具鏈&#xff1a;Python爬蟲核心庫1. 網…

VUE+SPRINGBOOT從0-1打造前后端-前后臺系統-用戶管理

在現代Web應用開發中&#xff0c;前后端分離架構已經成為主流模式。本文將通過一個完整的用戶管理系統案例&#xff0c;詳細介紹如何使用Vue.js Element UI構建前端界面&#xff0c;結合Spring Boot實現后端服務&#xff0c;實現前后端分離開發。該系統包含用戶信息的增刪改查…

基于uni-app+vue3實現的微信小程序地圖范圍限制與單點標記功能實現指南

一、功能概述本文將分步驟講解如何使用uni-app框架在微信小程序中實現以下功能&#xff1a;顯示基礎地圖繪制特定區域范圍&#xff08;以鄭州市為例&#xff09;實現點擊地圖添加標記點限制標記點只能在指定區域內添加顯示選中位置的坐標信息二、分步驟實現步驟1&#xff1a;搭…

C# 反射和特性(關于應用特性的更多內容)

關于應用特性的更多內容 至此&#xff0c;我們演示了特性的簡單使用&#xff0c;都是為方法應用單個特性。本節將講述特性的其他使 用方式。 多個特性 可以為單個結構應用多個特性。 多個特性可以使用下面任何一種格式列出。 獨立的特性片段一個接一個。通常&#xff0c;它們彼…

【iOS】KVC原理及自定義

目錄 前言 KVC定義及API KVC的使用 基本類型 集合類型 訪問非對象類型——結構體 集合操作符 層層嵌套 KVC底層原理 設值過程 取值過程 自定義KVC setter方法 getter方法 KVC異常小技巧 自動轉換類型 設置空值 未定義的key 前言 在平時的開發中我們經常用到K…

完整設計 之 智能合約系統:主題約定、代理協議和智能合約 (臨時命名)----PromptPilot (助手)答問之2

摘要&#xff08;CSDN的AI助手生成的&#xff09;智能合約系統架構設計摘要本設計構建了一個多層次智能合約系統&#xff0c;包含150字以內的核心架構&#xff1a;三級架構體系&#xff1a;元級&#xff08;序分&#xff09;&#xff1a;MetaModel合約定義系統核心原則模型級&a…

Java基礎 8.16

1.final關鍵字基本介紹final中文意思&#xff1a;最后的&#xff0c;最終的final可以修飾類、屬性、方法和局部變量在某些情況下&#xff0c;程序員可能有以下需求&#xff0c;就會使用到final當不希望類被繼承時,可以用final修飾當不希望父類的某個方法被子類覆蓋/重寫(overri…

YOLOv8目標檢測網絡結構理論

目錄 YOLOv8的網絡結構圖&#xff1a; Backbone 卷積塊&#xff08;Conv Block&#xff09; Conv2d層 BatchNorm2d層 SiLU激活函數 瓶頸塊(Bottleneck Block) C2f 模塊結構 Neck SPPF(空間金字塔池化快速) PAN - FPN Head 結構1.卷積層和激活函數: 2.預測層(Predi…

docker部署hadoop集群

Docker部署hadoop集群下載資源構建鏡像啟動容器搭建集群配置ssh免密節點職責安排修改配置文件啟動集群測試上傳下載執行wordcount程序補充配置歷史服務器日志聚集單節點啟動Java客戶端使用HDFSMapReduce下載資源 java華為鏡像下載地址&#xff1a;Index of java-local/jdk (hu…

常用的T-SQL命令

文章目錄1. 數據庫操作2. 表操作3. 數據插入、更新、刪除4. 數據查詢5. 存儲過程6. 事務處理7、如何使用T-SQL在表中設置主鍵和外鍵&#xff1f;1. 設置主鍵&#xff08;PRIMARY KEY&#xff09;方法1&#xff1a;創建表時定義主鍵方法2&#xff1a;通過ALTER TABLE添加主鍵2. …

C++面試題及詳細答案100道( 31-40 )

《前后端面試題》專欄集合了前后端各個知識模塊的面試題&#xff0c;包括html&#xff0c;javascript&#xff0c;css&#xff0c;vue&#xff0c;react&#xff0c;java&#xff0c;Openlayers&#xff0c;leaflet&#xff0c;cesium&#xff0c;mapboxGL&#xff0c;threejs&…

給純小白的 Python 操作 Excel 筆記

&#x1f9f0; 1. 先裝工具電腦鍵盤按 Win R&#xff0c;輸入 cmd&#xff0c;回車&#xff0c;把下面一行粘進去回車&#xff0c;等它跑完。 bashpip install openpyxl——————————————————&#x1f6e0;? 2. 打開一個空白的 Excel 打開 Jupyter Notebook…

HTML 常用屬性介紹

目錄 HTML 屬性 HTML 屬性速查表 一、通用屬性&#xff08;所有元素適用&#xff09; 二、鏈接與引用相關屬性 三、表單與輸入控件屬性 四、媒體與多媒體屬性 五、事件屬性&#xff08;常用 JavaScript 事件&#xff09; 六、其他常用屬性 核心通用屬性 id 屬性 cla…

HTML5練習代碼集:學習與實踐核心特性

本文還有配套的精品資源&#xff0c;點擊獲取 簡介&#xff1a;HTML5作為新一代網頁標準&#xff0c;對Web開發提供了更豐富的功能和工具。本練習代碼集專門針對HTML5的核心特性&#xff0c;包括語義化標簽、離線存儲、多媒體支持、圖形繪制等&#xff0c;以及CSS3的3D效果和…

【RH134知識點問答題】第 10 章:控制啟動過程

目錄 1. 請簡要說明 RHEL9 的啟動過程。 2. 系統重啟和關機的命令分別是什么? 3. Systemd target 是什么&#xff1f; 4. 重置丟失的 root 密碼需要哪些步驟&#xff1f; 5. 如何讓系統日志在重啟后持久保留 1. 請簡要說明 RHEL9 的啟動過程。 答&#xff1a;①開機自檢…

Apollo10.0學習之固態雷達與IMU的外參標定

固態雷達&#xff08;如Livox、禾賽等非旋轉式激光雷達&#xff09;與IMU&#xff08;慣性測量單元&#xff09;的外參標定&#xff08;Extrinsic Calibration&#xff09;是自動駕駛、機器人定位&#xff08;如LIO-SAM、FAST-LIO&#xff09;的關鍵步驟。1. 標定原理 外參標定…

HTML5實現古典音樂網站源碼模板1

文章目錄 1.設計來源1.1 網站首頁1.2 古典音樂界面1.3 著名人物界面1.4 古典樂器界面1.5 歷史起源界面 2.效果和源碼2.1 動態效果2.2 源代碼 源碼下載萬套模板&#xff0c;程序開發&#xff0c;在線開發&#xff0c;在線溝通 作者&#xff1a;xcLeigh 文章地址&#xff1a;http…

40 C++ STL模板庫9-容器2-vector

C STL模板庫9-容器2-vector 文章目錄C STL模板庫9-容器2-vector一、基礎概念1. 類型成員&#xff08;Type Members&#xff09;2. 模板參數二、構造函數1. 語法2. 示例三、元素訪問1. 函數說明2. 示例代碼四、容量操作1. 函數說明2. 關鍵點說明3. 關鍵操作解析4. 操作示例五、修…

GPT-5系列文章2——新功能、測試與性能基準全解析

引言 2025年8月&#xff0c;OpenAI正式發布了其新一代旗艦模型GPT-5。與業界此前期待的AGI(人工通用智能)突破不同&#xff0c;GPT-5更像是OpenAI對現有技術的一次深度整合與用戶體驗優化。本文將全面解析GPT-5的新特性、實際測試表現以及官方發布的基準數據&#xff0c;幫助開…

利用cursor+MCP實現瀏覽器自動化釋放雙手

小伙伴們&#xff0c;我們今天利用cursorMCP實現瀏覽器自動化&#xff0c;釋放雙手&#xff0c;工作效率嘎嘎提升&#xff01;前期準備&#xff1a;安裝node.js網址&#xff1a;https://nodejs.org/zh-cn下載下來安裝即可。 下載browser-tools-mcp擴展程序&#xff1a;下載擴展…