【MyBash項目】二、實現

一、項目背景

我們對Linux進行相關操作時,會先打開一個類似Windows下的cmd命令終端,輸入要操作的命令,回車執行。那麼這個終端我們稱其爲Bash。那我們需要先了解下什麼是Bash:
Bash是許多Linux發行版的默認Shell,而Shell是一種命令解析器,接受用戶命令,然後調用相應的應用程序。總結來說:Bash是一種命令行模式的命令解析器,可以解析用戶輸入的命令,根據命令做出反應。和Windows系統上的的cmd.exe類似。

爲了鞏固前期學過的Linux基礎知識,我們對系統Bash進行模擬實現,完成屬於自己的Bash命令解析器。

二、項目功能

對系統Bash進行模擬實現,儘量做到基本功能的還原。根據系統Bash的基本功能,我們將項目實現的功能分爲這幾塊:

  • MyBash終端的打開界面模擬實現。
  • MyBash根據輸入的命令進行解析,調用相應程序,實現對應功能。
  • 根據Linux基本命令的功能,利用程序實現基本命令,如pwd,ls,su,kill,cp等命令。

三、項目知識點

MyBash項目中需要用到Linux的基本知識點,所有會碰到的相關庫函數我已經在知識儲備中詳細講解,用到的知識點在往前博客也已講解,我們列出用到的基本知識點:

  • fgets獲取鍵盤上輸入的命令,strtok進行命令和參數的分割,分別存儲在數組中。
  • MyBash打開初始界面信息:根據getpwuid獲取用戶名;uname獲取主機名;getcwd獲取當前工作路徑;getuid獲取用戶UID。
  • 命令實現分爲:內置命令和外置命令。內置命令是shell程序的一部分,包含一些比較簡單的linux系統命令,這些命令由shell程序識別並在shell程序內部完成運行,通常在linux系統加載運行時,內置命令就被加載並駐留在系統內存中。內置命令是寫在bashy源碼裏面的,其執行速度比外部命令快,因爲解析內置命令shell不需要創建子進程,所以我們可以直接在當前文件中實現。比如:exit,cd等。
  • 外置命令是linux系統中的實用程序部分,因爲實用程序的功能通常都比較強大,所以其包含的程序量也會很大,在系統加載時並不隨系統一起被加載到內存中,而是在需要時纔將其調到內存中。通常外置命令的實體並不包含在shell中,但是其命令執行過程是由shell程序控制的。shell程序管理外置命令執行的路徑查找、加載存放,並控制命令的執行。外置命令通常放在/bin,/usr/bin等等。所以在實現外置命令時需要創建新的進程,替換進程實現外置命令。
  • 主函數循環處理命令,命令處理分爲:空,內置命令,外置命令;內置命令爲cd,exit在當前進程即可實現,外置命令需要創建進程,替換進程實現。外置命令在/bin下可以查看。
  • fork創建子進程,子進程execv替換進程,替換爲需要執行的命令。
  • 當前終端需要等待調用的命令完成,處於阻塞狀態,所以父進程前臺執行,利用wait處理僵死進程。
  • 自己實現的命令.c文件,可執行文件全部存儲到Mybin文件夾下,統一路徑方便進程替換。

四、項目框架

MyBash項目的主要功能已經明確,我們可以通過流程圖表示項目整體實現框架:
在這裏插入圖片描述
我們對項目的功能進行模塊函數處理,並不是全部都放在主函數中。根據上述項目框圖,我們可以將項目分爲下面幾個函數模塊:

  • 主函數實現
  • 打印終端初始信息函數
  • 處理獲取信息函數
  • 處理cd內置命令函數
  • 處理外置命令函數
  • 自己實現系統命令的函數

下面我們對這幾大函數模塊的實現進行分析實現。

五、函數模塊實現

(一)主函數實現

主函數要循環運行,即循環等待用戶輸入命令,判斷命令類型,調用對應函數處理命令;處理完命令後繼續等待用戶輸入下一次的命令,直到用戶輸入退出exit退出命令,結束程序

  • fgets獲取鍵盤輸入的信息。
  • 字符串分割函數的調用。
  • 通過字符串比較strncmp函數來判斷命令類型,進行對應的操作。

所以主函數如果作爲父進程,那它是處於阻塞狀態的,即一直等待命令處理成功後,父進程繼續運行。故可以使用wait方法處理僵死進程。

int main()
{
    while(1)
    {
        //1.輸出終端提示信息
        PrintfMessage();
        //2.獲取
        char com[SIZE]={0};
        fgets(com,127,stdin);
        com[strlen(com)-1]=0;
        //3.處理空命令
        if(strlen(com)==0)
        {
            continue;
        }
        //4.將命令和參數分割        
 	    char* comarr[NUM]={0};
        CutCommand(com,comarr);
        //5.處理內置命令
        //5.1 實現Cd命令
        if(strlen(comarr[0])==2 && strncmp(comarr[0],"cd",2)==0)
        {
            DealCd(comarr[1]);
        }
        //5.2 實現exit命令
        else if(strlen(comarr[0])==4 && strncmp(comarr[0],"exit",4)==0)
        {
            exit(0);
        }
        //6.處理外置命令
        else
        {
            Dealoutcmd(comarr);
        }
        
    }
}

(二)打印終端提示信息函數

我們首先看一下Linux自帶的bash命令解釋器打開輸出的信息是什麼:
在這裏插入圖片描述
我們實現的MyBash終端輸出的信息也應該包含這幾項,並按照一致的輸出格式輸出,儘量達到和系統的一致性。要注意這一串提示信息,並不是寫死的,是隨着當前目錄位置或者用戶身份等信息在不斷變化,所以不能一次定義,終身打印,必須用函數獲取獲取信息,打印出終端提示信息。我們根據系統提供的函數獲取到用戶名,主機名,當前工作目錄,標識符這幾項信息就可以實現這個打印函數。

用戶名:可以通過函數getpwuid(getuid())getpwnam獲取到一個指向用戶密碼數據庫結構passwd結構的指針pw(自己定義的),根據pw->pw_name得到用戶名。

主機名:可以通過函數uname得到保存主機信息的utsname類型的名爲host(自己定義的)結構體,根據host.nodename獲取主機名。

當前工作目錄:系統終端根據不同的工作目錄會進行不同的處理,主要分爲3種情況:

  • 如果當前工作目錄爲根目錄/,那麼當前工作目錄就是/。
    在這裏插入圖片描述
  • 如果當前工作目錄爲家目錄,那麼當前工作目錄就是~,所以我們可以將獲取到的主機名和pw指向的用戶信息數據庫中的用戶家目錄pw->pw_dir進行比較,如果一樣當前目錄爲~
    在這裏插入圖片描述
  • 其他情況,我們根據getcwd獲取自己當前的絕對目錄,但是我們可以看到終端沒有完全顯示所有信息,如:/home/stu/Desktop,只顯示Desktop,這就需要進行字符串處理,通過指針移動,從後移動到D字符指向的位置即可得到Desktop。

標識符:管理員標識符爲#,普通用戶爲$。我們根據getuid()獲取當前UID的值進行判斷,如果UID==0,表示管理員爲#,其他表示普通用戶爲$。

那麼這個模塊函數實現的內部流程圖爲:
在這裏插入圖片描述

//1.打印終端信息
void PrintfMessage()
{
    struct passwd* pw=getpwuid(getuid());//獲取指向用戶信息結構體的指針
    assert(pw!=NULL);
    struct utsname uts;//獲取主機信息結構體
    uname(&uts);
    char path[SIZE]={0};//保存當前絕對目錄
    getcwd(path,127);//獲取
    char* dir=NULL;//保存最後目錄
    //家目錄
    if(strcmp(path,pw->pw_dir)==0)
    {
        dir="~";
    }
    //普通目錄和/目錄
    else
    {
        char* p=path+strlen(path);//指向文件末尾
        while(*p!='/')
        {
            p--;
        }
        if(strlen(path)!=1)//等於1表示爲/,不用處理,直接輸出
        {
            p++;
        }
        dir=p;
    }
    char symbol='$';
    if(getuid()==0)
    {
        symbol='#';
    }

    printf("[%s@%s %s]%c ",pw->pw_name,uts.nodename,dir,symbol);

}

