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;
}
相关推荐
大树884 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠4 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质5 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush45 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5205 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz5 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工6 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智6 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩6 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_6 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化