Linux自定义shell编写

2023-12-29 11:30:11


经过了创建进程,终止进程,进程等待和进程程序替换之后,
我们就可以借助这些知识实现一个简单的shell命令行解释器了

温馨提示:
建议大家自己写一遍,这些代码分块之后每一个函数都很简单,
不过实现过程中可能会有各种各样非常细枝末节的地方被我们所忽视
因此可能会发生一看就懂,一写就废的情况…

一.最终版本展示

输入命令行时想要删除字符时不能直接按backspace,而是要按ctrl+backspace才能成功删除

1.动图展示

在这里插入图片描述

2.代码展示

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//#define DEBUG 1
#define SEP " "

char cwd[1024]={'\0'};
int lastcode=0;//上一次进程退出时的退出码

char env[1024][1024]={'\0'};
int my_index=0;

const char* getUsername()
{
    const char* username=getenv("USER");
    if(username==NULL) return "none";
    return username;
}

const char* getHostname()
{

    const char* hostname=getenv("HOSTNAME");
    if(hostname==NULL) return "none";
    return hostname;
}

const char* getPwd()
{

    const char* pwd=getenv("PWD");
    if(pwd==NULL) return "none";
    return pwd;
}

//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{
    int i=0;
    usercommand[i++]=strtok(command,SEP);
    while(usercommand[i++]=strtok(NULL,SEP));
}

//解析命令行
void GetCommand(char* command,char* usercommand[])
{
    command[strlen(command)-1]='\0';//清理掉最后的'\0'
    CommandSplit(usercommand,command);
#ifdef DEBUG
    int i=0;
    while(usercommand[i]!=NULL)
    {
        printf("%d : %s\n",i,usercommand[i]);
        i++;
    }
#endif
}

//创建子进程,完成任务
void Execute(char* usercommand[])
{
    pid_t id=fork();
    if(id==0)
    {
        //子进程执行部分
        execvp(usercommand[0],usercommand);
        //如果子进程程序替换失败,已退出码为1的状态返回
        exit(1);
    }
    else
    {
        //父进程执行部分
        int status=0;
        //阻塞等待
        pid_t rid=waitpid(id,&status,0);
        if(rid>0)
        {
            lastcode=WEXITSTATUS(status);
        }
    }
}

void cd(char* usercommand[])
{
    chdir(usercommand[1]);
    char tmp[1024]={'\0'};
    getcwd(tmp,sizeof(tmp));
    sprintf(cwd,"PWD=%s",tmp);
    putenv(cwd);
    lastcode=0;
}   

int echo(char* usercommand[])
{
    //1.echo后面什么都没有,相当于'\n'
    if(usercommand[1]==NULL)
    {
        printf("\n");
        lastcode=0;
        return 1;
    }
    //2.echo $?  echo $PWD echo $
    char* cmd=usercommand[1];
    int len=strlen(cmd);
    if(cmd[0]=='$' && len>1)
    {
        //echo $?
        if(cmd[1]=='?')
        {
            printf("%d\n",lastcode);
            lastcode=0;
        }
        //echo $PWD
        else
        {
            char* tmp=cmd+1;
            const char* env=getenv(tmp);
            //找不到该环境变量,打印'\n',退出码依旧为0
            if(env==NULL)
            {
                printf("\n");
            }
            else
            {
                printf("%s\n",env);
            }
            lastcode=0;
        }
    }
    else
    {
        printf("%s\n",cmd);
    }
    return 1;
}

void export(char* usercommand[])
{
    //export
    if(usercommand[1]==NULL)
    {
        lastcode=0;
        return;
    }
    strcpy(env[my_index],usercommand[1]);
    putenv(env[my_index]);
    my_index++;
}

int doBuildIn(char* usercommand[])
{
    //cd
    if(strcmp(usercommand[0],"cd")==0)
    {
        if(usercommand[1]==NULL) return -1;
        cd(usercommand);
        return 1;
    }
    //echo
    else if(strcmp(usercommand[0],"echo")==0)
    {
        return echo(usercommand);
    }
    //export
    else if(strcmp(usercommand[0],"export")==0)
    {
        export(usercommand);
    }
    return 0;
}

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
        char* usercommand[1024]={NULL};
        //2.解析command字符串,放入usercommand指针数组当中
        GetCommand(command,usercommand);
        //3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
        int flag=doBuildIn(usercommand);
        //返回值!=0说明是内建命令,无需执行第4步
        if(flag!=0) continue;
        //4.创建子进程,交由子进程完成任务
        Execute(usercommand);
    }
    return 0;
}

二.具体步骤

1.打印提示符

