【Linux】实战小项目-----Bash的简易版

目录

一、什么是Bash

二、实现Bash:

1、整体需求分析:

2、初始化:

3、分割字符串:

4、执行普通命令:

5、内建命令与特殊处理:

1、ls的颜色:

2、内建命令cd:

3、export:

4、echo

三、源码:


一、什么是Bash

首先理解shell,Bash,命令行解释器在Linux中扮演的角色:

Shell ‌:用于解释和执行用户的命令,提供用户与操作系统之间的交互接口

Bash ‌:作为Shell的一种实现 ,提供了更多的功能和增强特性,是大多数Linux发行版的默认Shell‌命令行解释器‌:负责解释和执行用户输入的命令,是Shell的核心功能之一

Bash也是一个进程,并且是不断运行中的进程,那么在我们输入指令的时候实际上就是Bash创建了子进程,然后子进程进行进程替换为要执行的进程,进而完成我们的命令,

当我们执行一个指令进程之后,可以看到这个进程的父进程是Bash,证明了所有指令都是Bash的子进程

二、实现Bash:

1、整体需求分析:

Bash的任务:就是把一个指令进程进行解释,然后进行进程替换

所以整体思路:

1、实现命令行的外表
2、把用户输入指令的看做一个字符串
3、把这个字符串进行分解
4、分解后创建子进程进行进程替换
5、对特殊的指令(内建命令)进行特殊处理

看看主函数的整体逻辑:

首先就是初始化,这是拿到输入的指令

接着分割字符串,如果没有输入就continue重新读取输入

然后执行内建命令,如果执行成功返回1,没有执行则返回0

最后执行普通命令

2、初始化:

对于这两个可以封装为一个函数

上面,传过来的这个line是一个数组,作用是读取命令行中的一整行字符串,

printf后面通过自己定义的宏(这样个性化的时候就只需修改宏即可)和自己所写的函数来完成命令行外表

接着定义一个char* 类型来在输入流中拿到字符串放在target数组中

最后一行是处理回车符将读取的回车变为 \0 这样就能够得到最干净的字符串了

3、分割字符串:

然后就是对字符串进行分割

如上,这里是用strtok函数进行分割的,具体解析可参考下述文章

strtok()函数详解! - Yuxi001 - 博客园https://www.cnblogs.com/yuxiyuxi/p/17770807.html

如上采用的是使用函数strtok进行分割,DEL是定义的宏,使用其的原因同样是方便修改,总体思路很简单,首先在全局里定义一个数组(这里是char* argv[ ])来放字符串,所以每次分割后都放到argv中,最后返回 i - 1 也就是切割后的子字符串的个数方便后续维护

4、执行普通命令:

前面了解到执行普通命令就是Bash创建子进程然后进行进程替换进而完成

思路:

封装一个函数,在函数中首先fork创建子进程,
在子进程代码块 中使用函数execvp进行进程替换,第一个参数就是分割后的argv中的0下标位置,第二个参数就是argv数组的所有,直接传数组名即可,然后如果替换失败就结束进程,退出码设置为自己所设置的特殊退出码
在父进程代码块中等待子进程即可

这样就可以执行普通指令了

5、内建命令与特殊处理:

1、ls的颜色:

实际上就是在指令ls后面加上 --color 即可,所以可以在内建命令中进行一次特殊处理,即可实现ls的颜色显示

思路:

通过strcmp函数进行判断,发现如果是ls指令的话就在此时的argv数组中的argc位置加上--color,然后把argc++,之后注意把argv的argc位置置为NULL

2、内建命令cd:

为什么cd命令需要进行特殊处理呢?

这是因为Bash已经创建子进程了,然后子进程进行cd到其他目录下,这个时候就虽然确实加载了cd命令,但是是不会影响父进程的,和父进程就没有啥关系了,所以就需要父进程自己去处理,

这里采用的是chdir函数进行内建命令cd的处理:

如下,当修改成功时,返回0,修改失败时返回 -1

实现思路:

在这里的Bash实现,cd命令后面必须跟上一个字符串,然后也是使用strcmp函数继续比较

在实现中直接先使用chdir()函数,里面的参数就是argv数组的第二个,

然后下面的getpwd()是之前写的一个函数,作用是得到当前工作路径下的绝对路径,这样就能够修改环境变量中的PWD了

最后在把环境变量的PWD修改,这里使用sprintf()函数实现,将格式化的数据写入PWD环境变量中

3、export:

同cd命令一样,如果正常替换程序在使用export函数只是修改了子进程的环境变量,就不会影响父进程了,所以就需要对export函数继续特殊处理,

处理思路:

首先依然是用strcmp函数进行判断后进入命令为export的代码块中,在处理的过程中不能简单的认为直接使用putenv导入环境变量

这是因为环境变量表是一个字符指针数组,每个指针指向对应的环境变量字符串,所以当简单地putenv的时候是在这个字符指针数组中找一个没有被使用的地方,指向_argv[1]的,这样就会导致下一次输指令的时候就会覆盖掉被_argv[1]的位置,这样尽管环境变量表中还是指向原来导入的环境变量位置的,但是由于这个位置被其他指令覆盖了,所以每次导入环境变量后再输入别的指令,这个环境变量就没了(这就是类似于浅拷贝)

解决方法:

在堆上开辟一块空间或者定义一个二维数组维护(从易变区到不变区方便维护)

把堆上的空间或者二维数组和_argv[1]处待添加的环境变量交换

然后putenv读取新开辟的空间添加到环境变量表

(其实就是类似于深拷贝的处理过程)

这样输入指令后就能够在环境变量中看到了,并且不会被覆盖

4、echo

echo指令一般情况下是正常打印字符串,但是如果添加了$符号那么就证明是要打印环境变量之类的,所以也需要进行特殊处理:

如上,在进入echo指令区域后进行判断,如果是$?就打印退出码,这个退出码在普通命令中已经得到了如下:

在打印过后就把退出码重新赋值为0表示正常退出,

然后在判断,如果_argv[1]位置处的第一个字符是$就证明需要打印环境变量,此时getenv获得环境变量后打印出来即可,

最后一个else就正常打印即可

如果需要增加其他功能,继续添加else if即可

三、源码:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>

#define LEFT "["
#define RIGHT "]"
#define LAB "#"
#define MAX_SIZE 1024
#define DEL " \t"
#define ARGC 32
#define EXIT_CODE 1


char target[MAX_SIZE];//拿去命令行中的一整行字符串
char* argv[ARGC];//把字符串分解后放入该数组
int last_normal_code = 0;//退出码
char pwd[MAX_SIZE];

const char* getusrname()
{
	return getenv("USER");
}

const char* gethost()
{
	return getenv("HOME");
}

void getpwd()
{
	//获取当前工作目录下的绝对路径
	getcwd(pwd,sizeof(pwd));
}

void Init(char* line)
{
	getpwd();
	printf(LEFT"%s@%s %s"RIGHT""LAB" ",getusrname(),gethost(),pwd);
	char* str = fgets(target,MAX_SIZE,stdin);
	assert(str);//输入的指令不能为空,但是一般是不可能的
	(void)str;//要把str使用一下,在release版本下assert就没有了,如果不使用可能会报错
	//"ls -a -l\n\0"
	line[strlen(line)-1] = '\0';
}

int split(char* line)
{
	int i = 0;
	argv[i++] = strtok(line,DEL);
	while(argv[i++] = strtok(NULL,DEL));
	return i - 1;
}

int InbuildExe(int argc,char* _argv[])
{
	if(argc == 2 && strcmp(_argv[0],"cd") == 0)
	{
		chdir(_argv[1]);
		getpwd();
		sprintf(getenv("PWD"),"%s",pwd);
		return 1;
	}
	else if(argc == 2 && strcmp(_argv[0],"export") == 0)
	{
		char* tmp = (char*)malloc(sizeof(char)*strlen(_argv[1]));
		strcpy(tmp,_argv[1]);
		putenv(tmp);
		return 1;
	}
	else if(argc == 2 && strcmp(_argv[0],"echo") == 0)
	{
		if(strcmp(_argv[1],"$?")==0)
		{
			printf("%d\n",last_normal_code);
			last_normal_code = 0;
		}
		else if(*_argv[1] == '$')
		{
			char* val = getenv(_argv[1]+1);
			if(val)
			printf("%s\n",val);
		}
		else
		{
			printf("%s\n",_argv[1]);
		}
		return 1;
	}

	if(strcmp(_argv[0],"ls") == 0)
	{
		_argv[argc++] = "--color";
		_argv[argc] = NULL;
	}
	return 0;
}

//argv是一个数组,里面的元素是char*,然后用char* _argv[](或者是char** _argv)接收argv的话,是接收的是首元素的地址,
//首元素的地址就有一个*,然后又是char* 类型的有个*,所以就是char** _argv等价于char* argv[]
void NormalExe(char* _argv[])
{
	pid_t id = fork();
	if(id < 0)
	{
		perror("进程创建失败\n");
		return;
	}
	else if(id == 0)
	{
		execvp(_argv[0],_argv);
		exit(EXIT_CODE);
	}
	else 
	{
		int status = 0;
		pid_t ret = waitpid(id,&status,0);
		if(ret == id)
		{
			//printf("等待成功\n");
			last_normal_code = WEXITSTATUS(status);//获取进程的退出码
		}
	}
}

int main()
{
	while(1)
	{
		//初始化
		Init(target);
		
		//分割字符串
		int ret = split(target);
		if(ret == 0) continue;

		//执行内建命令
		int flag = InbuildExe(ret,argv);
		//执行普通命令
		if(!flag) NormalExe(argv);
	}
	return 0;
}
相关推荐
清水加冰17 分钟前
【Linux进程】进程间的通信
linux·进程
YRr YRr35 分钟前
详细指南:在Ubuntu 20.04上安装和配置Orbbec SDK及USB设备权限
linux·运维·ubuntu
黑客Jack37 分钟前
网络安全概论——入侵检测系统IDS
服务器·安全·web安全
托尼沙滩裤41 分钟前
【MAC】深入浅出 Homebrew 下 Nginx 的安装与配置指南
运维·nginx·macos
yuanbenshidiaos1 小时前
linux----文件访问(c语言)
linux·服务器·算法
ghostwritten1 小时前
Linux 下的 GPT 和 MBR 分区表详解
linux·运维·gpt
ElePower95271 小时前
linux常用命令(touch、cat、less、head、tail)
linux
初心_20241 小时前
10. 虚拟机VMware Workstation Pro下共享Ubuntu和Win11文件夹
linux·运维·服务器
时空无限2 小时前
raid 状态查看 storcli64
linux
羊村懒哥2 小时前
nginx-虚拟主机配置笔记
服务器·笔记