Linux:自定义Shell

本文旨在通过自己完成一个简单的Shell来帮助理解命令行Shell这个程序。

目录

一、输出"提示"

二、获取输入

三、切割字符串

四、执行指令


一、输出"提示"

这个项目基于虚拟机Ubuntu22.04.5实现。

打开终端界面如图所示。

其中。

cpp 复制代码
@之前:utocoo是用户名
@之后:utocoo-virtul-machine是主机名
":"之后是当前路径,"~"表示用户家目录
"$"是普通用户的提示符,如果是root用户,则为"#"
光标闪烁位置在等待输入

当前的用户名主机名当前工作目录这些信息都有对应的环境变量,故可以利用getenv拿到对应的值。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
//获取用户名
const char* UserName()
{
	const char* username = getenv("USER");
	if(username)
		return username;
	else
		return "None";
}
//获取主机名
const char* HostName()
{
	const char* hostname = getenv("HOSTNAME");
	if(hostname)
		return hostname;
	else
		return "None";
}
//获取目录
const char* CurrentWorkDir()
{
	const char* cwd = getenv("PWD");
	if(cwd)return cwd;
	else return "None";
}
int main()
{
	printf("%s@%s:%s$",UserName(),HostName(),CurrentWorkDir());
	return 0;
}

二、获取输入

用户的输入是作为一个字符串被输入,故需要定义一个数组作为缓冲区。

使用scanf 输入时,遇到空格则会刷新缓冲区,故推荐使用fgets 函数作为输入函数。关于C语言的各组输入函数,这篇文章做了很好的说明。https://blog.csdn.net/qq_53139964/article/details/142820767

补全其他板块的代码完成"获取输入"这一步骤。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

#define SIZE 1024 //定义缓冲区数组大小

const char* HostName()
{
	const char* hostname = getenv("HOSTNAME");
	if(hostname)
		return hostname;
	else
		return "None";
}
const char* UserName()
{
	const char* username = getenv("USER");
	if(username)
		return username;
	else
		return "None";
}
const char* CurrentWorkDir()
{
	const char* cwd = getenv("PWD");
	if(cwd)return cwd;
	else return "None";
}
int main()
{
	char commandline[SIZE];
	printf("%s@%s:%s$ ",UserName(),HostName(),CurrentWorkDir());
	//获取用户输入
	fgets(commandline,SIZE,stdin);
	printf("test:%s\n",commandline);

	return 0;
}

测试结果如下。

不难看出,打印结果中有两次"换行操作 "。原因是fgets在stdin流中读取一定数量的信息时,会将我们自己输入的**'\n'也读取进来,因此需要在commandline数组**中去掉这个字符。

cpp 复制代码
commandline[strlen(commandline)-1] = 0;//将'\n'修改为'0'

封装处理后。

cpp 复制代码
//命令行交互
void Interactive(char* out,int size)
{	
	printf("%s@%s:%s$ ",UserName(),HostName(),CurrentWorkDir());
	//获取用户输入
	fgets(out,size,stdin);
	out[strlen(out)-1] = 0;//将'\n'修改为'0'
}

三、切割字符串

我们知道,命令行也是正在运行的程序,而在命令行执行输入的指令,其实是命令行这个进程创建子程序后再做程序替换,注意程序替换时,传参方式要么是可变参数,要么是指针数组,因此,无论如何,都要先将当前的字符串按照空格切割成一个个的子串,如果是指针数组的形式,要以NULL结尾。

切割字符串可以利用C语言的字符串处理函数strtok

cpp 复制代码
#define MAX_ARGC 64
#define SPC " "
char* argv[MAX_ARGC];

//切割字符串
void Split(char* in)
{
	int i = 0;
	argv[i++] = strtok(in,SPC);
	while(argv[i++] = strtok(NULL,SPC));
}

在这里,切割字符串的语句是这样一句while循环 ,循环体为空。仅仅这样一行代码就可以实现我们对字符串切割的要求,因为argv数组要求要以NULL结尾,而这句赋值语句将最后一个字符'\0'赋值给数组元素后,数组尾的数据就是NULL,同时表达式的值也为假,跳出循环。

