一、bash的定义
大家还记得小编曾经在哪一篇文章里介绍过bash呢?
小编带领大家回顾一下,在《Linux(九)fork复制进程与写时拷贝技术》中,小编曾经介绍过shell。

在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(command interpreter,命令解析器)。它类似于DOS下的COMMAND.COM和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。
我们就是通过命令解释器(称为shell)(bash是命令解释器中的一种)和内核和系统进行交互的
(Windows通过图形界面进行交互的);例如我们把Is交给bash,bash帮我们运行Is,然后把结果给用
户;


总结一下,这个项目是mybash(也可以叫myshell),目的是打造自己的命令解释器。目前我们Linux的系统默认的命令解释器是bash;命令解释器(也称为命令行解释器或shell)是计算机操作系统中的一个重要组件,它负责接收用户输入的命令,并解释和执行这些命令;其实命令解释器就是解析命令,执行命令,输出反馈。
二、命令的分类
我们在学习上一篇文章中的exec替换系列时,曾完成过“execl结合fork创建ps命令”的程序。在那里,我们总结了创建新进程是先fork后exec。那大家是否有想过,是所有的命令都可以用fork+execl完成吗?
为了回答上面的问题,小编要和大家介绍一个新知识——内置命令和外置命令(普通命令)。
内置命令是 Shell 程序的一部分,由Shell 自身提供。执行时,Shell 直接在内部调用相关函数来处理,无需创建新的进程。这使得它们执行速度快,能快速响应。例如,cd exit 。
外置命令是独立于 Shell 的可执行文件,通常存放在/usr/bin目录下(bin用于存放二进制可执行文件)。当执行外置命令时,系统会创建一个新的子进程来运行该命令。例如,ls pwd cp ps等。
在这里,小编也向大家分享一个区分内置和普通命令的方法——使用which查找。

通过上面的图片,大家可以发现,能够使用which找到的都是外置(普通)命令,而内置命令是用which找不到的。
而我们今天要模仿的对象bash也是一个可执行程序,使用which bash可以找到。

简单来讲,就是普通命令是通过fork+exec实现的;而内置命令是Bash自身通过调用相应接口实现的。
三、项目框架
下面,小编向大家介绍一下项目框架。
大家可以和小编一起分析上面这张图,图片就是我们刚刚打开终端的样子。大家在使用过程中会发现,在终端界面只有使用exit才能退出,因此在框架中有了while(1)这个循环。
接下来就是用户名,主机名,当前位置,用户角色这一行,我们直接使用printf打印,先将这一行定死,后面在完成细节时进行改进。
然后创建了一个缓冲区buff,用于存放用户从键盘输入的命令。
命令保存到缓冲区后,我们需要分割命令;这里就需要使用myargv和strtok 。strtok这个函数小编会在下面详细介绍。
最后我们使用if else语句完成内置和普通命令的实现。在这里,小编也向大家分享一下使用if-else语句区分内置外置的原因。内置命令,大家可以理解为内部执行,也就是只能父进程执行,子进程无法替代,因为不同的进程执行结果不一样。外置(普通)命令,其实就是通过fork创建一个子进程,然后通过子进程完成,因为对于外置命令来说,父子进程执行的结果是一样的,所以谁执行都可以;也可以放在内部执行,但是这样就会很复杂。
四、strtok的介绍
按照小编一直以来的方法,学习新的内容时要使用帮助手册。
man strtok


注意:strtok线程不安全,原因就是函数实现使用了一个static的变量(指针记录下次分割的地址,再次调用要沿用上次的,所以需要静态变量)。在多线程中,如果两个线程都使用了strtok的话,这个变量的值就会被另一个线程不定期的进行修改。
五、各模块内容
//第一步
//大框架
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#define ARG_MAX 10//防止参数不够,可以做到一改全改
int main()
{
while(1)
{
printf("stu@localhost ~$");//用户和路径先写定,后期会补充判断
fflush(stdout);//刷新缓冲区
char buff[128]={0};
fgets(buff,128,stdin);
char *myargv[ARG_MAX] = {0};
char *cmd = get_cmd(buff,myargv);//提取buff里面的命令
if(cmd == NULL)
{
continue;
}
else if(strcmp(cmd,"cd")==0){
//内置命令
}
else if(strcmp(cmd,"exit")==0){
break;//若为exit,则退出内部
//exit(0);//可以,但不好
}
else{
//普通命令
//fork + exec
}
}
exit(0);
}
//第二步
//完成get_cmd函数(也就是分割命令)
char *get_cmd(char *buff,char *myargv[]){
if(buff == NULL||myargv == NULL){
return NULL;//说明用户没有输入命令
}
char *s = strtok(buff," ");//第一次调用strtok
while(s!=NULL){//不确定用户输入的命令需要分割几次,只要分割到字符串末尾为'\0',就会结束
myargv[i++] = s;//保存分割的结果
s = strtok(NULL," ");//后续调用不用再传buff,因为内部有指针会记录
}
return myargv[0];
}
//处理了exit和exit\n不匹配的问题
//第三步,实现普通命令
void run_cmd(char *path,char *myargv[]){//这个函数的作用是当输入命令为普通命令时,创建子进程执行
if(path == NULL || myargv == NULL)
return;
pid_t pid = fork();
if(pid == -1)
return;
//下面我们要让父子进程做不一样的事情
if(pid == 0)//子进程进入if
{
execvp(path,myargv);
//这里选择替换系列时,选带v,这样就可以传数组,选带p的,这样就不需要传环境变量
perror("execvp error!\n");//一定要记得打印错误信息,这样方便我们明确的知道execvp是否执行成功
exit(0);
}
else//父进程进入esle
{
//这里要记得处理将死进程
wait(NULL);
}
}
有小伙伴可能会注意到,在if语句中最后写了exit(0);那不写这个可不可以呢?那就回出现第二张图片的情况。

通过下面这张图片帮助大家更好的了解替换的过程。
在这里,我们简单运行一下程序,就可以发现我们自己完成运行的mybash的父进程是bash,子进程是执行的命令。

//第四步,将一开始定死的提示信息进行修改完善
void printf_info(){//打印提示信息,获取用户名,主机名,当前位置,获取用户角色(普通用户还是管理员)
char *user_str="$";//默认为普通用户
int user_id = getuid();
if(user_id == 0){//当uid=0时为root,将$改为#
user_str = "#";
}
struct passwd * ptr = getpwuid(user_id);//得到用户名
if(ptr == NULL)//如果为NULL,也就是出现问题了,那么打印bash的版本号
{
printf("mybash1.0>> ");
fflush(stdout);
return;
}
//获取主机名
char hostname[128] = {0};
if(gethostname(hostname,128)==-1){//如果获取主机名失败,打印版本号
printf("mybash1.0>> ");
fflush(stdout);
return;
}
//获取主机路径
char dir[256] = {0};
if(getcwd(dir,256)==NULL){//如果获取路径失败,打印版本号
printf("mybash1.0>> ");
fflush(stdout);
return;
}
printf("%s@%s %s%s ",ptr->pw_name,hostname,dir,user_str);
fflush(stdout);
}
第四步就到了获取主机信息的部分,为了能够动态获取用户名,主机名等信息,这里小编要先向大家介绍几个函数。
获取用户名,在这个结构体中就可以得到。



获取主机名


获取当前路径。

//第五步,实现cd命令
else if(strcmp(cmd,"cd")==0){
if(myargv[1] != NULL){//当cd后面有路径时
if(chdir(myargv[1])==-1){
perror("cd err!\n");
}
}
//当用户只输入cd时,会进入家目录(和系统保持一致)
}
//第六步
//为了和系统保持一致,使字体有颜色(使用printf)
printf("\033[1;32m%s@%s\033[0m \033[1;34m%s\033[0m%s ",ptr->pw_name,hostname,d ir,user_str);
六、项目改进
在上述代码中,我们普通命令的实现都是依靠系统完成的,也就是/user/bin里的二进制可执行程序(在标准路径下找到的)。
那么,这也就是我们改进的方向,就是写入自己的环境变量,调用自己实现的程序实现MyBash命令解释器(就是将自己实现的命令放入mybin下)。实际上,我们已经自己实现了3个命令了,分别是mycp、mykill、myps。
下面,我们还将实现一些其他的命令。在实现之前,我们在存放mybash.c的路径mkdir mybin这个目录存放我们自己完成的命令,和系统的user/bin进行区分。
首先是clear命令。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
printf("\033[2J\033[0;0H");
}
//pwd.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
char path[256] = {0};
if(getcwd(path,256) == NULL)
{
perror("getcwd error!\n");
exit(1);
}
printf("%s\n",path);
exit(0);
}
现在我们的mybin下面有两个命令了。

大家和小编一起运行一下看看能否成功。

大家要注意,我们在通过gcc生成可执行文件后,把.c文件删除是否还可以成功运行呢?答案当然是可以的,大家可以自行尝试。
接下来就是实现ls命令了,ls命令稍微有点复杂,大家要注意哦。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<dirent.h>
#include<sys/stat.h>
int main(){
char path[256] = {0};
if(getcwd(path,256) == NULL){
perror("getcwd error!\n");
exit(1);
}
DIR *pdir = opendir(path);
if(pdir == NULL)
{
perror("opendir error!\n");
exit(1);
}
struct dirent *s = NULL;
while((s=readdir(pdir))!=NULL)
{
if(strncmp(s->d_name,".",1)==0)
{
continue;
}
//是目录文件打印为蓝色,不是目录文件分为两种,普通文件是黑色,可执行文件是绿色
struct stat filestat;
stat(s->d_name,&filestat);
if(S_ISDIR(filestat.st_mode))
{
printf("\033[1;34m%s\033[0m ",s->d_name);
}
else{
if(filestat.st_mode &(S_IXUSR|S_IXGRP|S_IXOTH))
{
printf("\033[1;32m%s\033[0m ",s->d_name);
}
else
printf("%s ",s->d_name);
}
}
printf("\n");
closedir(pdir);
exit(0);
}
小编在这里就只写这三个命令啦,其他命令小伙伴也能自己写。接下来就是要在前面代码的基础上使用我们自己的环境变量mybin了。
首先,我们要知道我们自己的环境变量的路径是什么,这个是小编正确的路径 /home/stu/quzijie/class03/test15/mybin,那么我们在最上面定义一个宏。大家的mybin在哪,就放哪的路径,不是和小编完全一样噢!还要注意在mybin后面加一个/,因为我们还要拼接路径。


让我们编译运行一下看看能否成功。


现在使用的是我们的mybin里面的命令,我们没有写ps所以无法执行;但我们输入/usr/bin/ps就可以执行系统的ps命令了。
在这里也告诉大家一个偷懒的办法,我们可以使用which查找命令的路径,然后复制到mybin下面就可以使用啦。


七、相关知识点回顾
Linux(九)fork复制进程与写时拷贝技术:回顾bash
Linux(十三)fork + exec进程创建:回顾fork+exec创建进程
Linux(七)进程管理、用户管理和文件压缩命令:回顾用户管理(用户信息保存的位置)


被折叠的 条评论
为什么被折叠?



