Linux 實現簡易的 Shell 命令行解釋器

一、前言

前面學到了進程創建,進程終止,進程等待,進程替換,那麼通過這些來製作一個簡易的 [Shell 命令] 行解釋器。

首先這是與 Shell 的互動::

用下圖的 [時間軸] 來表示事件的發生次序。其中時間從> > 左向右。shell 由標識爲 sh 的方塊代表,它隨着時間的流逝從左向右移動。shell 從用戶讀入字符串 "ls"。shell 建立一個新的進程,然後在那個進程中運行 ls 程序並等待那個進程結束。

然後 shell 讀取新的一行輸入,建立一個新的進程,在這個進程中運行程序 並等待這個進程結束。所以要寫一個 shell,需要循環以下過程:

二、準備工作

1. 輸出提示符

這裏的提示字符爲用戶名 @主機名 當前路徑# 直接打印出來作爲提示所用

printf("用戶名@主機名 當前路徑#");

這裏沒有 \ n,會有緩衝區的問題,類似於我們之前所說的進度條所遇到的問題,可以用 fflush(stdout) 刷新緩衝區。

2. 輸入和獲取命令

輸入

我們需要輸入一連串命令,其中可能出現空格,所以不能使用 gets 函數,需要用到 fgets 函數,同時,可以定義一個 lineCommand[NUM] 數組

#define NUM 1024
char lineCommand[NUM];
char* s = fgets(lineCommand,sizeof(lineCommand) - 1, stdin);
assert(s != NULL);

但是打印的時候卻多換了一行,這是我們把 \ n 也讀取到了,直接進行處理即可, 清除最後一個 \ n

lineCommand[strlen(lineCommand) - 1] = 0;

可以通過打印看看效果和測試是否有 BUG

printf("test:%s\n",lineCommand);

獲取

輸入之後,我們自然需要去進行獲取,我們需要分割命令行,這個地方用 strtok。把字符串切割成若干個子串:
strtok: 第一次直接傳遞參數,第二次則必須傳 NULL。且在最終 strtok 會返回 NULL。

3.shell 運行原理

同時,在理解一下 shell 的運行原理:shell 內部提取命令行做分析,然後調用 exec. shell 執行命令必須通過創建子進程,如果不創建子進程會把我們所有的 shell 全部替換,所以執行命令時一般磁盤上的程序必須創建子進程。

4. 內建命令

我們在運行自己寫的 shell 的時候,發現輸入 cd … 輸入 cd path 等命令時發現路徑並沒有改變!

沒有發生改變是因爲自己寫的 shell 執行很多命令都要 fork() 創建子進程,讓子進程執行的 cd,子進程有自己的工作目錄,所以更改的子進程的目錄,子進程執行完畢,繼續用的是父進程,既 shell,並沒有影響父進程,所以並沒有改變。

對於 cd, 我們可以採用內建命令:不需要創建子進程執行,讓 shell 自己執行命令,稱爲內建命令。本質就是執行系統接口,我們可以調用一個系統接口 chdir,可解決上述問題:


5. 替換

採用 execvp 進行替換進程

pid_t id = fork();
assert(id != -1);
if(id == 0)
{
 execvp(myargv[0],myargv);
 exit(1);
}

三、整體代碼

 #include<stdio.h>
 #include<stdlib.h>
 #include<unistd.h>
 #include<sys/types.h>
 #include<sys/wait.h>
 #include<assert.h>
 #include<string.h>
 #define NUM 1024
 #define OPT_NUM 64
 char lineCommand[NUM];
 char *myargv[OPT_NUM];//指針數組
 int lastcode = 0;
 int lastsig = 0;
 int main()
 {
     while(1)
     {
         // 1.輸出提示符
         printf("lj@VM-8-2-centos 當前路徑#");
         fflush(stdout);
         // 2.獲取用戶輸入的命令,輸入的時候,用戶最後還輸入了\n
         char* s = fgets(lineCommand,sizeof(lineCommand) - 1, stdin);
         assert(s != NULL);
         (void)s; //避免Linux認爲s變量未使用,導致警告
         // 清除最後一個\n;例如:abcd\n
         lineCommand[strlen(lineCommand) - 1] = 0;
         //printf("test:%s\n",lineCommand);
         // "ls -a -l -i" -->字符串分割-->"ls" "-a" "-l" "-i"
         myargv[0] = strtok(lineCommand, " ");
         int i = 1;
         if(myargv[0] != NULL && (strcmp(myargv[0],"ls") == 0))
         {
             myargv[i++] = (char*)"--color=auto";
         }
         //如果沒有子串了,strtok會返回NULL,即myargv[end] = NULL
         while(myargv[i++] = strtok(NULL," "));
         //如果是cd命令,不需要創建子進程,讓shell自己執行對應的命令,本質就是執行系統接口
         //像這種不需要讓我們的子進程來執行,而是讓shell自己執行的命令—內建命令
         //其中echo是一個自建命令
         if(myargv[0] != NULL && (strcmp(myargv[0],"cd") == 0))
         {
             if(myargv[1] != NULL) chdir(myargv[1]);
             continue;
         }
         if(myargv[0] != NULL && myargv[1] != NULL && (strcmp(myargv[0],"echo") == 0))
         {
             if(strcmp(myargv[1],"$?") == 0)
             {
                 printf("%d,%d\n",lastcode,lastsig);
             }
             else
             {
                 printf("%s\n",myargv[i]);
             }
             continue;
         }
         //利用條件編譯測試是否成功
 #ifdef DEBUG
         for(int i = 0; myargv[i]; ++i)
         {
             printf("myargv[%d]:%s\n",i,myargv[i]);
         }
 #endif
         //執行命令
         pid_t id = fork();
         assert(id != -1);
         if(id == 0)
         {
             execvp(myargv[0],myargv);
             exit(1);
         }
         int status = 0;
         pid_t ret = waitpid(id,&status,0);
         assert(ret > 0);
         (void) ret;
         lastcode = (status >> 8) & 0xFF;
         lastsig = status & 0x7F;
     }
     return 0;
 }

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xxX2cbzdXpGqMR0kYyO8wg