【Linux】Linux 操作系统 - 21 , 手把手带你写一个 shell命令行解释器 ~ 深入理解 shell 原理 !!!!!!!!


前言

我们平常一直在使用命令行写 Linux 相关命令 , 但是这个命令行的背后的本质真的了解吗 ? 本文将会带你深入了解 shell 到底做了什么 ??? 重点是了解其原理 , 真正意义上的了解 !


一、前提知识

什么是 shell ?

  笔者之前文章详细介绍过 shell 原理概念 , 详细请看 shell 原理初步理解 !

  这里在回顾一下 !

在这里插入图片描述

  • Linux 操作系统中 shell 为 : bash !

  所以 , 到目前为止我们 只知道 shell 可以处理命令 , 保护操作系统 , 但是底层到底是怎样 , 请继续阅读 !!


什么是命令行 ?


  这个概念还是要了解一下的 , 我们用的命令行到底是什么 ???

在这里插入图片描述
看似我们在写命令 , 但是对于 shell 来说真的是吗 ??

  我们写的所有命令 , 选项 , 对 shell 来说就是一个字符串 !


什么内建命令 ?


  普通命令和内建命令有什么区别 ??

  • 这里要了解一点 , 我们写的大部分命令都是 bash 创建子进程去执行的 !

  • 一般命令都是在 PATH 环境变量中进行查找的 !
    这也就意味着 , 该 PATH 环境变量没了 , 一般命令就找不到了 , 找不到就不会执行了 !

在这里插入图片描述

  • 为什么内建命令可以执行呢 ??

在这里插入图片描述

在这里插入图片描述


什么进程程序替换 ?

详细请看 : 进程替换


二、shell

1 . 要写一个怎样的命令行解释器

在这里插入图片描述


2 . 代码编写

  笔者在代码中每个部分都给了详细解释 !

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string>
//// 0. 会创建两张表 , 一张命令行参数表 , 一张环境变量表 , 全局的

////////// 命令行参数表 , 里面存的是一个一个的字符串 ,即 : 第一个数组是 指令 , 其余的是选项 .
// 例如 :  ls -a -l 
#define ARGVMAX 1024
char* g_argv[ARGVMAX];
int g_argc = 0;


////////// 环境变量表 , 这个全局里面存的是父进程的环境变量 , 可以继承给子进程
#define ENVMAX 1024
char* g_env[ENVMAX];
int g_enc = 0;


//存工作目录的 buff
char cwd[1024];
char cwdenv[1024];


//获取最近一次退出码
int last_exit_code = 0;

//***  1 . 获取环境变量 , 本来是从系统配置文件中获取 , 现在用子进程继承父进程模拟 , 因为父进程的环境变量就是从系统配置文件中来的 ***
void Init_env()
{
	// extern char **environ; 有这个指向全局的环境变量表 
	extern char** environ; // 指向全局的环境变量表	, 可以从其拿到父进程环境变量
	
	/////1 . 获取环境变量 , 模拟从配置文件获取
	memset(g_env , 0 , sizeof(g_env)); // 将表都置为 0  ,防止乱码
	g_enc = 0;
	// 环境变量表是以 NULL 结尾 , 环境变量表这个数组的下标里要存每个环境变量的内容
	for(int i = 0; environ[i]; i++)
	{
		//申请空间 , 存字符串 , 每个下标都申请
		g_env[i] = (char*)malloc(strlen(environ[i]) + 1);
		//拷贝全局对于位置内容
		strcpy(g_env[i] , environ[i]);
		g_enc++;
	}	

	///////////////// 以上就我们定义的全局的环境变量表中就获得了系统中的环境变量了! 但是目前只是在我们定义的 g_env[] 中
	// 2 . 需要把全局的环境变量表导入到进程的 mm_struct 中 , 这样就可以通过进程地址空间找到环境变量了 
	
	/// for test , 这里添加一个 , 观察看是否是导入到了进程上下文中
	g_env[g_enc++] = (char*)"MYVALUE=1234";
	g_env[g_enc] == nullptr;

	for(int i = 0; g_env[i]; i++)
	{
		putenv(g_env[i]); // 这个接口可以增加环境变量 , 所以可以把全局的表导入到每个进程上下文数据(mm_strut) 中 
	}
}




/////***  2. 显示出命令行 ***
// [用户名@主机名 当前工作目录]#/$ 

