👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 前言
- 一、用户输入
- 二、指令分割
- 三、程序替换
- 3.1 外部命令
- 3.2 内建命令
- 3.2.1 cd
- 3.2.2 export
- 3.2.3 echo
- 四、总结及源码
前言
简单回顾一下往期知识,命令行解释器bash
只是一个”外壳程序",而操作系统则称为“内壳程序”,这是因为操作系统不相信用户,因此我们用户只能通过“外壳程序”将指令进行翻译给操作系统,操作系统再将结果通过“外壳程序”返回给用户。
以上图片来自于【往期博客】
由于目前学习到的知识有限,后面会慢慢更新相关接口 ~
一、用户输入
首先命令行bash
需要提示类似于:[用户名@主机名 当前目录]$
。我们可以使用以下系统调用接口来获取它们:
- 获取用户名
#include <unistd.h>
char *getlogin();
- 获取主机名
#include <unistd.h>
int gethostname(char *name, size_t len);
其中:
-
该函数的功能是将主机名复制到
name
指向的缓冲区中(字符数组),注意name
缓冲区应该足够大以容纳主机名。 -
第二个参数
len
是缓冲区的长度(数组长度)。 -
返回值:
- 成功返回
0
,并将主机名复制到name
指向的缓冲区中。 - 失败返回
-1
。
- 成功返回
- 获取当前工作目录路径
#include <unistd.h>
char *getcwd(char *buf, size_t size);
其中:
-
该函数的功能是将当前工作目录的绝对路径复制到
buf
指向的缓冲区中,并保证以空字符\0
结尾。注意:传递给getcwd()
的缓冲区应该足够大。 -
size
参数表示缓冲区的大小。 -
函数的返回值:
- 如果成功,
buf
指向的缓冲区地址。 - 如果失败,返回
NULL
。
- 如果成功,
有了以上接口,我们就可以用代码来实现了
接下来就应该轮到用户输入指令,本质就是输入字符串。
这里需要注意的是,由于我们输入的指令可以带选项,那么必定是带空格的(如ls -al
),而 scanf
默认遇到空格或者换行就不读取了。除非你使用修饰符配合scanf
函数
char str[100];
scanf("%[^\n]", str); // 可以读空格和换行
除以上方法以外,fgets
函数也可以读取空格和换行
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
其中:
- 第一个参数:用于存储从输入流中读取的数据。一般是一个字符数组。
- 第二个参数:计算整个字符数组的大小。
- 第三个参数:这个参数指定了从哪个文件流中读取数据。在大多数情况下,我们使用标准输入流,即键盘输入,因此会传递
stdin
。 - 返回值:读取数据成功时返回一个指向目标缓冲区的指针,如果读取失败或者到达文件结尾时返回
NULL
。
以上程序还有缺陷,那就是当我们输入完一条指令后,bash
把结果返回给我们后,会继续重复提示我们输入指令。而我们目前写的程序执行完一条指令后就退出进程。因此,以上程序应当是一个循环。
二、指令分割
当用户输入完指令,我们要进行指令分割为后面【程序替换】做准备。
由于一开始我们使用fgets
函数将最后的回车\n
给读取到了command
数组,因此我们要将其去掉(置为'\0'
即可)
接下来我们进行分割命令行参数,C
语言提供了字符串分割函数 strtok
。当然你也可以自己手撕一个hh
#include <string.h>
char *strtok(char *str, const char *delim);
其中:
str
:要分割的字符串,第一次调用时传入待分割的字符串,后续调用传NULL
继续分割该字符串,函数会继续在上一次调用的字符串中查找下一个标记的位置。delim
:分隔符的字符串,即用来确定标记边界的字符集合。strtok()
函数返回一个指向分割后的标记的指针,如果没有找到标记,则返回NULL
。
三、程序替换
3.1 外部命令
对于外部命令,shell
则会创建一个子进程,并在子进程中进行程序替换来执行这些命令。在执行完成后,Shell
会等待子进程退出,并获取子进程的退出码。
如上所示,有很多替换函数供我们选择,为了方便,尽量不要选择带l
,因为我们已经将命令行参数分割好了在字符指针数组argv
中,而无需一一列举;另外,我们也不要选择不带p
的,因为这样还需要我们自己去写完整的文件路径。
综上,我们可以使用execvpe
函数。另外,environ
是全局变量它是由标准C
库提供的,当用户登录时,shell
会读取用户目录下的.bash_profile
文件,里面保存了导入环境变量的方式。
如上所示,我们执行的命令确实起效了,但是还是有些缺陷,比如ls
显示出来的文件没有高亮;以及ll
(ls -l
重命名)没有效果,因此我们的代码还是可以再改造改造。
我们可以先来解决ls
显示的文件没有高亮的问题
因此,我们只需要对argv
数组添加一个命令行参数,也就是--color=auto
即可
最后来解决ll
未显示出结果的问题。
3.2 内建命令
什么是内建命令呢?比如以cd
为例,子进程执行cd
命令改变了子进程的工作目录,由于父子进程是相互独立的,子进程改变了,而父进程bash
却没有影响。因此,内建命令是不需要通过创建子进程来执行。
Linux
中有很多内建命令:
这里我只挑选一些来完善
3.2.1 cd
我们可以使用系统调用接口chdir
函数来改变当前进程的工作目录,并且它对于特殊的路径 ..
也可以完成对应的更改,但除了cd ~
和cd -
,分别是返回家目录和返回最近一次访问的目录,注意:家目录和最近一次访问目录可以通过环境变量来获取。
但需要注意的是:改变当前进程的工作目录不会直接影响环境变量 PWD
,我们需要手动更新。(以上环境变量只截取了部分)
步骤如下:
-
调用
getcwd
函数更新pwd
数组 -
将
pwd
替换掉原来环境变量PWD
的值即可
我们可以使用sprintf
函数来替换。sprintf
函数是 C 语言中的一个标准库函数,用于将格式化的数据写入一个字符串中。
#include <stdio.h>
int sprintf(char *str, const char *format, ...);
其中:
str
是一个指向字符数组的指针,指向需要修改的字符串- 后面的参数就和
printf
函数一样了
(以上环境变量只截取了部分)
3.2.2 export
这个看似非常简单,比如添加环境变量export x=333
,那么直接使用putenv(argv[1])
(其中argv[0]
表示export
,argv[1]
表示x=333
)
如果你是以上这样做法导致第一次添加可能成功,但第二次添加后,第一次添加的就没了。这是因为argv[1]
中的内容是不断变化的,第二次添加就覆盖了第一次添加。
正确做法:
-
一般用户自定义的环境变量,在
bash
中需要用户自己维护一个字符指针数组。 -
先将待添加的环境变量拷贝至指针数组
-
再从中读取,并调用
putenv
函数添加至环境变量表
3.2.3 echo
echo
首先需要能获取最近一次进程的退出状态
本应当返回ls进程的退出状态,而他原原本本返回了$?
- 我们打印环境变量,例如
$PATH
会出现什么都没输出的现象
- 输出字符串会带双引号的情况
四、总结及源码
所谓的shell
也是一个进程,它可以获取用户的输入,然后对用户的输入做分析。对于内建命令,shell
会直接调用函数来执行;而对于外部命令,shell
则会创建一个子进程,并在子进程中进行进程替换来执行相对应的命令。在执行完成后,shell
会等待子进程退出,并获取子进程的退出码。
- 获取源码:点击跳转