Linux:手搓shell
之前学了一些和进程有关的特性,什么进程控制啊进程替换啊,我们来尝试自己搓一个shell()吧
首先我们观察shell的界面,发现centos的界面上有命令提示符:
[主机名@用户名+当前路径]
我们可以通过调用系统函数获取当前路径,调用环境变量来获取我们的用户名、主机名
//打印命令提示行的函数,命令提示行包括用户名+主机名+当前工作目录
//此为获取用户名
int checkChild();
void Redirection();
void checkRedir();
int length(char* arr[]);
const char* getUser(){
char* user=getenv("USER");
if(user){
return user;
}else{
return "None";
}
}
//此为获取主机名
const char* getHost(){
char* host=getenv("HOSTNAME");
if(host){
return host;
}else{
return "None";
}
}
//此为获取当前路径
const char* getPwd(){
static char cwd[SIZE];
if(getcwd(cwd,sizeof(cwd))!=NULL){
return cwd;
}else{
return "None";
}
}
大概就长这样:
但是我发现了一个问题:写这个程序的时候我的Linux系统是centos,可以通过环境变量获取
但是我吧系统换成ubantu的时候,ubantu的环境变量设置里默认没有hostname这个环境变量
解决这个办法有两种:
1.在环境变量里添加我们的hostname,手动添加
2.通过C语言的gethostname函数来获取
我们这里用一下第二个吧:
const char* getUser(){
static char hostname[1024];
if(gethostname(hostname,sizeof(hostname))==0){
return hostname;
}else{
return "userNone";
}
}
无语了。。。写错接口了
应该是这样:
const char* getUser(){
char* user=getenv("USER");
if(user){
return user;
}else{
return "usernameNone";
}
}
//此为获取主机名
const char* getHost(){
static char hostname[1024];
if(gethostname(hostname,sizeof(hostname))==0){
return hostname;
}else{
return "userNone";
}
}
//此为获取当前路径
const char* getPwd(){
static char cwd[SIZE];
if(getcwd(cwd,sizeof(cwd))!=NULL){
return cwd;
}else{
return "None";
}
}
然后再加入一个整合上面三个函数的接口:
//这是整合上面三者的函数
void MakeCommandLine(){
char line[SIZE];
const char* username=getUser();
const char* hostname=getHost();
const char* cwd=getPwd();
snprintf(line,sizeof(line), "[%s@%s %s]#", username, hostname, cwd);
//snprintf是给定大小的更安全的向内存空间中写入的printf(写到缓冲区)
printf("%s",line);
fflush(stdout);
//printf("[%s@%s %s]#",getUser(),getHost(),getPwd());
}
这个整合上面三个函数的接口中使用了snprintf(),平常我们打印字符串到显示器上使用的是printf(),根据我们之前学习的缓存区的概念,snprintf其实就是把数据输入到缓存区内,sprintf函数也可以,但ssnprintf函数更安全
然后再刷新输出
这样就可以正常显示了(我恨你ubantu)
命令提示符,就是提示我们输入命令。如何让我们自己写的shell获取我们输入的命令?
我们在输入指令时,指令是有选项和目标文件的,例如:ls的常用选项有ls -a, ls -l,我们发现命令本身和选项之间是有空格的,并且内核在拿到我们输入的指令时,需要先查找指令,再查找选项。两个重要的工作:获取字符串&打散我们的字符串
获取字符串:我们肯定知道不能用scanf,因为scanf不能读取空格,上面函数可以按照行获取输入的数据?
fgets()
如何打散我们的字符串:使用函数strtok()
合起来的效果就是:
getCommand()用来获取命令,其中的ZERO是一个宏定义,在用户输入\n的时候,要将\n当\0对待,使命令有效
commandSplit()来打散字符串,SEP也是一个宏定义,代表空格,意思是在in这个数据中,遇到空格就断开,下面被注释的一段是检验是否打散完成
我们获取完命令后,就要执行。我们之前学习了环境变量和进程替换的知识。我们不能输入的命令替代我们的这个shell的进程,不然就被覆盖了。所以我们要在这个shell里面写一个子进程,这个子进程来执行我们的命令
我们只需要把我们调用的子进程导入就好了
但是子进程除了调用我们本来有的命令,还可以使用我们Linux下的小特性:重定向
所以我们可以把重定向的功能也加进去:
//进程替换
int execute(){
pid_t id=fork();//创建子进程
if(id==0){
int fd;
printf("redir=%d\n",redir);
printf("filename=%s",filename);
if(redir==3){
fd=open(filename,O_RDONLY);
if(fd<0){
perror("open output file");
exit(EXIT_FAILURE);
}
dup2(fd,STDIN_FILENO);
close(fd);
}else if(redir==2){
fd=open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);
if (fd < 0) {
perror("open output file");
dup2(fd,STDIN_FILENO);
close(fd);
}else if(redir==2){
fd=open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);
if (fd < 0) {
perror("open output file");
exit(EXIT_FAILURE);
}
dup2(fd,STDOUT_FILENO);
close(fd);
}else if(redir==1){
fd=open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);
if (fd < 0) {
perror("open output file");
exit(EXIT_FAILURE);
}
dup2(fd,STDOUT_FILENO);
close(fd);
}else{
printf("son process dafult");
}
// printf("son process begining...");
execvp(argv[0],argv);//替换进程
exit(EXIT_FAILURE);//替换失败就会退出
}else{
int status=0;
pid_t rid=waitpid(-1,&status,0);
if(rid==id){//父进程在这里只需要等待子进程就好了
printf("wait success\n");
lastcode=WEXITSTATUS(status);
printf("%d",lastcode);
return 0;
}
}
return 0;
}
其中有一个bug是我一直在改的,在执行echo 1234 > log.txt 的时候,是应该echo的基本命令:打印到显示器上,还是执行重定向,把结果输出在文件内呢?
所以其中的redir就是一个判定的标准,在执行命令的时候,先判定它是不是Echo。如果是Echo则子执行我们设定的Echo命令如果不是Echo,那么执行重定向。
void checkRedir(){
//ls -a -l > log.txt
//ls -a -l >> log.txt
// char* filename=NULL;
int len=strlen(userCommand);
char* start=userCommand;
char* end=userCommand+len-1;
while(end>start){
if((*end)=='>'){
if(*(end-1)=='>'){
*(end-1)='\0';
filename=end+1;
SkipSpace(filename);//如果有空格,就跳过
redir=1;
break;
}else{
*end='\0';
filename=end+1;
SkipSpace(filename);
redir=2;
break;
}
}else if(*end=='<'){
*end='\0';
filename=end+1;
SkipSpace(filename);
redir=3;
break;
}else{
end--;
}
}
}
判断是不是echo:
//echo的内建命令
int echo(){
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{
char* enval=getenv(val);
if(enval){
printf("%s\n",enval);
}else{
printf("\n");
}
}
return 1;
}
if(redir!=0)return 0;
}
return 1;
}
除了Echo命令和重定向,我们还需要实现CD命令,也就是切换当前目录,并且在切换当前目录的同时改变我们命令提示符的当前路径选项:
//切换home路径
const char*Home(){
const char* home=getenv("Home");
if(home==NULL){
return "/";
}
return home;
}
//改变路径的函数
void cd(){
const char* path=argv[1];
if(path==NULL){
path=Home();//如果为空回到家目录
}
if(chdir(path)==0){
setenv("PWD",getcwd(NULL,0),1);//setenv函数会修改进程的环境变量;修改后只有当前进程及其子进程能够看到这些变化
}else{
perror("cd faild");
}
}
判断内建命令、环境变量里的命令的函数:
int checkChild(){
int yes=0;
const char* enter_cmd=argv[0];
if(strcmp(enter_cmd,"cd")==0){
yes=1;
cd();
}else{
if(strcmp(enter_cmd,"echo")==0){
if(redir==0){
return echo();
}
}
}
return 0;
}
大概就是这样。。。
放一下源码:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<errno.h>
#include<sys/wait.h>
#include<ctype.h>
#include<fcntl.h>//open函数的库,是POSIX的系统调用函数
#define SIZE 512
#define ZERO '\0'//剔除\n
#define SEP " "
#define NUM 32
char* filename;
int lastcode=0;
char* argv[SIZE];//被打散的命令存这里
char userCommand[SIZE];//输入的命令存这里
//打印命令提示行的函数,命令提示行包括用户名+主机名+当前工作目录
//此为获取用户名
int redir=0;
#define SkipSpace(pos) do{while(isspace(*pos)) pos++; }while(0)//跳过空格的函数
int checkChild();
void Redirection();
void checkRedir();
int length(char* arr[]);
const char* getUser(){
char* user=getenv("USER");
if(user){
return user;
}else{
return "usernameNone";
}
}
//此为获取主机名
const char* getHost(){
static char hostname[1024];
if(gethostname(hostname,sizeof(hostname))==0){
return hostname;
}else{
return "userNone";
}
}
//此为获取当前路径
const char* getPwd(){
static char cwd[SIZE];
if(getcwd(cwd,sizeof(cwd))!=NULL){
return cwd;
}else{
return "None";
}
}
//这是整合上面三者的函数
void MakeCommandLine(){
char line[SIZE];
const char* username=getUser();
const char* hostname=getHost();
const char* cwd=getPwd();
snprintf(line,sizeof(line), "[%s@%s %s]#", username, hostname, cwd);
//snprintf是给定大小的更安全的向内存空间中写入的printf(写到缓冲区)
printf("%s",line);
fflush(stdout);
//printf("[%s@%s %s]#",getUser(),getHost(),getPwd());
}
//获取用户命令
int getCommand(char userCommand[],int n){
char* s=fgets(userCommand,n,stdin);//使用fgets()函数获取命令
if(s==NULL){
return -1;
}
userCommand[strlen(userCommand)-1]=ZERO;
return strlen(userCommand);
}
//分散字符串
void commandSplit(char* in,char* out[]){//in是输入的字符串,out[]是打散的字符数组
int argc=0;
out[argc++]=strtok(in,SEP);//此处的SEP是宏定义,SEP是空格的意思
while((out[argc++]=strtok(NULL,SEP))!=NULL);
out[argc]=NULL;
#ifdef debug
int i=0;
for(i=0;out[i];i++){
printf("%s\n",out[i]);
}
#endif
}
//进程替换
int execute(){
pid_t id=fork();//创建子进程
if(id==0){
int fd;
printf("redir=%d\n",redir);
printf("filename=%s",filename);
if(redir==3){
fd=open(filename,O_RDONLY);
if(fd<0){
perror("open output file");
exit(EXIT_FAILURE);
}
dup2(fd,STDIN_FILENO);
close(fd);
}else if(redir==2){
fd=open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);
if (fd < 0) {
perror("open output file");
exit(EXIT_FAILURE);
}
dup2(fd,STDOUT_FILENO);
close(fd);
}else if(redir==1){
fd=open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);
if (fd < 0) {
perror("open output file");
exit(EXIT_FAILURE);
}
dup2(fd,STDOUT_FILENO);
close(fd);
}else{
printf("son process dafult");
}
// printf("son process begining...");
execvp(argv[0],argv);//替换进程
exit(EXIT_FAILURE);//替换失败就会退出
}else{
int status=0;
pid_t rid=waitpid(-1,&status,0);
if(rid==id){//父进程在这里只需要等待子进程就好了
printf("wait success\n");
lastcode=WEXITSTATUS(status);
printf("%d",lastcode);
return 0;
}
}
return 0;
}
//切换home路径
const char*Home(){
const char* home=getenv("Home");
if(home==NULL){
return "/";
}
return home;
}
//改变路径的函数
void cd(){
const char* path=argv[1];
if(path==NULL){
path=Home();//如果为空回到家目录
}
if(chdir(path)==0){
setenv("PWD",getcwd(NULL,0),1);//setenv函数会修改进程的环境变量;修改后只有当前进程及其子进程能够看到这些变化
}else{
perror("cd faild");
}
}
//echo的内建命令
int echo(){
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{
char* enval=getenv(val);
if(enval){
printf("%s\n",enval);
}else{
printf("\n");
}
}
return 1;
}
if(redir!=0)return 0;
}
return 1;
}
void checkRedir(){
//ls -a -l > log.txt
//ls -a -l >> log.txt
// char* filename=NULL;
int len=strlen(userCommand);
char* start=userCommand;
char* end=userCommand+len-1;
while(end>start){
if((*end)=='>'){
if(*(end-1)=='>'){
*(end-1)='\0';
filename=end+1;
SkipSpace(filename);//如果有空格,就跳过
redir=1;
break;
}else{
*end='\0';
filename=end+1;
SkipSpace(filename);
redir=2;
break;
}
}else if(*end=='<'){
*end='\0';
filename=end+1;
SkipSpace(filename);
redir=3;
break;
}else{
end--;
}
}
}
int checkChild(){
int yes=0;
const char* enter_cmd=argv[0];
if(strcmp(enter_cmd,"cd")==0){
yes=1;
cd();
}else{
if(strcmp(enter_cmd,"echo")==0){
if(redir==0){
return echo();
}
}
}
return 0;
}
int length(char* arr[]){
int i=0;
while(arr[i]!=NULL){
i++;
}
return i;
}
int main(){
while(1){
MakeCommandLine();
getCommand(userCommand,sizeof(userCommand));
redir=0;
filename=NULL;
checkRedir();
commandSplit(userCommand,argv);
if(checkChild())continue;
execute();
}
return 0;
}