【打造你自己的Shell:编写定制化命令行体验】(四)

简介: 【打造你自己的Shell:编写定制化命令行体验】

【打造你自己的Shell:编写定制化命令行体验】(三):https://developer.aliyun.com/article/1425823


此时确实获取了用户输入的字符串,但是为什么中间还多了一个空行呢?是不是因为我们打印输出的时候带了一个换行符呢?不是,这里有一个空行,说明我们换行了两次,这是因为用户在输入ls -a -l时还敲了一下回车,加上打印输出的时候带了一个换行符,所以这里会打印一个空行,那我们想要解决就要读取到用户的换行符,这里我们可以将\n修改为\0。


运行结果:


现在我们把上面的功能封装一下,命名为交互。

void interactive(char out[], int size)
{
 //输出提示符并获取用户输入的命令字符串"ls -a -l"
  getInfo();
  printf("[%s@%s %s]$",info[0],info[1],info[2]);
  //获取用户输入的字符串
  //scanf("%s",commandline);
  fgets(out,size,stdin);
  //将\n修改成\0
  out[strlen(out) - 1] = '\0';//也能处理空串的情况
}
int main()
{
  //1.打印命令行提示符,获取用户输入的命令字符串
  char commandline[SIZE];
  interactive(commandline, SIZE);
  return 0;
}


现在我们获取了用户输入的命令字符串,现在我们想要执行它,就必须要分隔字符串,这里可以使用strtok函数。

#include <string.h> char *strtok(char *str, const char *delim);
  • str 是要拆分的字符串,第一次调用时传入要拆分的字符串,之后传入 NULL 表示继续使用上一次拆分的字符串。
  • delim 是用来指定分隔符的字符串,即根据哪些字符来拆分字符串。


strtok 函数返回一个指向拆分后的子字符串的指针,如果没有找到子字符串,则返回 NULL。每次调用 strtok 都返回拆分后的一个子字符串,直到没有更多的子字符串为止。注意,strtok 在每次调用时会修改原始字符串,需要使用 NULL 继续拆分。


这里我们来解释一下我们上面的写法,第一次strtok分隔的字符【ls】存放到了argv[0]的位置,随后i就变为1,因为strtok在每次调用时会修改原始字符串,需要使用NULL继续拆分,随后进入while循环,argv[1]的位置就存放了【-a】,argv[2]的位置存放了【-l】,随后继续自增,然后此时就没有找到字符,由于这里是等号【=】,就将NULL返回给了argv[3],此时while循环的条件就不满足,循环也就退出了。


运行结果:


此时我们就分隔了我们的用户输入的命令行字符串了,这里我们也封装一下,命名为切割。

void Split(char in[])
{
  int i = 0;
  argv[i++] = strtok(in, " ");
  while(argv[i++] = strtok(NULL, " "));//这里写成=,不是==
}
int main()
{
  //1.打印命令行提示符,获取用户输入的命令字符串
  char commandline[SIZE];
  interactive(commandline, SIZE);
  //2.对命令行字符串进行分隔
  Split(commandline);
  int i = 0;
  for(i = 0; argv[i]; ++i)
  {
    printf("argv[%d]:%s\n", i, argv[i]);
  }
  return 0;
}


现在我们这里就要执行我们的命令了,这里我们不能直接调用我们的程序替换接口,而应该创建一个子进程让它去执行命令。


运行结果:


现在我们对执行过程也做一下封装,命名execute。

void Execute()
{
  pid_t id = fork();
  if(id == 0)
  {
    //子进程执行命令
    execvp(argv[0],argv);
    exit(1);
  }
  //父进程回收子进程
  waitpid(id,NULL,0);
}
int main()
{
  //1.打印命令行提示符,获取用户输入的命令字符串
  char commandline[SIZE];
  interactive(commandline, SIZE);
  //2.对命令行字符串进行分隔
  Split(commandline);
  //3.执行命令
  Execute();
  return 0;
}


但是我们发现我们的命令行只能做一次执行,所以这里我们想要多次循环就要使用循环。


运行结果:


此时我们的命令行解释器shell就可以一直运行,但是当我们运行cd命令为什么就跑不起来呢?


