《从零手写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")。
overwrite:1(非零):若变量已存在,则覆盖旧值。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 本应进行以下操作:
变量替换:将
$HHH替换为环境变量HHH的值。执行命令:调用
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博客,涉及所用到的代码,本文所绑定的资源就是上篇文章的主要代码。 完整代码在文章末尾 目录 1.实现编写代码输出一个命令行 a.如何获取自己的用户名,主机名,路径…...
【Go语言圣经2.4】
目标 理解 在 Go 中,赋值操作既包括最基本的形式(左边一个变量,右边一个表达式),也包括复合赋值、元组赋值和隐式赋值。表达式求值的顺序、变量更新时的副作用以及如何处理多返回值和下划线(_)…...
运维工具推荐 -- 宝塔面板:一键部署服务器
标题:宝塔面板:一键部署服务器,轻松管理你的云端世界 引言 在数字化时代,服务器管理对于个人开发者、中小企业或站长来说既是机遇也是挑战。手动配置服务器环境耗时费力,而 宝塔面板 作为一款 免费开源、功能全面 的服…...
C# 异常处理的核心概念
文章目录 一、异常处理的核心概念二、C# 异常处理的基本语法三、常见异常类型四、最佳实践五、示例:文件读取异常处理六、总结 C# 异常处理的详细说明,包括核心概念、使用方法和最佳实践: 一、异常处理的核心概念 …...
腾讯云点播key防盗链生成到期自动失效url
package com.xmkjsoft.protect_key;import java.nio.charset.StandardCharsets; import java.security.MessageDigest;public class TencentKeyAntiTheft {private static final String SECRET_KEY ""; // 请替换为腾讯云 VOD 控制台中的 Key/*** 生成腾讯云 Key 防…...
深入 Spring Boot 注解
深入 Spring Boot 注解:我的开发心得与常用注解详解 大家好,我是 [你的 CSDN 昵称/名字],一位热爱 Spring Boot 的技术博主。 在多年的 Spring Boot 开发实践中,我深深体会到注解的强大魅力。它们不仅让代码变得更简洁࿰…...
k8s环境部署
四台机器 分别是 k8s-master:172.25.254.100 k8s-node1:172.25.254.10 k8s-node2:172.25.254.20 docker-harbor:172.25.254.200 reg.timinglee.org 四台机器分别配置好网络和软件仓库 做好地址解析 scp -r /etc/hosts/ root17…...
CentOS 系统安装 docker 以及常用插件
博主用的的是WindTerm软件链接的服务器,因为好用 1.链接上服务器登入后,在/root/目录下 2.执行以下命令安装docker sudo yum install -y yum-utilssudo yum-config-manager \--add-repo \https://download.docker.com/linux/centos/docker-ce.reposudo…...
谷歌云服务器:服务器怎么安装???
谷歌云服务器:服务器怎么安装??? 以下是详细分步指南,帮助你在 Google Cloud Platform (GCP) 上快速创建并配置云服务器(Compute Engine 实例),并安装所需环境: 一、准备…...
Redis--Zset类型
目录 一、引言 二、介绍 三、命令 1.zadd 2.zrange,zrevrange,zrangebyscore 3.zcard,zcount 4.zpopmax,bzpopmax,zpopmin,bzpopmin 5.zrank,zrevrank,zscore 6.zrem,zremrangebyrank&a…...
《阿里云Data+AI:开启数据智能新时代》电子书上线啦!
本书整理了阿里云在DataAI领域的最新实践案例与深度洞察,涵盖电商、游戏、营销、数字内容等多个行业的成功经验,以及技术专家对数据库与AI融合趋势的专业解读。 通过理论与实践的结合,我们将共同探索DataAI如何成为企业智能化转型的核心驱动…...
图像处理篇---图像预处理
文章目录 前言一、通用目的1.1 数据标准化目的实现 1.2 噪声抑制目的实现高斯滤波中值滤波双边滤波 1.3 尺寸统一化目的实现 1.4 数据增强目的实现 1.5 特征增强目的实现:边缘检测直方图均衡化锐化 二、分领域预处理2.1 传统机器学习(如SVM、随机森林&am…...
Vue 3 事件总线详解:构建组件间高效通信的桥梁
Vue 3 事件总线详解:构建组件间高效通信的桥梁 为什么需要事件总线?使用 mitt 实现事件总线1. 安装 mitt2. 创建事件总线3. 在组件中使用事件总线发送端组件(例如 ComponentA.vue)接收端组件(例如 ComponentB.vue&…...
Golang编译器DIY,手搓 if err != nil { return err } 语法糖
前序 在go的社区里,下面这三行代码是被吐槽的最多的 if err ! nil {return err }从代码之整洁美观的角度看,这样的写法也是让人不舒服的。尤其是 当有很多错误需要处理的时候,就会发现通篇都是这三行。 所以想着看看修改一下编译器…...
图解多头注意力机制:维度变化一镜到底
目录 一、多头注意力机制概述二、代码实现1. pyTorch 实现2. tensorFlow实现 三、维度变化全流程详解1. 参数设定2. 维度变化流程图3. 关键步骤维度变化 四、关键实现细节解析1. 多头拆分与合并2. 注意力分数计算3. 掩码处理技巧 五、完整运行示例六、总结与常见问题1. 核心优势…...
.NET_Prism基本项目创建
Prism简述 模块(Module):独立的功能单元,可动态加载。依赖注入(Dependency Injection,DI):通过 IoC 容器(如 Unity、Autofac)管理模块之间的依赖关系。&#…...
BigEvent项目后端学习笔记(一)用户管理模块 | 注册登录与用户信息全流程解析(含优化)
📖 模块概述 用户管理模块是系统的核心基础模块,包含 注册、登录、用户信息维护 等功能。本模块涉及 JWT Token认证、密码加密存储、文件上传 等关键技术点,是理解前后端分离架构中安全与数据交互的典型实践。本篇对于原项目进行了代码优化&…...
[ISP] 人眼中的颜色
相机是如何记录颜色的,又是如何被显示器还原的? 相机通过记录RGB数值然后显示器显示RGB数值来实现颜色的记录和呈现。道理是这么个道理,但实际上各厂家生产的相机对光的响应各不相同,并且不同厂家显示器对三原色的显示也天差地别&…...
解锁MySQL 8.0.41源码调试:Mac 11.6+CLion 2024.3.4实战指南
文章目录 解锁MySQL 8.0.41源码调试:Mac 11.6CLion 2024.3.4实战指南前期准备环境搭建详细步骤安装 CLion安装 CMake 3.30.5准备 MySQL 8.0.41 源码配置 CMake 选项构建 MySQL 项目 调试环境配置与验证配置 LLDB 调试器启动调试验证调试环境 总结与拓展 解锁MySQL 8…...
celery入门
按照Celery 官方文档,用 Django Celery Redis 写的一个简单项目 如需转载,标记出处 环境准备 1. 安装依赖 pip install django celery redis 创建 Django 项目 1. 创建 Django 项目和 APP django-admin startproject myproject cd myproject python …...
关于xcode Project navigator/项目导航栏的一些说明
本文基于 xcode12.4 版本做说明 首先要明确一点,导航栏这里展示的并不是当前工程在电脑硬盘中的文件结构,它展示的是xxxxxx.xcodeproj/project.pbxproj文件(后文简.pbxproj文件)中的内容。我们在导航栏中的操作就是修改该文件,有些操作会修…...
深度解析扣减系统设计:从架构到实践
背景 在当今数字化业务蓬勃发展的时代,扣减系统在众多业务场景中扮演着关键角色。无论是电商平台的库存扣减,还是金融领域的资金扣减、积分系统的积分扣减,一个高效、可靠且数据一致的扣减系统都是业务稳健运行的基石。本文将深入探讨扣减系…...
视觉定位项目中可以任意修改拍照点位吗?
修改拍照点位不是那么简单 1. 背景2. 修改拍照点位意味着什么?3. 如何解决这个问题? 1. 背景 在视觉定位的项目中,会遇到这么一种情况:完成三步(9点标定,旋转中心标定,示教基准)之…...
深度学习常用操作笔记
深度学习常用操作笔记 指令报错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.概念的介绍 总括: 1. 栈(Stack) 存储内容: 局部变量(包括函数参数、非静态局部变量)。 函数调用的上下文信息(如返回地址、寄存器状态等)。 特点: 内存由编译器自动…...
git使用。创建仓库,拉取分支,新建分支开发
文章目录 安装 git自己新建仓库,进行代码管理合作开发的流程拉去主分支代码查看本地分支的状态查看远程分支查看远程的仓库信息本地分支切换切换并创建分支提交代码 made by NJITZX git 是一个版本控制工具,真正开发项目中是多个人开发一个项目的&#…...
itsdangerous加解密源码分析|BUG汇总
这是我这两天的思考 早知道密码学的课就不旷那么多了 纯个人见解 如需转载,标记出处 目录 一、官网介绍 二、事例代码 源码分析: 加密函数dump源码使用的函数如下: 解密 编辑 编辑 关于签名: 为什么这个数字签名没有…...
常见限流算法及实现
1. 固定窗口计数器(Fixed Window Counter) 原理:在固定时间窗口(如1分钟)内统计请求数,超过阈值则拒绝后续请求。优点:实现简单,内存占用低。缺点:存在窗口切换时的流量…...
计算机操作系统进程(4)
系列文章目录 第二章:进程的描述与控制 文章目录 系列文章目录前言一、临界区的概念和描述:二、硬件同步机制: 1.关中断2.利用Test-and-Set指令实现互斥3.利用Swap指令实现进程的互斥 总结 前言 上一篇我们仅仅讲了一点关于线程同步的概念&a…...
编程题《牛牛的链表删除》的python可以用非链表的方式
描述 牛牛从键盘输入了一个长度为 n 的数组,把这个数组转换成链表然后把链表中所有值是 x 的节点都删除。 输入描述: 第一行输入两个正整数 n 和 x 表示数组的长度和要删除的链表节点值 x 。 第二行输入 n 个正整数表示数组中每个元素的值。 输出描述&am…...









































