对于很多学习后端的同学来讲,学习了C语言,发现除了能写出那个经典的“hello world”以外,其他什么都做不了,如果你在烦恼着这些事的话,不妨来学习下如何实现Linux中的shell吧,如此不仅能提高你C语言功力,也能增进你对系统的理解。
文章知识点要求
如果你还没有学习过这些知识,可以我的以前写的文章中学习。
shell是一种作为命令解释器的应用程序,用于用户与操作系统之间的交互。因为系统内核的指令过于复杂,而且如果让用户直接执行,会轻易使系统出现错误,而shell就是为了这种情况而诞生的。
在写代码之前我们还得了解一下系统的环境变量该如何在C语言中获取。我们有两种方式获取系统中的环境变量,用getenv函数来获取某一个环境变量的值,或者使用全局变量environ,这是一个指向了环境变量表的指针。
如果需要需要修改或增加环境变量,则可以使用putenv。
使用实例
#include
#include
#include
int main()
{
// putenv
char ptr[] = "PATH=/usr/bin;/home;/tmp;";
int n = putenv(ptr);
for(int i = 0; environ[i]; ++i)
{
printf("%s\n", environ[i]);
}
printf("%s\n", getenv("PATH"));
return 0;
}
总所周知,shell是无时无刻都在运行着的,并且如果我们在shell中运行一个程序错误了,一般也不会波及到shell。那么,我们可以通过让父进程来检查输入命令,子进程来实现用户指令的方式来实现。
#include
#include
#include
#include
#include
#include
#include
#define NUM 1024
#define SIZE 64
#define SEP " " //需要切割的字符
const char* getCwd() //获取当前路径
{
const char* str = getenv("PWD");
if(!str)
return "none";
int n = strlen(str) - 1;
while (n)
{
if(str[n] == '/')
break;
--n;
}
return str+n+1; //返回当前文件夹的名字
}
void commandSplit(char* usercommand, char** argv) //切割字符串
{
int n = 0;
argv[n++] = strtok(usercommand, SEP);
while(argv[n++] = strtok(NULL, SEP)); //继续切割
}
int execute(char** argv) //执行命令
{
pid_t id = fork();
if(id == -1)
return -1;
else if(id == 0)
{
execvp(argv[0], argv); //子进程执行命令。
exit(1);
}
else
{
int status = 0;
pid_t rid = waitpid(id, &status, 0); //阻塞等待
}
return 0;
}
int getUserCommand(char* usercommand, size_t n)
{
printf("> %s:", getCwd());
char* cmd = fgets(usercommand, n, stdin);
if(!cmd)
return -1;
n = strlen(cmd);
usercommand[n-1] = '\0';
return n;
}
int main()
{
while (1)
{
char usercommand[NUM];
char* argv[SIZE];
int n = getUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) continue; //输入错误
commandSplit(usercommand, argv); //切割
execute(argv); //执行
}
}
我们可以看到程序可以正常的执行ls、pwd等命令,但如果使用cd echo 等命令则出现了异常,而cd、echo等命令就是内建命令。
内建命令是类似于shell自己执行的命令,类似与内部函数的存在。如果要制作shell,必须得要完善一下内建命令。
char cwd[1024]; //存储当前的目录位置
int lastRet = 0; //上一次子进程完成后的返回码
int cd(const char* path)
{//更改当前的PWD环境变量
chdir(path); //更改当前的工作目录
char temp[512];
getcwd(temp, sizeof(temp));
sprintf(cwd, "PWD=%s", temp);
return putenv(cwd); //更新env中的变量
}
int doBuildin(char* argv[])
{
if(strcmp(argv[0], "cd") == 0)
{
char* path = argv[1];
if(!path)
path = getenv("HOME");
return cd(path);
}
else if(strcmp(argv[0], "export") == 0)
{
if(argv[1] == NULL)
return 0;
/*需要注意putenv函数在enval释放后,新增的变量也会同时消失。*/
char* enval = (char*)malloc(strlen(argv[1])+1);
strcpy(enval, argv[1]);
return putenv(enval);
}
else if(strcmp(argv[0], "echo") == 0)
{
if(*argv[1] == '$' && strlen(argv[1]) > 1) //参数为变量的情况
{
char* val = argv[1] + 1;
if(strcmp(val, "?") == 0) //获取上一次执行的返回码
{
printf("%d\n", lastRet);
lastRet = 0;
}
else
{
const char* enval = getenv(val); //获取环境变量
if(!enval)
printf("\n");
else
printf("%s\n", enval);
}
return 0;
}
else //非变量
{
printf("%s\n", argv[1]);
return 0;
}
}
return 1;
}
运行结果
#include
#include
#include
#include
#include
#include
#define NUM 1024
#define SIZE 64
#define SEP " " //需要切割的字符
char cwd[1024]; //存储当前工作目录
int lastRet = 0; //存储上一次指令的执行结果
const char* getCwd() //获取当前路径
{
const char* str = getenv("PWD");
if(strcmp(str, getenv("HOME")) == 0)
return "~"; //家目录的情况返回"~";
int n = strlen(str) - 1; //获取长度
while (n)
{
if(str[n] == '/')
break;
--n;
}
return n == 0 ? str : str + n + 1; //返回当前文件夹的名字
}
void commandSplit(char* usercommand, char** argv) //切割字符串
{
int n = 0;
argv[n++] = strtok(usercommand, SEP);
while(argv[n++] = strtok(NULL, SEP)); //继续切割
}
int execute(char** argv) //执行命令
{
pid_t id = fork();
if(id == -1)
return -1;
else if(id == 0)
{
execvp(argv[0], argv); //子进程执行命令。
exit(127);
}
else
{
int status = 0;
pid_t rid = waitpid(id, &status, 0); //阻塞等待
lastRet = WEXITSTATUS(status);
}
return 0;
}
int cd(const char* path)
{
chdir(path); //改变工作目录
char temp[512];
getcwd(temp, sizeof(temp)); //获取当前目录
sprintf(cwd, "PWD=%s", temp); //打印当前目录到PWD
return putenv(cwd);
}
int doBuildin(char* argv[]) //执行内建命令
{
if(strcmp(argv[0], "cd") == 0) //对比字符串,为0即相同
{
char* path = argv[1];
if(!path)
path = getenv("HOME");
return cd(path);
}
else if(strcmp(argv[0], "export") == 0)
{
if(argv[1] == NULL)
return 0;
/*注意:putenv在参数释放后,新增的变量内容也会随着消失,
所以这里使用了动态内存开辟*/
char* enval = (char*)malloc(strlen(argv[1])+1);
strcpy(enval, argv[1]);
return putenv(enval);
}
else if(strcmp(argv[0], "echo") == 0)
{
if(!argv[1]) //空参数
{
return 0;
}
else if(*argv[1] == '$' && strlen(argv[1]) > 1) //为参数是变量
{
char* val = argv[1] + 1;
if(strcmp(val, "?") == 0) //返回上一次指令的返回码
{
printf("%d\n", lastRet);
lastRet = 0;
}
else //参数不是变量
{
const char* enval = getenv(val);
if(!enval)
printf("\n");
else
printf("%s\n", enval);
}
return 0;
}
else
{
printf("%s\n", argv[1]);
return 0;
}
}
return 1;
}
int getUserCommand(char* usercommand, size_t n)
{
printf("> %s:", getCwd());
char* cmd = fgets(usercommand, n, stdin);
if(!cmd)
return -1;
n = strlen(cmd);
usercommand[n-1] = '\0';
return n;
}
int main()
{
while (1)
{
char usercommand[NUM];
char* argv[SIZE];
int n = getUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) continue; //输入错误
commandSplit(usercommand, argv); //切割
n = doBuildin(argv); //执行内建指令
if(!n) continue; //执行内建执行后
execute(argv); //执行指令
}
}
博客主页:主页
我的专栏:C++
我的github:github