(三)處理用戶輸入信息函數

運行MyBash.c後,顯示提示信息,就可以開始輸入命令和參數,我們在主函數中根據用戶輸入的命令調用不同的函數,那麼我們就需要將命令和參數分割保存在數組中,採用strtok根據空格進行分割保存

函數實現步驟:

  • char * cmd保存輸入信息,數組cmdArr[]保存命令和參數信息。
  • char *p=strtok(cmd," ");cdmArr[0]=p;將命令保存到0號下標,就這樣不斷循環判斷,直到字符結尾。
void CutCommand(char* cmd,char* cmdArr[])
{
     char* p=strtok(cmd," ");
     int index=0;
     while(p!=NULL && index<cdmArr.length())
     {
           cmdArr[index++]=p;
           p=strtok(NULL," ");//表示接着上次沒分割完的字符串繼續分割
     }
} 

(四)實現內置命令函數

1. cd

可以使用chdir函數進行指定路徑的切換,模擬實現cd命令。
系統的cd命令常見使用方式爲:

cd       //切換到家目錄
cd ~     //切換到家目錄
cd -     //切換到上一次的位置
cd path  //切換到指定位置

根據cd傳入的參數不同,chdir函數切換的路徑有四種情況:

  • 如果傳入參數爲空或爲~,那麼我們需要切換到家目錄,通過函數getpwuid(getuid())getpwnam獲取到一個指向用戶密碼數據庫結構passwd結構的指針pw(自己定義的),將路徑設置爲家目錄:pw->pw_dir
  • 如果傳入參數爲-,表示切換到上一次的位置。那麼就需要用一個靜態字符串oldpwd保存記錄上一次的路徑,每次chdir函數切換路徑成功後就重新更新oldpwd的取值,將當前路徑賦給oldpwd,利用函數getcwd函數可以獲取當前路徑的值,保存在newpwd中。如果oldpwd長度爲0時,就表示沒有上一次的位置,進行出錯提示,大於0表示存在,那麼將路徑的值設爲oldpwd即可。
  • 如果傳入參數爲普通路徑,那麼路徑值接收傳入值即可。

使用靜態變量保存oldpwd可以避免每一次進入函數被初始化爲0,因爲我們會執行多個指令,執行多個自己寫的程序,如果定義爲普通變量,執行了cd /home後,再執行ls,再執行cd -,會對oldpwd初始化爲0,那麼我們無法保存/home這個路徑,所以要設置爲靜態變量。

那麼cd命令函數實現的流程圖如下:

在這裏插入圖片描述

//3.實現cd內置命令
void DealCd(char* path)
{
    static char oldpath[SIZE]={0};
    //1.實現cd -到達上一層路徑
    if(strncmp(path,"-",1)==0)
    {
        if(strlen(oldpath)==0)
        {
            printf("Cd error,No OLDPATH!\n");
            return;
        }

        path=oldpath;
    }
    //2.實現cd ~到達家目錄
    else if(strncmp(path,"~",1)==0||path==NULL)
    {
        struct passwd* pw=getpwuid(getuid());
        assert(pw!=NULL);
        path=pw->pw_dir;
    }
    //3.調用切換目錄函數
    char nowpath[SIZE]={0};
    getcwd(nowpath,SIZE-1);
    if(chdir(path)==-1)
    {
        perror("cd");
        return;
    }
    memset(oldpath,0,SIZE);
    strcpy(oldpath,nowpath);
}

2. exit

功能是退出MyBash,我們可以直接調用exit(0)函數即可。

(五)處理外置命令函數

外置命令不能在當前進程上運行,它需要生成新進程,進程替換實現外置命令。故在這個函數中我們需要進行下面的操作:

  • fork函數創建子進程。
  • 因爲父進程一直循環等待命令即子進程執行結束,所以設置父進程前臺阻塞運行,那麼就可以用wait(NULL)處理僵死進程。
  • 子進程進行進程替換,讓子進程去實現另一個功能,我們選擇execv函數進行進程替換,主要從:
    (1)用路徑方式給出需要替換的進程,方便多次替換,我們將自己寫的命令都放在Mybin文件夾下,如果用戶傳入地址,那麼直接進行替換即可,否則每次調用execv之前都將傳入的命令和Mybin文件夾的地址連接就可以得到替換進程的路徑。
    (2)命令後面可以加參數執行,如ls -l等,參數是不確定的,故我們選擇數組的方式進行參數傳遞。

如果還沒有實現自己寫的命令,那麼就需要使用系統自帶的外置命令,將"/bin/”和命令連接形成替換進程的路徑。

//4.實現外置命令
void Dealoutcmd(char* comarr[])
{
        pid_t pid=fork();
        assert(pid!=-1);
        if(pid==0)
        {
            char file[SIZE]={0};
            //如果用戶輸入的命令中給出了路徑
            if(strstr(comarr[0],"/")!=NULL)
            {
                strcpy(file,comarr[0]);
            }
            //用戶只輸入了命令,給出我們保存自己實現命令的函數的地址
            else
            {
                strcpy(file,"/home/Gripure/Desktop/review/mybash/Mybin/");
                strcat(file,comarr[0]);
            }
            execv(file,comarr);
        }
        else
        {
            wait(NULL);
        }
}

(六)實現系統外置命令

建立Mybin文件夾,將實現系統外置命令的所有.c文件都存儲在這個文件夾中,注意:實現xx命令的.c文件名也必須爲xx.c,否則會出現進程替換找不到對象,出錯的情況。

1. pwd

在Mybin文件夾中新建pwd.c,實現下面的功能:
實現pwd命令,即獲取當前的路徑,我們在打印終端信息函數模塊已經講過,就是調用函數getcwd函數可以獲得當前路徑,將路徑輸出即可。

代碼編寫完成後我們進行文件編譯即可,替換函數的路徑也替換爲Mybin文件的路徑,這時如果輸入pwd,就是用我們自己寫的代碼實現的了。

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<assert.h>


int main()
{
    char path[128]={0};
    getcwd(path,127);
    printf("%s\n",path);
}

2. ls

首先我們先分析系統的 ls 命令基本用法,主要有:

ls            //默認顯示當前工作路徑下的文件
ls 路徑        //顯示指定路徑下的文件(不包含隱藏文件)
ls 路徑1 路徑2  //顯示路徑1,路徑2的文件
ls -a          //顯示文件所有信息
ls -i          //顯示文件inode節點
ls -l          //顯示文件更多信息
ls -ai         //選項組合顯示信息

總結來說:ls後面可以有多個路徑,也可以有多個選項組合。

實現思想就是:循環所有選項,碰到-表示命令,跳過,命令在後面進行處理,對每一個路徑下的文件進行打印,在打印函數中根據選項解析函數的結果進行文件信息選擇性輸出。

故我們需要單獨寫一個函數解析用戶輸入的選項,判斷選項是a,i,l中的哪一個,主要實現如下

  • 如果傳入參數中不包含-選項,表示不包含選項,直接退出即可。
  • 根據strstr函數判斷傳入參數是否包含a,i,l 選項,如果包含就進行選項保存。
  • 使用int全局整型變量option進行按位保存這三個選項,因爲在解析函數中存儲,在打印函數中也需要用到這個變量,所以設爲全局的。0號位保存a,1號位保存 i ,2號位保存 l 。
  • 如何用option按位保存選項,判斷option位上是否存在選項這兩個是關鍵問題:我們採取宏定義,宏函數,位運算來解決。定義
    #define OPTION_A 0    //0表示a選項
    #define OPTION_I 1    //1表示i選項
    #define OPTION_L 2    //2表示l選項
    
    option爲32位整型,我們初始化爲0,通過位運算進行option位上保存選項,如果選項爲a,宏值爲0,那麼option應該爲:0000,0001,這樣表示存在a選項;如果選項爲i,宏值爲1,那麼option應該爲0000,0010,表示存在i選項。那麼可以總結出規律:option從0位開始保存,1左移val位,和option進行或運算,如果val爲a的宏值0,那麼1左移0位,即:
    0000,0000 | 0000,0001=0000,0001;//實現了option從0位開始保存選項
    
    規律就是:
    option=option | (1 << val);//val表示a,i,l的宏值
    
    我們可以將其寫爲一個給option中存儲選項的宏函數
    # define SETOPTION(option,val) (option)|=(1<<(val)) //給val加括號是因爲val也是一個宏替換,容易產生問題,所以爲了防止我們加上
    
    成功利用option存儲選項後,在輸出文件信息時,還需要判斷是否存在選項a,i 或者 l 選項。我們可以根據&運算來判斷,即只有option變量和判斷選項的宏值完全一致才返回1,表示存在,否則返回0,我們也將它寫爲一個宏函數,用來判斷option對應位上是否存在該選項
    # define ISSET(option,val) (option) & (1 << (val));//val表示判斷的選項宏值
    