//获取用户名
const char* GetName()
{
	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()
{
	// 用 getcwd , 放到一个 buff 里存当前工作路径
	const char* pwd = getcwd(cwd , sizeof(cwd)); // 获取当前工作目录
	// pwd 变化的时候 , 环境变量也要变 
	if(pwd != NULL)
	{	
		// 将最新的目录添加到 cwdenv 表中
		snprintf(cwdenv ,sizeof(cwdenv) , "PWD=%s" ,pwd );
		putenv(cwdenv); // 导入到进程上下文 
	}
	
	return pwd == NULL ? "None" : pwd;
}



// 显示当前工作目录的最后一个
#define SPLDEM "/"
std::string Dir(const char* pwd)
{
	//倒这找最后一个  /
  	std::string dir = pwd;
    	if(dir == SPLDEM) return SPLDEM;
   	auto pos = dir.rfind(SPLDEM);
	if(pos == std::string::npos) return "BUG?";
    	return dir.substr(pos+1);	
}



// 定义一下命令行的格式 !
#define FORMAT "[%s@%s %s]# " 

// 制作一个命令行
void MakeCmdLine(char cmd_line[] , int sz)
{	
	//  int snprintf(char *str, size_t size, const char *format, ...);
	//  向指定的 str 中打印
	snprintf(cmd_line , sz , FORMAT , GetName() , GetHostName() ,Dir(GetPwd()).c_str() );			
}





#define CMDMAX 1000
// 2 . 打印命令行 
void PrintCmdLine()
{

	char cmd[CMDMAX];
	// 制作的命令行放到数组中 , 后面方便打印
	MakeCmdLine(cmd , sizeof(cmd));
	printf("%s",cmd);
	//刷新缓冲区 , 让其快速显示
	fflush(stdout);
}



//***  3 . 获取用户输入的命令 ***

//获取的本质就是获取键盘内容 , 包括空格也要读入 , 所以用 C接口的 fgets()
bool GetCmdLine(char str[] , int sz)
{	
	// 
	char* cmd = fgets(str , sz , stdin);
	if(cmd == NULL) return false;
	
	//因为用户输入好以后最终会按回车 , 所以要清掉回车 , 即 : 把 str 最后一个元素置为 0
	str[strlen(str) - 1] = 0; // 清理 \n ,********不处理打印出来就会换行不符合预期*********
		
	// 命令行没有输入 , 就不获取了 
	if(strlen(str) == 0) return false;
	// 读取成功 	
	return true;	
}

		

//***  4 . 解析命令行 , 把命令行分割成一个一个字符 ***
// 我们输入的命令 ls -a -l 中间都是带有空格的 , 所以以空格为分割符 !
	
//定义以什么分割
#define DEM  " "
bool CmdLineParsing(char* CmdLine)
{
	//分割 , 以空格分割 
	//char *strtok(char *str, const char *delim);
	// 考虑一个问题 , 分割好的字符放哪 ???? 命令行参数表中 
	// 重点回顾一下 strtok 的使用方法 , 第一次传分割谁 , 后面传 NULL 即可
	
	g_argc = 0; // 每次进来都清 0 
	
	// 第一次分割
	g_argv[g_argc++] = strtok(CmdLine ,DEM);
	//命令行参数表最后一定是以 空 结尾
	while(g_argv[g_argc++] = strtok(NULL ,DEM)); //没分割完继续分割
	
	//****************  统计了 \0 , 所以要-- **************** 这里通过打印测试可以看出
	g_argc--;
	return g_argc > 0 ? true : false; // 如果没有要解析的命令行就返回 
}



/// 测试一下分割结果 
void Print()
{	
	printf("分割后的结果 : \n");
	for(int i = 0; i < g_argc; ++i)
	{
		printf("%d->%s\n",i,g_argv[i]);
	}
	
	printf("分割后的结果 g_argc :%d \n", g_argc);
}


// *** 5 . 分析内建命令 , 进行处理 ***
//内建命令就是 : 父进程亲自去执行 ! 父进程调用函数或系统调用


//获取家目录
const char *GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL ? "" : home;
}

// 获取上一次目录
const char* GetLatestDir()
{	
	const char* dir = getenv("OLDPWD");
	return dir == NULL ? "" : dir;
}

// 处理 cd 命令的函数

bool Cd()
{
    // 只有 cd , 默认是走到家目录下	
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if (!home.empty()) chdir(home.c_str());
    }
    // string 不能接受空对象 	
    else if (g_argv[1] != nullptr) // 检查参数是否有效
    {
        std::string where = g_argv[1];
        if (where == "-") 
	{
            std::string oldpwd = GetLatestDir();
            if (!oldpwd.empty()) chdir(oldpwd.c_str());
        }
	else if (where == "~") 
	{
            std::string home = GetHome();
            if (!home.empty()) chdir(home.c_str());
        } 
	else 
	{
            chdir(where.c_str());
        }
    }
    return true;
}


