使用C++编写一个Shell

100个C++实践项目系列

你可能在阅读代码或教程时发现一些愚蠢的错误。请不要担心,这些内容是作者在学习C++过程中编写的,你可以发邮件或者在Discussions中一起讨论,来纠正这些愚蠢的错误。

我并不推荐直接阅读源码,动手远比其他来的更好 - Source


如果你阅读了《用C编写一个简单的Shell应用》一文,你会发现这个程序实际上和c版本的功能是一样的。只不过这里使用c++ 11重新实现。

推荐先阅读c版本的文章,此文会省略很多重复内容。

我们将所有的业务逻辑代码都放到名为msh的namespace中,并且定义头文件如下:

#ifndef MSH_H_
#define MSH_H_


#include "CommandManager.h"
#include <iostream>
#include <vector>

namespace msh {

class Msh {
private:
	msh::CommandManager* commandManager;
	std::string readLine();
	std::vector<std::string> parseLine(std::string& line);
	int launch(std::vector<std::string>& args);
	int execute(std::vector<std::string>& args);

public:
	Msh();
	virtual ~Msh();
	void loop();
};

} /* namespace msh */

#endif /* MSH_H_ */

由于在入口函数中,我们只需要调用loop即可,所以注意只有loop定义为public权限。main.cpp内容如下:

#include "msh/Msh.h"

int main() {
	msh::Msh sh;
	sh.loop();
	return EXIT_SUCCESS;
}

内置命令

我们通过定义一个CommandManager类来管理所有的内置命令,同时所有的内置命令实现都继承基类Command

#ifndef COMMANDMANAGER_H_
#define COMMANDMANAGER_H_

#include <unordered_map>
#include <vector>

#include "Command.h"

namespace msh {

class CommandManager{
public:
	CommandManager();
	virtual ~CommandManager();
	void AddCommand(msh::Command* cmd);
	bool Check(std::vector<std::string>& args);
	int Run(std::vector<std::string>& args);

private:
	std::unordered_map<std::string, msh::Command*>* cmds;

};


} /* namespace msh */

#endif /* COMMANDMANAGER_H_ */

由于 Command 的作用仅仅是定义所有内置命令的行为接口,我们将它定义为抽象类。两个纯虚函数一个为该命令的名称,另外一个为执行动作(命令实现均从run接口调用)。

#ifndef COMMAND_H_
#define COMMAND_H_

#include <iostream>

namespace msh {

class Command {
public:
	virtual int run(std::vector<std::string> &args) = 0;
	virtual std::string name() = 0;
};

} /* namespace msh */

#endif /* COMMAND_H_ */

CommandManager

先来看一下我们如何维护所有的内置命令,通过一个hash map结构来进行维护,命令名称作为key , 而实例化的指针对象则作为value存储在内存中。

当shell启动后,我们应该先想管理器中添加自定义命令。

其构造函数需要对hashmap进行初始化。

CommandManager::CommandManager() {
	this->cmds = new std::unordered_map<std::string, msh::Command*>();
}

添加新命令

void CommandManager::AddCommand(msh::Command *cmd) {
	std::string name = cmd->name();
	std::pair<std::string, msh::Command*> c(name, cmd);
	this->cmds->insert(c);
}

注意:此处逻辑最好对key是否重复进行判定

下面是确认当前命令管理器中是否存在对应的命令,run函数则执行运行。

bool CommandManager::Check(std::vector<std::string> &args) {
	std::string cmd = args[0];
	if (this->cmds->find(cmd) == this->cmds->end()) {
		return false;
	}
	return MSH_BUILTIN_CMD;
}

int CommandManager::Run(std::vector<std::string> &args) {
	if (Check(args) == MSH_BUILTIN_CMD) {
		std::string c = args[0];
		std::unordered_map<std::string, msh::Command*>::const_iterator cmd = this->cmds->find(c);
		return cmd->second->run(args);
	}
	return -1;
}

核心逻辑

核心逻辑和c版本非常相似,你可以从中看到很多相同的api调用。

#include <iostream>
#include <unistd.h>

#include "Msh.h"
#include "Config.h"
#include "commands/Help.h"
#include "commands/Exit.h"

namespace msh {

Msh::Msh() {
	this->commandManager = new msh::CommandManager();
	this->commandManager->AddCommand(new Help());
	this->commandManager->AddCommand(new Exit());
}

Msh::~Msh() {
	delete this->commandManager;
}

void Msh::loop() {
	int status;
	do {
		std::cout << "> ";
		std::string line = readLine();
		std::vector<std::string> args = parseLine(line);
		status = execute(args);
	} while (status);
}

std::string Msh::readLine() {
	std::string str;
	std::getline(std::cin, str);
	return str;
}

std::vector<std::string> Msh::parseLine(std::string &line) {
	std::vector<std::string> res;
	if (line == "") {
		return res;
	}
	char *strs = new char[line.length() + 1];
	std::strcpy(strs, line.c_str());
	char *p = std::strtok(strs, MSH_TOK_DELIM);
	while (p) {
		std::string s = p;
		res.push_back(s);
		p = std::strtok(NULL, MSH_TOK_DELIM);
	}
	return res;
}

int Msh::launch(std::vector<std::string> &args) {
	pid_t pid, wpid;
	int status;
	pid = fork();
	if (pid == 0) {
		char *argv[args.size() + 1];
		for (int i = 0; i < args.size(); i++) {
			argv[i] = const_cast<char*>(args[i].c_str());
		}
		argv[args.size()] = NULL;
		if (execvp(argv[0], argv) == -1) {
			std::cerr << "msh" << std::endl;
		}
		exit(EXIT_FAILURE);
	} else if (pid < 0) {
		std::cerr << "msh" << std::endl;
	} else {
		do {
			wpid = waitpid(pid, &status, WUNTRACED);
		} while (!WIFEXITED(status) && !WIFSIGNALED(status));
	}
	return 1;
}

int Msh::execute(std::vector<std::string> &args) {
	if (args.empty()) {
		return MSH_DOT_HAVE_ARGS;
	}
	if (this->commandManager->Check(args) == MSH_BUILTIN_CMD) {
		return this->commandManager->Run(args);
	}
	return launch(args);
}

} /* namespace msh */

宏定义

将一些常用量定义为宏,然后放到一个独立文件中。

#ifndef CONFIG_H_
#define CONFIG_H_

#define MSH_TOK_DELIM " \t\r\n\a"
#define MSH_DOT_HAVE_ARGS 1
#define MSH_BUILTIN_CMD true

#endif /* CONFIG_H_ */

最后

你可以在源码的commands目录中看到2个内置命令的实现,分别是helpexit。你也可以创建一些新的命令,然后分享到Discussions中。