简介
本文将详细介绍简易minishell的流程,minishell能实现的功能主要是shell的外部命令,也能实现文件的相关操作(对文件进行打开、关闭、读写)。
在这里有必要提一下 linux/Unix 命令一般分为内建命令和外部命令这两类,(内部命令:构建在shell内部;外部命令:指的是Linux系统中能够完成特定功能的脚本文件或二进制程序,每个外部命令对应了系统中的一个文件,是属于Shell解释器程序之外的命令,所以称为外部命令。)
内建命令和外部命令的主要区别就是是否创建进程
内建命令诸如 exit、echo、cd、history 等等,这些命令执行的速度非常快,因为这些命令在执行的过程中不会有别的进程被创建;
而外部命令诸如 ls、cat 等等,这些命令在执行的时候,shell进程会fork一个子进程,父进程随后挂起,然后在子进程中exec加载外部文件,子进程返回后,父进程才继续执行。
在shell中可以用type来区分:
正文
那么进入主题,首先我们要实现 shell 的外部命令,要知道 shell 就是一个命令行解释器。
那么我们实现简易 shell 的步骤可以分为以下四步:
1、读取缓冲区
2、解析输入
3、创建子进程
4、程序替换
在输入命令时,只提取相应的命令有一定的困难,所以在输入这里用到了正则表达式可以很好的帮我们舍弃前后的空字符以及不需要的多余的字符。
if (scanf("%[^\n]%*c", buf) != 1) {
getchar();
continue;
}
-
这里的正则表达式 %[^\n] 的意思是从缓冲区中取数据,遇到\n为止,scanf原本的特性是遇到空格就停止
-
%*c 意思是遇到\n停止,将\n后面的数据丢弃
-
在这里用 if 语句判断是用到 scanf 的返回值,scanf 的返回值是读取数据的个数,如果读取失败,避免缓冲区中的换行取不出来,导致死循环(防备直接回车的情况)
-
文件重定向的本质就是改变描述符下标所对应的文件描述信息。在解析重文件定向命令的时候,最需要注意的就是 ‘>’ 的个数,一个 ‘>’ 是清空重定向,两个 ‘>’ 是追加重定向。根据解析到不同的数目的 ‘>’ 来决定使用哪种重定向命令。
所以文件输出重定向命令分析可以分为以下几步:
①:解析命令判断是否有重定向符号
②:获取重定向文件名
③:以相应方式打开文件
④:重定向(dup2)
这里用到的 dup2 是一个系统调用接口,它的接口如下:
int dup2(int oldfd, int newfd);
//参数是两个描述符,功能是让newfd这个描述符改变它的指向(通过改变描述符里的描述信息),指向oldfd指向的文件。如果newfd这个文件本身就指向一个文件,那么操作后就将newfd原本指向的文件关闭。
一个进程运行起来之后,默认就打开了三个文件:
所以让 1 号文件描述符(标准输出)指向我们要操作的文件,来完成相应的操作。
fd = open(redirect_file, O_CREAT | O_WRONLY | O_TRUNC, 0664);
dup2(fd, 1);
这样就让标准输出指向了我们所操作的文件。 -
需要注意的是,解析命令的过程一定是在子进程里面进行,否则当一次命令执行完之后程序就会退出。(这是因为子进程运行完之后,子进程的资源都会释放,子进程退出,最终回到父进程中,父进程运行完导致程序退出)。
完整代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main()
{
char buf[1024] = {0};
while (1) {
printf("minishell:"); //minishell界面
fflush(stdout);
memset(buf, 0x00, 1024);
if (scanf("%[^\n]%*c", buf) != 1) {
getchar();
continue;
}
//创建子进程
int pid = fork();
if(pid < 0) {
perror("fork error");
return -1;
}else if (pid == 0) {
//重定向解析部分
//解析文件命令 ls >> abc.txt
char *str = buf;
int redirect_flag = 0; //重定向符 '>'的个数,一个是清空重定向,两个是追加重定向
char *redirect_file = NULL; //重定向文件名
int fd = 0;
//这个循环是用于找重定向符号的个数
while (*str != '\0') {
//如果当前字符是重定向符,则进入判断,将命令从这里截断
//前面是命令,后面是重定向信息
if(*str == '>') {
redirect_flag++;
*str = '\0';
if(*(str + 1) == '>') {
redirect_flag++;
}
str += redirect_flag;
//走完文件名前面的字符(包括空白字符)
while (*str != '\0' && isspace(*str)) {
*str++ = '\0';
}
//此时str已经走到文件名的起始处,所以此时把文件名赋给redirect_file
redirect_file = str;
//走完文件名
while (*str != '\0' && !isspace(*str)) {
*str++;
}
//文件名结尾要是结束符
*str = '\0';
continue;
}
str++;
}
if (redirect_flag == 1) {
fd = open(redirect_file, O_CREAT | O_WRONLY | O_TRUNC, 0664);
dup2(fd, 1);
}
else if (redirect_flag == 2) {
fd = open(redirect_file, O_CREAT | O_WRONLY | O_APPEND, 0664);
dup2(fd, 1);
}
//普通外部命令解析部分
//解析命令 ls -a
char *ptr = buf;
char *argv[32];
int argc = 0;
while (*ptr != '\0') {
if (!isspace(*ptr)) {
argv[argc++] = ptr;
//将ls读完,指向空白字符处
while (*ptr != '\0' && !isspace(*ptr)) {
ptr++;
}
}
//将空白字符全部替换为字符串结尾标志
*ptr = '\0';
ptr++;
}
//参数结尾必须为空,否则会解析命令失败
argv[argc] = NULL;
//子进程程序替换,运行相应的命令
execvp(argv[0], argv);
exit(0);
}
//等待子进程退出,防止僵尸进程
waitpid(pid, NULL, 0);
}
return 0;
}
附上运行结果:(程序在CentOS Linux release 7.3.1611版本运行)
PS:欢迎各路大神批评指正!!!