//处理 echo 命令
void Echo()
{	
	// echo $?  echo $PATH echo hello
	if(g_argc == 2)
	{	
		std::string option = g_argv[1];
		if(option == "$?") 
		{
			// 打印最近一次的退出码 
			std::cout << last_exit_code << std::endl;
			//获取后 , 再次置为 0 ,等待下一次
			last_exit_code = 0;
		}
		// echo $PATH , 打印的是环境变量的内容 !
		else if(option[0] == '$')
		{	
			//分割,去除 $
			std::string env_name = option.substr(1); // 从第一个位置开始到结尾的内容留下来
			//获取该环境变量的内容
			const char*  env_name_value = getenv(env_name.c_str());
			if(env_name_value)
				std::cout << env_name_value << std::endl;		
		}
		else
		{
			std::cout << option << std::endl;
		}
	}

}


// *** 5 . 检查分析内建命令 *** 
bool AnalyzeBuilt_inCmd()
{	
	// 命令行参数表中第一个元素就是命令	
	std::string cmd = g_argv[0];

	if(cmd == "cd")
	{
		Cd();
		return true;
	}
	else if(cmd == "echo")
	{	
		Echo();
		return true;
	}
	else if(cmd == "export")
	{	
		// ....
		return true;
	}
	
	return false;	
}


//***  6. 执行命令 ! ***
// 执行命令的本质就是进行程序替换 , 这个任务一般都是交给子进程去做的(内建命令除外)
int ExecuteCmd()
{
	// 创建子进程 , 交给它去执行命令	
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork failed !\n");	
		return 3; // 3 - 没有这个进程
	}		
	if(id == 0)
	{
		//执行命令
		//执行命令的本质就是 : 程序替换
		// 因为现在已经有了 2张表 , 命令行参数表已经存在了 , 所以选择 exec 系列接口时 , 选择有 v 的,同时也有PWD,所以选择execvp
		execvp(g_argv[0] , g_argv) ; // 数组中的第一个参数就是命令 
		exit(1);
	}	
	
	int wstatus = 0;
	pid_t rid = waitpid(id , &wstatus , 0);
	if(rid > 0)
	{
		//等待成功 ,获取子进程的退出码
		last_exit_code = WEXITSTATUS(wstatus);
	}
	return 0; // 方便后知运行成功与否 !
}

int main()
{	
	// 当 shell 一旦启动 , 系统会先从系统的配置文件中获取环境变量 , 获取的变量放入到全局的环境变量表中 , 也就意味着这个表属于父进程
	// 但是父进程的环境变量表是可以继承给子进程的 , 所以子进程也可以看到 , 想要获取直接导入到子进程的环境变量表中即可 , 即 : 导入
 	// 子进程 mm_struct 中 , 这样子进程就可以使用了 ! 
	
	// 1 . 获取环境变量 , 将环境变量添加到进程的地址空间中
	Init_env();
	
	while(1)
	{	
		// *** 2. 打印命令行 ***
		PrintCmdLine();


		//***  3. 获取用户输入的命令 *** 
		// 用户输入一个 ls -a -l shell 要获取 , 用户不输入就会阻塞在这里等待用户输入 !!
		char CmdLine[CMDMAX]; // 用户输入的命令放到这个里面
		if(!GetCmdLine(CmdLine , sizeof(CmdLine)))
			continue;  // 没有获取成功 , 继续获取

		// 测试一下看获取用户输入是否成功	
		//printf("%s\n",CmdLine);
 
		// *** 4. 解析命令行 , 把命令行内容分割成一个一个字符 ***
		// "ls -a -l" ---- >  "ls" "-a" "-l" 
		if(!CmdLineParsing(CmdLine))
			continue; // 没有解析成功 , 继续解析

		//测试一下看解析是否成功 
		//Print();

			
		// *** 5 . 检测 , 分析内建命令 , 进行处理 ***
		if(AnalyzeBuilt_inCmd())
			continue; // 检测到了就继续处理 


		// *** 6. 执行命令 ***
		ExecuteCmd();
			
	}
	return 0;
}


三、重点理解

  一个 shell 到底会做什么 ??

在这里插入图片描述


总结

以上就是 shell , 写代码只是让我们更清楚理解原理底层实现方式 , 皆是更好的理解命令行解释器 !

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值