文章目录
前言
在实现shell的时候我们先创建自己myshell目录,在目录中创建myshell.cc文件,因为shell本来是用c语言写的,但为了方便我们这里使用c和c++混编。
首先我们做一个整体框架:
int main()
{
//shell启动的时候从系统获得环境变量
//我们的环境变量信息应该统一从父shell取
InitEnv();
while(1)
{
//1.打印提示信息
//printf("[%s@%s %s]#", GetUserName(),GetHostName(),GetPwd());
PrintCommandPrompt();
//2.获取命令行提示符
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline,sizeof(commandline)))
{
continue;
}
//3.重定向分析 "ls -a > file.txt" -> "ls -a" "file.txt"
//> >> < 判断重定向的方向
//> 输出 >> 拼接输出 < 输入
RedirCheck(commandline);
//printf("redir = %d filename = %s\n", redir, filename.c_str());
//4.命令行分析
if(!CommandParse(commandline))
{
continue;
}
//PrintArgv();
//5.检测是否是内键命令
if(CheckAndExecBuiltin())
{
continue;
}
//6.执行命令
Execute();
}
//清理工作
clear();
return 0;
}
1.打印命令提示符
首先我们需要给用户显示提示信息,就像我们在使用shell时所看到的提示信息一样,如下:
对它进行分析:
所以我们可以这样定义宏,一个是方便打印的,一个是命令长度:
#define FORMAT "[%s@%s %s]#"
#define COMMAND_SIZE 1024
对于用户名,主机号我们可以通过getenv从环境变量
中得到,但是获取当前路径我们不能使用getenv,因为我们myshell的环境变量使用的是父进程的环境变量,我们在当前使用cd命令切换路径环境变量中的pwd并不会有改变。
所以获取当前路径,我们可以使用getcwd,直接查询操作系统的文件系统,获取当前进程的工作目录的绝对路径
。而不用依赖环境变量。当然我们最好单独设计一个GetPwd把它封装起来,这样也方便把当前路径加入到环境变量中。如下:
//for test
char cwd[1024];
char cwdenv[1024];
//last exit code
int lastcode = 0;
const char* GetPwd()
{
//const char* pwd = getenv("PWD");
const char* pwd = getcwd(cwd,sizeof(cwd));
if(pwd != NULL)
{
snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",pwd);
putenv(cwdenv);
}
return pwd == NULL ? "None" : pwd;
}
const char* GetUserName()
{
const char* name = getenv("USER");
return name == NULL ? "None" : name;
}
const char* GetHostName()
{
const char* hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
const char* GetHome()
{
const char* home = getenv("HOME");
return home == NULL ? "" : home;
}
std::string DirName(const char* pwd)
{
#define SLASH "/"
std::string dir = pwd;
if(dir == SLASH)
{
return SLASH;
}
auto pos = dir.rfind(SLASH);
if(pos == std::string::npos)
{
return "BUG";
}
return dir.substr(pos + 1);
}
void MakeCommandLine(char cmd_prompt[],int size)
{
//snprintf(cmd_prompt,size,"[%s@%s %s]#",GetUserName(),GetHostName(),GetPwd());
//snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());
}
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt,sizeof(prompt));
printf("%s",prompt);
fflush(stdout);
}
2.获取用户输入指令
获取用户输入,因为用户输入的命令行参数是一个字符串,中间含有空格。所以我们不用scanf,cin进行输入。这里我们使用fgets
。
bool GetCommandLine(char* out,int size)
{
char* c = fgets(out, size,stdin);
if(c == nullptr)
{
return false;
}
//去掉\n
c[strlen(out) -1] = 0;
if(strlen(c) == 0)
{
return false;
}
return true;
}
3.重定向分析
在用户输入的指令中可能含有重定向操作,所以我们要提前特殊处理一下字符串,并把它做一个分割。
既然是重定向,也就是我们打开需要重向到的那个文件,所以我们需要获取打开方式和文件名。
重定向有三种:
<:输入重定向(以读的方式打开文件)
>:输出重定向(以写的方式打开文件)
>>:追加重定向(以追加的方式打开文件)
所以我们可以使用宏来标记这些情况。
//3.关于重定向的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUR_REDIR 2
#define APPEND_REDIR 3
int redir = NONE_REDIR;
string filename;
void TrimSpace(char cmd[],int &end)
{
while(isspace(cmd[end]))
{
end++;
}
}
void RedirCheck(char cmd[])
{
redir = NONE_REDIR;
filename.clear();
int start = 0;
int end = strlen(cmd) - 1;
//"ls -a -l > file.txt"
//> >> <
while(end > start)
{
if(cmd[end] == '<')
{
cmd[end++] = 0;
TrimSpace(cmd,end);
redir = INPUT_REDIR;
filename = cmd + end;
break;
}
else if(cmd[end] == '>')
{
//>>
if(cmd[end - 1] == '>')
{
cmd[end-1] = 0;
redir = APPEND_REDIR;
}
//>
else
{
redir = OUTPUR_REDIR;
}
cmd[end++] = 0;
TrimSpace(cmd,end);
filename = cmd + end;
break;
}
else
{
end--;
}
}
}
4.命令行参数表,环境变量表,初始化
在shell中有两张表命令行参数表和环境变量表
,实质都是字符串数组。
- 命令行参数表:用来储存用户输入的命令行参数。
- 环境变量表:用来储存当前进程的属性和状态。
所以我们可以这样做一个全局变量:
//shell自定义的全局变量
//1.命令行参数表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;
//2.环境变量表
#define MAXENVS 100
char* g_env[MAXENVS];
int g_envs = 0;
环境变量表需要我们在程序启动时就将它导入, 当然程序启动后环境变量默认是父进程的,所以我们可以重新开辟空间把原环境变量的数据拷贝过来,然后再把environ更新为新的地址。具体实现请参考下文源码。
void InitEnv()
{
extern char** environ;
memset(g_env,0,sizeof(g_env));
g_envs = 0;
//配置文件
//获取环境变量
for(int i = 0;environ[i];i++)
{
//申请空间
g_env[i] = (char*)malloc(strlen(environ[i]) + 1);
strcpy(g_env[i],environ[i]);
g_envs++;
}
//for test
g_env[g_envs++] = (char*)"MIHAYOU=666";
g_env[g_envs] = NULL;
//导成环境变量
for(int i = 0;g_env[i];i++)
{
putenv(g_env[i]);
}
environ = g_env;
}
5.命令解析
刚才我们获取到了用户的输入得到一个字符串,需要把它一个一个按空格分开,来得到一张命令行参数表。方便后面做进程替换。
//命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char* commandline)
{
#define SEP " "
g_argc = 0;
g_argv[g_argc++] = strtok(commandline,SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr,SEP)));
//会多一个减掉
g_argc--;
return g_argc > 0 ? true : false;
}
6.命令执行
6.1.创建子进程
shell执行命令的实质就是进程替换
,我们在做进程替换的时候不想结束父进程,那么需要我们创建一个新的子进程,让子进程来做替换。
6.2 处理内建命令
有一些命令比如cd,是一个内建命令,子进程是无法完成的,需要系统来执行。我们可以使用chdir来完成,chdir函数声明如下:
bool Cd()
{
if(g_argc == 1)
{
string home = GetHome();
if(home.empty())
{
return true;
}
chdir(home.c_str());
}
else
{
string where = g_argv[1];
chdir(where.c_str());
}
return true;
}
它的作用是进入某个目录,需要传一个目录的路径。
这里也支持echo的一些操作
void Echo()
{
if(g_argc == 2)
{
//echo "hello"
//echo $?
//echo $PATH
string opt = g_argv[1];
if(opt == "$?")
{
cout << lastcode << endl;
}
else if(opt[0] == '$')
{
string env_name = opt.substr(1);
const char* env_value = getenv(env_name.c_str());
if(env_value)
{
cout << env_value << endl;
}
}
else
{
cout << opt << endl;
}
}
}
bool CheckAndExecBuiltin()
{
string cmd = g_argv[0];
if(cmd == "cd")
{
Cd();
return true;
}
else if(cmd == "echo")
{
Echo();
return true;
}
return false;
}
6.3 文件重定向
- pcd文件数组:储存了这个pcb打开的所有文件信息地址。
- 文件描述符(
记fd
):pcb的文件数组中的一个下标,0下标的文件为标准输入流,1下标的文件为标准输出流,2下标的文件为标准错误流,这三个都是系统默认打开的文件。 - 重定向:系统在对文件进行操作时只认fd,
所以重定向的实质就是一个fd位置的信息被其他fd的信息覆盖。
" >,>>,指的都是从原来的标准输出(fd=1)重定向到某个文件,< 从原来的标准输入(fd=0)重定向到某个文件。所以这里我们只需要打开新的文件并获取到它的fd,然后使用dup2把新文件的地址信息覆盖到fd=1的文件上就行。然后关闭新文件的fd。"
int Execute()
{
pid_t id = fork();
if(id == 0)
{
int fd = 0;
//子进程检测重定向
if(redir == INPUT_REDIR)
{
fd = open(filename.c_str(), O_RDONLY);
if(fd < 0)
{
exit(1);
}
dup2(fd,0);
close(fd);
}
else if(redir == OUTPUR_REDIR)
{
fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC,0666);
if(fd < 0)
{
exit(2);
}
dup2(fd,1);
close(fd);
}
else if(redir == APPEND_REDIR)
{
fd = open(filename.c_str(), O_CREAT | O_WRONLY |O_APPEND,0666);
if(fd < 0)
{
exit(3);
}
dup2(fd,1);
close(fd);
}
else
{
}
//进程替换不会改变重定向的内容
//子进程执行
execvp(g_argv[0],g_argv);
//程序替换后面的程序就不执行了
exit(1);
}
//父进程等待
int status = 0;
pid_t rid = waitpid(id,&status,0);
////取消报警
//(void)rid;
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
return 0;
}
7.源码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]#"
//shell自定义的全局变量
//1.命令行参数表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;
//2.环境变量表
#define MAXENVS 100
char* g_env[MAXENVS];
int g_envs = 0;
//for test
char cwd[1024];
char cwdenv[1024];
//last exit code
int lastcode = 0;
//3.关于重定向的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUR_REDIR 2
#define APPEND_REDIR 3
int redir = NONE_REDIR;
string filename;
const char* GetUserName()
{
const char* name = getenv("USER");
return name == NULL ? "None" : name;
}
const char* GetHostName()
{
const char* hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
const char* GetPwd()
{
//const char* pwd = getenv("PWD");
const char* pwd = getcwd(cwd,sizeof(cwd));
if(pwd != NULL)
{
snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",pwd);
putenv(cwdenv);
}
return pwd == NULL ? "None" : pwd;
}
const char* GetHome()
{
const char* home = getenv("HOME");
return home == NULL ? "" : home;
}
std::string DirName(const char* pwd)
{
#define SLASH "/"
std::string dir = pwd;
if(dir == SLASH)
{
return SLASH;
}
auto pos = dir.rfind(SLASH);
if(pos == std::string::npos)
{
return "BUG";
}
return dir.substr(pos + 1);
}
void MakeCommandLine(char cmd_prompt[],int size)
{
//snprintf(cmd_prompt,size,"[%s@%s %s]#",GetUserName(),GetHostName(),GetPwd());
//snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());
}
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt,sizeof(prompt));
printf("%s",prompt);
fflush(stdout);
}
bool GetCommandLine(char* out,int size)
{
char* c = fgets(out, size,stdin);
if(c == nullptr)
{
return false;
}
//去掉\n
c[strlen(out) -1] = 0;
if(strlen(c) == 0)
{
return false;
}
return true;
}
//命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char* commandline)
{
#define SEP " "
g_argc = 0;
g_argv[g_argc++] = strtok(commandline,SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr,SEP)));
//会多一个减掉
g_argc--;
return g_argc > 0 ? true : false;
}
void InitEnv()
{
extern char** environ;
memset(g_env,0,sizeof(g_env));
g_envs = 0;
//配置文件
//获取环境变量
for(int i = 0;environ[i];i++)
{
//申请空间
g_env[i] = (char*)malloc(strlen(environ[i]) + 1);
strcpy(g_env[i],environ[i]);
g_envs++;
}
//for test
g_env[g_envs++] = (char*)"MIHAYOU=666";
g_env[g_envs] = NULL;
//导成环境变量
for(int i = 0;g_env[i];i++)
{
putenv(g_env[i]);
}
environ = g_env;
}
bool Cd()
{
if(g_argc == 1)
{
string home = GetHome();
if(home.empty())
{
return true;
}
chdir(home.c_str());
}
else
{
string where = g_argv[1];
chdir(where.c_str());
}
return true;
}
void Echo()
{
if(g_argc == 2)
{
//echo "hello"
//echo $?
//echo $PATH
string opt = g_argv[1];
if(opt == "$?")
{
cout << lastcode << endl;
}
else if(opt[0] == '$')
{
string env_name = opt.substr(1);
const char* env_value = getenv(env_name.c_str());
if(env_value)
{
cout << env_value << endl;
}
}
else
{
cout << opt << endl;
}
}
}
bool CheckAndExecBuiltin()
{
string cmd = g_argv[0];
if(cmd == "cd")
{
Cd();
return true;
}
else if(cmd == "echo")
{
Echo();
return true;
}
return false;
}
void PrintArgv()
{
for(int i = 0;g_argv[i];i++)
{
printf("argv[%d]->%s\n", i,g_argv[i]);
}
printf("size = %d\n", g_argc);
}
void clear()
{
for(int i = 0;g_env[i];i++)
{
free(g_env[i]);
g_env[i] = NULL;
}
}
int Execute()
{
pid_t id = fork();
if(id == 0)
{
int fd = 0;
//子进程检测重定向
if(redir == INPUT_REDIR)
{
fd = open(filename.c_str(), O_RDONLY);
if(fd < 0)
{
exit(1);
}
dup2(fd,0);
close(fd);
}
else if(redir == OUTPUR_REDIR)
{
fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC,0666);
if(fd < 0)
{
exit(2);
}
dup2(fd,1);
close(fd);
}
else if(redir == APPEND_REDIR)
{
fd = open(filename.c_str(), O_CREAT | O_WRONLY |O_APPEND,0666);
if(fd < 0)
{
exit(3);
}
dup2(fd,1);
close(fd);
}
else
{
}
//进程替换不会改变重定向的内容
//子进程执行
execvp(g_argv[0],g_argv);
//程序替换后面的程序就不执行了
exit(1);
}
//父进程等待
int status = 0;
pid_t rid = waitpid(id,&status,0);
////取消报警
//(void)rid;
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
return 0;
}
void TrimSpace(char cmd[],int &end)
{
while(isspace(cmd[end]))
{
end++;
}
}
void RedirCheck(char cmd[])
{
redir = NONE_REDIR;
filename.clear();
int start = 0;
int end = strlen(cmd) - 1;
//"ls -a -l > file.txt"
//> >> <
while(end > start)
{
if(cmd[end] == '<')
{
cmd[end++] = 0;
TrimSpace(cmd,end);
redir = INPUT_REDIR;
filename = cmd + end;
break;
}
else if(cmd[end] == '>')
{
//>>
if(cmd[end - 1] == '>')
{
cmd[end-1] = 0;
redir = APPEND_REDIR;
}
//>
else
{
redir = OUTPUR_REDIR;
}
cmd[end++] = 0;
TrimSpace(cmd,end);
filename = cmd + end;
break;
}
else
{
end--;
}
}
}
int main()
{
//shell启动的时候从系统获得环境变量
//我们的环境变量信息应该统一从父shell取
InitEnv();
while(1)
{
//1.打印提示信息
//printf("[%s@%s %s]#", GetUserName(),GetHostName(),GetPwd());
PrintCommandPrompt();
//2.获取命令行提示符
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline,sizeof(commandline)))
{
continue;
}
//3.重定向分析 "ls -a > file.txt" -> "ls -a" "file.txt"
//> >> < 判断重定向的方向
//> 输出 >> 拼接输出 < 输入
RedirCheck(commandline);
//printf("redir = %d filename = %s\n", redir, filename.c_str());
//4.命令行分析
if(!CommandParse(commandline))
{
continue;
}
//PrintArgv();
//5.检测是否是内键命令
if(CheckAndExecBuiltin())
{
continue;
}
//6.执行命令
Execute();
}
//清理工作
clear();
return 0;
}