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

news2025/3/17 17:26:37

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

完整代码在文章末尾

目录

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

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

b.ubuntu的HOSTNAME的获取方法:

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

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

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

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

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

n.创建子进程执行命令

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

n + 1:将命令多次执行

shell 2.0 需补坑完整代码:

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

原因:

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

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

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

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

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

6.内建命令echo $?问题

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

Export()函数

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

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

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

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

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

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

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


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

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

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

pupu@VM-8-15-ubuntu:~/bitclass/class_20/myshell$ 

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

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

ps ajx | head -1 && ps ajx | grep bash | grep -v grep

得到:

   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

测试获取登录名:

#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.将命令行路径改为相对路径)

PWD=/home/pupu/bitclass/class_20/myshell

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

b.ubuntu的HOSTNAME的获取方法:

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

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

解决办法:

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

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进行打印的时候数据存在缓冲区:

#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() 获取一整个字符串

char *fgets(char *s, int size, FILE *stream);

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

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

运行此代码进行测试:

#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的时候,就结束了。

char *strtok(char *str, const char *delim);

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

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

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


n.创建子进程执行命令

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

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

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

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

运行结果:


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

#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 需补坑完整代码:

#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>, 用于更改当前的工作路径

int chdir(const char *path);

更改我当前的工作路径:

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

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

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

运行结果:

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

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

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

char *getcwd(char *buf, size_t size);

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

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

因此我直接:

运行结果:完全正确


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

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

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

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

运行结果:

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

运行结果:

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

再修改:

运行结果:

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

#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指向字符串首地址,避免修改原始命令字符串

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

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

char *eq = strchr(arg, '=');

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

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

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

 *eq = '\0';

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

#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中添加这个新的环境变量。并且判断是否创建,执行成功。

 if (setenv(arg, eq+1, 1) != 0) {
        perror("export");
    }

最后释放arg所指向的空间

 free(arg); // 正常路径释放内存

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

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

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

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

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

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

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的模拟实现。

完整代码

#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; 
 }

结语:

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2316733.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

k8s环境部署

四台机器 分别是 k8s-master&#xff1a;172.25.254.100 k8s-node1&#xff1a;172.25.254.10 k8s-node2&#xff1a;172.25.254.20 docker-harbor&#xff1a;172.25.254.200 reg.timinglee.org 四台机器分别配置好网络和软件仓库 做好地址解析 scp -r /etc/hosts/ root17…

CentOS 系统安装 docker 以及常用插件

博主用的的是WindTerm软件链接的服务器&#xff0c;因为好用 1.链接上服务器登入后&#xff0c;在/root/目录下 2.执行以下命令安装docker sudo yum install -y yum-utilssudo yum-config-manager \--add-repo \https://download.docker.com/linux/centos/docker-ce.reposudo…

谷歌云服务器:服务器怎么安装???

谷歌云服务器&#xff1a;服务器怎么安装&#xff1f;&#xff1f;&#xff1f; 以下是详细分步指南&#xff0c;帮助你在 Google Cloud Platform (GCP) 上快速创建并配置云服务器&#xff08;Compute Engine 实例&#xff09;&#xff0c;并安装所需环境&#xff1a; 一、准备…

Redis--Zset类型

目录 一、引言 二、介绍 三、命令 1.zadd 2.zrange&#xff0c;zrevrange&#xff0c;zrangebyscore 3.zcard&#xff0c;zcount 4.zpopmax&#xff0c;bzpopmax&#xff0c;zpopmin&#xff0c;bzpopmin 5.zrank,zrevrank,zscore 6.zrem&#xff0c;zremrangebyrank&a…

《阿里云Data+AI:开启数据智能新时代》电子书上线啦!

本书整理了阿里云在DataAI领域的最新实践案例与深度洞察&#xff0c;涵盖电商、游戏、营销、数字内容等多个行业的成功经验&#xff0c;以及技术专家对数据库与AI融合趋势的专业解读。 通过理论与实践的结合&#xff0c;我们将共同探索DataAI如何成为企业智能化转型的核心驱动…

Golang编译器DIY,手搓 if err != nil { return err } 语法糖

前序 在go的社区里&#xff0c;下面这三行代码是被吐槽的最多的 if err ! nil {return err }从代码之整洁美观的角度看&#xff0c;这样的写法也是让人不舒服的。尤其是 当有很多错误需要处理的时候&#xff0c;就会发现通篇都是这三行。 所以想着看看修改一下编译器&#xf…

图解多头注意力机制:维度变化一镜到底

目录 一、多头注意力机制概述二、代码实现1. pyTorch 实现2. tensorFlow实现 三、维度变化全流程详解1. 参数设定2. 维度变化流程图3. 关键步骤维度变化 四、关键实现细节解析1. 多头拆分与合并2. 注意力分数计算3. 掩码处理技巧 五、完整运行示例六、总结与常见问题1. 核心优势…

[ISP] 人眼中的颜色

相机是如何记录颜色的&#xff0c;又是如何被显示器还原的&#xff1f; 相机通过记录RGB数值然后显示器显示RGB数值来实现颜色的记录和呈现。道理是这么个道理&#xff0c;但实际上各厂家生产的相机对光的响应各不相同&#xff0c;并且不同厂家显示器对三原色的显示也天差地别&…

解锁MySQL 8.0.41源码调试:Mac 11.6+CLion 2024.3.4实战指南

文章目录 解锁MySQL 8.0.41源码调试&#xff1a;Mac 11.6CLion 2024.3.4实战指南前期准备环境搭建详细步骤安装 CLion安装 CMake 3.30.5准备 MySQL 8.0.41 源码配置 CMake 选项构建 MySQL 项目 调试环境配置与验证配置 LLDB 调试器启动调试验证调试环境 总结与拓展 解锁MySQL 8…

关于xcode Project navigator/项目导航栏的一些说明

本文基于 xcode12.4 版本做说明 首先要明确一点&#xff0c;导航栏这里展示的并不是当前工程在电脑硬盘中的文件结构&#xff0c;它展示的是xxxxxx.xcodeproj/project.pbxproj文件(后文简.pbxproj文件)中的内容。我们在导航栏中的操作就是修改该文件&#xff0c;有些操作会修…

深度解析扣减系统设计:从架构到实践

背景 在当今数字化业务蓬勃发展的时代&#xff0c;扣减系统在众多业务场景中扮演着关键角色。无论是电商平台的库存扣减&#xff0c;还是金融领域的资金扣减、积分系统的积分扣减&#xff0c;一个高效、可靠且数据一致的扣减系统都是业务稳健运行的基石。本文将深入探讨扣减系…

视觉定位项目中可以任意修改拍照点位吗?

修改拍照点位不是那么简单 1. 背景2. 修改拍照点位意味着什么&#xff1f;3. 如何解决这个问题&#xff1f; 1. 背景 在视觉定位的项目中&#xff0c;会遇到这么一种情况&#xff1a;完成三步&#xff08;9点标定&#xff0c;旋转中心标定&#xff0c;示教基准&#xff09;之…

深度学习常用操作笔记

深度学习常用操作笔记 指令报错cannot import name Config from mmcvImportError: cannot import name print_log from mmcvImportError: cannot import name init_dist from mmengine.runnerWARNING: Retrying (Retry(total4, connectNone, readNone, redirectNone, statusNon…

C++学习内存管理

1.概念的介绍 总括&#xff1a; 1. 栈&#xff08;Stack&#xff09; 存储内容&#xff1a; 局部变量&#xff08;包括函数参数、非静态局部变量&#xff09;。 函数调用的上下文信息&#xff08;如返回地址、寄存器状态等&#xff09;。 特点&#xff1a; 内存由编译器自动…

git使用。创建仓库,拉取分支,新建分支开发

文章目录 安装 git自己新建仓库&#xff0c;进行代码管理合作开发的流程拉去主分支代码查看本地分支的状态查看远程分支查看远程的仓库信息本地分支切换切换并创建分支提交代码 made by NJITZX git 是一个版本控制工具&#xff0c;真正开发项目中是多个人开发一个项目的&#…

itsdangerous加解密源码分析|BUG汇总

这是我这两天的思考 早知道密码学的课就不旷那么多了 纯个人见解 如需转载&#xff0c;标记出处 目录 一、官网介绍 二、事例代码 源码分析&#xff1a; 加密函数dump源码使用的函数如下&#xff1a; 解密 ​编辑 ​编辑 关于签名&#xff1a; 为什么这个数字签名没有…

不像人做的题————十四届蓝桥杯省赛真题解析(上)A,B,C,D题解析

题目A&#xff1a;日期统计 思路分析&#xff1a; 本题的题目比较繁琐&#xff0c;我们采用暴力加DFS剪枝的方式去做&#xff0c;我们在DFS中按照8位日期的每一个位的要求进行初步剪枝找出所有的八位子串&#xff0c;但是还是会存在19月的情况&#xff0c;为此还需要在CHECK函数…

JavaScript 中 call 和 apply 的用法与区别

文章目录 前言一、 call 方法1.1 基本用法1.2 传递多个参数 二、apply 方法2.1 基本用法2.2 传递数组参数 三、call 和 apply 的区别四、实际应用场景4.1 借用方法4.2 继承与构造函数 五、总结 前言 在 JavaScript 中&#xff0c;call 和 apply 是两个非常重要的函数方法&…

面试系列|蚂蚁金服技术面【1】

哈喽&#xff0c;大家好&#xff01;今天分享一下蚂蚁金服的 Java 后端开发岗位真实社招面经&#xff0c;复盘面试过程中踩过的坑&#xff0c;整理面试过程中提到的知识点&#xff0c;希望能给正在准备面试的你一些参考和启发&#xff0c;希望对你有帮助&#xff0c;愿你能够获…

使用傅里叶变换测量声卡的频率失真

文章目录 一、说明二、关于声卡的技术详述三、实验代码获取四、结论 一、说明 假如我希望使用我的声卡来模拟软件无线电&#xff0c;利用声音而不是射频信号。我的声卡能胜任这项任务吗&#xff1f;本文将研究一种技术来找出答案。另外&#xff0c;需要了解音频技术的读者也可…