目录
makefile 文件内容:
myshell:myshell.c gcc -o $@ $^ -std=c99 .PHONY:clean clean: rm -f myshell
1.打印提示符并且获取用户字符串
当我们登录 xshell 时,它的命令行提示符为:
[zrsg@hecs-393616 test_12_10]$
当我们自行实现 shell 时,需要我们打印出这一行的内容,而这一行的内容和当前所处的环境变量息息相关,获取环境变量的命令是:env
查看当前环境变量:
我们主要关注以上标黄色框框的部分。
获取环境变量的函数为 getenv():
在 myshell.c 文件中,代码为:
#include<stdio.h>
#include<stdlib.h>
const char* getUsername()
{
const char* name = getenv("USER");
if(name)
{
return name;
}
else
{
return "none";
}
}
const char* getHostname()
{
const char* hostname = getenv("HOSTNAME");
if(hostname)
{
return hostname;
}
else
{
return "none";
}
}
const char* getCwd()
{
const char* cwd = getenv("PWD");
if(cwd)
{
return cwd;
}
else
{
return "none";
}
}
int main()
{
printf("[%s@%s %s]# \n",getUsername(),getHostname(),getCwd());
return 0;
}
运行效果为:
有了命令提示符之后,我们发现程序直接结束,而显示使用过程中,光标应该卡住,需要用户进行输入,接下来我们对代码进行修改,加一个宏定义,和对 main 函数进行修改,修改内容如下:
#define NUM 1024//新增
int main()//添加内容
{
char usercommand[NUM];
printf("[%s@%s %s]# \n",getUsername(),getHostname(),getCwd());
//输入
scanf("%s",usercommand);
return 0;
}
运行效果为:
达到了我们想要的结果,但是在我们使用 shell 的时候我们不难发现,我们的光标是在行末的,此时我们对 main 函数中的代码进行修改:
int main()
{
char usercommand[NUM];
printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());
//输入
scanf("%s",usercommand);
return 0;
}
运行效果为:
此时的光标在等候我们进行输入,我们可以先对输入的内容进行一个回显,看我们输入的内容 main 函数代码为:
int main()
{
char usercommand[NUM];
printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());
//输入
scanf("%s",usercommand);
printf("echo:\n%s\n",usercommand);
return 0;
}
运行效果为:
此时,我们会发现在回显时,仅回显了 ls ,为什莫???
scanf 在读取时,遇到空格会自动停下来!!!
fatges --- 获取命令行
使用 fgets 对 main 函数再次进行修改,修改后 main 函数代码为:
int main()
{
char usercommand[NUM];
printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());
//输入
char* tmp = fgets(usercommand,sizeof(usercommand),stdin);
if(tmp == NULL)
{
return 1;
}
printf("%s\n",usercommand);
return 0;
}
运行效果为:
我们会发现,在回显之后多了一个空行???
在此处,我们可以尝试去掉 printf("%s\n",usercommand); 中的 \n ,去掉后的运行效果为:
我们会发现,此处,仍然另起了新的一行,并没有跟在回显内容之后 !!!
原因,在我们输入 ls -a -l 之后,我们还按下了回车键,也就是说,回显内容中包含 \n 。
如何解决上述问题呢?
由于,我们都是输入内容之后,再按下回车键,所以说 \n 一定位于最后一个位置,我们可以对字符串长度进行减一操作,从而达到去掉多余 \n 的目的。
修改后 main 函数代码为:
#include<string.h>
int main()
{
char usercommand[NUM];
printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());
//输入
char* tmp = fgets(usercommand,sizeof(usercommand),stdin);
if(tmp == NULL)
{
return 1;
}
usercommand[strlen(usercommand) - 1] = '\0';
printf("%s",usercommand);
return 0;
}
usercommand[strlen(usercommand) - 1] = '\0';
在此处不会存在越界问题,在字符串中,至少有一个 \n ,因为即使为空,我们也需要按下回车键,对长度进行减一的操作可以正常进行。
为了后续更加方便,我们现在对 main 函数中的内容进行封装,即增加一个 ---
int getUserCommand(char* command,int num) 函数,修改后代码为:
int getUserCommand(char* command,int num)
{
char usercommand[NUM];
printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());
//输入
char* tmp = fgets(command,num,stdin);
if(tmp == NULL)
{
return 1;
}
command[strlen(command) - 1] = '\0';
return 0;
}
int main()
{
char usercommand[NUM];
getUserCommand(usercommand,sizeof(usercommand));
printf("%s",usercommand);
return 0;
}
运行效果为:
至此,我们完成了,打印提示符并且获取用户字符串获取成功。
2.字符串分割
此时,我们需要进行字符串分割,例如:将 ls -a -l 进行分割为 "ls" "-a" "-l"
分割字符串 --- strtok:
成功返回起始地址,失败返回空。
添加和修改的代码为:
#define SIZE 64
#define SEP " "
int main()
{
char usercommand[NUM];
char* argv[SIZE];
int argc = 0;
getUserCommand(usercommand,sizeof(usercommand));
//分割字符串
argv[argc++] = strtok(usercommand,SEP);
while(argv[argc++] = strtok(NULL,SEP));
//下述for循环用于检测是否按顺序提取到各个内容
for(int i = 0; argv[i];i++)
{
printf("%d:%s\n",i,argv[i]);
}
printf("%s",usercommand);
return 0;
}
运行效果为:
接下来,我们对分割字符串部分进行封装,即增加一个 ---
void commandSplit(char* in,char* out[]) 函数,修改后代码为:
#define Debug 1
void commandSplit(char* in,char* out[])
{
int argc = 0;
out[argc++] = strtok(in,SEP);
while(out[argc++] = strtok(NULL,SEP));
#ifdef Debug
for(int i = 0; out[i];i++)
{
printf("%d:%s\n",i,out[i]);
}
#endif
}
int main()
{
char usercommand[NUM];
char* argv[SIZE];
getUserCommand(usercommand,sizeof(usercommand));
//分割字符串
commandSplit(usercommand,argv);
return 0;
}
运行效果为:
至此,分割字符串成功。
3.执行命令
接下来,需要我们去执行对应的命令。
此时,需要使用程序替换,程序替换函数 --- execvp
程序替换时,不能让本程序去执行对应的命令,若为本程序执行,剩余的代码不会执行完就退出,此时,需要我们去创建子进程去执行对应的命令 --- 创建子进程 fork
子进程被创建之后,去执行对应的命令,此时,父进程需要等待子进程,等待子进程 waitpid
代码为:
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
char usercommand[NUM];
char* argv[SIZE];
getUserCommand(usercommand,sizeof(usercommand));
//分割字符串
commandSplit(usercommand,argv);
//执行对应的命令
pid_t id = fork();
if(id < 0)
{
return 0;
}
else if(id == 0)
{
//子进程 --- 程序替换
execvp(argv[0],argv);
exit(1);
}
else
{
//父进程 --- 等待子进程
pid_t rid = waitpid(id,NULL,0);
if(rid > 0)
{
//等待成功
}
}
return 0;
}
运行效果为:
此时,命令行只跑了一次就结束了!!!
我们只需要将 main 函数中的内容放入死循环,如下图所示:
运行效果为:
接下来,对上述执行命令部分进行修改并且进行封装:
int getUserCommand(char* command,int num)
{
char usercommand[NUM];
printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());
//输入
char* tmp = fgets(command,num,stdin);
if(tmp == NULL)
{
return 1;
}
command[strlen(command) - 1] = '\0';
return strlen(command);
}
int execute(char* argv[])
{
pid_t id = fork();
if(id < 0)
{
return -1;
}
else if(id == 0)
{
//子进程 --- 程序替换
execvp(argv[0],argv);
exit(1);
}
else
{
//父进程 --- 等待子进程
pid_t rid = waitpid(id,NULL,0);
if(rid > 0)
{
//等待成功
}
}
return 0;
}
int main()
{
while(1)
{
char usercommand[NUM];
char* argv[SIZE];
int n = getUserCommand(usercommand,sizeof(usercommand));
if(n <= 0)
{
continue;
}
//分割字符串
commandSplit(usercommand,argv);
//执行对应的命令
execute(argv);
}
return 0;
}
运行效果为:
至此,一个简易的 shell 已经初具雏形。
<1>: 内建命令
接下来,需要我们进行一些问题的解决。
执行 ./myshell 观察现象:
当我们输入,cd .. 和 pwd 之后,我们会发现所属路径没有变化,但现实应该是:
有一批命令,不能让子进程执行,必须让父进程 bash 自己执行!这些命令叫做内建命令。
内建命令就是 bash 自己执行的,类似于自己内部的一个函数!!!
在执行一个命令之前,需要知道其是否为内建命令???
对代码进行修改:
void cd(const char* path)
{
chdir(path);
}
int doBuildin(char *argv[])
{
if(strcmp(argv[0],"cd") == 0)
{
// cd();任何实现 cd 命令??? 后面跟路径
char* path = NULL;
if(argv[1] == NULL)
{
path = ".";
}
else
{
path = argv[1];
cd(path);
}
return 1;
}
return 0;
}
int main()
{
while(1)
{
char usercommand[NUM];
char* argv[SIZE];
int n = getUserCommand(usercommand,sizeof(usercommand));
if(n <= 0)
{
continue;
}
//分割字符串
commandSplit(usercommand,argv);
//检查是否为内建命令
n = doBuildin(argv);
if(n)
{
continue;
}
//执行对应的命令
execute(argv);
}
return 0;
}
运行效果为:
但是此时,命令行提示符的路径没有改变 !
我们需要对环境变量进行修改,或者是获取当前环境变量。 代码为:
void cd(const char* path)
{
chdir(path);
char tmp[1024];
getcwd(tmp,sizeof(tmp));
sprintf(cwd,"PWD=%s",tmp);
putenv(cwd);
}
运行效果为:
此时,与现实相符合。
<2>:export 的实现
实现 export ,代码为:
int doBuildin(char *argv[])
{
if(strcmp(argv[0],"cd") == 0)
{
// cd();任何实现 cd 命令??? 后面跟路径
char* path = NULL;
if(argv[1] == NULL)
{
path = ".";
}
else
{
path = argv[1];
cd(path);
}
return 1;
}
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] == NULL)
{
return 1;
}
strcpy(enval,argv[1]);
putenv(enval);
return 1;
}
return 0;
}
运行效果为:
进阶 ---> 解决 if(argv[1] == NULL){path = ".";} 处的问题,代码添加:
char* homepath()
{
char* home = getenv("HOME");
if(home) return home;
else return(char*)".";
}
int doBuildin(char *argv[])
{
if(strcmp(argv[0],"cd") == 0)
{
// cd();任何实现 cd 命令??? 后面跟路径
char* path = NULL;
if(argv[1] == NULL)
{
path = homepath();
}
else
{
path = argv[1];
cd(path);
}
return 1;
}
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] == NULL)
{
return 1;
}
strcpy(enval,argv[1]);
putenv(enval);
return 1;
}
else if(strcmp(argv[0],"echo") == 0)
{
char *val = argv[1]+1;//$PATH $?
if(strcmp(val,"?") == 0)
{
printf("%d\n",lastcode);//查看退出码
lastcode = 0;
}
else
{
printf("%s\n",getenv(val));//打印环境变量
}
return 1;
}
else
{}
return 0;
}
运行效果为:
<3>: echo 的实现
实现 echo ,代码为:
int doBuildin(char *argv[])
{
if(strcmp(argv[0],"cd") == 0)
{
// cd();任何实现 cd 命令??? 后面跟路径
char* path = NULL;
if(argv[1] == NULL)
{
path = ".";
}
else
{
path = argv[1];
cd(path);
}
return 1;
}
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] == NULL)
{
return 1;
}
strcpy(enval,argv[1]);
putenv(enval);
return 1;
}
else if(strcmp(argv[0],"echo") == 0)
{
char *val = argv[1]+1;//$PATH $?
if(strcmp(val,"?") == 0)
{
printf("%d\n",lastcode);//查看退出码
lastcode = 0;
}
else
{
printf("%s\n",getenv(val));//打印环境变量
}
return 1;
}
return 0;
}
运行效果为:
echo 在打印环境变量时,获取的环境变量可能为空,即输入 echo $myval 输出 一串错误信息提示,正确情况下,如果没有定义,会进行换行操作,为解决该问题,对代码进行修改:
else if(strcmp(argv[0],"echo") == 0)
{
if(argv[1] == NULL)
{
printf("\n");
return 1;
}
if(*(argv[1]) == '$' && strlen(argv[1]) >1)
{
char *val = argv[1]+1;//$PATH $?
if(strcmp(val,"?") == 0)
{
printf("%d\n",lastcode);//查看退出码
lastcode = 0;
}
else
{
const char* enval = getenv(val);
if(enval) printf("%s\n",enval);
else printf("\n");
//printf("%s\n",getenv(val));//打印环境变量
}
return 1;
}
else
{
printf("%s\n",argv[1]);
return 1;
}
}
运行效果为:
4.可执行文件加颜色
在 myshell 中,可执行文件不带颜色:
如果想要带颜色需要我们添加 ls -l --color:
5.完整代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define NUM 1024
#define SIZE 64
#define SEP " "
//#define Debug 1
char cwd[1024];
char enval[1024];//测试用
int lastcode = 0;
char* homepath()
{
char* home = getenv("HOME");
if(home) return home;
else return(char*)".";
}
const char* getUsername()
{
const char* name = getenv("USER");
if(name)
{
return name;
}
else
{
return "none";
}
}
const char* getHostname()
{
const char* hostname = getenv("HOSTNAME");
if(hostname)
{
return hostname;
}
else
{
return "none";
}
}
const char* getCwd()
{
const char* cwd = getenv("PWD");
if(cwd)
{
return cwd;
}
else
{
return "none";
}
}
int getUserCommand(char* command,int num)
{
char usercommand[NUM];
printf("[%s@%s %s]# ",getUsername(),getHostname(),getCwd());
//输入
char* tmp = fgets(command,num,stdin);
if(tmp == NULL)
{
return 1;
}
command[strlen(command) - 1] = '\0';
return strlen(command);
}
void commandSplit(char* in,char* out[])
{
int argc = 0;
out[argc++] = strtok(in,SEP);
while(out[argc++] = strtok(NULL,SEP));
#ifdef Debug
for(int i = 0; out[i];i++)
{
printf("%d:%s\n",i,out[i]);
}
#endif
}
int execute(char* argv[])
{
pid_t id = fork();
if(id < 0)
{
return -1;
}
else if(id == 0)
{
//子进程 --- 程序替换
execvp(argv[0],argv);
exit(1);
}
else
{
//父进程 --- 等待子进程
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{
//等待成功
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
void cd(const char* path)
{
chdir(path);
char tmp[1024];
getcwd(tmp,sizeof(tmp));
sprintf(cwd,"PWD=%s",tmp);
putenv(cwd);
}
// 1 yes 0 no -1 错误
int doBuildin(char *argv[])
{
if(strcmp(argv[0],"cd") == 0)
{
// cd();任何实现 cd 命令??? 后面跟路径
char* path = NULL;
if(argv[1] == NULL)
{
path = homepath();
}
else
{
path = argv[1];
cd(path);
}
return 1;
}
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] == NULL)
{
return 1;
}
strcpy(enval,argv[1]);
putenv(enval);
return 1;
}
else if(strcmp(argv[0],"echo") == 0)
{
if(argv[1] == NULL)
{
printf("\n");
return 1;
}
if(*(argv[1]) == '$' && strlen(argv[1]) >1)
{
char *val = argv[1]+1;//$PATH $?
if(strcmp(val,"?") == 0)
{
printf("%d\n",lastcode);//查看退出码
lastcode = 0;
}
else
{
const char* enval = getenv(val);
if(enval) printf("%s\n",enval);
else printf("\n");
//printf("%s\n",getenv(val));//打印环境变量
}
return 1;
}
else
{
printf("%s\n",argv[1]);
return 1;
}
}
else
{}
return 0;
}
int main()
{
while(1)
{
char usercommand[NUM];
char* argv[SIZE];
int n = getUserCommand(usercommand,sizeof(usercommand));
if(n <= 0)
{
continue;
}
//分割字符串
commandSplit(usercommand,argv);
//检查是否为内建命令
n = doBuildin(argv);
if(n)
{
continue;
}
//执行对应的命令
execute(argv);
}
return 0;
}
make 时,会有一处告警:
解决方法通过去掉 makefile 文件中的 -std=c99 进行抹除: