💻文章目录
- 📄前言
- 简易shell实现
- shell的概念
- 系统环境变量
- shell的结构定义
- 内建命令
- 完整代码
- 📓总结
📄前言
对于很多学习后端的同学来讲,学习了C语言,发现除了能写出那个经典的“hello world”以外,其他什么都做不了,如果你在烦恼着这些事的话,不妨来学习下如何实现Linux中的shell吧,如此不仅能提高你C语言功力,也能增进你对系统的理解。
文章知识点要求
- 系统环境变量
- 多进程编程(fork函数)
- 程序替换
- 程序等待
如果你还没有学习过这些知识,可以我的以前写的文章中学习。
简易shell实现
shell的概念
shell是一种作为命令解释器的应用程序,用于用户与操作系统之间的交互。因为系统内核的指令过于复杂,而且如果让用户直接执行,会轻易使系统出现错误,而shell就是为了这种情况而诞生的。
系统环境变量
在写代码之前我们还得了解一下系统的环境变量该如何在C语言中获取。我们有两种方式获取系统中的环境变量,用getenv函数来获取某一个环境变量的值,或者使用全局变量environ,这是一个指向了环境变量表的指针。
如果需要需要修改或增加环境变量,则可以使用putenv。
- getenv(const char* name ) -> char* :参数为环境变量的名字,返回名字对于的内容。
- putenv(char* string) -> int :参数为需要新增的环境变量.
- char** environ ; 包含在unistd.h,可以通过便利来遍历所有环境变量
使用实例
#include <stdlib.h>
#include <stdio.h>
#include <string.h>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中运行一个程序错误了,一般也不会波及到shell。那么,我们可以通过让父进程来检查输入命令,子进程来实现用户指令的方式来实现。
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>#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 <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>#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); //打印当前目录到PWDreturn 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