Linux | 红帽认证 | IT技术 | 运维工程师
👇1000人技术交流 备注【公众号】更快通过
一. 获取输入
在构建简易Shell的时候我们首先就是要获取输入
获取环境变量:能够像shell一样运行会出现部分环境变量
获取用户输入:获取用户输入的指令
获取环境变量
在运行shell时就会出现一些环境变量,我们自定义构建的shell中,也可以实现这一步
用户名:pxt
主机名:hecs -
当前目录:myshell
// 获取环境变量 user,hostname,pwd,home
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 "none";
}
const char *getHostname()
{
const char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "none";
}
const char *getCwd()
{
const char *cwd = getenv("PWD");
if(cwd) return cwd;
else return "none";
}
printf("[%s@%s %s]$ ",getUsername(), getHostname(), getCwd());
这里我们直接将绝对路径展示了出来,当然没什么影响
这里我们用到了一个函数getenv(),这个函数用于获取环境变量的值,它的头文件是<stdlib.h>,在shell脚本中,获取环境变量的值是通过直接使用变量名来实现的,而不需要特别的函数或方法
在我们完成最基础的一步之后,我们要开始模拟我们使用的shell的使用方式,接下来一步就是获取用户输入
获取用户输入
获取用户输入时,我们可以创建一个字符数组用来存储用户的输入
char usercommand[NUM];
在获取用户输入时,输完一个指令后不换行,进行下一次输入。
但是在我们输入完成时,总会回车确认,因此我们需要修改最后一次输入
getUserCommand:
// 对用户输入进行封装
int getUserCommand(char *command, int num)
{
printf("[%s@%s %s]$ ",getUsername(), getHostname(), getCwd());
char *r = fgets(command, num, stdin); // 最后还是会输入'\n',回车
if(r == NULL) return -1; // TODO
// 将最后一次输入修改成'\0'
command[strlen(command)-1] = '\0'; // 这里不会越界
return strlen(command);
}
二. 分割字符串
在Shell中,分割字符串是一个常见的操作,它涉及到将一串包含多个子字符串(可能由空格、逗号、冒号等分隔符分隔)的文本分割成单独的部分,以便进行进一步的处理或赋值给不同的变量
在我们完成用户输入指令的读取之后,我们需要将字符串进行分割,让我们的指令能够被正确的读取并且实现出来,通常我们的分隔符是' '(空格)
char *argv[SIZE]; // 用来储存分割后的字符串
而关于分割字符串,我们在C语言的学习中可能提到过一个字符串函数strtok(),它的头文件是<string.h>,用于分割字符串。它基于一系列的分隔符来分割字符串,并返回指向被分割出的字符串的指针
char *strtok(char *str, const char *delim);
commandSplit:
void commandSplit(char *in, char *out[]) // in -> usercommand
{ // out -> argv
int argc = 0;
out[argc++] = strtok(in, SEP);
while(out[argc++] = strtok(NULL, SEP)); // 我们只需要将用户输入全部读取即可
// 用来测试是否能够成功分割字符串
int i = 0;
for(i = 0; out[i]; i++)
{
printf("%d:%s\n", i, out[i]);
}
}
在完成分割字符串之后,我们就可以用很久之前学习的进程的知识,让它运行起来的,但是仅仅如此我们的很多命令是根本无法实现的,所以我们还需要进行一步,检查内建命令!
三. 检查内建命令
内建命令(也称为内置命令)是由Shell(如Bash)自身提供的命令,而不是文件系统中的某个可执行文件。这些命令通常比外部命令执行得更快,因为它们不需要通过磁盘I/O来加载外部程序,也不需要创建新的进程
我们简单实现3个内建命令:cd,exprot,echo
doBuildin:
char cwd[1024]; // 存储
char enval[1024]; // for test
int lastcode = 0; // 存储上一条指令的返回信息
void cd(const char *pash)
{
chdir(pash);
char tmp[1024];
getcwd(tmp, sizeof(tmp));
sprintf(cwd, "PWD=%s", tmp); // 改变当前的路径
putenv(cwd); // 改变环境变量
}
int doBuildin(char *argv[])
{
if(strcmp(argv[0], "cd") == 0)
{
char *pash = NULL;
if(argv[1] == NULL) pash = homepath(); // 当我们cd之后不更任何输入时,
//我们直接返回家目录
else
{
pash = argv[1];
}
cd(pash);
return 1;
}
else if(strcmp(argv[0], "exprot") == 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; // argv[1]为'$',argv[1]+1则为'$'后的字符
if(strcmp(val, "?") == 0)
{
// lastcode 存储上一条指令的返回信息,初始状态为0
// 当程序执行时,出错时会改变lastcode的值,打印后,重新赋值为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;
}
}
return 0;
}
在内建命令的检查中,我们又会用到一些函数:
getcwd:用于获取当前工作目录的绝对路径
chdir:用于改变当前工作目录
chdir命令通常通过cd命令来实现,因为cd是chdir的别名,两者具有相同的功能。不过,在编程语言中(如C、PHP等),chdir则是一个具体的函数,用于在程序中动态改变当前工作目录
sprintf:用于将格式化的数据写入字符数组中
putenv:用于改变或增加环境变量内容的函数
四. 执行程序
在完成前面的铺垫之后,我们就可以创建进程来实现我们的程序了,在之前学习进程时,我们需要用到3个头文件
execute:
int execute(char *argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0)
{
// 子进程
execvp(argv[0], argv);
exit(1);
}
else{
// 父进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
// 将现在状态提供个lastcode
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
execvp是C语言(特别是在Unix和类Unix系统,如Linux)中用于执行外部程序的一个系统调用函数。这个函数通过查找环境变量(特别是PATH环境变量)来定位并执行指定的文件,同时将参数列表传递给该程序
以上就是对一些基本操作的封装,让我们看一下主函数main
main:
int main()
{
// shell是一个一直循环的程序
while(1)
{
char usercommand[NUM];
char *argv[SIZE];
// 获取输入
int n = getUserCommand(usercommand, sizeof(usercommand));
// 当获取输入时,返回一个小于0的数时,我们直接continue返回,不用往下继续走了
if(n <= 0) continue;
// 分割字符串 (strtok)
commandSplit(usercommand, argv);
// check Buildin
n = doBuildin(argv);
// 一般内建命令不会出错
if(n) continue;
// 执行命令
execute(argv);
}
return 0;
}
注意:当我们输入指令输出字符时,Backspace键是不能直接删除的,我们需要Ctrl + Backspace 组合键用于删除光标前的一个单词
我们来思考函数和进程之间的相似性
exec/exit就像call/return
一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信
这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间
一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值
五. 总结
在探索和学习编写Linux中简易shell脚本的旅程即将告一段落之际,我们不禁回望这段充满挑战与收获的时光。Shell脚本,作为Linux系统中不可或缺的一部分,以其强大的自动化能力和灵活的语法结构,成为了系统管理员、开发者以及任何希望提高工作效率的用户的得力助手
课程咨询添加:HCIE666CCIE
↑或者扫描上方二维码↑
你有什么想看的技术点和内容
可以在下方留言告诉小盟哦!
到此这篇shell编程实战(shell编程实例详解)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/shellbc/47925.html