這樣我們就處理了選項,下面處理路徑

  • 我們用flag標誌是否輸入了路徑,如果輸入了就按照這個路徑顯示所有文件。
  • 如果沒有輸入路徑,就輸出當前目錄的文件,用getcwd函數獲取當前路徑。

最後需要根據選項,路徑輸出對應目錄下的文件信息,根據選項選擇信息輸出

  • 首先我們利用函數opendir打開路徑下的目錄流。
  • 利用readdir函數進行目錄文件的讀取,得到指向文件存儲結構的dirent指針dt,根據dt指針我們可以循環輸出dt->d_name文件名。 == 綠色文件: 可執行文件,可執行的程序,藍色文件:目錄 黃色:表示設備文件 ,黑色普通文件==
  • 調用選項判斷函數,對3個選項進行判斷,判斷是否存在a選項,如果不存在,dt->d_name中包含【.】的不進行輸出,因爲爲隱藏文件;判斷是否包含 i 選項,包含我們輸出dt->d_ino文件節點號;判斷是否包含 l 選項,包含我們輸出文件的詳細,包含文件類型,權限等。

我們對思路進行一個總結,畫出程序流程圖:
在這裏插入圖片描述

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<assert.h>
# include<sys/types.h>//opendir,readdir,closedir
# include<dirent.h>
# include<sys/stat.h>

# define OPTION_A 0  //a
# define OPTION_I 1  //i
# define OPTION_L 2  //l

int option=0;//按位保存傳入的選項
# define SetOption(option,val) option|=(1<<val)//設置選項
# define GetOption(option,val) option&(1<<val)//獲得選項