cd 是一个特殊的命令,它用于改变当前工作目录。然而,cd 命令通常需要由父进程直接来执行,而不能通过创建子进程的方式来运行。如果你的命令行解释器是通过创建子进程来执行用户输入的命令,那么 cd 命令可能无法按预期工作,因为它改变的是子进程的工作目录,而不影响父进程的工作目录。每个子进程都有自己的工作目录,改变其中一个不会影响其他进程。为了使 cd 命令在你的 shell 中生效,你可能需要在父进程中执行该命令,而不是创建一个子进程来执行。这样,cd 命令就能够改变你的 shell 进程的当前工作目录。

int BuildCmd()
{
  int ret = 0;
  //1.检查是否是内键命令,是1,否0
  if(strcmp(argv[0],"cd") == 0)
  {
    ret = 1;
    //执行cd命令
    char* target = argv[1];//cd xxx or cd
    if(!target) target = getenv("HOME"); 
    chdir(target);//更改当前进程的工作目录
  }
  return ret;
}
int main()
{
  while(1)
  {
    //1.打印命令行提示符,获取用户输入的命令字符串
    char commandline[SIZE];
    interactive(commandline, SIZE);
    //2.对命令行字符串进行分隔
    Split(commandline);
    //3.处理内键命令 - 由父进程执行
    int n = BuildCmd();
    if(n) continue;
    //4.执行命令
    Execute();
  }
  return 0;
}

运行结果:


上面已经可以做到目录的切换,但是还是有一点问题,首先是空串的处理,其次是命令行提示符哪里工作目录没有改变,这里是因为环境变量没有变化,虽然当前进程的工作目录发生了改变,但是我们的环境变量并没有更新。


运行结果:


但是我们发现我们这里的上级目录..没有处理好,在这里需要使用我们的getcwd函数。


我们先来看一下关于getcwd的使用。


运行结果:


默认情况下,我们的子进程会继承父进程的环境变量,随后我们更改当前工作目录为上级目录,便就能用getcwd获取到该目录,而不是我们的..了。


运行结果:


随后我们导入一下环境变量,进而用env查询一下,可是我们没有查到,为什么呢?


这是因为我们是在子进程中执行了export,只在子进程导入了环境变量,而后通过 env 在父进程中查询,是看不到这个环境变量的。所以导入环境变量这个过程我们也要让父进程执行。    


我们运行后导入我们的环境变量确实存在了,但是当我们再次执行其他命令的时候,再次env查询环境变量却有没有了,为什么呢?这是因为我们argv指向是用户输入的commandline分隔的字串,当我们执行后续指令,此时用户输入的命令就变化了,argv所指向的分隔的字串也发生了变化,此时环境变量就会被一直覆盖,所以我们就需要一个数组保存之前的导入的环境变量。


此时我们的环境变量就不会被覆盖了,然后我们再来执行我们的echo命令,发现也不能执行,说明echo也是内键命令,所以我们的代码也要修改,我们先来看看echo的特点。

echo       # 输出空行
echo $?    # 输出退出码
echo $HOME # 输出环境变量
echo ""    # 输出字符串


代码实现:

else if(strcmp(argv[0],"echo") == 0)
  {
    ret = 1;
    if(argv[1] == NULL)
    {
      printf("\n");//输出空行
    }
    else 
    {
      if(argv[1][0] == '$')
      {
        if(argv[1][1] == '?')
        {
          printf("%d\n",lastcode);//输出退出码
          lastcode = 0;
        }
        else
        {
          char* e = getenv(argv[1] + 1);
          if(e) printf("%s\n",e);
        }
      }
      else
      { 
        printf("%s\n",argv[1]);//输出字符串
      }
    }
  }


运行结果:


现在我们的shell功能已经齐全,但是我们发现没有颜色区别,我们再来修改一下


运行结果:


此时我们就拥有了我们的颜色。


但是此时我们的输出重定项和管道都不能使用,我们后面再添加。


