请看上面的shell,其本质就是一个字符串,我们知道bash本质上就是一个进程,只不过命令行就是一个输出的字符串,
我们输入的命令"ls -a -l"实际上是我们在输入行输入的字符串,所以,如果我们想要做一个简易的shell的时候,首先要输出"[ghs@hecs-406886 myshell]"这样的字符串,然后再接收并解析写入的命令字符串,然后一条命令就可以执行了。在"\[ghs@hecs-406886 myshell\]"这样一行字符串中,ghs表示用户名,hecs-406886表示当前主机的主机名,myshell表示当前所处的路径,[ @ ]$为提示符。
所以,我们也需要构建一个类似的命令行,首先,创建一个myshell.c,
在main函数中,第一步我们需要自己输出一个命令行:命令行包括用户名、主机名、当前路径, 这些内容可以从环境变量中获取,
可以通过getenv函数获取环境变量内容,我们自定义三个函数来获取上面三个环境变量:
const char* getusername()
{
const char* name = getenv("USER");
if(name == NULL) return "None";
return name;
}
const char* gethostname()
{
const char* hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
return hostname;
}
const char* getcwd()
{
const char* cwd=getenv("PWD");
if(cwd == NULL)return "None";
return cwd;
}
先介绍一个函数snprintf:把指定参数按照特定格式写到指定长度的内存里,
然后综合上面三个函数,封装出MakeCommandLineAndPrint()函数,制作并打印命令行:
void MakeCommandLineAndPrint()
{
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);
}
第二步就是获取用户命令字符串,输入的指令("ls -l -a")站在开发者的角度本质是一个字符串,我们想一下,可以用scanf获取指令吗?不可以!因为指令选项个数不定。其实,我们想按行来获取字符串,在C语言中,我们可以使用fgets函数,它可以按行从特定的文件流中获取指定内容,获取内容指向由指针s指向的缓冲区,
char *fgets(char *s, int size, FILE *stream);
定义usercommand数组用于存储字符串,然后封装GetUserCommand()函数用于获取用户命令字符串,
int GetUserCommand(char command[],size_t n)
{
char* s = fgets(command, n ,stdin);
if(s == NULL) return -1;
command[strlen(command)-1] = ZERO;
return strlen(command);
}
第三步就是命令行字符串分割,使用' '(空格)作为分隔符,我们想要得到一个数组,这个数组叫做char *argv[],对于这样"ls -l -a"一个字符串,定义指针指向第一个'l',再定义另一个指针也指向第一个'l',往后遍历,遇到第一个空格把其置为'\0',把指向第一个'l'的指针放到agrv数组的第一个位置,然后指针往后走,指向'-',再定义一个指针向后走,遇到第一个空格把其置为'\0',把指向第一个'-'的指针放到agrv数组的第二个位置,依次类推。为了实现这个功能,我们使用strtok函数:
#define NUM 32
char* gArgv[NUM];
void SplitCommand(char command[],size_t n)
{
gArgv[0] = strtok(command,SEP);
int index = 1;
while((gArgv[index++] = strtok(NULL,SEP)));//故意写成=,表示先赋值再判断,分割之后,strtok
//会返回NULL,刚好让gArgv最后一个元素是NULL,
//并且while判断结束
}
下一步就要执行命令,我们需要创建子进程去执行,在子进程中我们要选择使用哪一个程序替换函数,第一,由于上面分割出来的命令是不带路径的,我要执行的命令全是系统的默认路径,应该让它在环境变量中找,所以选择的函数一定带p,第二,由于我提供的是一个命令数组argv,所以一定要带v,所以,选择execvp函数,这个函数在失败时返回-1,同时会设置错误码errno。
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
//child
execvp(gArgv[0],gArgv);
exit(errno);
}
但是,上面这个代码只能执行一次,为了能一直执行下去,我们创建一个循环:
int main()
{
int quit = 0;
while(!quit)
{
//1.我们需要自己输出一个命令行
MakeCommandLineAndPrint();
//2.获取用户命令字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand,sizeof(usercommand));
if(n <= 0) return 1;
//3.命令行字符串分割
SplitCommand(usercommand,sizeof(usercommand));
//n.执行命令
ExecuteCommand();
}
return 0;
}
上面我们完成一个粗犷的shell,但是有一点:
我们无法使用cd ..完成路径回退,这是为什么呢?因为我们上面的程序是在子进程中进行的,cd ..是在子进程中进行的,是把子进程的路径进行了回退,但是和父进程无关,父进程没有回退,cd这样的命令应该让父进程进程回退,而不应该让子进程回退,因此,需要对这些内建命令进行单独处理,在执行命令之前,要检查命令是否是内建命令,
//3.命令行字符串分割
SplitCommand(usercommand,sizeof(usercommand));
//4.检查命令是否是内建命令
n = CheckBuilding();
if(n) continue;
void Cd()
{
const char* path = gArgv[1];
if(path == NULL) path = GetHome();
//path一定存在
chdir(path);
}
上面代码的意思是,如果"cd",那么回退到用户家目录,否则把命令行里的路径更改为当前路径。
int CheckBuilding()
{
int yes = 0;
const char* enter_cmd = gArgv[0];
if(strcmp("cd",enter_cmd) == 0)
{
yes = 1;
Cd();
}
return yes;
}
当时,运行上面代码后,发现命令行的当前路径提示一直不变,只有pwd里的路径才改变,导致这样的原因是没有对环境变量进行更新,需要导入环境变量,将当前的路径放到temp中,然后将cwd导入环境变量中。
char cwd[SIZE*2];
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);
putenv(cwd);
}
执行结果虽然正确了,但是我们只想让最后一个路径显示出来(和正常的shell一样),因此,我定义了一个宏函数:
#define SkipPath(p) do{ p += strlen(p)-1; while(*p != '/') p--; }while(0)
这个宏将穿进去的路径字符串指向最后一个/,
最后的效果如上图。
但是还有一个问题,当我们回退到根目录后,命令行不显示路径了,需要做一下特殊处理:
此外,当进程退出时,我们也想用echo看一下退出码,由于echo也是一种内建命令,因此也需要在第四步特殊判断一下:
int CheckBuilding()
{
int yes = 0;
const char* enter_cmd = gArgv[0];
if(strcmp("cd",enter_cmd) == 0)
{
yes = 1;
Cd();
}
else if(strcmp(enter_cmd,"echo") == 0 && strcmp("$?",gArgv[1]) == 0)
{
yes = 1;
printf("%d\n",lastcode);
lastcode = 0;
}
return yes;
}
至此,简易的shell完成。
下面附上完整代码:
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
#define ZERO '\0'
#define SIZE 512
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += strlen(p)-1; while(*p != '/') p--; }while(0)
char cwd[SIZE*2];
char* gArgv[NUM];
int lastcode = 0;
const char* GetHome()
{
const char* home = getenv("HOME");
if(home == NULL) return "/";
return home;
}
const char* GetUserName()
{
const char* name = getenv("USER");
if(name == NULL) return "None";
return name;
}
const char* GetHostName()
{
const char* hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
return hostname;
}
//临时
const char* GetCwd()
{
const char* cwd=getenv("PWD");
if(cwd == NULL)return "None";
return cwd;
}
void MakeCommandLineAndPrint()
{
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 GetUserCommand(char command[],size_t n)
{
char* s = fgets(command, n ,stdin);
if(s == NULL) return -1;
command[strlen(command)-1] = ZERO;
return strlen(command);
}
void SplitCommand(char command[],size_t n)
{
gArgv[0] = strtok(command,SEP);
int index = 1;
while((gArgv[index++] = strtok(NULL,SEP)));//故意写成=,表示先赋值再判断,分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL,并且while判断结束
}
void Die()
{
exit(-1);
}
void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
//child
execvp(gArgv[0],gArgv);
exit(errno);
}
else
{
//father
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode);
}
}
}
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);
putenv(cwd);
}
int CheckBuilding()
{
int yes = 0;
const char* enter_cmd = gArgv[0];
if(strcmp("cd",enter_cmd) == 0)
{
yes = 1;
Cd();
}
else if(strcmp(enter_cmd,"echo") == 0 && strcmp("$?",gArgv[1]) == 0)
{
yes = 1;
printf("%d\n",lastcode);
lastcode = 0;
}
return yes;
}
int main()
{
int quit = 0;
while(!quit)
{
//1.我们需要自己输出一个命令行
MakeCommandLineAndPrint();
//2.获取用户命令字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand,sizeof(usercommand));
if(n <= 0) return 1;
//3.命令行字符串分割
SplitCommand(usercommand,sizeof(usercommand));
//4.检查命令是否是内建命令
n = CheckBuilding();
if(n) continue;
//5.执行命令
ExecuteCommand();
}
return 0;
}