//1.判斷選項,保存到option中
void JugeArg(int argc,char* argv[])
{
    int i=1;
    for(;i<argc;i++)
    {
        if(strncmp(argv[i],"-",1)!=0)//沒有選項,直接下一次判斷
        {
            continue;
        }
        if(strstr(argv[i],"a")!=NULL)//有a選項
        {
            SetOption(option,OPTION_A);
        }
        else if(strstr(argv[i],"i")!=NULL)//有i選項
        {
            SetOption(option,OPTION_I);
        }
        else if(strstr(argv[i],"l")!=NULL)//有l選項
        {
            SetOption(option,OPTION_L);
        }
    }
}
//2-1 打印帶有顏色的文件名 
void PrintName(char* path,char* file)
{
    char filename[128]={0};
    strcpy(filename,path);
    strcat(filename,"/");
    strcat(filename,file);

    struct stat st;
    stat(filename,&st);

    //藍色目錄文件,黑色普通文件,綠色可執行文件
    if(S_ISDIR(st.st_mode))//判斷文件類型
    {
        printf("\033[1;34m%s\033[0m  ",file);
    }
    if(S_ISREG(st.st_mode))
    {
        if(st.st_mode & S_IXUSR||
           st.st_mode & S_IXGRP || st.st_mode & S_IXOTH)//判斷是否爲可執行文件
        {
            printf("\033[1;32m%s\033[0m  ",file);

        }
        else
            printf("%s  ",file);
    }
}
//2-2 實現ls -l
//2-2-1 輸出文件類型
void PrintType(struct stat st)
{
    if(S_ISDIR(st.st_mode))
        printf("d");
    else
        printf("-");
}
//2-2-2 輸出文件權限
void PrintMode(struct stat st)
{
    if(st.st_mode & S_IRUSR)
        printf("r");
    else
        printf("-");
    if(st.st_mode & S_IWUSR)
        printf("w");
    else
        printf("-");
    if(st.st_mode & S_IXUSR)
        printf("x");
    else
        printf("-");
    printf(" ");
}
void PrintMore(char* path,char* file)
{
    char filename[128]={0};
    strcpy(filename,path);
    strcat(filename,"/");
    strcat(filename,file);
    
    struct stat st;
    stat(filename,&st);

    PrintType(st);
    PrintMode(st);
    printf("%d ",st.st_nlink);
}
//2 根據選項輸出文件信息
void PrintFile(char* path)
{
    DIR* dp;
    struct dirent *dt;
    dp=opendir(path);//打開目錄流
    int flag=0;
    while((dt=readdir(dp))!=NULL)
    {
        //無a選項,但文件是隱藏文件,直接跳過
        if((!GetOption(option,OPTION_A)) && strncmp(dt->d_name,".",1)==0)
        {
            continue;
        }
        //i選項
        if(GetOption(option,OPTION_I))
        {
            printf("%d ",dt->d_ino);//輸出文件節點
        }
        //l選項
        if(GetOption(option,OPTION_L))
        {
            PrintMore(path,dt->d_name);//進行更詳細信息的輸出
            flag=1;
        
        }
        PrintName(path,dt->d_name);//輸出帶顏色的文件名
        if(flag)
        {
            printf("\n");
        }
    }
    closedir(dp);
    if(!flag)
    {
        printf("\n");
    }
}
int main(int argc,char* argv[])
{
    JugeArg(argc,argv);
    int flag=0;//標誌是否傳入路徑
    int i=1;
    for(;i<argc;i++)
    {
        if(strncmp(argv[i],"-",1)==0)//爲選項,跳過
        {
            continue;
        }
        PrintFile(argv[i]);//輸出文件名
        flag=1;
    }
    if(!flag)//沒有傳入路徑,輸出當前路徑下的文件
    {
        char path[128]={0};
        getcwd(path,127);//獲取當前目錄地址
        PrintFile(path);
    }
}

3. su

切換爲管理員權限,我們可以先看一下系統的su的實現機制:

在這裏插入圖片描述

從下面幾個方面分析系統實現的su:

  • 用戶的切換,用到setuid函數。
  • 輸入密碼切換,所以涉及到密碼的驗證和管理員權限的設置。
  • 輸入密碼沒有顯示,涉及到本地模式的回顯控制,取消su的回顯控制。
  • 進程的變化:bash創建su子進程,su創建出來了一個新的bash默認終端,所以在su的實現過程中,需要進行fork創建子進程,子進程替換爲默認終端。

su基本用法有兩種:

su       //默認切換爲管理員:
su  用戶  //切換爲指定用戶

我們首先分析密碼如何加密,基本思路是:獲取用戶輸入的密碼,獲取該用戶系統中的密碼信息,按照相同的加密算法,密碼對輸入的密碼進行加密,和系統密碼比較,判斷密碼是否正確。

  • fgets獲取鍵盤輸入的密碼,fgets會將最後的回車符一起讀入,所以處理最後一個字符,變爲0即可。
  • 用函數getspnam獲取一個spwd類型的結構體,得到其中的成員變量sp_pwdp用戶加密的密碼信息。這個函數從/etc/shadow文件獲取,需要管理員權限,所以將su文件權限暫時設置爲管理員權限:chmod a+s su
  • 分割函數獲取到用戶密碼的加密算法id,密鑰,用crypt函數對用戶輸入的密碼進行加密。注意crypt不在C的默認庫中,在crypt庫中,需要編譯時手動連接-lcrypt
  • 用加密後的用戶密碼和系統中的密碼進行比較,密碼正確,可以進行下一步操作。否則進行提示。