整体shell实现代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h> 
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SIZE 1024
#define MAX_ARGC 64
const char* info[3];
char* argv[MAX_ARGC];//保存分隔字串
char pwd[SIZE];//更新环境变量
char env[SIZE];//环境变量表
int lastcode = 0;
void getInfo()
{
  char* user = getenv("USER");
  if(user) info[0] = user;
  else info[0] = "None";
  char* hostname = getenv("HOSTNAME");
  if(hostname) info[1] = hostname;
  else info[1] = "None";
  char* pwd = getenv("PWD");
  if(pwd) info[2] = pwd;
  else info[2] = "None";
}
int interactive(char out[], int size)
{
 //输出提示符并获取用户输入的命令字符串"ls -a -l"
  getInfo();
  printf("[%s@%s %s]$ ",info[0],info[1],info[2]);
  //获取用户输入的字符串
  //scanf("%s",commandline);
  fgets(out,size,stdin);
  //将\n修改成\0
  out[strlen(out) - 1] = '\0';//也能处理空串的情况
  return strlen(out);
}
void Split(char in[])
{
  int i = 0;
  argv[i++] = strtok(in, " ");
  while(argv[i++] = strtok(NULL, " "));//这里写成=,不是==
  if(strcmp(argv[0],"ls") == 0)
  {
    argv[i-1] = (char*)"--color";
    argv[i] = NULL;
  }
}
void Execute()
{
  pid_t id = fork();
  if(id == 0)
  {
    //子进程执行命令
    execvp(argv[0],argv);
    exit(1);
  }
  //父进程回收子进程
  int status = 0;
  pid_t rid = waitpid(id,&status,0);
  if(rid == id) lastcode = WEXITSTATUS(status);//获取进程退出码
}
int BuildCmd()
{
  int ret = 0;
  //1.检查是否是内键命令,是1,否0
  if(strcmp(argv[0],"cd") == 0)
  {
    ret = 1;
    //执行cd命令
    char* target = argv[1];//cd xxx or cd
    if(!target) target = getenv("HOME"); 
    chdir(target);//更改当前进程的工作目录
    char temp[1024];
    getcwd(temp,1024);//使用绝对路径
    snprintf(pwd,SIZE,"PWD=%s",temp);//更新环境变量
    putenv(pwd);
  }
  else if(strcmp(argv[0],"export") == 0)
  {
    ret = 1;
    if(argv[1]) 
    {
      strcpy(env,argv[1]);
      putenv(env);//导入环境变量
    }
  }
  else if(strcmp(argv[0],"echo") == 0)
  {
    ret = 1;
    if(argv[1] == NULL)
    {
      printf("\n");//输出空行
    }
    else 
    {
      if(argv[1][0] == '$')
      {
        if(argv[1][1] == '?')
        {
          printf("%d\n",lastcode);//输出退出码
          lastcode = 0;
        }
        else
        {
          char* e = getenv(argv[1] + 1);
          if(e) printf("%s\n",e);
        }
      }
      else
      { 
        printf("%s\n",argv[1]);//输出字符串
      }
    }
  }
  return ret;
}
int main()
{
  while(1)
  {
    //1.打印命令行提示符,获取用户输入的命令字符串
    char commandline[SIZE];
    int n = interactive(commandline, SIZE);
    if(!n) continue;
    //2.对命令行字符串进行分隔
    Split(commandline);
    //3.处理内键命令 - 由父进程执行
    n = BuildCmd();
    if(n) continue;
    //4.执行命令
    Execute();
  }
  return 0;
}


相关文章
|
6天前
|
Shell
在Shell脚本或命令行中,标准错误输出
在Shell脚本或命令行中,标准错误输出
193 1
|
6天前
|
监控 Shell
在Shell脚本编程或命令行交互
在Shell脚本编程或命令行交互
30 3
|
6天前
|
存储 Unix Shell
【打造你自己的Shell:编写定制化命令行体验】(二)
【打造你自己的Shell:编写定制化命令行体验】
|
6天前
|
Shell
【shell】shell命令行放在变量中执行以及变量的常用方法
【shell】shell命令行放在变量中执行以及变量的常用方法
|
6天前
|
存储 Shell Linux
【Shell 命令集合 系统设置 】Linux 将参数作为命令行输入 eval命令 使用指南
【Shell 命令集合 系统设置 】Linux 将参数作为命令行输入 eval命令 使用指南
31 0
|
6天前
|
Shell Linux C语言
【打造你自己的Shell:编写定制化命令行体验】(三)
【打造你自己的Shell:编写定制化命令行体验】
|
6天前
|
缓存 Shell Linux
【打造你自己的Shell:编写定制化命令行体验】(一)
【打造你自己的Shell:编写定制化命令行体验】
|
6天前
|
Shell Linux
Linux之简单的Shell命令行解释器
Linux之简单的Shell命令行解释器
24 0
|
6天前
|
Shell
Shell(如Bash)命令行技巧
Shell(如Bash)命令行技巧
31 2