在这里插入图片描述

const char* getUsername()
{
    const char* username=getenv("USER");
    if(username==NULL) return "none";
    return username;
}

const char* getHostname()
{
    const char* hostname=getenv("HOSTNAME");
    if(hostname==NULL) return "none";
    return hostname;
}

const char* getPwd()
{
    const char* pwd=getenv("PWD");
    if(pwd==NULL) return "none";
    return pwd;
}

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
    }
    return 0;
}

注意:
1.因为scanf默认读取到空格或者’\n’时就会停止继续读取,
可是我们命令行中要求读取到空格也不能停止,(否则我们的指令就无法带选项了,因为选项之间是用空格来分割的)
因此我们需要fgets函数

char command[1024]={'\0'};
fgets(command,sizeof(command),stdin);

从命令行当中读取一行字符串
在这里插入图片描述
2.因为我们用户输入的时候在最后的时候一定会输入一个’\n’,因此我们需要把’\n’置为’\0’

command[strlen(command)-1]='\0';//去除我们最后输入的'\n'

把’\n’置为’\0’的操作我们放到了下一个函数当中来完成

2.解析命令行

因为有些命令带有选项:例如 “ls -a -l”
我们在进行程序替换的时候需要分别传入"ls" “-a” "-l"这几个字符串,所以需要把用户输入的字符串分割为若干个字符串存放到一个指针数组当中,可以使用strtok字符串切割函数
在这里插入图片描述

#define DEBUG 1
#define SEP " "

//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{
    int i=0;
    usercommand[i++]=strtok(command,SEP);
    while(usercommand[i++]=strtok(NULL,SEP));
}

//解析命令行
void GetCommand(char* command,char* usercommand[])
{
    command[strlen(command)-1]='\0';//清理掉最后的'\0'
    CommandSplit(usercommand,command);
#ifdef DEBUG
    int i=0;
    while(usercommand[i]!=NULL)
    {
        printf("%d : %s\n",i,usercommand[i]);
        i++;
    }
#endif
}

我们可以使用条件编译来方便我们自由选择是否需要打印
解析命令行后的usercommand数组中的内容

3.分析是否是内建命令

1.shell对于内建名令的处理

在这里插入图片描述
下面我们就一起来实现一下
cd,echo,export这几个内建命令

2.cd命令

在这里插入图片描述
可是如果shell进行进程程序替换了,那么shell执行完之后不就没了吗?
因此shell执行内建命令时直接封装函数调用系统调用接口即可

内建命令的个数是有限的,所以shell是可以对内建命令进行穷举的

因此我们就能更好地理解内建命令了:

内建命令:不需要创建子进程执行,shell自己执行,本质就是调用系统调用接口

3.cd函数的实现

对于cd而言,我们可以调用chdir这个系统调用接口来改变当前进程的工作目录
在这里插入图片描述
同时也可以设置一个全局的char类型的数组cwd来保存当前路径
每次cd之后用cwd来记录新路径,然后通过putenv来修改环境变量PWD
让我们第一步打印的提示符中的PWD路径可以动态调整

//cwd:存放当前路径,是一个全局变量
char cwd[1024]={'\0'};

void cd(char* usercommand[])
{
	//1.chdir改变当前进程的工作目录
    chdir(usercommand[1]);
    //2.获取当前进程所在工作目录到tmp数组当中
    char tmp[1024]={'\0'};
    getcwd(tmp,sizeof(tmp));
    //3.把tmp数组中的内容格式化为"PWD=tmp数组中的内容"放到cwd数组当中
    sprintf(cwd,"PWD=%s",tmp);
    //4.导入环境变量
    putenv(cwd);
    //5.最后一次进程的退出码置为0(是为了echo $?获取最后一次进程的退出码的实现,跟cd无关)
    lastcode=0;
}  

注意:cwd数组必须是全局变量,如果cwd是局部变量,那么出了cd这个函数之后cwd数组就会销毁,其中的内容也将会消失
而我们putenv导入环境变量时其实是把cwd的地址放入了环境变量当中,
而不是拷贝一份这个cwd放入环境变量当中,因此cwd数组销毁时,对应导入的环境变量中的内容就不是原来的内容了

4.echo命令的实现

echo命令也是一个内建命令

echo 空串      打印换行
echo $ ?      打印上次进程退出时的退出码
echo $环境变量  打印环境变量
echo 字符串    打印字符串
注意:
echo $       打印$这个字符串
因此即使判断了$,还要继续判断$后面还有没有字符
//全局变量
int lastcode=0;//上一次进程退出时的退出码

int echo(char* usercommand[])
{
      //1.echo后面什么都没有,相当于'\n'
      if(usercommand[1]==NULL)
      {
          printf("\n");
          lastcode=0;
          return 1;
      }
      //2.echo $?  echo $PWD echo $
      char* cmd=usercommand[1];
      int len=strlen(cmd);
      if(cmd[0]=='$' && len>1)
      {
          //echo $?
          if(cmd[1]=='?')
          {
              printf("%d\n",lastcode);
              lastcode=0;
          }
          //echo $PWD
          else
          {
              char* tmp=cmd+1;
              const char* env=getenv(tmp);
              //找不到该环境变量,打印'\n',退出码依旧为0
              if(env==NULL)
              {
                  printf("\n");
              }
              else
              {
                  printf("%s\n",env);
              }
              lastcode=0;
          }
      }
      else
      {
          printf("%s\n",cmd);
      }
      return 1;
}

5.export命令的实现

export导入环境变量
注意:
1.刚才介绍cd函数的时候.我们说明了环境变量导入时其实是导入的对应字符串的地址
因此我们环境变量字符串必须要保证全局有效

2.由于我们可以导入很多环境变量,因此env需要是一个二维数组,同时还需要一个index下标来标记该数组当中已经导入过的环境变量

char env[1024][1024]={'\0'};
int my_index=0;

void export(char* usercommand[])
{
	//1.export后面什么都没跟,什么都不执行,直接返回即可
    if(usercommand[1]==NULL)
    {
        lastcode=0;
        return;
    }
    //2.要导入的环境变量拷贝到env数组当中
    strcpy(env[my_index],usercommand[1]);
    //3.将env数组当中的环境变量导入该进程当中
    putenv(env[my_index]);
    my_index++;
}

6.内建命令函数的实现

写好了cd,echo,export这几个函数之后,我们只需要在内建命令函数当中调用这几个函数即可

//返回值=0,说明不是内建命令
//返回值=1,说明是内建命令并且执行成功
int doBuildIn(char* usercommand[])
{
    //cd
    if(strcmp(usercommand[0],"cd")==0)
    {
        if(usercommand[1]==NULL) return -1;
        cd(usercommand);
        return 1;
    }
    //echo
    else if(strcmp(usercommand[0],"echo")==0)
    {
        return echo(usercommand);
    }
    //export
    else if(strcmp(usercommand[0],"export")==0)
    {
        export(usercommand);
    }
    return 0;
}

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
        char* usercommand[1024]={NULL};
        
        //2.解析command字符串,放入usercommand指针数组当中
        GetCommand(command,usercommand);
        
        //3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
        int flag=doBuildIn(usercommand);
        //返回值!=0说明是内建命令,无需执行第4步
        if(flag!=0) continue;
        
        //4.创建子进程,交由子进程完成任务
        Execute(usercommand);
    }
    return 0;
}

4.创建子进程通过程序替换执行命令

使用execvp这个函数来进行程序替换
带上v:因为我们的命令都是放在数组当中的
带上p:因为我们输入的都是系统指令,带上p才可以自动在环境变量中查找我们的命令
否则就要显式传入路径

注意lastcode的设置

//创建子进程,完成任务
void Execute(char* usercommand[])
{
    pid_t id=fork();
    if(id==0)
    {
        //子进程执行部分
        execvp(usercommand[0],usercommand);
        //如果子进程程序替换失败,已退出码为1的状态返回
        exit(1);
    }
    else
    {
        //父进程执行部分
        int status=0;
        //阻塞等待
        pid_t rid=waitpid(id,&status,0);
        if(rid>0)
        {
            lastcode=WEXITSTATUS(status);
        }
    }
}

5.循环往复即可

main函数加上while(1)死循环即可

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
        char* usercommand[1024]={NULL};
        //2.解析command字符串,放入usercommand指针数组当中
        GetCommand(command,usercommand);
        //3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
        int flag=doBuildIn(usercommand);
        //返回值!=0说明是内建命令,无需执行第4步
        if(flag!=0) continue;
        //4.创建子进程,交由子进程完成任务
        Execute(usercommand);
    }
    return 0;
}

三.shell运行原理

shell内部提取用户输入的命令行进行解析
判断是否是内建命令,
1.如果是内建命令的话,shell自己通过调用自己封装的函数来执行该命令
2.如果不是内建命令,shell创建子进程,通过程序替换来然子进程执行该命令
shell进程阻塞等待回收子进程的退出状态
然后循环往复

以上就是Linux自定义shell编写的全部内容,希望能对大家有所帮助!

文章来源:https://blog.csdn.net/Wzs040810/article/details/135181981
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。