【Linux】打造你自己的 Linux Shell:编写简易 Shell 的入门教程
Shell 是一种提供用户与操作系统交互的命令行解释器,它接受用户的命令并调用操作系统的功能来执行这些命令。Shell 既可以作为一种交互式的命令行工具,又可以作为编写和运行脚本的编程环境。广泛使用于 Unix 和 Linux 系统中,Shell 也在其他操作系统中有类似的实现。
为了实现这么一个简易版本的自定义shell我们需要的知识有进程控制,进程等待,进程程序替换。学完这些我们就能给实现一个自己的简易shell。这些前置知识可翻阅我的往期文章。
1.准备阶段
在准备阶段我们就需要把下面的代码都写上,至于为什么在后续的代码会讲解。
1.2 头文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdbool.h>
#include <sys/wait.h>
#include <sys/types.h>
这些都是必要的头文件,后续的函数都会用到。
1.2 提供环境变量的函数
使用const是因为,这些字符串都是只读的,不需要修改。
char* HOME()
{
char* home = getenv("HOME");
if(home == NULL) return "None";
else return home;
}
const char* USER()
{
char* user = getenv("USER");
if(user == NULL) return "None";
else return user;
}
const char* HOSTNAME()
{
char* hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
else return hostname;
}
const char* PWD()
{
char* pwd = getenv("PWD");
if(pwd == NULL) return "None";
else return pwd;
}
1.3 标识常量与全局变量
#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
char* argv[SIZE];
char env[SIZE];
char pwd[SIZE];
int lastcode = 0;
2.实现shell
为了实现一个这么一个简易shell,首先我们先来观察它的操作页面。
观察这张图片我们可以ubuntu@VM-20-9-ubuntu:~/myShell$
这段信息那么它们分别是什么意思呢?从左到右:
ubuntu是用户名,VM-20-9-ubuntu是主机名,~/myShell是当前路径。这些信息都可以在环境变量中找到。
echo $USER
echo $HOSTNAME
echo $PWD
ubuntu@VM-20-9-ubuntu:~/myShell$ echo $USER
ubuntu
ubuntu@VM-20-9-ubuntu:~/myShell$ echo $PWD
/home/ubuntu/myShell
ubuntu@VM-20-9-ubuntu:~/myShell$ echo $HOSTNAME
VM-20-9-ubuntu
在C语言中我们可以通过函数getenv()
来帮助我们拿到这些环境变量。那么上面我们写的提供环境变量的函数就起到了作用了。
printf("[%s@%s%s]$",USER(),HOSTNAME(),PWD());
//这个$是普通用户的意思,root用户为#
交互页面完成后就是获取用户输入的命令字符串了。我们可以在一个函数里实现。
2.1 交互界面
int Interactive(char in[],int size)
{
printf("[%s@%s%s]$",USER(),HOSTNAME(),PWD());
//开始输入命令行
fgets(in,size,stdin);
//去除最后的'\n’
in[strlen(in)-1] = 0;
return strlen(in);
}
提问:为什么需要返回值?
回答:用来判断用户是否进行了输入,字符串长度为0表示未输入。
2.2 对字符串进行切割
在获取了用户输入的命令后,我们就需要对字符串进行分割了。
当用户输入了ls -a -l
时。我们就需要将这个命令分割为"la","-a","-l"
。这是为了后续的进程程序替换而准备的。需要作为参数传给exec
函数。
void Split(char in[])
{
int i = 0;
argv[i++] = strtok(in,SEP);
while(argv[i++] = strtok(NULL,SEP));
if(strcmp(argv[0],"ls") == 0)
{
argv[i-1] = (char*)"--color";
argv[i] = NULL;
}
}
可能大家strtok
函数用的不多,不知道它是如何使用的,这就需要大家自己取去搜索下咯。
这里我们主要讲这段代码:
if(strcmp(argv[0],"ls") == 0)
{
argv[i-1] = (char*)"--color";
argv[i] = NULL;
}
加这段代码的目的是为了让我们在使用了ls
后看到这文件有颜色区分。
所以我们需要让argv[i-1] = (char*)"--color"
。
2.3 处理内建命令
提问:什么是内建命令?
回答:内建命令(Built-in Command) 是指由 shell 自身直接提供和执行的命令,而不是系统上独立的可执行程序(如 /bin/ls
这样的外部命令)。内建命令是 shell 的一部分,执行时不需要启动新进程。这使它们在执行某些操作时更加高效,尤其是那些涉及 shell 本身的行为或配置的操作。
因为这是shell自身提供的命令,所以我们无法直接调用可执行程序,需要自己实现。
常见的内建命令
不同的 shell(如 Bash、Zsh、Sh 等)可能提供不同的内建命令,但以下是一些常见的 Bash 内建命令:
cd
:更改当前工作目录。exit
:退出当前 shell。echo
:打印文本到终端。alias
:为命令创建别名。set
:设置或显示 shell 变量。export
:将 shell 变量导出为环境变量。pwd
:显示当前工作目录。history
:显示命令历史记录。read
:从标准输入读取输入。kill
:向进程发送信号(如终止信号)。type
:显示命令的类型(内建命令或外部命令)。
本篇文章不会实现太多的内建命令,只会涉及比较常见的几个内建命令的实现。
int Bulidcmd()
{
int ret = 0;
//ret 为0表示非内建命令,ret为1表示内建命令
if(strcmp("cd",argv[0]) == 0)
{
ret = 1;
char* target = argv[1];
if(target == NULL) target = HOME();
chdir(target);
char temp[SIZE];
getcwd(temp,SIZE);
int result = snprintf(pwd,SIZE,"PWD=%s",temp);
//格式化输入数据到指定的字符串当中。
if(result<0)
{
perror("snprintf()");
exit(1);
}
else if(result>=SIZE)
{
fprintf(stderr,"error");
exit(1);
}
putenv(pwd);
}
else if(strcmp("export",argv[0]) == 0)
{
ret = 0;
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;
}
可能这段代码的难点就是一些函数大家可能没有见过。
2.3.1 chdir()
chdir
是一个C语言中的标准库函数,用于更改当前工作目录。它的全称是 “change directory”(更改目录),常用于改变进程的当前工作路径。
2.3.2 getcwd()
getcwd
是 C 语言中的标准库函数,用于获取当前工作目录的绝对路径。它的全称是 “get current working directory”(获取当前工作目录)。该函数可以帮助程序在更改目录后获取当前的路径,或者在程序中随时查看当前的工作目录。
2.3.3 putenv()
putenv
是 C 语言中的标准库函数,用于设置或修改环境变量。它的全称是 “put environment”(设置环境)。通过 putenv
,你可以在程序运行时动态地添加或修改环境变量。
2.4 执行非自建命令
就是普通的进程程序替换。
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);
}
最后我们需要写一个死循环来执行这些函数
int main()
{
while(true)
{
//1.提供用户的交互界面,并获取用户输入的命令字串
char commandline[SIZE];
int n = Interactive(commandline,SIZE);
if(n == 0) continue;
//2.对字符串进行分割
Split(commandline);
//3.处理内建命令
n = Bulidcmd();
if(n) continue;
//执行非自建命令
Execute();
}
return 0;
}
3. 运行结果
ubuntu@VM-20-9-ubuntu:~/SHELL$ ./shell #后续为自定义shell的执行
[ubuntu@None/home/ubuntu/SHELL]$pwd
/home/ubuntu/SHELL
[ubuntu@None/home/ubuntu/SHELL]$ls
makefile shell shell.c shell.c~
[ubuntu@None/home/ubuntu/SHELL]$cd
[ubuntu@None/home/ubuntu]$pwd
/home/ubuntu
[ubuntu@None/home/ubuntu]$cd ^H^H^H^C
目前自定义shell存在的缺点:
- 内建命令未实现完整。
- 无法进行删除。
4.代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <stdbool.h>
#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
char* argv[SIZE];
char env[SIZE];
char pwd[SIZE];
int lastcode = 0;
char* HOME()
{
char* home = getenv("HOME");
if(home == NULL) return "None";
else return home;
}
const char* USER()
{
char* user = getenv("USER");
if(user == NULL) return "None";
else return user;
}
const char* HOSTNAME()
{
char* hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
else return hostname;
}
const char* PWD()
{
char* pwd = getenv("PWD");
if(pwd == NULL) return "None";
else return pwd;
}
int Interactive(char in[],int size)
{
printf("[%s@%s%s]$",USER(),HOSTNAME(),PWD());
//开始输入命令行
fgets(in,size,stdin);
//取出最后的'\n’
in[strlen(in)-1] = 0;
return strlen(in);
}
void Split(char in[])
{
int i = 0;
argv[i++] = strtok(in,SEP);
while(argv[i++] = strtok(NULL,SEP));
if(strcmp(argv[0],"ls") == 0)
{
argv[i-1] = (char*)"--color";
argv[i] = NULL;
}
}
int Bulidcmd()
{
int ret = 0;
//ret 为0表示非内建命令,ret为1表示内建命令
if(strcmp("cd",argv[0]) == 0)
{
ret = 1;
char* target = argv[1];
if(target == NULL) target = HOME();
chdir(target);
char temp[SIZE];
getcwd(temp,SIZE);
int result = snprintf(pwd,SIZE,"PWD=%s",temp);
if(result<0)
{
perror("snprintf()");
exit(1);
}
else if(result>=SIZE)
{
fprintf(stderr,"error");
exit(1);
}
putenv(pwd);
}
else if(strcmp("export",argv[0]) == 0)
{
ret = 0;
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)
{
//让子进程执行
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(true)
{
//1.提供用户的交互界面,并获取用户输入的命令字串
char commandline[SIZE];
int n = Interactive(commandline,SIZE);
if(n == 0) continue;
//2.对字符串进行分割
Split(commandline);
//3.处理内建命令
n = Bulidcmd();
if(n) continue;
//执行非自建命令
Execute();
}
return 0;
}
- 点赞
- 收藏
- 关注作者
评论(0)