四、执行指令

我们当前做的所有工作都是模拟shell这个程序,而模拟的命令行要执行我们输入的指令,必然要通过程序替换来完成,但是不能用shell这个进程做替换,应该创建子进程,让子进程做程序替换,父进程等待子进程。

cpp 复制代码
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
	
//3.执行命令
pid_t id = fork();
if(id == 0)
{
	//程序替换,子进程执行命令
	exit(1);
}

pid_t rid = waitpid(id,Null,0);
printf("run done!:%d",rid);
return 0;

程序替换时,选择适当的替换函数也是很重要的,我们在模拟的时候是创建了argv数组,故选择带v的exec函数,其次,带p的exec函数可以不用指定系统指令的全部路径,故选择execvp这个函数。

cpp 复制代码
//执行指令
void Execute()
{
	pid_t id = fork();
	if(id == 0)
	{
		//程序替换,子进程执行命令
		execvp(argv[0],argv);
		exit(1);
	}

	pid_t rid = waitpid(id,NULL,0);
	printf("run done!:%d\n",rid);
}

由于我的Ubuntu系统当前的环境变量没有主机名这个变量,因此主机名结果显示了None。

但是,当前的shell只能执行一次程序替换,所以需要加上死循环,让shell一直运行。

bash 复制代码
int main()
{
	while(1)
	{
    	char commandline[SIZE];
    	//1.打印命令行信息
    	Interactive(commandline,SIZE);
    	//2.切割字符串
    	Split(commandline);
    	//3.执行命令
    	Execute();
	}
	return 0;
}

但是有些指令的执行结果是不符合预期的,比如cd指令这部分指令称为内建指令,具体请看这篇文章。https://blog.csdn.net/chen1415886044/article/details/103015950

基于这一点,我们模拟的shell程序在执行cd指令的时候,其实是子程序替换为cd 指令,子程序执行了cd指令,路径发生改变的仅仅是子程序的路径,而我们平时在命令行所打印出来的路径,其实都是shell程序的路径,运行结果当然不符合预期。

因此,在子程序替换执行指令之前,先判断要执行的指令是否要内建指令,如果是内建指令,则不需要创建子进程做替换,而是shell这个进程直接执行。

cpp 复制代码
int main()
{
	while(1)
	{
    	char commandline[SIZE];
    	//1.打印命令行信息
    	Interactive(commandline,SIZE);
    	//2.切割字符串
    	Split(commandline);
	    //3.执行内建指令
		int i = BuildinCmd();
		if(i) continue;
    	//4.执行命令
    	Execute();
	}
	return 0;
}
cpp 复制代码
/执行内建指令
int BuildinCmd()
{
	//判断是否为内建指令,如果是,则返回1,否则返回0
	//并且执行内建指令
	//此处只列举cd这一条内建指令
	int ret = 0;
	if(strcmp("cd",argv[0])== 0)
	{
	   // cd *** :cd到具体路径 
       // cd 空 :cd到家目录
		char* Target = argv[1];
		if(!Target) Target = getenv("HOME");

		chdir(Target);
		ret = 1;
	}
	return ret;
}

执行结果。

在执行结果中,依旧有两个错误。

1.输入为空,结果是段错误。

2.cd指令执行后,命令行的输出提示中路径并未发生改变。


要解决输入为空后发生的段错误,可以在交互的函数中返回输入字符串的长度,然后做if条件判断,特殊处理。


至于在执行cd指令后,显示结果的路径并未发生改变,原因就是显示结果是由getenv得到 ,而此时的环境变量PWD并没有发生改变,因为当前myshell进程所在路径没有发生改变

同时,不难总结出来,cd指令的执行和环境变量PWD的value息息相关

可以利用snprintf这个函数,将格式化信息输出到指定大小的pwd字符串中,再利用putenv导入环境变量,则myshell程序就模拟出修改环境变量PWD的效果了

