13.番外:自定义Shell
一,设计
Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令。
设计思路:
-
获取命令行
-
解析命令行
-
建立一个子进程(fork)
-
替换子进程(execvp)
-
父进程等待子进程的退出(wait)
二,代码实现解析
伪代码:
int main()
{
do_bash_cin负责显示提示符,读取用户输入的命令
do_bash_parse解析命令,拆分为独立的参数
do_exec创建子进程并执行程序命令
}
main函数:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#define MAX_CMD 1024
char command[MAX_CMD];
int main(int argc, char *argv[])
{
while(1) {
if (do_bash_cin() < 0) // 调用do_face读取用户输入
continue; // 如果读取失败,继续下一次循环
do_exec(command); // 执行命令
}
return 0;
}
do_bash_cin()
int do_bash_cin()
{
memset(command, 0x00, MAX_CMD); // 将command数组清零
printf("minishell$ "); // 打印提示符
fflush(stdout); // 刷新输出缓冲区
if (scanf("%[^\n]%*c", command) == 0) { // 从标准输入读取一行命令
getchar(); // 读取并丢弃一个字符(换行符)
return -1; // 返回-1表示读取失败
}
return 0; // 成功读取命令,返回0
}
具体解析一下关于scanf()这一行:
do_exec()
int do_exec(char *buff)
{
char **argv = {NULL}; // 参数数组初始化为NULL
int pid = fork(); // 创建子进程
if (pid == 0)
{ // 子进程
argv = do_bash_parse(buff); // 解析命令行参数
if (argv[0] == NULL) {
exit(-1); // 如果没有命令,退出子进程
}
execvp(argv[0], argv); // 执行命令
}
else
{
waitpid(pid, NULL, 0); // 父进程等待子进程结束
}
return 0;
}
**char **argv = {NULL};**
这一行代码初始化了一个指向字符指针的指针,并将其设置为 **NULL**
。
这种初始化方式确保在使用前指针是有效的空指针,从而避免未初始化指针可能带来的错误。
ps:由此可见那么do_bash_parse一定是指向字符指针的指针
do_bash_parse()
char **do_bash_parse(char *buff)
{
int argc = 0; // 参数计数
static char *argv[32]; // 存储命令行参数的数组,最大支持32个参数
char *ptr = buff; // 指向命令字符串的指针
while(*ptr != '\0')
{
if (!isspace(*ptr)) // 如果当前字符不是空白字符
{
argv[argc++] = ptr; // 将参数的起始地址存储在argv中
while((!isspace(*ptr)) && (*ptr) != '\0') {
ptr++; // 跳过参数的所有字符
}
}
//if结构处理非空白字符
else
{
while(isspace(*ptr)) {
*ptr = '\0'; // 将空白字符替换为字符串结束符
ptr++; // 跳过所有连续的空白字符
}
}
}
argv[argc] = NULL; // 最后一个参数后置NULL
return argv; // 返回参数数组
}
解析函数:
Q1:static char *argv[32];为什么使用static呢?
A:如果不使用static,每次调用do_bash_parse函数时,argv数组都会重新创建和初始化,解析的结果无法保留。而我们需要在函数返回后仍然能够访问解析后的参数。
使用static可以确保返回的指针指向的数组在整个程序运行期间是有效的。
Q2:解析逻辑是什么?
A:
三,代码实现与效果
代码:这是用easy_shell打印的结果minishell$ cat test.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<ctype.h>
#include<sys/wait.h>
#define MAX_CMD 1024
char command[MAX_CMD];
int do_bash_cin()
{
memset(command,0x00,MAX_CMD);
printf("minishell$ ");
fflush(stdout);
if(scanf("%[^\n]%*c",command)==0)
{
getchar();
return -1;
}
return 0;
}
char **do_bash_parse(char *buff)
{
int argc=0;
static char *argv[32];
char *ptr = buff;
while(*ptr!='\0')
{
if(!isspace(*ptr))
{
argv[argc++]=ptr;
while((!isspace(*ptr))&&(*ptr)!='\0')
{
ptr++;
}
}
else
{
while(isspace(*ptr))
{
*ptr='\0';
ptr++;
}
}
}
argv[argc]=NULL;
return argv;
}
int do_exec(char *buff)
{
char **argv={NULL};
pid_t pid =fork();
if(pid==0)
{
argv=do_bash_parse(buff);
if(argv[0]==NULL)
{
exit(-1);
}
execvp(argv[0],argv);
}
else{
waitpid(pid,NULL,0);
}
return 0;
}
int main(int argc,char *argv[])
{
while(1)
{
if(do_bash_cin()<0) continue;
do_exec(command);
}
return 0;
}
效果:
问题:为什么输入ll不显示结果呢?
在代码中,**execvp**
函数试图执行 **ll**
命令,但是这个命令在标准的命令行环境中可能不存在。**ll**
是一个常用的 Linux/Unix 命令,用于显示目录中的文件列表及其详细信息,但是它通常是 **ls -l**
的别名
四,代码传递展现
-
首先,程序提示用户输入命令:
minishell$
-
用户输入命令
ls -al
并按下回车键。minishell$ ls -al
-
do_bash_cin
函数读取用户输入的命令,并存储在command
数组中。command[] = “ls -al\0”
-
main
函数调用do_exec
函数,将command
数组作为参数传递给它。 -
do_exec
函数创建一个子进程,并在子进程中调用execvp
函数来执行命令。 -
子进程调用
do_bash_parse
函数来解析命令,并得到参数数组argv
。argv[0] = “ls\0”
argv[1] = “-al\0”
argv[2] = NULL -
execvp
函数执行ls
命令,并将参数数组传递给它。此时,ls
命令被执行,显示当前目录下所有文件及其详细信息。 -
子进程执行完毕,返回到父进程,父进程调用
waitpid
函数等待子进程退出。
Q:请依据此详细阐述一下do_bash_parse()的while过程
A: