标题:[Linux] Linux 模拟实现 Shell
个人主页@水墨不写bug(图片来源于网络)
目录
一、什么是shell
二、shell的理解
三、模拟实现shell
1)打印命令行提示
2)获取用户的输入字符串
3)分割命令字符串
4)检查是否是内建命令
1. 什么是内建命令
常见的内建命令
5)进程程序替换
正文开始:
一、什么是shell
Shell是用C语言编写的一个独立的可执行程序,它是用户与Linux或其他类Unix系统(如Mac OS)交流的桥梁,既是命令语言又是程序设计语言。
定义:Shell是Linux内核的一个外层保护工具,并负责完成用户与内核之间的交互。
功能:Shell是一个命令行解释器,它将用户输入的命令解析为操作系统所能理解的指令,实现用户与操作系统的交互。同时,Shell也提供了一种脚本编写功能,允许用户将一系列命令按照特定顺序排列,形成脚本文件,由Shell解释器逐行执行,以完成特定任务或实现一系列操作。
Shell提供了丰富的内置命令和外部命令,如cd用于切换目录,ls用于列出目录内容,grep用于搜索文本等。这些命令可以组合使用,形成复杂的脚本,以实现各种功能。
二、shell的理解
站在shell的角度,一个可执行程序,运行起来成为进程,它读取的一段指令其实一串字符串,操作系统无法理解这串字符串的具体意义,无法与用户交流,操作系统也就无法为用户服务。shell是外壳程序,就像一个包在操作系统外面的一层壳子,shell可以把用户输入的一串字符“翻译”
给操作系统,把字符串转化为操作系统可以“听懂的”语言,这样操作系统就能听懂用户的需求,可以进一步为用户服务了。
所以,根据这一理解,我们可以先设计一个框架,表明我们实现的shell的大致思路:
1)打印命令行提示
2)获取用户的输入字符串
3)分割命令字符串,并存储到全局的字符串数组:myg_argv
4)检查是否是内建命令,内建命令需要父进程自己执行
5)进程程序替换
如果不知道什么是进程程序替换,可以浏览这一篇:《[Linux]进程程序替换》
这也就表明了shell的运行机制,:
假如shell读取到“ls”,shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
三、模拟实现shell
由于我们当前实现的shell比较简单,目前没有分离编译最后链接,简单点说就是没有把声明和实现分离为源文件和头文件。而是简单的把一个一个的步骤封装为一个一个的函数。
1)打印命令行提示
我们在与Linux的bash(shell的一种)命令行进行交互的时候,会发现每一次输入命令的前面,都会有一个命令行提示:
这个命令行提示包含的信息有:
用户名+主机名+当前目录
幸运的是,这些信息我们可以直接从环境变量中获取。我们可以通过<stdlib.h>中的getenv()接口获得环境变量,获取了这些环境变量之后就好办了,只需要用printf(标准格式化输出)即可实现打印命令行提示。具体实现如下:
const char *get_username()
{
const char *name = getenv("USER");
if (name == NULL)
{
return "NONE";
}
return name;
}
const char *get_hostname()
{
const char *hostname = getenv("HOSTNAME");
if(hostname == NULL)
{
return "NONE";
}
return hostname;
}
const char *get_cwd()
{
const char *cwd = getenv("PWD");
if (cwd == NULL)
{
return "NONE";
}
return cwd;
}
void output_sng_line()
{
// 直接打印输出到stdout
const char *cwd = get_cwd();
if (strlen(cwd) != 1)//特殊处理根目录“/”的情况
{
printf("%s@%s:~%s$ ", get_username(), get_hostname(), cwd);
}
else
{
printf("%s@%s:%s$ ", get_username(), get_hostname(), cwd);
}
fflush(stdout); // 刷新缓冲区
}
总结:
通过库函数getenv获得环境变量,printf格式化输出即可;
输出的格式可以对照你的本地的命令行提示符。
上面的实例是按照我的阿里云服务器的命令行提示符来设计的,这是我的命令行提示符:
(包括普通目录和 特殊处理的根目录)
注意:获取的环境变量PATH是一串完整的路径,与上图的路径不同,这就需要我们对路径进行截取,由于通过函数进行修改需要传递二级指针,比较麻烦,所以我们可以考虑使用宏替换函数实现:
// 这样写do{}while(0):形成代码块;while(0)后面可随便带分号 #define Re_back(p) \ do \ { \ p += strlen(p) - 1; \ while (*p != '/') \ --p; \ } while (0)
修改后的函数具体实现:
void output_sng_line() { // 直接打印输出到stdout const char *cwd = get_cwd(); // 这个宏后面可以随便加分号,看起来更像一个函数 Re_back(cwd); if (strlen(cwd) != 1) { printf("%s@%s:~%s$ ", get_username(), get_hostname(), cwd); } else { printf("%s@%s:%s$ ", get_username(), get_hostname(), cwd); } fflush(stdout); // 刷新缓冲区 }
2)获取用户的输入字符串
我们需要再设计一个缓冲区,用来存储读取的命令行信息——usercommand,SIZE的大小可以自己设计,可以大一些,这样允许一次输入比较长的指令。
返回值表示获取命令是否成功,如果失败返回-1——如果失败,就没有运行的意义了,所以退出即可。
main函数接口调用:
获取用户的输入字符串 函数具体实现:
一个小细节,在读取的时候,由于我们最后输入指令的时候,会附带一个换行符,表示输入命令结束,这个换行符也会被读入,于是需要将缓冲区的 '\0'的位置提前一个位置。
int get_user_command(char line[], int n)
{
char *s = fgets(line, n, stdin);
if (s == NULL)
{
printf("err\n");
return -1;
}
// 处理字符串结尾的\n
line[strlen(line) - 1] = ZERO;
return 0;
// printf("%s\n",line);
}
3)分割命令字符串
main函数接口调用:
分割命令字符串具体实现:(复用库函数strtok(),字符串分割函数)
void partation_command(char *command, int n)
{
(void)n; // 暂时禁用n,防止编译老是提醒
myg_argv[0] = strtok(command, SKP);
int index = 1;
// 有意的设计 “=” :先赋值再判断;当srtok无法分割时,返回NULL,正好让g_argc最后一个元素为NULL
while ((myg_argv[index++] = strtok(NULL, SKP)))
{
}
}
4)检查是否是内建命令
1. 什么是内建命令
在Linux操作系统中,内建命令(builtin commands)是指那些直接由shell(如Bash、Zsh等)自身实现和提供的命令,而不是作为独立的可执行文件存在于文件系统中的命令。内建命令通常比外部命令(external commands)执行得更快,因为它们不需要启动一个新的进程来执行。
常见的内建命令
以下是一些常见的Bash内建命令:
cd
:更改当前工作目录。echo
:在标准输出上显示一行文本。exec
:用指定的命令替换当前的shell进程。等等....
其实,内建命令之所以必须是内建命令,一定有一定的原因:
以cd为例子,执行cd,如果父进程创建一个子进程让他帮自己执行,那么子进程的目录确实是切换了,但是父进程的目录还是没有变化啊!所以cd之所以必须是内建命令,是因为必须让父进程自身执行cd命令。让父进程自身切换工作路径,需要系统调用接口:
chdir()
main函数接口调用:
返回值表示:
1——是内建命令,父进程需要自己完成命令,则不需要执行第五步操作;
其他——不是内建命令,父进程可以通过创建子进程来让子进程替自己完成命令。
检查进程是否是内建命令其实是很简单朴素的,就是一个一个的条件判断,具体函数实现如下:
(这里也给出了CD内建命令的实现方式)
void _CD()
{
char *newpath = myg_argv[1];
// 说明只有一个cd,默认切换到家目录
if (newpath == NULL)
{
newpath = (char *)get_home();
}
chdir(newpath);
// 这个时候,环境变量没有被改变,使用cd的时候,虽然可以切换路径,但是getpath打印出来的不变
// path环境变量需要shell自己维护
char tem[SIZE];
getcwd(tem, sizeof(tem));
snprintf(cwd, sizeof(cwd), "PWD=%s", tem);
putenv(cwd);
}
int check_inbuildcommand_execute()
// 内建命令需要父进程执行,如果子进程执行cd,则子进程目录切换,父进程没切换
{
int yes = 0;
// myg_argv[0]一定是命令
char *inb_cmd = myg_argv[0];
// 检查是否是cd命令,
if (strcmp(inb_cmd, "cd") == 0)
{
yes = 1;
_CD();
}
else if (strcmp(inb_cmd, "echo") == 0 && strcmp(myg_argv[1], "$?") == 0)
{
yes = 1;
printf("%d\n", lastcode);
}
return yes;
}
5)进程程序替换
main函数接口调用:
父进程创建一个子进程帮自己干活,父进程只需要坐在一旁等着就行了:
void execute_command()
{
pid_t id = fork();
if (id == 0) // 子进程给父进程做事
{
int ret = execvp(myg_argv[0], myg_argv);
if (ret == -1)
{
exit(errno);
}
}
else // 父进程
{
int status = 0;
pid_t rid = waitpid(id, &status, 0); // 等待子进程退出
if (rid > 0) // 等待子进程成功
{
if (WIFEXITED(status))
{
lastcode = WEXITSTATUS(status);
}
else
{
printf("lastcode 不重要了,子进程被信号杀掉\n");
}
}
else
{
printf("waitpid fail");
exit(-1); // 父进程shell退出
}
}
}
最后不要忘了回收子进程的退出信息。
完~
未经作者同意禁止转载