putenv,导出环境变量,新建或者修改一个环境变量,如果putenv的参数是新的环境变量,则新建,如果是已经存在的环境变量,则修改。

snprintf,和printf是一类函数,printf默认把格式化信息输出到屏幕,而多加了s (string)和n(表示字符串的大小)的snprintf表示把格式化信息输出到n长度的字符串中。

cpp 复制代码
//关联环境变量,定义一个字符串,或者字符数组
char pwd[SIZE];
//执行内建指令
int BuildinCmd()
{
	//判断是否为内建指令,如果是,则返回1,否则返回0
	//并且执行内建指令
	//此处只列举cd这一条内建指令
	int ret = 0;
	if(strcmp("cd",argv[0])== 0)
	{
	   // cd *** | cd 
		char* Target = argv[1];
		if(!Target) Target = getenv("HOME");

		chdir(Target);
		
		//关联环境变量,格式化信息会输出到pwd字符数组中
		snprintf(pwd,SIZE,"PWD=%s",Target);
		putenv(pwd);
		ret = 1;
	}
	return ret;
}

但是在用cd指令执行下面这样的情况后,路径名并不达预期。

原因是我们是使用Target来更新了环境变量,Target是我们输入的内容。

可以利用getcwd函数获取当前进程的绝对路径,再用getcwd的返回结果来更新环境变量。

cpp 复制代码
//执行内建指令
int BuildinCmd()
{
	//判断是否为内建指令,如果是,则返回1,否则返回0
	//并且执行内建指令
	//此处只列举cd这一条内建指令
	int ret = 0;
	if(strcmp("cd",argv[0])== 0)
	{
	   // cd *** | cd 
		char* Target = argv[1];
		if(!Target) Target = getenv("HOME");

		chdir(Target);
		
		//关联环境变量,格式化信息会输出到pwd字符数组中
		char tmp[999];
		getcwd(tmp,999);
		snprintf(pwd,SIZE,"PWD=%s",tmp);
		putenv(pwd);
		ret = 1;
	}
	return ret;
}

export指令也是内建指令,在export的指令被切割为argv数组后,argv数组的第二个元素就是要导入环境变量的字符串,可以直接putenv导入。

cpp 复制代码
if(strcmp("export",argv[0])==0)
{
	ret = 1;
	if((argv[1]))putenv(argv[1]);
}

随便导入一个环境变量,执行env命令后就能看到这个环境变量。但是在你执行一系列指令后,再执行env指令查看这个环境变量,可能会出现找不到的情况。

原因就是,上面这段代码是通过argv数组导入的,在执行env指令,显示的时候指向了argv数组的值,而argv数组中的值在一次次执行指令的过程中会不断变换,因此已经导入的环境变量可能又会消失不见。

正确做法是用数组保存要导入的环境变量。

cpp 复制代码
//存储新的环境变量
char env[SIZE];

if(strcmp("export",argv[0])==0)
{
	ret = 1;
	if((argv[1]))
	{
		  strcpy(env,argv[1]);
	  	  putenv(env);
	}
}
相关推荐
1900432 分钟前
linux复习5:C prog
linux·运维·服务器
猫猫的小茶馆12 分钟前
【C语言】指针常量和常量指针
linux·c语言·开发语言·嵌入式软件
朝九晚五ฺ34 分钟前
【Linux探索学习】第十五弹——环境变量:深入解析操作系统中的进程环境变量
linux·运维·学习
ernesto_ji1 小时前
Jenkins下载安装、构建部署到linux远程启动运行
linux·servlet·jenkins
李迟1 小时前
某Linux发行版本无法使用nodejs程序重命名文件问题的研究
java·linux·服务器
施努卡机器视觉1 小时前
电解车间铜业机器人剥片技术是现代铜冶炼过程中自动化和智能化的重要体现
运维·机器人·自动化
徐浪老师1 小时前
深入实践 Shell 脚本编程:高效自动化操作指南
运维·chrome·自动化
King's King2 小时前
自动化立体仓库:详解
运维·自动化
东隆科技2 小时前
晶圆测试中自动化上下料的重要性与应用
运维·自动化