👀樊梓慕:个人主页
🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》
🌝每一个不曾起舞的日子,都是对生命的辜负
目录
前言
1.简单了解命令行解释器
2.为什么要手写一个命令行解释器?
3.命令行解释器脚本编写
3.1打印提示符
3.2获取用户输入
3.3解析用户输入(分割字符串)
3.4创建子进程进行程序替换
3.5内建命令的处理
3.5.1什么是内建命令?
3.5.2『 cd』
3.5.3『 export』
3.5.4『 echo』
3.6重定向
4.完整代码
前言
综合前面所学,我们今天来写一个经典的shell脚本,『 命令行解释器』。
欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。
=========================================================================
GITEE相关代码:🌟樊飞 (fanfei_c) - Gitee.com🌟
=========================================================================
1.简单了解命令行解释器
学习linux,我们最先接触到的就是命令行解释器,与windows这种注重用户体验以及简单易操作的操作系统不同,linux并没有设计出像windows一样的美观的图形化界面。
第一次打开linux系统,你看到的只有这一行孤零零的字符串,而这行字符串就是我们与linux系统进行交互的重要工具,它被称为命令行解释器。
我们可以通过不同的指令与linux系统进行交互,比如这样:
它是与计算机进行交互的一种文本界面,相比于图形用户界面,命令行界面更加灵活和高效。
这样的软件程序我们常常称其为Shell。
2.为什么要手写一个命令行解释器?
鉴于之前对于『 进程周边』的学习,包括进程创建,进程终止,进程等待,进程程序替换等,并且有关『 重定向』我们也有了一定的了解。
为了更好的『 理解与掌握』,我们需要搭建一个适合的『 应用场景』用来实践,通过自己手写一个简单的命令行解释器,我们可以『 更好地理解』这些概念。
3.命令行解释器脚本编写
3.1打印提示符
const char* HostName()
{
char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "None";
}
const char* UserName()
{
char *hostname = getenv("USER");
if(hostname) return hostname;
else return "None";
}
const char *CurrentWorkDir()
{
char *hostname = getenv("PWD");
if(hostname) return hostname;
else return "None";
}
int main()
{
// 输出提示符并获取用户输入的命令字符串"ls -a -l"
printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
}
3.2获取用户输入
因为用户输入中可能会有空格存在,所以获取用户输入我们采用fgets函数。
- 第一个参数是用于读取文本的字符数组的指针。
- 第二个参数是最大读取的字符数(包括换行符和空字符)。
- 第三个参数是要读取的文件流,这里我们传入标准输入stdin即可。
#define SIZE 1024
int main()
{
char commandline[SIZE];//声明用户输入的字符串
fgets(commandline, SIZE, stdin);//获取输入
commandline[strlen(commandline)-1] = 0; //清除最后的\n
return 0;
}
如果用户直接回车,传入一个\n怎么办?
所以我们可以这样设计:
#define SIZE 1024
int Interactive(char out[], int size)
{
// 输出提示符并获取用户输入的命令字符串"ls -a -l"
printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
fgets(out, size, stdin);
out[strlen(out)-1] = 0;
return strlen(out);
}
int main()
{
while(1)//命令行解释器的本质就是一直运行死循环获取指令
{
char commandline[SIZE];
// 1. 打印命令行提示符,获取用户输入的命令字符串
int n = Interactive(commandline, SIZE);
if(n == 0) continue;
}
}
我们将打印提示符与获取输入封装为一个函数,然后检测如果为空串则跳过本次循环。
3.3解析用户输入(分割字符串)
在获取用户输入后,我们要获取指令,然后根据参数执行具体操作。
首先我们对字符串进行分割,需要用到strtok函数:
- 第一个参数是要进行分割的字符串,
- 第二个参数为用于指定分割子字符串的分隔符字符串。
- 返回值为子字符串的指针
strtok的函数原型为char *strtok(char *s, char *delim),功能为“Parse S into tokens separated by characters in DELIM.If S is NULL, the saved pointer in SAVE_PTR is used as the next starting point. ” 翻译成汉语就是:作用于字符串s,以包含在delim中的字符为分界符,将s切分成一个个子串;如果,s为空值NULL,则函数保存的指针SAVE_PTR在下一次调用中将作为起始位置。
根据返回值,所以我们可以这样设计:
#define SEP " "
char *argv[MAX_ARGC];
void Split(char in[])
{
int i = 0;
argv[i++] = strtok(in, SEP); // "ls -a -l"
while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
if(strcmp(argv[0], "ls") ==0)//如果是ls指令
{
argv[i-1] = (char*)"--color";//加上后,颜色为auto
argv[i] = NULL;
}
}
3.4创建子进程进行程序替换
Linux操作系统中,命令行解释器为bash,bash执行命令往往创建一个子进程再进程程序替换为指定的进程去执行,这样做的好处就是确保bash的稳定运行,由于进程的独立性,当出现错误时,只有子进程会出问题,而bash进程不会受到任何影响。
主进程创建子进程,并使用execvp函数进行进程程序替换,最后父进程回收子进程的资源。
为了获取子进程的退出信息,定义一个全局变量lastCode。
int lashcode = 0;
void Execute()
{
pid_t id = fork();
if(id == 0)
{
// 让子进程执行命令
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id) lastcode = WEXITSTATUS(status);
}
有关进程程序替换函数『 execvp』详解请见:【Linux】进程周边007之进程控制-CSDN博客
3.5内建命令的处理
3.5.1什么是内建命令?
所谓内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件。
可以使用 type 来确定一个命令是否是内建命令。
通常来说,内建命令会比外部命令执行得更快,执行『 外部命令』时不但会触发磁盘 I/O,还需要 『 fork 』出一个单独的进程来执行,执行完成后再退出。
而执行内建命令相当于调用当前 Shell 进程的一个函数。
常见的内建命令有cd、export和echo。
3.5.2『 cd』
我们的shell还没有对内建命令进行单独处理,所以此时cd也被默认为外部命令执行。
让我们来看看现象:
这是为什么呢?
因为cd此时默认为外部命令,所以执行cd会创建子进程,子进程的当前工作路径被修改了,然后子进程子会被父进程回收,但是父进程myshell的工作路径并没有修改,这也就是为什么cd前后当前工作目录没有被修改的原因。
所以如果我们要改变父进程的工作路径,不能创建子进程!在父进程中对工作路径进行修改,用的是chdir函数,但是chdir并不会修改环境变量,如果不修改可能会造成如下图问题:
实际改了,但是提示符处并没有修改。
因为我们打印提示符那块打印当前工作目录用的是环境变量的获取,所以这里我们最好也将环境变量PWD一并修改。
解决方案如下:
void BuildinCmd()
{
if(strcmp("cd", argv[0]) == 0)
{
char *target = argv[1]; //cd XXX or cd
if(!target) target = Home();
chdir(target);//修改当前工作目录
char temp[1024];
getcwd(temp, 1024);//获取当前工作目录
snprintf(pwd, SIZE, "PWD=%s", temp);
putenv(pwd);//修改环境变量
}
}
3.5.3『 export』
void BuildinCmd()
{
if(strcmp("export", argv[0]) == 0)
{
if(argv[1])
{
strcpy(env, argv[1]);
putenv(env);
}
}
}
3.5.4『 echo』
void BuildinCmd()
{
if(strcmp("echo", argv[0]) == 0)
{
if(argv[1] == NULL) {
printf("\n");
}
else{
if(argv[1][0] == '$')
{
if(argv[1][1] == '?')//打印进程退出码
{
printf("%d\n", lastcode);
lastcode = 0;
}
else{
char *e = getenv(argv[1]+1);//打印环境变量
if(e) printf("%s\n", e);
}
}
else{
printf("%s\n", argv[1]);
}
}
}
}
当检测到是内建命令时,我们执行以上逻辑,不走执行外部命令的逻辑,所以我们将内建命令的处理进行封装,然后定义一个ret返回值,在执行外部命令之前先进行检测是否为内建命令,如果是返回1否则返回0,根据返回值判断是否跳过执行外部命令的函数:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
char *argv[MAX_ARGC];
char pwd[SIZE];
char env[SIZE];
int lastcode = 0;
const char* HostName()
{
char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "None";
}
const char* UserName()
{
char *hostname = getenv("USER");
if(hostname) return hostname;
else return "None";
}
const char *CurrentWorkDir()
{
char *hostname = getenv("PWD");
if(hostname) return hostname;
else return "None";
}
char *Home()
{
return getenv("HOME");
}
int Interactive(char out[], int size)
{
// 输出提示符并获取用户输入的命令字符串"ls -a -l"
printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
fgets(out, size, stdin);
out[strlen(out)-1] = 0;
return strlen(out);
}
void Split(char in[])
{
int i = 0;
argv[i++] = strtok(in, SEP); // "ls -a -l"
while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
if(strcmp(argv[0], "ls") ==0)//如果是ls命令
{
argv[i-1] = (char*)"--color";//加上后,颜色为auto
argv[i] = NULL;
}
}
void Execute()
{
pid_t id = fork();
if(id == 0)
{
// 让子进程执行命名
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id) lastcode = WEXITSTATUS(status);
//printf("run done, rid: %d\n", rid);
}
int BuildinCmd()
{
int ret = 0;
// 1. 检测是否是内建命令, 是 1, 否 0
if(strcmp("cd", argv[0]) == 0)
{
// 2. 执行
ret = 1;
char *target = argv[1]; //cd XXX or cd
if(!target) target = Home();
chdir(target);//修改当前工作目录
char temp[1024];
getcwd(temp, 1024);//获取当前工作目录
snprintf(pwd, SIZE, "PWD=%s", temp);
putenv(pwd);//修改环境变量
}
else if(strcmp("export", argv[0]) == 0)
{
ret = 1;
if(argv[1])
{
strcpy(env, argv[1]);
putenv(env);
}
}
else if(strcmp("echo", argv[0]) == 0)
{
ret = 1;
if(argv[1] == NULL) {
printf("\n");
}
else{
if(argv[1][0] == '$')
{
if(argv[1][1] == '?')//打印进程退出码
{
printf("%d\n", lastcode);
lastcode = 0;
}
else{
char *e = getenv(argv[1]+1);//打印环境变量
if(e) printf("%s\n", e);
}
}
else{
printf("%s\n", argv[1]);
}
}
}
return ret;
}
int main()
{
while(1)
{
char commandline[SIZE];
// 1. 打印命令行提示符,获取用户输入的命令字符串
int n = Interactive(commandline, SIZE);
if(n == 0) continue;
// 2. 对命令行字符串进行切割
Split(commandline);
// 3. 处理内建命令
n = BuildinCmd();
if(n) continue;
// 4. 执行这个命令
Execute();
}
return 0;
}
3.6重定向
重定向符号有三种情况:
- 输出重定向 >
- 追加重定向 >>
- 输入重定向 <
一般重定向指令由三部分内容构成:
ls -a > log.txt
左面为指令及参数,中间为重定向符号,最后为文件名。
(1)我们需要获取的是重定向的类型,这里我们可以宏定义为多个整型值,比如:
#define NoneRedir -1 //无重定向
#define StdinRedir 0 //输入重定向
#define StdoutRedir 1 //输出重定向
#define AppendRedir 2 //追加重定向
然后我们在定义一个全局变量,通过改变改变量的值,获取当前指令是那种类型的重定向:
int redir_type = NoneRedir; //初始化为无重定向
(2)获取文件名,定义一个全局变量:
char *filename = NULL;
(3)记得重定向符号与文件名之间可能会存在空格,我们可以设计一个宏用来跳过空格:
#define IgnSpace(buf,pos) do{ while(isspace(buf[pos])) pos++; }while(0)
while(0)的目的是可以在语句中结尾加;,可以消除宏与函数之间的这部分差异,让该宏看起来像个函数。
以上操作我们放在分割字符串之前进行, 然后在执行外部命令的函数中,程序替换之前,判断是否需要重定向,然后利用dup2函数替换对应的输入或输出。
思路如上,开始模拟实现:
#define NoneRedir -1
#define StdinRedir 0
#define StdoutRedir 1
#define AppendRedir 2
#define STREND '\0'
#define SEP " "
#define IgnSpace(buf,pos) do{ while(isspace(buf[pos])) pos++; }while(0)
int redir_type = NoneRedir;
char *filename = NULL;
void CheckRedir(char in[])
{
// ls -a -l
// ls -a -l > log.txt
// ls -a -l >> log.txt
// cat < log.txt
redir_type = NoneRedir; //初始化为无重定向
filename = NULL;
int pos = strlen(in) - 1;
while( pos >= 0 )
{
if(in[pos] == '>')
{
if(in[pos-1] == '>') //如果是追加重定向
{
redir_type = AppendRedir; //设置为追加重定向
in[pos-1] = STREND; //该位置设置为\0,方便后续切割字符串
pos++;
IgnSpace(in, pos); //跳过空格
filename = in+pos; //获取文件名
break;
}
else //否则为输出重定向
{
redir_type = StdoutRedir; //设置为输出重定向
in[pos++] = STREND; //该位置设置为\0,方便后续切割字符串
IgnSpace(in, pos); //跳过空格
filename = in+pos; //获取文件名
break;
}
}
else if(in[pos] == '<') //如果是输入重定向
{
redir_type = StdinRedir; //设置为输入重定向
in[pos++] = STREND; //该位置设置为\0,方便后续切割字符串
IgnSpace(in, pos); //跳过空格
filename = in+pos; //获取文件名
break;
}
else
{
pos--;
}
}
}
void Split(char in[])
{
CheckRedir(in);//分割字符串之前进行判断
int i = 0;
argv[i++] = strtok(in, SEP); // "ls -a -l"
while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
if(strcmp(argv[0], "ls") ==0)
{
argv[i-1] = (char*)"--color";
argv[i] = NULL;
}
}
void Execute()
{
pid_t id = fork();
if(id == 0)
{
//程序替换之前完成重定向处理
int fd = -1;
if(redir_type == StdinRedir)
{
fd = open(filename, O_RDONLY);
dup2(fd, 0);
}
else if(redir_type == StdoutRedir)
{
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC);
dup2(fd, 1);
}
else if(redir_type == AppendRedir)
{
fd = open(filename, O_CREAT | O_WRONLY | O_APPEND);
dup2(fd, 1);
}
else
{
// do nothing
}
// 让子进程执行命名
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id) lastcode = WEXITSTATUS(status);
}
4.完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
#define STREND '\0'
char *argv[MAX_ARGC];
char pwd[SIZE];
char env[SIZE];
int lastcode = 0;
#define NoneRedir -1
#define StdinRedir 0
#define StdoutRedir 1
#define AppendRedir 2
#define IgnSpace(buf,pos) do{ while(isspace(buf[pos])) pos++; }while(0)
int redir_type = NoneRedir;
char *filename = NULL;
const char* HostName()
{
char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "None";
}
const char* UserName()
{
char *hostname = getenv("USER");
if(hostname) return hostname;
else return "None";
}
const char *CurrentWorkDir()
{
char *hostname = getenv("PWD");
if(hostname) return hostname;
else return "None";
}
char *Home()
{
return getenv("HOME");
}
int Interactive(char out[], int size)
{
// 输出提示符并获取用户输入的命令字符串"ls -a -l"
printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
fgets(out, size, stdin);
out[strlen(out)-1] = 0;
return strlen(out);
}
void CheckRedir(char in[])
{
// ls -a -l
// ls -a -l > log.txt
// ls -a -l >> log.txt
// cat < log.txt
redir_type = NoneRedir; //初始化为无重定向
filename = NULL;
int pos = strlen(in) - 1;
while( pos >= 0 )
{
if(in[pos] == '>')
{
if(in[pos-1] == '>') //如果是追加重定向
{
redir_type = AppendRedir; //设置为追加重定向
in[pos-1] = STREND; //该位置设置为\0,方便后续切割字符串
pos++;
IgnSpace(in, pos); //跳过空格
filename = in+pos; //获取文件名
break;
}
else //否则为输出重定向
{
redir_type = StdoutRedir; //设置为输出重定向
in[pos++] = STREND; //该位置设置为\0,方便后续切割字符串
IgnSpace(in, pos); //跳过空格
filename = in+pos; //获取文件名
break;
}
}
else if(in[pos] == '<') //如果是输入重定向
{
redir_type = StdinRedir; //设置为输入重定向
in[pos++] = STREND; //该位置设置为\0,方便后续切割字符串
IgnSpace(in, pos); //跳过空格
filename = in+pos; //获取文件名
break;
}
else
{
pos--;
}
}
}
void Split(char in[])
{
CheckRedir(in);//分割字符串之前进行判断
int i = 0;
argv[i++] = strtok(in, SEP); // "ls -a -l"
while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
if(strcmp(argv[0], "ls") ==0)
{
argv[i-1] = (char*)"--color";
argv[i] = NULL;
}
}
int BuildinCmd()
{
int ret = 0;
// 1. 检测是否是内建命令, 是 1, 否 0
if(strcmp("cd", argv[0]) == 0)
{
// 2. 执行
ret = 1;
char *target = argv[1]; //cd XXX or cd
if(!target) target = Home();
chdir(target);//修改当前工作目录
char temp[1024];
getcwd(temp, 1024);//获取当前工作目录
snprintf(pwd, SIZE, "PWD=%s", temp);
putenv(pwd);//修改环境变量
}
else if(strcmp("export", argv[0]) == 0)
{
ret = 1;
if(argv[1])
{
strcpy(env, argv[1]);
putenv(env);
}
}
else if(strcmp("echo", argv[0]) == 0)
{
ret = 1;
if(argv[1] == NULL) {
printf("\n");
}
else{
if(argv[1][0] == '$')
{
if(argv[1][1] == '?')//打印进程退出码
{
printf("%d\n", lastcode);
lastcode = 0;
}
else{
char *e = getenv(argv[1]+1);//打印环境变量
if(e) printf("%s\n", e);
}
}
else{
printf("%s\n", argv[1]);
}
}
}
return ret;
}
void Execute()
{
pid_t id = fork();
if(id == 0)
{
//程序替换之前完成重定向处理
int fd = -1;
if(redir_type == StdinRedir)
{
fd = open(filename, O_RDONLY);
dup2(fd, 0);
}
else if(redir_type == StdoutRedir)
{
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC);
dup2(fd, 1);
}
else if(redir_type == AppendRedir)
{
fd = open(filename, O_CREAT | O_WRONLY | O_APPEND);
dup2(fd, 1);
}
else
{
// do nothing
}
// 让子进程执行命名
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id) lastcode = WEXITSTATUS(status);
}
int main()
{
while(1)
{
char commandline[SIZE];
// 1. 打印命令行提示符,获取用户输入的命令字符串
int n = Interactive(commandline, SIZE);
if(n == 0) continue;
// 2. 对命令行字符串进行切割
Split(commandline);
// 3. 处理内建命令
n = BuildinCmd();
if(n) continue;
// 4. 执行这个命令
Execute();
}
return 0;
}
=========================================================================
如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容
🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎
🌟~ 点赞收藏+关注 ~🌟
=========================================================================