目录
1、目标
实现一个Linux的myshell,有以下基本的功能。
- 显示命令提示符
- 获取用户输入的命令
- 解析命令
- 处理内建命令
- 处理外部命令
myshell有一张命令行参数表和环境变量表(继承bash的,其实应该要从配置文件中获取,但比较麻烦)。
2、显示命令提示符
这里就要了解一下:getenv(),getcwd(),putenv()了。
2.1 getenv
获取环境变量。
char *getenv(const char *name);
根据环境变量名(如 "PATH")返回其对应的值(字符串)。
如果变量不存在,返回 NULL。
2.2 getcwd
获取当前工作路径。
char *getcwd(char *buf, size_t size);
将当前工作目录的绝对路径写入 buf,并返回 buf。
需确保 buf 足够大(否则返回 NULL,errno = ERANGE)。
注意:
进程的环境变量表中的PWD,是根据进程的CWD(进程当前的工作路径,在/proc/pid/下可以看到),进行更新的。
2.3 putenv
设置环境变量。
int putenv(char *string);
设置环境变量,格式为 "KEY=VALUE" 的字符串。如果已存在相同的KEY,就覆盖VALUE。
成功返回 0,失败返回非零。
注意:
putenv(),传的是指针,要放在环境变量表里,生命周期要和程序一样长,所以传全局变量。
改变这个全局变量,环境变量表中也会改变(当时不太理解,中坑了。)
const char* getUserName()
{
const char* USER = getenv("USER");
return USER == NULL?"None":USER;
}
const char* getHostName()
{
const char* HOSTNAME = getenv("HOSTNAME");
return HOSTNAME == NULL?"None":HOSTNAME;
}
std::string getCwd()
{
char cwdenv[1024];
char* cwd = getcwd(cwdenv,sizeof(cwdenv));
return cwd == NULL? "None":cwdenv; // 因为返回的是局部变量,所以使用string,进行拷贝
}
char pwd[1024];
void cmdPrompt()
{
std::string path = getCwd();
// 更新环境变量PWD,不能putenv局部变量。
snprintf(pwd,sizeof(pwd),"PWD=%s",path.c_str());
putenv(pwd);
if(path.size() != 1) // 不是/根目录
{
int index = path.rfind('/');
path = path.substr(index+1);
}
std::cout<<"["<<getUserName()<<"@"<<getHostName()<<" "<<path<<"]"<<"$$";
}
3、获取用户输入的命令
bool getCmd(char cmd[],int size)
{
char* s = fgets(cmd,size,stdin);
if(s != NULL)
{
int len = strlen(cmd);
cmd[len-1] = '\0'; // fgets会读取'\n',需要处理
}
return s != NULL;
}
获取成功,返回true,获取失败,返回false。
4、解析命令
bool parseCmd(char cmd[],char* argv[])
{
const char* delim = " ";
char* token = strtok(cmd,delim);
int i = 0;
while(token != NULL)
{
argv[i] = token;
token = strtok(NULL,delim);
++i;
}
argv[i] = NULL; // 进程替换,要求以NULL结尾
return i != 0;
}
char *strtok(char *str, const char *delim);
str:要分割的字符串。第一次调用时传入原始字符串,后续调用传入 NULL。
delim:包含所有可能分隔符的字符串。
返回分割出的子字符串的指针。如果没有更多子字符串,则返回 NULL。
strtok 会在找到的分隔符位置插入 '\0' 字符,因此会修改原始字符串。
5、处理内建命令
内建命令,需要父进程自己执行(如:cd,需要改变自己的路径),或父进程自己执行,效率更高(如:echo)。
std::string getCwd()
{
char cwdenv[1024];
char* cwd = getcwd(cwdenv,sizeof(cwdenv));
return cwd == NULL? "None":cwdenv; // 因为返回的是局部变量,所以使用string,进行拷贝
}
const char* getHome()
{
const char* HOME = getenv("HOME");
return HOME == NULL?"None":HOME;
}
const char* getOldPwd()
{
const char* OLDPWD = getenv("OLDPWD");
return OLDPWD == NULL?"None":OLDPWD;
}
char oldPwd[1024];
void cd(char* argv[])
{
std::string cwdenv = getCwd();
if(argv[1] == NULL || strcmp(argv[1],"~") == 0)
{
snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
putenv(oldPwd); // 更新OLDPWD
chdir(getHome());
}
else if(strcmp(argv[1],"-") == 0)
{
chdir(getOldPwd());
snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
putenv(oldPwd);
}
else
{
snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
putenv(oldPwd); // 更新OLDPWD
chdir(argv[1]);
}
}
int lastExitno = 0;
void echo(char* argv[])
{
if(argv[1] == NULL)
return;
std::string cmd = argv[1];
// echo $?
// echo $PATH
// echo "hello world"
if(cmd[0] == '$')
{
if(cmd.substr(1) == "?")
{
std::cout<<lastExitno<<std::endl;
lastExitno = 0;
}
else
{
if(getenv(cmd.substr(1).c_str()))
std::cout<<getenv(cmd.substr(1).c_str())<<std::endl;
}
}
else
{
std::cout<<cmd<<std::endl;
}
}
bool executeBuiltIn(char* argv[])
{
std::string cmd = argv[0];
if(cmd == "cd")
{
cd(argv);
return true;
}
else if(cmd == "echo")
{
echo(argv);
return true;
}
// ...
else
{
}
return false;
}
注意:
else if(strcmp(argv[1],"-") == 0)
{
chdir(getOldPwd());
snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
putenv(oldPwd);
}
如果之前putenv(oldPwd),传的是指针,
若先snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
那么,环境变量表中的OLDPWD就已经是CWD了,那么chdir(getOldPwd());就出错了。
6、处理外部命令
外部命令,防止父进程挂了,所以创建子进程,进行程序替换(可执行任意程序)。
int executeExternal(char* argv[])
{
pid_t id = fork();
if(id == -1)
return 1;
if(id == 0)
{
execvp(argv[0],argv);
exit(2);
}
int status = 0;
pid_t wid = waitpid(id,&status,0);
if(wid == id && WIFEXITED(status)) // 子进程退出,且是正常退出
lastExitno = WEXITSTATUS(status);
return 0;
}
7、完整代码
7.1 myshell.cpp
#include <iostream>
#include <stdio.h>
#include <string>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
const char* getUserName()
{
const char* USER = getenv("USER");
return USER == NULL?"None":USER;
}
const char* getHostName()
{
const char* HOSTNAME = getenv("HOSTNAME");
return HOSTNAME == NULL?"None":HOSTNAME;
}
const char* getHome()
{
const char* HOME = getenv("HOME");
return HOME == NULL?"None":HOME;
}
std::string getCwd()
{
char cwdenv[1024];
char* cwd = getcwd(cwdenv,sizeof(cwdenv));
return cwd == NULL? "None":cwdenv; // 因为返回的是局部变量,所以使用string,进行拷贝
}
const char* getOldPwd()
{
const char* OLDPWD = getenv("OLDPWD");
return OLDPWD == NULL?"None":OLDPWD;
}
char pwd[1024];
void cmdPrompt()
{
std::string path = getCwd();
// 更新环境变量PWD,不能putenv局部变量。
snprintf(pwd,sizeof(pwd),"PWD=%s",path.c_str());
putenv(pwd);
if(path.size() != 1) // 不是/根目录
{
int index = path.rfind('/');
path = path.substr(index+1);
}
std::cout<<"["<<getUserName()<<"@"<<getHostName()<<" "<<path<<"]"<<"$$";
}
bool getCmd(char cmd[],int size)
{
char* s = fgets(cmd,size,stdin);
if(s != NULL)
{
int len = strlen(cmd);
cmd[len-1] = '\0'; // fgets会读取'\n',需要处理
}
return s != NULL;
}
bool parseCmd(char cmd[],char* argv[])
{
const char* delim = " ";
char* token = strtok(cmd,delim);
int i = 0;
while(token != NULL)
{
argv[i] = token;
token = strtok(NULL,delim);
++i;
}
argv[i] = NULL; // 进程替换,要求以NULL结尾
return i != 0;
}
char oldPwd[1024];
void cd(char* argv[])
{
std::string cwdenv = getCwd();
if(argv[1] == NULL || strcmp(argv[1],"~") == 0)
{
snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
putenv(oldPwd); // 更新OLDPWD
chdir(getHome());
}
else if(strcmp(argv[1],"-") == 0)
{
chdir(getOldPwd());
snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
putenv(oldPwd);
}
else
{
snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
putenv(oldPwd); // 更新OLDPWD
chdir(argv[1]);
}
}
int lastExitno = 0;
void echo(char* argv[])
{
if(argv[1] == NULL)
return;
std::string cmd = argv[1];
// echo $?
// echo $PATH
// echo "hello world"
if(cmd[0] == '$')
{
if(cmd.substr(1) == "?")
{
std::cout<<lastExitno<<std::endl;
lastExitno = 0;
}
else
{
if(getenv(cmd.substr(1).c_str()))
std::cout<<getenv(cmd.substr(1).c_str())<<std::endl;
}
}
else
{
std::cout<<cmd<<std::endl;
}
}
bool executeBuiltIn(char* argv[])
{
std::string cmd = argv[0];
if(cmd == "cd")
{
cd(argv);
return true;
}
else if(cmd == "echo")
{
echo(argv);
return true;
}
// ...
else
{
}
return false;
}
int executeExternal(char* argv[])
{
pid_t id = fork();
if(id == -1)
return 1;
if(id == 0)
{
execvp(argv[0],argv);
exit(2);
}
int status = 0;
pid_t wid = waitpid(id,&status,0);
if(wid == id && WIFEXITED(status)) // 子进程退出,且是正常退出
lastExitno = WEXITSTATUS(status);
return 0;
}
int main()
{
while(true)
{
// 命令提示符
cmdPrompt();
// 获取用户输入命令
char cmd[1024] = {0};
if(!getCmd(cmd,sizeof(cmd)))
continue;
// 解析命令
char* argv[1024] = {0};
if(!parseCmd(cmd,argv))
continue;
// 内建命令
if(executeBuiltIn(argv))
continue;
// 执行命令
executeExternal(argv);
}
return 0;
}
7.2 Makefile
TARGET := myshell
SRCS := myshell.cpp
SUFFIX := .cpp
OBJS := $(SRCS:$(SUFFIX)=.o)
CC := g++
$(TARGET): $(OBJS)
$(CC) -o $@ $^
%.o: %$(SUFFIX)
$(CC) -c $<
.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET)