初識Linux · 自主Shell編寫

本文介紹了自主shell編寫的過程,模擬實現了bash解釋器,并詳細講解了所需的預備知識,如進程的多方面知識。接下來,我們將直接進入shell編寫部分。

1 命令行解釋器部分

初識Linux · 自主Shell編寫

我們在centos版本下進行演示,通??吹降拿钚薪忉屍黠@示為當前用戶名(如_lazy)、主機名(如VM-12-14-centos)和當前目錄(如~)。我們的目標是復制一個類似的命令行解釋器。

首先,如何獲取用戶名、主機名和當前目錄?我們可以通過環境變量來獲取這些信息。

初識Linux · 自主Shell編寫初識Linux · 自主Shell編寫初識Linux · 自主Shell編寫

在環境變量表中,我們可以看到HOSTNAME、PWD和USER分別代表主機名、當前路徑和當前用戶名。我們可以通過三種方式獲取這些變量:environ、命令行參數表和getenv。我們選擇使用getenv來獲取這些信息:

char* argv[] = {     getenv("HOSTNAME"),     getenv("USER"),     getenv("PWD") };

將獲取到的環境變量放在數組argv中,然后進行打印。我們使用printf來打印數組的三個元素,但由于命令行參數是在后面輸入的,我們不能使用n作為結束符。相反,我們使用snprintf函數來將所有輸出放入一個字符串中,這樣更容易控制輸出。如果你的man手冊配置不全,可以使用以下命令:

初識Linux · 自主Shell編寫初識Linux · 自主Shell編寫

因此,第一部分的臨時代碼如下:

void OutputBash() {     char line[SIZE]; <pre class="brush:php;toolbar:false">char* username = GetUser(); char* hostname = Gethost(); char* cwd = Getcwd();  snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, cwd); printf("%s", line); fflush(stdout);  // char* argv[] = { //     getenv("HOSTNAME"), //     getenv("USER"), //     getenv("PWD") // }; // char* line; // //printf("[%s@%s %s]>", argv[0], argv[1], argv[2]); // fflush(stdout);

}

相關函數的定義如下:

#define SIZE 512</p><p>char<em> GetUser() { char</em> user = getenv("USER"); if(user == NULL) return NULL; return user; }</p><p>char<em> Gethost() { char</em> host = getenv("HOSTNAME"); if(host == NULL) return NULL; return host; }</p><p>char<em> Getcwd() { char</em> cwd = getenv("PWD"); if(cwd == NULL) return NULL; return cwd; }

但這只是臨時的,因為我們的pwd顯示不完善:

初識Linux · 自主Shell編寫

目前顯示的并不是最完善的,應該只顯示當前目錄。我們可以通過分割字符串來解決這個問題,使用指針指向最后一個/,但不建議使用函數,因為需要二級指針。我們可以使用宏來實現:

初識Linux · 自主Shell編寫

我們可以使用以下方法操作:

初識Linux · 自主Shell編寫

這樣,較為完善的命令行解釋器部分就完成了。

2 獲取用戶命令行參數

我們已經解決了第一個問題,現在需要獲取用戶的命令行參數。在獲取用戶命令行參數時,我們應該使用什么函數呢?可以使用scanf嗎?如果使用scanf,輸入ls -l -n -a時,只能獲取到ls,因為scanf是通過空格或換行符來獲取的。因此,我們推薦使用fgets或gets,但由于后面有文件IO操作,我們選擇使用fgets作為緩沖:

int GetUserCommand(char<em> usercommand, size_t n) { char</em> s = fgets(usercommand, n, stdin); if(s == NULL) return -1;</p><pre class="brush:php;toolbar:false">return strlen(s);

}

但此代碼存在一些缺陷,詳見第4部分。

3 命令行參數進行分割

獲取命令后,不能帶空格執行,因此我們需要使用函數將命令行參數分割。我們使用c語言的庫函數strtok來進行分割:

初識Linux · 自主Shell編寫

第一個參數是待分割的字符串,第二個參數是分割符。第一次分割后,將第一個參數置為NULL以繼續分割。我們將分割后的字符串放入數組中,方便后面的進程替換工作。我們定義一個全局變量來存儲分割后的字符串:

#define SEP " " char<em> gArgv[SIZE];

需要注意的是,使用單引號的空格與strtok不匹配,因為這只是一個字符,不是const char。

void SplitCommand(char* usercommand) { gArgv[0] = strtok(usercommand, SEP); int index = 1; while((gArgv[index++] = strtok(NULL, SEP)));//分割之后函數返回NULL 恰好作為結尾 }

分割后的函數返回NULL,可以作為數組的結束標志。

4 執行命令

現在我們可以直接執行命令了,至少目前可以執行簡單的命令。我們需要涉及進程程序替換,因為分割好的命令已經放在了全局變量中,我們可以直接創建函數:

void ExcuteCommand() { pid_t id = fork();</p><pre class="brush:php;toolbar:false">if(id < 0) {     perror("fork error");     return; } else if(id == 0) {     execvp(gArgv[0], gArgv);     perror("execvp error");     exit(-1); } else {     int status;     waitpid(id, &status, 0);     int lastcode = WEXITSTATUS(status);     if(lastcode != 0) printf("%s:%s:%dn", gArgv[0], strerror(lastcode), lastcode); }

}

這些代碼是進程替換時介紹過的,我們只需進行一些修飾,使代碼更美觀。然而,執行時仍會遇到問題,因為命令行輸入時會自動輸入回車,導致無法執行。我們需要去掉回車:

初識Linux · 自主Shell編寫

使用宏定義ZERO來解決這個問題。

5 判斷命令是否為內建命令

如果我們執行的是echo、cd等內建命令,只能由父進程執行,我們不能創建子進程。我們需要判斷是否為內建命令,如果是,則由父進程執行,并跳過下一步。我們可以通過strcmp來判斷內建命令:

void Cd() { const char *path = gArgv[1]; if(path == NULL) path = Gethost(); // path 一定存在 chdir(path);</p><pre class="brush:php;toolbar:false">// 刷新環境變量 char temp[SIZE*2]; getcwd(temp, sizeof(temp)); snprintf(cwd, sizeof(cwd), "PWD=%s", temp); putenv(cwd); // OK

}

int IsInorder() { int yes = 0; const char *enter_cmd = gArgv[0]; if(strcmp(enter_cmd, “cd”) == 0) { yes = 1; Cd(); } else if(strcmp(enter_cmd, “echo”) == 0 && strcmp(gArgv[1], “$?”) == 0) { yes = 1; printf(“%dn”, lastcode); lastcode = 0; } return yes; }

以cd為例,判斷cd是內建命令后,在cd函數中實現。我們使用chdir函數改變當前工作目錄,之后更新環境變量中的PATH。此時,自主Shell編寫工作基本完成。

感謝閱讀!

? 版權聲明
THE END
喜歡就支持一下吧
點贊8 分享