回顯功能的設置,我們可以用tcgetattr函數得到終端的控制信息,將本地模式設置爲不回顯,利用tcgetattr函數進行重新設置。記得最後還原,所以要再定義一個變量保存原來的控制信息。
su創建子進程,子進程替換爲默認終端l,如果密碼驗證成功後,我們就需要進行用戶的切換和進程的創建替換,主要有下面的操作:

  • fork創建子進程,子進程中利用getpwnam函數獲取到切換用戶的UID,利用函數setuid進行用戶切換。
  • execl替換子進程爲默認終端,傳入默認終端名稱。
  • 父進程阻塞,等待子進程結束,進行wait僵死進程處理。

那我們進行思路總結,流程圖:
在這裏插入圖片描述

# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
# include<string.h>
# include<assert.h>
# include<sys/types.h>//opendir,readdir,closedir
# include<dirent.h>
# include<sys/stat.h>
# include<termios.h>
# include<shadow.h>
# include<pwd.h>

int main(int argc,char* argv[])
{
    char password[128]={0};
    char* user="root";
    if(argv[1]!=NULL)
    {
        user=argv[1];
    }
    printf("Password:");
    //1. 取消回顯
    struct termios oldter,newter;
    tcgetattr(0,&oldter);
    newter=oldter;
    newter.c_lflag &= ~ECHO;
    tcsetattr(0,TCSANOW,&newter);
    fgets(password,127,stdin);
    tcsetattr(0,TCSANOW,&oldter);
    password[strlen(password)-1]=0;
    //2.獲得用戶在系統中的密碼,分割得到加密算法和密鑰
    struct spwd* sp=getspnam(user);
    assert(sp!=NULL);
    char* p=sp->sp_pwdp;
    //3.
    char salt[128]={0};
    int count=0;
    int index=0;
    while(*p)
    {
        salt[index]=*p;
        if(salt[index]=='$')
        {
            count++;
            if(count==3)
            {
                break;
            }
        }
        p++;
        index++;
    }
    //3.對輸入的密碼進行按照一樣的算法,密鑰加密
    char* mypasswd=(char*)crypt(password,salt);
    if(strcmp(mypasswd,sp->sp_pwdp)!=0)
    {
        printf("Passwd error\n");
        exit(0);
    }
    //5.成功創建進程,子進程進行新的UID設置,替換爲新的默認終端,父進程阻塞
    pid_t pid=fork();
    if(pid==0)
    {
        struct passwd *pw=getpwnam(user);
        assert(pw!=NULL);
        setuid(pw->pw_uid);
        execl(pw->pw_shell,pw->pw_shell,(char*)0);
        perror(pw->pw_shell);
    }
    else
    {

        printf("\n");
        wait(NULL);
    }
    exit(0);
}

4. clear

清理界面函數是一個很重要的命令,它的實現很簡單,利用printf函數進行控制輸出即可。我們通過控制符就可以:

