《从零手写Linux Shell:详解进程控制、环境变量与内建命令实现 --- 还会更新》

承接上文Linux 进程的创建、终止、等待与程序替换保姆级讲解-CSDN博客,涉及所用到的代码,本文所绑定的资源就是上篇文章的主要代码。

完整代码在文章末尾

目录

1.实现编写代码输出一个命令行

a.如何获取自己的用户名,主机名,路径名?

b.ubuntu的HOSTNAME的获取方法:

c.完整代码(改前)printf进行打印的时候数据存在缓冲区:

2.实现编写代码获取用户命令字符串

[a."ls -a -l -i"本质上是一个字符串,使用 fgets() 获取一整个字符串](#a.“ls -a -l -i”本质上是一个字符串,使用 fgets() 获取一整个字符串)

[b.注意当在echo :%d后加换行符:](#b.注意当在echo :%d后加换行符:)

3.分割获取的用户命令字符串

n.创建子进程执行命令

[shell 1.0 代码,程序只能运行一次](#shell 1.0 代码,程序只能运行一次)

[n + 1:将命令多次执行](#n + 1:将命令多次执行)

[shell 2.0 需补坑完整代码:](#shell 2.0 需补坑完整代码:)

nn:填补上述shell代码的坑(cd无用的问题)

原因:

4.检查命令是否是内建命令(只有bash能执行的命令)

chdir()更改当前的工作路径

getcwd()获取进程当前工作目录的绝对路径

5.将命令行路径通过使用绝对路径改为相对路径

为什么定义宏,以及使用do{}while(0)?

[6.内建命令echo ?问题](#6.内建命令echo ?问题)

[7.自定义环境变量export HELLO=12345](#7.自定义环境变量export HELLO=12345)

Export()函数

a.使用strdup()函数复制gArgv[1],arg指向字符串首地址,避免修改原始命令字符串

[b. 使用strchr()函数查询是否有 = ,如果有则返回 = 的地址,没有则返回NULL](#b. 使用strchr()函数查询是否有 = ,如果有则返回 = 的地址,没有则返回NULL)

c.判断是否为NULL,如果为空说明export使用的格式错误

[d.再将*eq指向的 = ,位置置为\0,截取arg = 前的字符串,eq+1,得到 = 后面的字符串。](#d.再将*eq指向的 = ,位置置为\0,截取arg = 前的字符串,eq+1,得到 = 后面的字符串。)

e.setenv()设置环境变量:

[2. echo HELLO 的时候需要将HELLO替换成它对应的值12345,从而输出12345](#2. echo HELLO 的时候需要将HELLO替换成它对应的值12345,从而输出12345)

[a.添加变量替换函数 ReplaceEnvVars():](#a.添加变量替换函数 ReplaceEnvVars():)


1.上文所写到的程序可以执行系统的所有命令,包括自己写的可执行程序。

  1. 在执行命令的时候,只执行了一次就结束,本篇文章主要讲如何让程序不断地执行不同的命令(可执行程序) -----> shell ---> 模拟实现命令行

实际上我们所看到的简单的命令行,本质上是一个字符串,并且我们输入的命令也是字符串 。将读进来的字符串进行分析,解析成命令,再fork(), 再exec, 这条命令就执行了

bash 复制代码
pupu@VM-8-15-ubuntu:~/bitclass/class_20/myshell$ 

bash 本质上是一个进程,有独立的pid

显示进程列表的表头,以及列出bash 进程信息,并且过滤掉grep bash自身进程:

bash 复制代码
ps ajx | head -1 && ps ajx | grep bash | grep -v grep

得到:

bash 复制代码
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
1366539 1366540 1366540 1366540 pts/0    1368748 Ss    1002   0:00 -bash
1372289 1372290 1372290 1372290 pts/1    1386639 Ss    1002   0:00 -bash

以上算本文的周边笔记知识提及。

1.实现编写代码输出一个命令行

a.如何获取自己的用户名,主机名,路径名?

环境变量可以通过函数getenv() 头文件<stdlib.h>来获取,获取自己的用户名,主机名,路径名从环境变量**(命令行输入env)**里定向获取。

查取到用户名为

LOGNAME=pupu

测试获取登录名:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>

#define SIZE 512

const char *getusername()
{
  const char *name = getenv("LOGNAME");
  if(name == NULL) return "none";
  return name;
}

int main()
{
  //1.我们需要自己输出一个命令行
  char output[SIZE];
  
  printf("name: %s\n",getusername());

  return 0;
}

输出结果:获取成功

我只想要当前路径,往往PWD中所存储的是绝对路径,如何截取字符串获得当前路径(可以定义尾指针,到 ' / '截取停止,请看目录5.将命令行路径改为相对路径)

cpp 复制代码
PWD=/home/pupu/bitclass/class_20/myshell

ubuntu系统环境变量中默认没有HOSTNAME,centos系统环境变量中可以直接通过env查取到,可以用类似于获取用户名的方式来做:

b.ubuntu的HOSTNAME的获取方法:

cpp 复制代码
const char *GetHostName()
{
  char buffer[256];
  char *hostname = buffer;
  if (gethostname(hostname, sizeof(buffer)) == 0) return hostname;
  return "none";
}

此时,我的代码存在 内存作用域问题buffer 是局部数组 ,在函数返回后其内存会被释放,导致返回的 hostname 指针成为 悬垂指针,访问时可能输出随机内容或截断的字符串。

解决办法:

将 buffer 声明为 static(处于静态存储区) , 延长其生命周期至程序结束:

cpp 复制代码
const char *GetHostName() {
    static char buffer[512];  // 静态存储期,函数返回后内存仍有效
    if (gethostname(buffer, sizeof(buffer)) == 0) {
        buffer[sizeof(buffer)-1] = '\0'; // 确保字符串终止
        return buffer;
    }
    return "none";
}

此时便能获取到正确的hostname了。

c.完整代码(改前)printf进行打印的时候数据存在缓冲区:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>

#define SIZE 512

const char *GetUserName()
{
  const char *name = getenv("LOGNAME");
  if(name == NULL) return "none";
  return name;
}

const char *GetHostName()
{
  static char buffer[256];
  char *hostname = buffer;
  if (gethostname(hostname, sizeof(buffer)) == 0) return hostname;
  return "none";
}

//获取当前的路径
const char *GetCwd()
{
  const char *cwd = getenv("PWD");
  if(cwd == NULL) return "none";
  return cwd;
}

void MakeCommendLine(char line[], size_t size)
{
  //获取三个字符串
  const char *username = GetUserName();
  const char *hostname = GetHostName();
  const char *cwd = GetCwd();
  //拼接字符串
  snprintf(line, size, "%s@%s:%s^_^ -> ", username, hostname, cwd);
}

int main()
{
  //1.我们需要自己输出一个命令行
  char commendline[SIZE];

  MakeCommendLine(commendline, sizeof(commendline));
  
  printf("%s", commendline);

  sleep(5);
  return 0;
}

但是在运行程序的时候会发现,我们想要的字符串会等上5s才打印出来,这是因为printf在进行打印的时候数据是会写在缓冲区中的,当程序结束时才会出来,这里的想法是将制作命令行与打印命令行放进一个函数里,并使用fflush(stdout),刷新标准输出流stdout将缓冲区中的输出数据立即写到输出设备:

此时运行代码:就会直接先打印出我们制作的命令行。延迟5s的原因是为了能够看到这个效果:


2.实现编写代码获取用户命令字符串

a."ls -a -l -i"本质上是一个字符串,使用 fgets() 获取一整个字符串

cpp 复制代码
char *fgets(char *s, int size, FILE *stream);

按行从特定的文件流当中获取指定的内容,成功获取字符串时,返回的是获取到的字符串的起始地址,失败则返回none。

如图:为了使代码更具有可读性

运行此代码进行测试:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>

#define SIZE 512

const char *GetUserName()
{
  const char *name = getenv("LOGNAME");
  if(name == NULL) return "none";
  return name;
}

const char *GetHostName()
{
  static char buffer[256];
  char *hostname = buffer;
  if (gethostname(hostname, sizeof(buffer)) == 0) return hostname;
  return "none";
}

//获取当前的路径
const char *GetCwd()
{
  const char *cwd = getenv("PWD");
  if(cwd == NULL) return "none";//暂时这样写,后续会修改
  return cwd;
}

void MakeCommendLineAndPrint()
{
  //实现输出一个命令行
  char line[SIZE];
  //获取三个字符串
  const char *username = GetUserName();
  const char *hostname = GetHostName();
  const char *cwd = GetCwd();

  //拼接字符串
  snprintf(line, sizeof(line), "%s@%s:%s^_^ -> ", username, hostname, cwd);
  
  printf("%s", line);
  fflush(stdout);
}

int main()
{
  //自己输出一个命令行
  MakeCommendLineAndPrint();
  //获取用户命令字符串
  //1.再定一个缓冲区
  char usercommend[SIZE];
  //2.获取:从标准输入流中获取
  char *s = fgets(usercommend, sizeof usercommend, stdin);//起到一个输入停留的作用
  if(s == NULL) return 1; 
  printf("echo : %s", usercommend);
  return 0;
}

输出结果:

b.注意当在echo :%d后加换行符:

原代码并没有加\n,当加上\n后按理来说只会多一个空行,这里却空了两行相当于有两个\n?是为什么:因为,在我输完ls -a -l之后还摁了回车,回车符也被读入\r\n。

修改:

运行代码:此时就正常打印出

为了使代码具有可读性,我封装获取命令字符串的代码:


3.分割获取的用户命令字符串

a.封装一个函数SplitCommend()用于分割命令行字符串,创建一个全局变量的表gArgv[NUM],#define NUM 32,分隔符:define SEP " "

需要做到的是:将"ls -a -l -n" ----> "ls", "-a", "-l", "-n"

使用strtok函数,将一个子串,按照指定的分隔符进行分割,返回值就是从左往右分割出的第一个字符,第一次调用时把字符串保存下来,将这个位置设置为NULL,第二次调用就会对历史字符串继续分割,最后为NULL的时候,就结束了。

cpp 复制代码
char *strtok(char *str, const char *delim);

define SEP**" "** 请注意,分隔符得设置成字符串才能传进去,不能是' ' 字符。

请阅读下面我修改后代码,对代码的提示:

运行代码:此时已将字符分割存入表内


n.创建子进程执行命令

只能让子进程去执行具体原因参见我的上一篇博客进程替换部分:Linux 进程的创建、终止、等待与程序替换函数 保姆级讲解-CSDN博客

图中就是我对代码进行的修改

运行结果:已经成功,再删除多余代码就行。

将函数封装,删去多余代码

运行结果:


shell 1.0 代码,程序只能运行一次

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32


void Die()
{
  exit(1);
}

const char *GetUserName()
{
  const char *name = getenv("LOGNAME");
  if(name == NULL) return "none";
  return name;
}

const char *GetHostName()
{
  static char buffer[256];
  char *hostname = buffer;
  if (gethostname(hostname, sizeof(buffer)) == 0) return hostname;
  return "none";
}

//获取当前的路径
const char *GetCwd()
{
  const char *cwd = getenv("PWD");
  if(cwd == NULL) return "none";
  return cwd;
}

void MakeCommendLineAndPrint()
{
  //实现输出一个命令行
  char line[SIZE];
  //获取三个字符串
  const char *username = GetUserName();
  const char *hostname = GetHostName();
  const char *cwd = GetCwd();

  //拼接字符串
  snprintf(line, sizeof(line), "%s@%s:%s^_^ -> ", username, hostname, cwd);
  
  printf("%s", line);
  fflush(stdout);
}

//获取用户命令字符串
int GetUserCommend(char commend[], size_t n)
{
  //2.再定一个缓冲区
  //2.1.获取:从标准输入流中获取
  char *s = fgets(commend, n, stdin);
  if(s == NULL) return -1; 
  
  commend[strlen(commend) - 1] = ZERO;
  return strlen(commend);
}
//定义一张全局的表
char *gArgv[NUM];

void SplitCommend(char commend[], size_t n)
{
  //"ls -a -l -n" ---> "ls", "-a", "-l", "-n"
  gArgv[0] = strtok(commend, SEP);
  int index = 1;
  while(gArgv[index++] = strtok(NULL, SEP));//故意写成 = ,表示先赋值,再判断。刚好让gArgv最后一个元素是NULL,并且while判断结束
}


void ExecuteCommend()
{
 //n.执行命令:
  pid_t id = fork();

  //创建的子进程失败
  if(id < 0) Die();
  else if(id == 0)
  {
    //child
    execvp(gArgv[0],gArgv);
    exit(errno);
  }
  else
  {
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);

    if(rid == id)
    {
     // printf("father wait success!, child exit code : %d\n", WEXITSTATUS(status));
    }
  }
}

int main()
{
  //1.自己输出一个命令行
  MakeCommendLineAndPrint();

  //2.获取用户命令字符串
  char usercommend[SIZE];
  int n = GetUserCommend(usercommend, sizeof(usercommend));
  if(n <= 0) return 1;

  //printf("echo : %s\n", usercommend);

  //3.命令行字符串分割
  
  SplitCommend(usercommend, sizeof(usercommend));
  
  for(int i = 0; gArgv[i]; i++)//当i = 最后一个字符串的下标时,因为为NULL,所以退出循环
  {
   // printf("gArgv[%d]: %s\n", i, gArgv[i]);
  }
  
  //执行命令
   ExecuteCommend();
   return 0; 
 }

以上我所写的shell只能跑一次,想要像真正的命令行一样就需要可以执行多次。

n + 1:将命令多次执行

while 循环,不退出就能一直执行:

运行结果:

以上就是一个简单shell的制作。

shell 2.0 需补坑完整代码:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32


void Die()
{
  exit(1);
}

const char *GetUserName()
{
  const char *name = getenv("LOGNAME");
  if(name == NULL) return "none";
  return name;
}

const char *GetHostName()
{
  static char buffer[256];
  char *hostname = buffer;
  if (gethostname(hostname, sizeof(buffer)) == 0) return hostname;
  return "none";
}

//获取当前的路径
const char *GetCwd()
{
  const char *cwd = getenv("PWD");
  if(cwd == NULL) return "none";
  return cwd;
}

void MakeCommendLineAndPrint()
{
  //实现输出一个命令行
  char line[SIZE];
  //获取三个字符串
  const char *username = GetUserName();
  const char *hostname = GetHostName();
  const char *cwd = GetCwd();

  //拼接字符串
  snprintf(line, sizeof(line), "%s@%s:%s^_^ -> ", username, hostname, cwd);
  
  printf("%s", line);
  fflush(stdout);
}

//获取用户命令字符串
int GetUserCommend(char commend[], size_t n)
{
  //2.再定一个缓冲区
  //2.1.获取:从标准输入流中获取
  char *s = fgets(commend, n, stdin);
  if(s == NULL) return -1; 
  
  commend[strlen(commend) - 1] = ZERO;
  return strlen(commend);
}
//定义一张全局的表
char *gArgv[NUM];

void SplitCommend(char commend[], size_t n)
{
  //"ls -a -l -n" ---> "ls", "-a", "-l", "-n"
  gArgv[0] = strtok(commend, SEP);
  int index = 1;
  while(gArgv[index++] = strtok(NULL, SEP));//故意写成 = ,表示先赋值,再判断。刚好让gArgv最后一个元素是NULL,并且while判断结束
}


void ExecuteCommend()
{
 //n.执行命令:
  pid_t id = fork();

  //创建的子进程失败
  if(id < 0) Die();
  else if(id == 0)
  {
    //child
    execvp(gArgv[0],gArgv);
    exit(errno);
  }
  else
  {
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);

    if(rid == id)
    {
     // printf("father wait success!, child exit code : %d\n", WEXITSTATUS(status));
    }
  }
}


int main()
{
  int quit = 0;
  while(!quit)
  {
    //1.自己输出一个命令行
    MakeCommendLineAndPrint();

    //2.获取用户命令字符串
    char usercommend[SIZE];
    int n = GetUserCommend(usercommend, sizeof(usercommend));
    if(n <= 0) return 1;

    //printf("echo : %s\n", usercommend);

    //3.命令行字符串分割
    
    SplitCommend(usercommend, sizeof(usercommend));
    
    for(int i = 0; gArgv[i]; i++)//当i = 最后一个字符串的下标时,因为为NULL,所以退出循环
    {
     // printf("gArgv[%d]: %s\n", i, gArgv[i]);
    }
    
    //执行命令
     ExecuteCommend();
  }
   return 0; 
 }

nn:填补上述shell代码的坑(cd无用的问题)

我们当前的shell无法进行路径的切换:

每个进程都会记录当前所属的路径,所以父进程有,子进程有。

原因:

我的shell中子进程进行cd ..,和父进程没有关系,也就是和bash没有关系,因此不会切换。

因此当需执行的命令是 cd 时,应该让此命令给父进程执行 -----> cd是内建命令

4.检查命令是否是内建命令(只有bash能执行的命令)

chdir()更改当前的工作路径

这里需要使用到chdir()系统调用命令,<unistd.h>, 用于更改当前的工作路径

cpp 复制代码
int chdir(const char *path);

更改我当前的工作路径:

cd 命令一般只有 cd(进入家目录), cd 相对路径/绝对路径(进入路径所处目录),cd ..(进入上级目录),cd ~(进入家目录),cd -(打印上级目录并进入上级目录,在这里没有写)
因此gArgv[1],便是cd的选择命令,如果为空,则进入家目录,不为空就可以直接 使用,直接调用系统命令,更改当前的工作路径。

更改成功,但是依然存在问题:命令行中的路径始终未发生改变

因此我们再次更改代码,将输入的有效路径传给cwd,并更新环境变量:

运行结果:

这是因为,此时我将获取到的字符串直接给cwd了,并且还更新了环境变量导致PWD="path",当我输入 ..,那么PWD=..,因此我们需要得到当前工作目录的绝对路径,再将他的值传给cwd,更新环境变量。

**getcwd()**获取进程当前工作目录的绝对路径

这告诉我们,每次刷新命令行路径的时候也需要采用绝对路径 ,使用系统调用命令getcwd()

cpp 复制代码
char *getcwd(char *buf, size_t size);

这里使用temp[SIZE*2]用于存储获得的绝对路径

运行结果:因为我定义的cwd[SIZE*2] -->1024个字节,PWD+%s --->1028个字节,超出范围。

因此我直接:

运行结果:完全正确


5.将命令行路径通过使用绝对路径改为相对路径

在centos系统之下,命令行路径只会记录当前的相对路径:

因此就需要我们对路径进行剪切:

定义一个宏函数(解释:看目录)

运行结果:

此时还不够完美,其中还有 ' / ',这是因为指针指向/停止,将 / 的地址传回来,因此直接对cwd + 1就可以:

运行结果:

当到达根目录时,却没有路径字符串了

再修改:

运行结果:

为什么定义宏,以及使用do{}while(0)?

cpp 复制代码
#define SkipPath(p) do{ p += (strlen(p)- 1); while(*p != '/') p--; }while(0)

首先,这里涉及到对指针做操作 ,如果我想封装一个函数 对这个指针操作,那就需要传二级指针, 因此我们用宏,使cwd -被替换成--> p ,do{}while(0)形成代码块 ,并且do{}while()后面可以随便带' ;',方便后续的使用:就很像一个函数了,特别是需要写在 if 里面,也不会出什么错。

当在写宏函数需要用代码块的的时候建议写在do{}while()里面(编码小技巧)

echo $?,返回最后一次进程的返回值(退出码 ):

运行结果:

6.内建命令echo $?问题

图片里为什么还要把lastcode --> 0 不懂可以看:Linux 进程的创建、终止、等待与程序替换函数 保姆级讲解-CSDN博客

运行结果:


7.自定义环境变量export HELLO=12345

当我们导入环境变量的时候:

  1. export HELLO=12345,又需要识别到是内建命令,通过strcmp来判断。

创建函数Export()来执行此代码:

Export()函数

a.使用strdup()函数复制gArgv[1],arg指向字符串首地址,避免修改原始命令字符串

cpp 复制代码
 char *arg = strdup(gArgv[1]);

b. 使用strchr()函数查询是否有 = ,如果有则返回 = 的地址,没有则返回NULL

cpp 复制代码
char *eq = strchr(arg, '=');

c.判断是否为NULL,如果为空说明export使用的格式错误

cpp 复制代码
if (eq == NULL) {
        fprintf(stderr, "export: invalid format\n");
        free(arg); // 错误分支也要释放内存
        return;
    }

d.再将*eq指向的 = ,位置置为\0,截取arg = 前的字符串,eq+1,得到 = 后面的字符串。

cpp 复制代码
 *eq = '\0';

e.**setenv()**设置环境变量:

cpp 复制代码
#include <stdlib.h> // 需要包含头文件

int setenv(const char *name, const char *value, int overwrite);

参数

name:环境变量名(如 "PATH")|value:要设置的值(如 "/usr/bin")。

overwrite1(非零) :若变量已存在,则覆盖旧值。0:若变量已存在,则保留旧值,不修改。

返回值0 :成功。-1 :失败(错误原因存于 errno,如 ENOMEM 内存不足)。

将(arg = HELLO,eq = 12345,1-->确认覆盖),将环境变量名为HELLO的值确认使用12345覆盖。如果原本这个环境变量不存在,则在env中添加这个新的环境变量。并且判断是否创建,执行成功。

cpp 复制代码
 if (setenv(arg, eq+1, 1) != 0) {
        perror("export");
    }

最后释放arg所指向的空间

cpp 复制代码
 free(arg); // 正常路径释放内存

2. echo HELLO 的时候需要将HELLO替换成它对应的值12345,从而输出12345

执行 echo $HHH 时,Shell 本应进行以下操作:

  1. 变量替换 :将 $HHH 替换为环境变量 HHH 的值。

  2. 执行命令 :调用 echo 并传入替换后的参数。

a.添加变量替换函数 ReplaceEnvVars():

如果$后跟的是?就直接使用前面写的获取退出码的那个代码,这里要排除一下

cpp 复制代码
void ReplaceEnvVars()
{
  for (int i = 0; gArgv[i] != NULL; i++)
  {
    if(gArgv[i][0] == '$')
    {
      if(gArgv[i][1] != '?')//?就直接使用前面写的获取退出码的那个代码,这里要排除一下
      {
        char *var_name = gArgv[i] + 1; //跳过'$',获取变量名
        char *value = getenv(var_name);
        if(value)
        {
          //如果这个变量名已经在环境变量中存在
          gArgv[i] = strdup(value);
        }
        else
        {
          gArgv[i] = strdup("");
        }
      }
      
    }
   }
}

将这个函数在调用判断内建命令的函数前进行调用:

运行代码:

以上就是shell的模拟实现。

完整代码

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)- 1); while(*p != '/') p--; }while(0)

char cwd[SIZE*4];

//定义一张全局的表
char *gArgv[NUM];

int lastcode = 0;


void Die()
{
  exit(1);
}

const char *GetHome()
{
  const char *home = getenv("HOME");
  if(home == NULL) return "/";
  return home;
}

const char *GetUserName()
{
  const char *name = getenv("LOGNAME");
  if(name == NULL) return "none";
  return name;
}

const char *GetHostName()
{
  static char buffer[256];
  char *hostname = buffer;
  if (gethostname(hostname, sizeof(buffer)) == 0) return hostname;
  return "none";
}

//获取当前的路径
const char *GetCwd()
{
  const char *cwd = getenv("PWD");
  if(cwd == NULL) return "none";

  return cwd;
}

void MakeCommendLineAndPrint()
{
  //实现输出一个命令行
  char line[SIZE];
  //获取三个字符串
  const char *username = GetUserName();
  const char *hostname = GetHostName();
  const char *cwd = GetCwd();
  SkipPath(cwd);
  //拼接字符串
  snprintf(line, sizeof(line), "%s@%s:%s^_^ -> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd + 1);
  
  printf("%s", line);
  fflush(stdout);
}

//获取用户命令字符串
int GetUserCommend(char commend[], size_t n)
{
  //2.再定一个缓冲区
  //2.1.获取:从标准输入流中获取
  char *s = fgets(commend, n, stdin);
  if(s == NULL) return -1; 
  
  commend[strlen(commend) - 1] = ZERO;
  return strlen(commend);
}

void SplitCommend(char commend[], size_t n)
{
  //"ls -a -l -n" ---> "ls", "-a", "-l", "-n"
  gArgv[0] = strtok(commend, SEP);
  int index = 1;
  while(gArgv[index++] = strtok(NULL, SEP));//故意写成 = ,表示先赋值,再判断。刚好让gArgv最后一个元素是NULL,并且while判断结束
}


void ExecuteCommend()
{
 //n.执行命令:
  pid_t id = fork();

  //创建的子进程失败
  if(id < 0) Die();
  else if(id == 0)
  {
    //child
    execvp(gArgv[0],gArgv);
    exit(errno);
  }
  else
  {
    int status = 0; 
    pid_t rid = waitpid(id, &status, 0);

    if(rid == id)
    {
     // printf("father wait success!, child exit code : %d\n", WEXITSTATUS(status));
      lastcode = WEXITSTATUS(status);
      if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);  
    }
  }
}

 

//判断是哪一种cd
void Cd()
{
    const char* path = gArgv[1];
    
    if(path == NULL) path = GetHome();
    //PATH 一定存在
    chdir(path);
    //刷新环境变量
    char temp[SIZE*2];
    getcwd(temp, sizeof(temp));

    snprintf(cwd, sizeof(cwd),"PWD=%s", temp);//将cwd设置为一个全局变量,实时更新
    putenv(cwd); //再更新环境变量
}

void ReplaceEnvVars()
{
  for (int i = 0; gArgv[i] != NULL; i++)
  {
    if(gArgv[i][0] == '$')
    {
      if(gArgv[i][1] != '?')
      {
        char *var_name = gArgv[i] + 1; //跳过'$',获取变量名
        char *value = getenv(var_name);
        if(value)
        {
          //如果这个变量名已经在环境变量中存在
          gArgv[i] = strdup(value);
        }
        else
        {
          gArgv[i] = strdup("");
        }
      }
      
    }
   }
}


void Export()
{
  if(!gArgv[1])
  {
    fprintf(stderr, "export: missing argument!\n");
    return;
  }

  char *arg = strdup(gArgv[1]);

  char *eq = strchr(arg,'=');//查找是否有 = ,如果有就返回 = 的地址
  if(eq == NULL)
  {
    fprintf(stderr, "export: invalid format\n");
    free(arg);
    return;
  }

  *eq = '\0';//将 = 的位置的字符置为\0提前结束
  if(setenv(arg, eq+1, 1) != 0)
  {
    perror("export");
  }
  free(arg);
}


//检查是否是内建命令
int CheckBuildin()
{
  int yesorno = 0;
  const char *enter_cmd = gArgv[0];
  if(strcmp(enter_cmd, "cd") == 0)
  {
    yesorno = 1; 
    Cd();
  }
  else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1],"$?") == 0)
  {
    yesorno = 1;
    printf("%d\n", lastcode);
    lastcode = 0;
  }
  else if(strcmp(enter_cmd, "export") == 0)
  {
    Export();
    yesorno = 1;
  }
  return yesorno;
}


int main()
{
  int quit = 0;
  while(!quit)
  {
    //1.自己输出一个命令行
    MakeCommendLineAndPrint();

    //2.获取用户命令字符串
    char usercommend[SIZE];
    int n = GetUserCommend(usercommend, sizeof(usercommend));
    if(n <= 0) return 1;

    //printf("echo : %s\n", usercommend);

    //3.命令行字符串分割
    
    SplitCommend(usercommend, sizeof(usercommend));
    
    for(int i = 0; gArgv[i]; i++)//当i = 最后一个字符串的下标时,因为为NULL,所以退出循环
    {
     // printf("gArgv[%d]: %s\n", i, gArgv[i]);
    }
    ReplaceEnvVars();

    //4,检查是否是内建命令
    n = CheckBuildin();
    if(n) continue;
    //执行命令
     ExecuteCommend();
  }
   return 0; 
 }

结语:

随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。

在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。

你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。

相关推荐
weixin_4082663416 分钟前
深度学习-服务器训练SparseDrive过程记录
服务器·人工智能·深度学习
让子弹飞0220 分钟前
10.2linux内核定时器实验(详细编程)_csdn
linux·驱动开发·ubuntu·定时器·stm32mp157·自旋锁
charlie11451419128 分钟前
Linux驱动开发框架基础——新旧字符设备驱动笔记整理(1)
linux·驱动开发·笔记·学习·操作系统·教程
小白学安全hhhh30 分钟前
VPC4-通达oa-docker逃逸-shiro反序列化-hash传递-CrackMapExec喷射-历史ptt攻击-进程注入
运维·安全·网络安全·docker·容器·网络攻击模型·安全架构
luopeng20766343635 分钟前
jenkins通过ssh连接远程服务器出错解决方案(Algorithm negotiation fail)
服务器·ssh·jenkins
别惊鹊35 分钟前
hadoop集群配置-xsync脚本同步环境变量
大数据·linux·hadoop
酷熊代理1 小时前
软路由如何屏蔽国外IP?RouterOS保姆级实战教程(附自动化脚本)
运维·网络协议·tcp/ip·自动化·智能路由器·软路由·小猫pptp
tanyongxi661 小时前
Linux 中 Git 使用指南:从零开始掌握版本控制
linux·git
UpUpUp……2 小时前
Linux中Gdb调试工具常用指令大全
linux·运维·服务器·笔记
cxy_62 小时前
nginx中间件部署
运维·nginx·中间件