当前位置: 首页 > news >正文

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

承接上文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 512const 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 512const 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 512const 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 32void 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){//childexecvp(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 32void 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){//childexecvp(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){//childexecvp(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; }

结语:

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

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

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

相关文章:

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

承接上文Linux 进程的创建、终止、等待与程序替换保姆级讲解-CSDN博客&#xff0c;涉及所用到的代码&#xff0c;本文所绑定的资源就是上篇文章的主要代码。 完整代码在文章末尾 目录 1.实现编写代码输出一个命令行 a.如何获取自己的用户名&#xff0c;主机名&#xff0c;路径…...

postgresql 高版本pgsql备份在低版本pgsql中恢复失败,报错:“unsupported version”

关键字 PostgreSQL、pg_restore、版本兼容性、数据库迁移、pg_dump、备份恢复、unsupported version in file header 背景环境 系统配置 环境类型操作系统PostgreSQL版本内存工具链测试环境Windows 111616GBNavicat/PgAdmin生产环境Windows Server 2012 R2128GBPgAdmin/命令…...

裸机开发-GPIO外设

重新开始学ZYNQ开发&#xff0c;学完上linux系统 基础知识&#xff1a;ZYNQ 的三种GPIO &#xff1a;MIO、EMIO、AXI - FPGA/ASIC技术 - 电子发烧友网 GPIO是ZYNQ PS端的一个IO外设&#xff0c;用于观测&#xff08;input&#xff09;和控制&#xff08;output&#xff09;器…...

STM32——独立看门狗(IWDG)

IWDG 简介 独立看门狗本质上是一个 定时器 &#xff0c;这个定时器有一个输出端&#xff0c;可以输出复位信号。该定时器是一个 12 位的递减计数器 &#xff0c;当计数器的值减到 0 的时候&#xff0c;就会产生一个复位信号。如果 在计 数没减到 0 之前&#xff0c;重置计…...

使用STM32CubeMX+DMA+空闲中断实现串口接收和发送数据(STM32G070CBT6)

1.STM32CubeMX配置 &#xff08;1&#xff09;配置SYS &#xff08;2&#xff09;配置RCC &#xff08;3&#xff09;配置串口&#xff0c;此处我用的是串口4&#xff0c;其他串口也是一样的 &#xff08;4&#xff09;配置DMA&#xff0c;将串口4的TX和RX添加到DMA中 &#…...

连续出现的字符(信息学奥赛一本通-1148)

【题目描述】 给定一个字符串&#xff0c;在字符串中找到第一个连续出现至少k次的字符。 【输入】 第一行包含一个正整数k&#xff0c;表示至少需要连续出现的次数。1 ≤ k ≤ 1000。 第二行包含需要查找的字符串。字符串长度在1到2500之间&#xff0c;且不包含任何空白符。 【…...

matlab 正态分布

目录 一、概述1、参数2、概率密度函数3、累积分布函数二、代码案例1、拟合正态分布对象2、估计参数3、计算并绘制正态分布的概率密度函数4、绘制标准正态分布的正态累积分布函数5、比较 gamma 和正态分布的概率密度函数6、正态分布和对数正态分布之间的关系7、比较 Student t 和…...

C# MVC项目部署II后错误,403禁止访问:访问被拒绝问题处理

C# MVC项目部署II后错误&#xff0c;403禁止访问&#xff1a;访问被拒绝问题处理 问题如下&#xff1a; 解决办法&#xff1a; 1. 应用程序池要选v4.xx&#xff0c;托管模式选“集成” 2. 把asp.net 4.xx安装在iis上&#xff0c;方法&#xff1a; cd \Windows\Microsoft .NE…...

有趣的算法实践:整数反转与回文检测(Java实现)

题目描述&#xff1a;整数反转与回文检测 要求实现两个功能&#xff1a; 将输入的整数反转&#xff08;保留符号&#xff0c;如输入-123返回-321&#xff09;判断反转后的数是否为回文数&#xff08;正反读相同&#xff09; 示例&#xff1a; 输入&#xff1a;123 → 反转结…...

数学建模:MATLAB循环神经网络

一、简述 1.循环神经网络 循环神经网络&#xff08;RNN&#xff09;是一种用于处理序列数据的神经网络。不同于传统的前馈神经网络&#xff0c;RNN在隐藏层中加入了自反馈连接&#xff0c;使得网络能够对序列中的每个元素执行相同的操作&#xff0c;同时保持一个“记忆”状态…...

东隆科技携手PRIMES成立中国校准实验室,开启激光诊断高精度新时代

3月12日&#xff0c;上海慕尼黑光博会期间&#xff0c;东隆科技正式宣布与德国PRIMES共同成立“中国校准实验室”。这一重要合作标志着东隆科技在本地化服务领域的优势与PRIMES在激光光束诊断领域的顶尖技术深度融合&#xff0c;旨在为中国客户提供更快速、更高精度的服务以及本…...

【MySQL】B树和B+树的区别?MySQL为什么选用B+树作为索引数据结构?

B树和B树的区别&#xff1a; 结构方面&#xff1a; 1.节点存储内容&#xff1a; B树&#xff1a; 节点同时存储索引和数据。B树&#xff1a;只有叶子节点存储数据记录或指向数据记录的指针&#xff0c;非叶子节点只存键值&#xff0c;用于索引。 B 树的非叶子节点可以存储更…...

使用yolov8+flask实现精美登录界面+图片视频摄像头检测系统

这个是使用flask实现好看登录界面和友好的检测界面实现yolov8推理和展示&#xff0c;代码仅仅有2个html文件和一个python文件&#xff0c;真正做到了用最简洁的代码实现复杂功能。 测试通过环境&#xff1a; windows x64 anaconda3python3.8 ultralytics8.3.81 flask1.1.2…...

Cursor的使用感受,帮你使用好自动化编程工具,整理笔记

使用感受 说实话&#xff0c;我觉得cursor还是好用的&#xff0c;可能我刚开始使用&#xff0c;没有使用的非常的熟练&#xff0c;运用也没有非常的透彻&#xff0c;总体体验还是不错的&#xff0c;在使用它时&#xff0c;我优先考虑&#xff0c;前端页面功能复用的时候&#…...

2024浙江大学计算机考研上机真题

2024浙江大学计算机考研上机真题 2024浙江大学计算机考研复试上机真题 2024浙江大学计算机考研机试真题 2024浙江大学计算机考研复试机试真题 历年浙江大学计算机复试上机真题 历年浙江大学计算机复试机试真题 2024浙江大学计算机复试上机真题 2024浙江大学计算机复试机试真题 …...

JS逆向:通达OA Office Anywhere 2019 的前端密码加密逆向分析,并使用Python构建通达OA登录

免责声明 本文仅为技术研究与渗透测试思路分享,旨在帮助安全从业人员更好地理解相关技术原理和防御措施。任何个人或组织不得利用本文内容从事非法活动或攻击他人系统。 如果任何人因违反法律法规或不当使用本文内容而导致任何法律后果,本文作者概不负责。 请务必遵守法律…...

贴吧ip什么意思?贴吧ip可以查到姓名吗

贴吧作为百度旗下的一个重要社区平台&#xff0c;一直以来都吸引着大量用户进行交流和讨论。然而&#xff0c;随着网络环境的日益复杂&#xff0c;用户的隐私保护问题也日益凸显。其中&#xff0c;贴吧IP地址的显示及其与个人信息的关系&#xff0c;成为不少用户关注的焦点。本…...

【css酷炫效果】纯CSS实现进度条加载动画

【css酷炫效果】纯CSS实现进度条加载动画 缘创作背景html结构css样式完整代码基础版进阶版 效果图 通过CSS渐变与背景位移动画&#xff0c;无需JavaScript即可创建流体动态进度条。 想直接拿走的老板&#xff0c;链接放在这里&#xff1a;https://download.csdn.net/download/u…...

【后端开发面试题】每日 3 题(十三)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;https://blog.csdn.net/newin2020/category_12903849.html &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享后端开发面试中常见的面试题给大家&#xff0c;每天的题目都是独…...

【Android Studio】解决遇到的一些问题

目录 前言 一、Invalid Gradle JDK configuration found. Open Gradle Settings Change JDK location 报错场景 解决方法 二、adb 不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件。 报错场景 解决方法 前言 Android Studio的安装过程&#xff0c;可以参…...

UnitTest框架管理测试用例——python自动化测试

UnitTest框架 UnitTest是Python自带一个单元测试框架&#xff0c;常用它来做单元测试。 注意:对于测试来说&#xff0c;UnitTest框架的作用是 自动化脚本(用例代码)执行框架————(使用UnitTest框架来管理 运行多个测试用例的) 为什么使用UnitTest框架 能够组织多个用例去执…...

后端接口开发完成后,接口地址访问不到提示404,Spring项目的包结构错误

后端接口开发完成后&#xff0c;接口地址访问不到提示404&#xff0c;Spring项目的包结构错误 是因为包结构错误&#xff0c;导致无法在请求地址下找到对应的方法&#xff0c;原来错误的包结构&#xff08;自建controller、service、config等包建到了项目包之外、而非项目包之下…...

算法竞赛-基础算法-位运算

目录 1.快速幂 2.快速乘 3.lowbit(n) 4.其他 5.相关题目 6.小结 引言&#xff1a;位运算的主要特点之一是在二进制表示下不进位&#xff0c;一下为一些基础的位运算操作&#xff1a; 与或非异或and,&or,|not,~xor 1.快速幂 快速幂的计算原理就是基于位运算&#x…...

DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加导出数据功能

前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加导出数据功能📚页面效果📚指令输入�…...

深入解析音频编解码器(Audio CODEC):硬件、接口与驱动开发

音频编解码器&#xff08;Audio CODEC&#xff09;是音频处理系统中的核心组件&#xff0c;负责 模拟信号与数字信号的相互转换&#xff0c;广泛应用于 智能音箱、嵌入式系统、消费电子产品 等设备。本篇文章将从 硬件结构、接口解析、驱动开发 和 软件配置 等方面&#xff0c;…...

什么是 Fisher 信息矩阵

什么是 Fisher 信息矩阵 Fisher 信息矩阵是统计学和机器学习中一个重要的概念,它用于衡量样本数据所包含的关于模型参数的信息量。 伯努利分布示例 问题描述 假设我们有一个服从伯努利分布的随机变量 X X X,其概率质量函数为 P ( X ...

【项目合集】智能语音小车-微信小程序控制

功能需求&#xff1a; 车子检测环境温度、湿度&#xff0c;上报 APP、WEB 端显示实时数据可通过 APP 控制小车前进、左转、右转可通过语音控制小车前进后退车上一个 LED 灯&#xff0c;可通过 WEB、小程序控制在 APP、WEB 上均可注册登录 硬件清单 硬件 功能 备注 ESP32 …...

Vue3项目中可以尝试封装那些组件

在 Vue 3 项目中&#xff0c;组件的封装可以根据功能、复用性和业务需求进行划分。以下是一些常见的组件类型&#xff0c;适合封装为独立组件&#xff1a; 1. 基础 UI 组件 按钮 (Button) 封装不同样式、大小、状态的按钮。支持 disabled、loading 等状态。 输入框 (Input) 封…...

Leetcode——151.反转字符串中的单词

题解一 思路 最开始的想法是把一个字符串分为字符串数组&#xff0c;但是不知道一共有几个单词&#xff08;当时没想起来split()&#xff09;&#xff0c;所以选择了用ArrayList储存字符串&#xff0c;在输出时没有考虑ArrayList可以存储空字符串&#xff0c;所以最开始的输出…...

Deepseek+QuickAPI:打造 MySQL AI 智能体入门篇(一)

目录 一、什么是 MySQL AI 智能体&#xff1f; 二、准备工作&#xff1a;认识工具 1. Deepseek 的大模型能力 2. QuickAPI 的功能 3. MySQL 数据库 三、动手实践&#xff1a;用自然语言打造智能体 1. 创建一个用户表 2. 添加样本数据 3. 执行查询 四、效果展示 五、…...

Redis系列:深入理解缓存穿透、缓存击穿、缓存雪崩及其解决方案

在使用Redis作为缓存系统时&#xff0c;我们经常会遇到“缓存穿透”、“缓存击穿”和“缓存雪崩”等问题&#xff0c;这些问题一旦出现&#xff0c;会严重影响应用性能甚至造成服务不可用。因此&#xff0c;理解这些问题的产生原因和解决方案非常重要。 本文将全面讲解缓存穿透…...

python局部变量和全局变量

文章目录 1.局部变量和全局变量2.局部变量2.1 局部变量的作用2.2 局部变量的生命周期 3. 全局变量3.1 函数不能直接修改全局变量的引用3.2 在函数内部修改全局变量的值3.3 全局变量定义的位置3.4 全局变量命名的建议 1.局部变量和全局变量 &#xff08;1&#xff09;局部变量 …...

⭐算法OJ⭐两数之和【哈希表】(C++ 实现)Two Sum

“两数之和”&#xff08;Two Sum&#xff09;是一道非常经典的算法题目&#xff0c;几乎是算法入门和面试准备的必做题之一。它的经典性体现在以下几个方面&#xff1a; 1. 算法入门的基础题目 这道题目是许多初学者接触 哈希表&#xff08;Hash Table&#xff09; 或 字典&…...

【AVRCP】Notification PDUs 深入解析与应用

目录 一、Notification PDUs 概述 二、GetPlayStatus:同步查询播放状态 2.1 命令功能与应用场景 2.2 请求格式(CT → TG) 2.3 响应格式(TG → CT) 2.4 注意事项 2.5 协议实现示例(伪代码) 三、RegisterNotification:异步事件订阅 3.1 命令概述 3.2 命令格式 …...

算法题(100):腐烂的苹果

审题&#xff1a; 本题需要我们判断苹果是否可以完全腐烂&#xff0c;若可以完全腐烂&#xff0c;那么最短腐烂的所需时间是多少 思路&#xff1a; 方法一&#xff1a;多源BFS 首先我们分析腐烂过程&#xff0c;第一批腐烂苹果开始扩散&#xff0c;然后第二批腐烂苹果继续扩散&…...

某快餐店用户市场数据挖掘与可视化

1、必要库的载入 import pandas as pd import matplotlib.pyplot as plt import seaborn as sns2、加载并清洗数据 # 2.1 加载数据 df pd.read_csv(/home/mw/input/survey6263/mcdonalds.csv)# 2.2 数据清洗 # 2.2.1 检查缺失值 print(缺失值情况&#xff1a;) print(df.isn…...

【微服务】如何用Azure容器应用Job处理异步HTTP API请求

【微服务】如何用Azure容器应用Job处理异步HTTP API请求 推荐超级课程: 本地离线DeepSeek AI方案部署实战教程【完全版】Docker快速入门到精通Kubernetes入门到大师通关课AWS云服务快速入门实战目录 【微服务】如何用Azure容器应用Job处理异步HTTP API请求Azure容器应用中的长…...

安卓edge://inspect 和 chrome://inspect调试移动设备上的网页

edge://inspect 和 chrome://inspect 是用于调试浏览器中运行的网页和移动设备上的网页的工具。这两个工具分别属于 Microsoft Edge 和 Google Chrome 浏览器。以下是它们的详细介绍&#xff1a; chrome://inspect 如果是直接使用数据线调试&#xff0c;则只需要执行下面的第一…...

TX-LCN 框架

TX-LCN 框架通俗教学&#xff08;面试场景版&#xff09; 一句话概括 TX-LCN 是分布式事务的 “交通警察”&#xff0c;确保多个微服务操作要么全部成功&#xff08;比如转账扣款和到账&#xff09;&#xff0c;要么全部回滚&#xff08;比如网购下单失败后库存自动恢复&#…...

玩转github

me github 可以给仓库添加开发人员吗 4o 是的&#xff0c;GitHub允许仓库管理员为仓库添加开发人员&#xff0c;并设置这些开发人员的角色和权限。这里是一个简单的步骤指导&#xff0c;教你如何给一个 GitHub 仓库添加开发人员&#xff1a; 前提条件 你必须有这个仓库的权限&…...

Dubbo 深度解析

Dubbo 深度解析与实战指南 一、Dubbo 核心设计理念与应用场景 1.1 为什么需要 Dubbo&#xff1f; 随着互联网业务规模扩大&#xff0c;单体架构面临以下挑战&#xff1a; 服务依赖复杂&#xff1a;模块间耦合度高&#xff0c;难以独立迭代[[5]]。性能瓶颈&#xff1a;单一应…...

基于javaweb的SpringBoot校园运动会管理系统设计与实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论…...

Socket封装---模板方法类

目录 一、模板方法类 二、Socket的框架 三、TCPSocket对父类的虚函数重写 在平时写网络代码的时候&#xff0c;经常会涉及到socket套接字的部分&#xff0c;这一部分用的十分频繁&#xff0c;但是流程都是固定的&#xff0c;我实在是饱受其苦&#xff0c;但是由于C不像java一…...

牛客周赛84 题解 Java ABCDEFG AK实录

目录 题目地址 做题情况 A 题 B 题 C / D 题 E 题 F / G 题 题目地址 牛客竞赛_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ 做题情况 A 题 import java.io.*; import java.math.*; import java.util.*;// xixi♡西 public class Main {static IOS scnew…...

如何使用MySQL快速定位慢SQL问题?企业级开发中常见业务场景中实际发生的例子,涉及分页查询问题。(二)

如何使用MySQL快速定位慢SQL问题&#xff1f; 在企业级开发中&#xff0c;尤其是涉及到订单查询的业务时&#xff0c;经常会发生慢查询的问题。比如用户翻页到后面的页数时&#xff0c;查询变慢&#xff0c;因为传统的LIMIT offset, size在数据量大时效率低下。这时候&#xff…...

双链笔记新选择!使用Docker私有化部署Logseq知识库远程团队协作

前言&#xff1a;嘿&#xff0c;小伙伴们&#xff0c;今天要给大家安利一个超酷的技能——如何在本地Linux服务器上使用Docker轻松搭建Logseq笔记软件&#xff0c;并通过cpolar内网穿透工具实现远程访问。大家都知道&#xff0c;在快节奏的工作和学习中&#xff0c;一个好的笔记…...

C# 不同框架如何调用framework 和 net core

在 C# 中实现进程间通信&#xff08;IPC&#xff0c;Inter-Process Communication&#xff09;有多种方式&#xff0c;适用于不同场景。以下是常见 IPC 方法的实现方案、代码示例及适用场景对比&#xff1a; 1. 命名管道&#xff08;Named Pipes&#xff09; 特点&#xff1a;…...

【Linux-传输层协议TCP】TCP协议段格式+确认应答+超时重传+连接管理机制(三次握手、四次挥手、理解TIME_WAIT + CLOSE_WAIT)

TCP协议 TCP全称为“传输控制协议&#xff08;Transmission Control Protocol&#xff09;”人如其名&#xff0c;要对数据的传输进行一个详细的控制。 1.TCP协议段格式 下面是TCP报头各个字段的表格形式&#xff1a; 字段名称字段大小描述源端口16位发送端TCP端口号。目的端…...

怎样使用Modbus转Profinet网关连接USB转485模拟从站配置案例

怎样使用Modbus转Profinet网关连接USB转485模拟从站配置案例 Modbus转profinet网关可以将Modbus协议转化为profinet协议&#xff0c;以实现设备之间的数据交互。在实际使用过程中&#xff0c;我们需要使用Modbus协议进行设备通讯&#xff0c;而profinet协议则是用于工业自动化…...

从“自习室令牌”到线程同步:探秘锁与条件变量

目录 互斥 为什么需要锁 锁的原理--互斥 锁的使用 同步 锁的问题 条件变量 互斥 为什么需要锁 先看结果&#xff1a; 以下代码是我模拟创建线程抢票&#xff0c;由于不加锁导致票抢到了负数 main.cc: #include<vector> #include<iostream> #include"…...