33[2J  清理屏幕
33[y;xH	設置光標位置,我們設置爲33[0;0H表示每次清理完屏幕光標回到第一行
# include<stdio.h>

int main(int argc,char* argv[])
{
   printf("\033[2J\033[0;0H");
    return 0;
}

5. kill

在學習信號時我們學過可以使用kill函數發送指定的信號到相應進程。不指定信號將默認發送SIGTERM(15)終止指定進程,也實現了kill指令,我們再整理一下思路:

  • 根據傳入的參數判斷需要發送的信號的種類:-9發送9號信號,-stop發送19號信號,無參數默認發送15號信號。
  • 獲取用戶輸入的pid號。
  • 調用kill函數發送信號。
# include<stdio.h>
# include<unistd.h>
# include<stdlib.h>
# include<assert.h>
# include<string.h>

int main(int argc,char* argv[])
{
    if(argc<2)
    {
        printf("argc error,too less\n");
        exit(0);
    }
    int sign=15;
    int i=1;
    for(;i<argc;i++)
    {
        if(i==1)
        {
            if(strncmp(argv[1],"-9",2)==0)
            {
                sign=9;
                continue;
            }
            if(strncmp(argv[1],"-stop",5)==0)
            {
                sign=19;
                continue;
            }
        }
        int pid=0;
        sscanf(argv[i],"%d",&pid);
        if(kill(pid,sign)==-1)
        {
            perror("kill error\n");
        }
    }
    exit(0);

}

6. cp

拷貝功能也是我們經常使用的命令,系統的拷貝主要有以下使用:

cp a.c b.c         //把當前目錄下的a.c拷貝爲b.c
cp a.c /home/Mybin //把a.c拷貝到/home/Mybin文件下,文件名還是a.c
cp a.c /home/b.c   //把a.c拷貝到/home下,文件名爲b.c

這3種基本使用方式我們可以分爲三類進行處理:

  • 用戶未指定拷貝路徑只輸入文件名,默認拷貝到當前目錄下,故在當前位置上創建指定文件,path=argv[2]。
  • 用戶指定拷貝路徑,未指定文件名,即指定了拷貝文件的目錄位置,屬於一個目錄文件,根據stat結構體成員進行判斷,就是文件夾,可以存儲其他文件。我們需要進行獲取到被拷貝文件的文件名,連接到該路徑的後面,即
    path=argv[2]+”/"+argv[1];//用字符串連接函數strcat實現
    
    在指定路徑下創建和源文件名字一樣的文件。
  • 用戶指定拷貝路徑和文件名,path=argv[2],直接在指定位置創建指定文件。

確定了需要打開和創建的文件,我們只需要根據文件系統調用open,read操作進行文件內容複製,即只讀方式打開源文件,不斷讀取數據到buff中;只寫方式,創建方式打開指定文件,將buff中的內容寫入文件中即可。

# include<stdio.h>
# include<stdlib.h>
# include<string.h>
# include<sys/stat.h>
# include<fcntl.h>
# include<assert.h>

int main(int argc,char* argv[])
{
    if(argc<3)
    {
        printf("Argument Less\n");
    }

    int fd=open(argv[1],O_RDONLY);
    if(fd==-1)
    {
        perror("open error\n");
        exit(0);
    }
    char path[128]={0};
    strcpy(path,argv[2]);

    struct stat st;
    int n=stat(argv[2],&st);
    if(n!=-1 && S_ISDIR(st.st_mode))
    {
        char* p=argv[1]+strlen(argv[1]);
        while(p!=argv[1] && *p!='/')
        {
            p--;
        }
        strcat(path,"/");
        strcat(path,p);
    }

    int fw=open(path,O_CREAT|O_WRONLY|O_TRUNC,0664);
    if(fw==-1)
    {
        perror("open2 error\n");
        exit(0);
    }

    while(1)
    {
        char buff[128]={0};
        int num=read(fd,buff,127);
        if(num<=0)
        {
            break;
        }
        int res=write(fw,buff,num);
        if(res<=0)
        {
            break;
        }
    }

    close(fd);
    close(fw);
}

六、效果演示

下面我們對MyBash進行編譯運行,測試項目功能是否全部實現,是否做到了和系統大致相似。

1.MyBash初始提示信息顯示

在這裏插入圖片描述
2. 開始測試命令,現在我們測試的命令都是我們自己實現的,主要有cd,ls,pwd,cp等功能,可以進行不斷地循環輸入命令測試。
cd ~
在這裏插入圖片描述
cd \

在這裏插入圖片描述
cd path
在這裏插入圖片描述
pwd

在這裏插入圖片描述
ls

在這裏插入圖片描述
ls -a
在這裏插入圖片描述
ls -i
在這裏插入圖片描述
ls -l
在這裏插入圖片描述
kill

在這裏插入圖片描述
cp

在這裏插入圖片描述
clear

在這裏插入圖片描述
在這裏插入圖片描述
su

在這裏插入圖片描述
exit

在這裏插入圖片描述

這就是我們實現的一個模擬Linux命令解釋器的項目。 這一篇主要講解項目實現思路和功能,涉及到的函數我們在項目基礎這篇博客中講解。
加油哦!💪。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章