目录
关于shell
1.打印提示符&&获取用户命令字符编辑
2.分割字符串
3.检查是否为内建命令
cd命令
export命令
echo命令
1.输出最后一个执行的命令的状态退出码(返回码)
2.输出指定环境变量
4.执行外部命令
关于shell
Shell 是计算机操作系统的一种命令行解释器,它允许用户与操作系统进行交互,执行各种操作和任务。Shell 接受用户输入的命令,并将其解释成操作系统能够理解的形式,然后将这些命令发送给操作系统内核执行。
Shell 的作用包括但不限于:
-
命令执行: 用户可以使用 Shell 来执行各种命令,包括系统命令、应用程序命令、脚本等,以完成各种任务和操作。
-
文件操作: 用户可以使用 Shell 进行文件和目录的创建、复制、移动、删除等操作,以及文件内容的查看、编辑等操作。
-
系统管理: 用户可以使用 Shell 进行系统资源的管理,包括进程管理、用户管理、权限管理等。
-
环境配置: 用户可以使用 Shell 来配置系统环境,包括设置环境变量、执行初始化脚本等。
-
脚本编程: 用户可以使用 Shell 编写脚本,实现自动化任务和流程控制,提高工作效率。
总的来说,Shell 提供了一个灵活而强大的界面,使用户能够通过简单的命令和脚本来与操作系统进行交互和控制,从而完成各种任务和操作。
1.打印提示符&&获取用户命令字符
首先,要自定义shell就必须要接收命令,可以看到我们在系统的shell中提示符有三部分:1.用户名 2.主机名 3.所在目录。所以我们自己的shell就必须先把这三个提示符打印出来,然后是接收用户输入的命令字符。而考虑到接收命令是一直持续的,所以我们用死循环来控制。
那如何在程序中获取用户名、主机名以及所在目录呢?这就需要环境变量了。
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,是操作系统为了满足不同的应用场景预先在系统内预先设置的一大批全局变量。
在操作系统里,我们可以输入env、printenv、echo等命令来打印出环境变量:
可以看到系统输出了很多我们不认识的东西,但我们只需要找到需要的用户名(USER)、主机名(HOSTNAME)以及当前目录(PWD)。
然后我们就可以编写程序了。
从程序中获取环境变量,我们需要用到一个函数
char *getenv(const char *name);
这个函数将返回我们传给的 name,然后它将环境列表中搜索名为name的环境变量,如果name不是环境列表的一部分,它将返回 NULL,否则返回具有所求环境变量值的C字符串。
const char *getUsername()
{
const char *name = getenv("USER");
if(name) return name;
else return NULL;
}
const char *getHostname()
{
const char* hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return NULL;
}
const char *getCwd()
{
const char* cwd = getenv("PWD");
if(cwd) return cwd;
else return NULL;
}
我们编写了上面三个函数,分别可以返回用户名、主机名和当前目录路径。
下面我们创建一个函数getUserCommand,使其能够打印出提示符信息并获取用户命令字符,在此之前,我们需要在主函数中创建一个字符数组username以便存放用户命令字符和后续操作。
#define NUM 1024
char usercommand[NUM];
那么如何从进程中接收用户输入的字符呢?有的人可能会想到用scanf,但是我们要直到,scanf碰到空格会自动停止,所以我们需要用到fgets,以便从输入流中截取字符。
fgets函数返回值为我们传过去的s字符指针,如果获取字符失败,则返回NULL,参数中第一个元素是我们存储字符的字符串,第二个是要截取的字符串的长度,第三个是标准流,我们需要的是标准输入流stdin,这样getUserCommand函数的需要的参数也就确定了,一个是存放字符的数组usercommand,另一个是数组的长度sizeof(usercommand) 。
可这样就结束了吗?我们打印出获取的字符串会发现,总是会多一个换行符,这是因为每次输入的回车也被接收了,只需要将字符数组中的最后一个元素换成'\0'就好,然后可以返回字符数组的长度,以便判断是否成功获取到命令字符。
int getUserCommand(char *command,int num)
{
printf("[%s@%s %s] $",getUsername(),getHostname(),getCwd());
char * r = fgets(command,num,stdin);
if(r == NULL) return -1;
command[strlen(command)-1] = '\0';
return strlen(command);
}
//如果函数返回 -1 或 0 ,则代表获取命令字符失败
2.分割字符串
在C++里,我们通常可以用substr来进行字符串的分割,但是在这我们用的是C,所以只能用strtok来对字符串进行分割。
str是我们想要分割的字符串,delim是我们想要在哪截断的符号,如果没有找到delim或者扫描的字符串到达末尾空字符时,strtok将返回NULL,否则返回指向以delim开头的字符串(不包括delim)。
需要注意的是,strtok会在内部保留一个静态指针,用于记录当前分割的位置,通过将第一个参数设置为 NULL,来告诉strtok函数接着上次的位置进行分割。
在此之前,我们需要在主函数中创建一个存储字符串指针的数组,以便后续操作,定义SIZE代表这个数组最多能存储多少个命令。
#define SIZE 64 char *argv[SIZE];
接着我们确定函数的参数,第一个是之前接收的字符数组command,第二个是存储命令的argv数组,这里in代表command,out代表argv。
void commandSplit(char *in,char* out[])
{
int argc = 0;
out[argc++] = strtok(in,SEP);
while(out[argc++] = strtok(NULL,SEP));
#ifdef DEBUG
//用于测试 commandSplit是否生效,如果DEBUG被定义,则运行代码,未定义不运行
for(int i = 0;out[i];i++)
{
printf("%d:%s\n",i,out[i]);
}
#endif
}
通过定义DEBUG来测试commandSplit函数能否成功分割字符串。
3.检查是否为内建命令
内建命令是指直接内置在操作系统的命令行解释器(如Bash、CMD)中的命令,而不是外部可执行文件。这些命令不需要从磁盘加载,而是作为解释器的一部分而存在。它们通常提供了一些基本的操作,比如文件系统操作、环境控制等。在linux系统中,常见的内建命令有:cd、echo、pwd、exit、export等。
没有内建命令,我们的shell可以说就是个空格,接下来我们将在我们的shell中中加入内建命令,来让其可以做到一些基本的功能。
int doBuildin(char *argv[]);
//返回值
cd命令
因为正常用户输入的命令都是第一个空格前的字符串代表的是命令,后面的代表命令选项,例如ls -a -l,所以我们需要先判断命令的字符串是否等于"cd",后面的命令也同样如此,如果相等,开始下一步。
因为cd命令后面跟的都是路径,所以我们判断argv中的第二个字符串是否为空,如果为空的话,就让path等于家目录,如果不为空,就让path等于它,然后进行我们的cd命令。
if(strcmp(argv[0],"cd") == 0)
{
char *path = NULL;
if(argv[1] == NULL) path = homepath();
else path = argv[1];
cd(path);
return 1;
}
进入自己的cd函数后,我们需要通过一个函数chdir来改变当前的目录地址
然后我们需要记录下已经改变后的目录地址,然后将当前目录地址设置为环境变量
然后我们需要记录下已经改变后的目录地址,然后将当前目录地址设置为环境变量
如此我们就实现了第一个内建命令cd,它可以在改变目录地址的同时,修改环境变量中的PWD变量,使其跟当前目录同步。
export命令
export
是一个命令行命令,用于设置环境变量。在Unix/Linux系统中,环境变量是在操作系统中存储的一组键值对,它们可以影响系统和用户进程的行为。通过使用 export
命令,你可以将一个变量设置为环境变量,使其在当前会话中对所有后续运行的程序可见。
export命令的实现相对简单,只需判断第二个字符是否为空,如果为空的话,直接返回,不为空就设置一个全局变量enval,先将argv[1]拷贝给enval,再用putenv命令将enval中的内容添加到环境变量中。
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] == NULL) return 1;
strcpy(enval,argv[1]);
putenv(enval);
return 1;
}
echo命令
echo
是一个命令行命令,用于在终端输出文本或变量的值。它是一个非常简单但功能强大的命令,常用于脚本编程、调试以及与其他命令的组合使用。
首先要判断第二个字符串的第一个字符是否为$,并且第二个字符串的长度要大于1,如果满足继续向下走,不满足直接输出argv[1]的内容并返回。
向下走:将指针argv[1]+1,使其跳过第一个字符$,然后将剩下的指针赋值给val,然后对val进行判断,如果val的值等于"?",执行下面第一个,否则执行第二个。
1.输出最后一个执行的命令的状态退出码(返回码)
2.输出指定环境变量
else if(strcmp(argv[0],"echo") == 0)
{
if(argv[1] == NULL)
{
printf("\n");
return 1;
}
if(*(argv[1]) == '$' && strlen(argv[1]) > 1)
{
char *val = argv[1]+1;
if(strcmp(val,"?") == 0) //如果是? 打印最后一个执行的命令的状态退出码
{
printf("%d\n",lastcode);
lastcode = 0;
}
else
{
const char *enval = getenv(val);
if(enval) printf("%s\n",enval);
else printf("\n");
}
return 1;
}
但是这个函数不足的点在于,无论用户输入的内建命令成功与否,输入echo $?的结果都是0,除非上一个执行的命令是外部命令。
4.执行外部命令
上面的代码处理了当用户输入的命令是内建命令的情况,那么当用户输入外部命令时应该怎么办呢?
首先,我们创建一个execute函数用来封装处理外部程序,参数为命令字符数组argv,
然后我们可以用exec系列的函数来实现在程序内处理外部命令,那么这几个exec函数应该选哪个呢?
我们选择execvp函数,因为我们可以直接传递命令名称,让execvp去系统里找可执行文件,所以选择'p',而我们的命令名臣存储在了argv这一个数组中,所以选择'v'。
在execute函数中,我们先用fork创建子进程,让子进程去执行命令,然后父进程用waitpid来等待子进程的退出,并获取其退出码,无论子进程是否正常退出,则其状态退出码将存储在status中,然后用WEXITSTATUS来获取子进程的退出状态码。
int execute(char *argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0) //child
{
//exec command
execvp(argv[0],argv);
exit(1);
}
else //father
{
int status;
pid_t rid = waitpid(id,&status,0);
if(rid>0)
{
lastcode = WEXITSTATUS(status);
}
}
}
总代码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#define NUM 1024
#define SIZE 64
#define SEP " "
//#define DEBUG 1
//
char cwd[1024];
char enval[1024]; // for test
int lastcode = 0;
char *homepath()
{
char *home = getenv("HOME");
if(home) return home;
else
return (char*)".";
}
const char *getUsername()
{
const char *name = getenv("USER");
if(name) return name;
else return NULL;
}
const char *getHostname()
{
const char* hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return NULL;
}
const char *getCwd()
{
const char* cwd = getenv("PWD");
if(cwd) return cwd;
else return NULL;
}
int getUserCommand(char *command,int num)
{
printf("[%s@%s %s] $",getUsername(),getHostname(),getCwd());
char * r = fgets(command,num,stdin);
if(r == NULL) return -1;
command[strlen(command)-1] = '\0';
return strlen(command);
}
void commandSplit(char *in,char* out[])
{
int argc = 0;
out[argc++] = strtok(in,SEP);
while(out[argc++] = strtok(NULL,SEP));
#ifdef DEBUG
//用于测试 commandSplit是否生效,如果DEBUG被定义,则运行代码,未定义不运行
for(int i = 0;out[i];i++)
{
printf("%d:%s\n",i,out[i]);
}
#endif
}
int execute(char *argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0) //child
{
//exec command
execvp(argv[0],argv);
exit(1);
}
else //father
{
int status;
pid_t rid = waitpid(id,&status,0);
if(rid>0)
{
lastcode = WEXITSTATUS(status);
}
}
}
void cd(const char *path)
{
chdir(path);
char tmp[1024];
getcwd(tmp,sizeof(tmp));
sprintf(cwd,"PWD=%s",tmp);
putenv(cwd);
}
//什么叫做内建命令:内建命令就是bash自己执行的,类似与自己内部的一个函数
//1->yes 0->no -1->error
int doBuildin(char *argv[])
{
if(strcmp(argv[0],"cd") == 0)
{
char *path = NULL;
if(argv[1] == NULL) path = homepath();
else path = argv[1];
cd(path);
return 1;
}
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] == NULL) return 1;
strcpy(enval,argv[1]);
putenv(enval);
return 1;
}
else if(strcmp(argv[0],"echo") == 0)
{
if(argv[1] == NULL)
{
printf("\n");
return 1;
}
if(*(argv[1]) == '$' && strlen(argv[1]) > 1)
{
char *val = argv[1]+1;
if(strcmp(val,"?") == 0) //如果是? 打印最后一个执行的命令的状态退出码
{
printf("%d\n",lastcode);
lastcode = 0;
}
else
{
const char *enval = getenv(val);
if(enval) printf("%s\n",enval);
else printf("\n");
}
return 1;
}
else
{
printf("%s\n",argv[1]);
return 1;
}
}
else if(0){}
return 0;
}
int main()
{
while(1)
{
char usercommand[NUM];
char *argv[SIZE];
//1.打印提示符&&获取用户命令字符串获取成功
int n = getUserCommand(usercommand,sizeof(usercommand));
if(n <= 0) continue; //跳出循环
//2.分割字符串
commandSplit(usercommand,argv);
//3.check build-in command
n = doBuildin(argv);
if(n) continue;
//4.执行对应的命令
execute(argv);
}
}
这样我们就完成了一个简单的自定义shell程序了,还有很多内容可以进行开发。