【linux系统编程】实现shell

本文基于进程创建等知识实现简易版shell。介绍了提示符、命令行参数的输入与分割,用execvp创建子进程执行指令并循环操作。还阐述了ls可执行程序、切换路径、进程退出码三个细节问题,实现了重定向功能,最后给出了myshell完整代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

前面学过了进程创建/终止/等待/替换。现在根据所学的内容实现一个简易版的shell。

//头文件
  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>  
  5 #include<sys/wait.h>  
  6 #include<string.h>

1.提示符

这是我们在命令行上首先出现的内容。
在这里插入图片描述
注意在命令行输入指令,是在同一行的。

int main()  
{
     //这里也可以使用环境变量来获取这些内容
     printf("用户名@主机名 当前路径#"); 
	 //这里printf后面不能带\n,但是要把内容打印到显示器,因此必须刷新缓冲区。
     fflush(stdout);                                                                                                                                                                                                                                                                                                        
     return 0;                                                                                                                                                
 }  

2.命令行参数

在这里插入图片描述

前面说过ls -a -l是命令行参数,是一个整体的字符串,分割层一个个字符串传到main函数中。

在这里插入图片描述
因此,我们需要把输入的字符串也要分割成一个个字符串。

2.1输入指令

    7 #define NUM 1024
    8 #define OPT_NUM 64
    9 //全局变量
   10 char lineCommand[NUM];//存放输入的指令字符串
   11 char* myargv[OPT_NUM];//指针数组,存放分割的一个个字符串
   12 

这里使用fgets函数,把输入流放在指定数组。

在这里插入图片描述

    9 #define NUM 1024
   10 #define OPT_NUM 64
   11 
   12 char lineCommand[NUM];
   13 char* myargv[OPT_NUM];//指针数组,存放分割的一个个字符串
   14 
   15 int main()
   16 {
   17     //这里也可以使用环境变量来获取这些内容
   18     printf("用户名@主机名 当前路径# ");
   19     fflush(stdout);
   20     //"ls -a -l"-----> "ls" "-a" "-l"
   21     //这里减1,是为了极端情况下放\0
   22     char* s=fgets(lineCommand,sizeof(lineCommand)-1,stdin);
   23     assert(s);
   24     //测试一下                                                                                                                                                 
   25     printf("test:%s\n",lineCommand);
   26 
   27     return 0;
   28 }


在这里插入图片描述

发现这里空了一格。这是因为我们输入指令的时候,最后一次肯定敲的是\n,因此这里我们消除一下\n。

    9 #define NUM 1024
   10 #define OPT_NUM 64
   11 
   12 char lineCommand[NUM];
   13 char* myargv[OPT_NUM];//指针数组,存放分割的一个个字符串
   14 
   15 int main()
   16 {
   17     //这里也可以使用环境变量来获取这些内容
   18     printf("用户名@主机名 当前路径# ");
   19     fflush(stdout);
   20     //"ls -a -l"-----> "ls" "-a" "-l"
   21     //这里减1,是为了极端情况下放\0
W> 22     char* s=fgets(lineCommand,sizeof(lineCommand)-1,stdin);
   23     assert(s);
   24     //清除最后一个\n  abc\n                                                                                                                                    
   25     lineCommand[strlen(lineCommand)-1]=0;                                                                                                    
   26     //测试一下                                                                                                                               
   27     printf("test:%s\n",lineCommand);                                                                                                         
   28                                                                                                                                              
   29     return 0;                                                                                                                                
   30 }                  

在这里插入图片描述

2.2分割指令

在C语言的时候,学过一个分割字符串的函数,strtok

在这里插入图片描述

   28     //这里以空格为分隔符
   29     myargv[0]=strtok(lineCommand," ");
   30     //分割的是同一个字符串,下一次第一个参数就置为NULL就可以了
   31     // myargv[1]=strtok(NULL,"");
   32     //这里需要实现循环,注意strtok到字符串结束,会返回NULL,myargv[end]=NULL                                                                                    
   33     int i=1;
   34     while(myargv[i++]=strtok(NULL," "));

   36 //条件编译,测试分割是否成功
   37 #ifdef DEBUG
   38     for(int i=0;myargv[i];++i)
   39     {
   40         printf("myargv[%d]:%s\n",i,myargv[i]);
   41     }
   42 #endif    
  1 myshell:myshell.c
  2     gcc -o $@ $^ -std=c99 -DDEBUG //定义宏  不需要就前面加个#注释掉                                                                                                                               
  3 
  4 .PHONY:clean
  5 clean:
  6     rm -f myshell

在这里插入图片描述

3.创建子进程执行指令

前面我们学了6个替换函数。这里我们选择execvp最合适。

在这里插入图片描述

   44     //创建子进程
   45     pid_t id = fork();
   46     assert(id != -1);
   47     if(id == 0)
   48     {
   49         //子进程
   50         execvp(myargv[0],myargv);    
   51     }
   52     //父进程
   53     //这里先不关心退出码
   54     waitpid(id,NULL,0);

在这里插入图片描述

但是这里只能实现一次,因此,需要把整体循环起来。

	1#include<stdio.h>
    2 #include<stdlib.h>
    3 #include<unistd.h>
    4 #include<assert.h>
    5 #include<sys/types.h>
    6 #include<sys/wait.h>
    7 #include<string.h>
    8 
    9 #define NUM 1024
   10 #define OPT_NUM 64
   11 
   12 char lineCommand[NUM];
   13 char* myargv[OPT_NUM];//指针数组,存放分割的一个个字符串
   14 
   15 int main()
   16 {
   17     while(1)
   18     {
   19          //这里也可以使用环境变量来获取这些内容
   20          printf("用户名@主机名 当前路径# "); 
   21          fflush(stdout);
   22          //"ls -a -l"-----> "ls" "-a" "-l"
   23          //这里减1,是为了极端情况下放\0
   24          char* s=fgets(lineCommand,sizeof(lineCommand)-1,stdin);
   25          assert(s);
   26          //清除最后一个\n  abc\n
   27          lineCommand[strlen(lineCommand)-1]=0;
   28          //测试一下
   29          //printf("test:%s\n",lineCommand);
   30          //这里以空格为分隔符
   31          myargv[0]=strtok(lineCommand," ");
   32          //分割的是同一个字符串,下一次第一个参数就置为NULL就可以了
   33          // myargv[1]=strtok(NULL,"");                                                                                                                         
   34          //这里需要实现循环,注意strtok到字符串结束,会返回NULL,myargv[end]=NULL
   35          int i=1;
   36          while(myargv[i++]=strtok(NULL," "));
   37 
   38 //测试分割是否成功
   39#ifdef DEBUG
   40          for(int i=0;myargv[i];++i)
   41          {
   42              printf("myargv[%d]:%s\n",i,myargv[i]);
   43          }
   44 #endif
   45 
   46          //创建子进程
   47          pid_t id = fork();
   48          assert(id != -1);
   49          if(id == 0)
   50          {
   51              //子进程
   52              execvp(myargv[0],myargv);    
   53          }
   54          //父进程
   55          //这里先不关心退出码
   56          waitpid(id,NULL,0);
   57     }
   58     return 0;
   59 }

在这里插入图片描述

4.三个细节

4.1ls可执行程序问题

在这里插入图片描述
自己实现的shell,可执行程序没有颜色,

   36          //这里对ls,特殊处理                                                                                                                                
   37          if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0)
   38          {
   39              myargv[i++]=(char*)"--color=auto";                                                                                                                
   40          }  

在这里插入图片描述

4.2切换路径问题

在这里插入图片描述
我们发现,当我切换到上层路径时,发现当前路径没有变。这是为什么呢?

什么是当前路径?
在前面我们学过一个查看进程的命令ls /proc

在这里插入图片描述
exe----> 是当前进程执行的是磁盘路径下哪一个程序!
cmd----> 是当前进程的工作目录

什么是当前路径,默认是你在那个路径下把程序跑起来。本质就是当前进程的工作目录。

当前路径知道了,那为什么自己写的shell,cd的时候,路径没有变化呢?

子进程也有自己的工作目录,默认和父进程一样。fork之后,子进程执行cd命令,更改的是子进程的目录!子进程执行完毕之后,被回收了。继续使用的是父进程。然后再创建子进程。默认和父进程工作目录一样,这时执行pwd命令,所以当前目录没有变。

如果就想更改当前工作目录,系统提供chdir函数。
在这里插入图片描述

   1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 
  5 int main()
  6 {
  7     //想让工作目录是谁,就把谁的路径传过来
  8     chdir("/home/wdl");
  9     while(1)
 10     {
 11         printf("我是一个进程,我的id是:%d\n",getpid());
 12         sleep(1);
 13     }
 14                                                                                                                                                                  
 15     return 0;
 16 } 

在这里插入图片描述
修改一下自己的代码。

				//如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
        		//像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
   42          if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0)                                                                                                
   43          {                                                                                                                                                   
   44              if(myargv[1] != NULL)
   45              {
   46                  chdir(myargv[1]);
   47                  continue;                                                                                                                                     
   48              }                                                                                                                                               
   49          }   

在这里插入图片描述

4.3进程退出码

echo $?  //记录最近一个程序的进程退出码

在这里插入图片描述
具体实现

	//两个全局变量记录子进程退出信号,退出码
   12 int lastCode=0;
   13 int lastsig=0;
				//特殊处理
   57          if(myargv[0] != NULL && strcmp(myargv[0],"echo") == 0)                                                                                            
   58          {                                                                                                                                                 
   59             if(strcmp(myargv[1],"$?") == 0)                                                                                                                
   60             {                                                                                                                                              
   61                 printf("%d,%d\n",lastCode,lastsig);
   62             }
   63             else
   64             {
   65                 printf("%s\n",myargv[1]);
   66             }
   67             continue;                                                                                                                                          
   68          }    
		       
   78          //创建子进程
   79          pid_t id = fork();
   80          assert(id != -1);
   81          if(id == 0)
   82          {
   83              //子进程
   84              execvp(myargv[0],myargv);
   85              exit(1);
   86          }
   87          //父进程
   88          int status=0;
   89          pid_t ret= waitpid(id,NULL,0);
   90          assert(ret>0);
   				//记录退出信号,退出码
   91          lastCode=(status>>8)&0xFF;
   92          lastsig=status&0x7F;
   93     return 0;

在这里插入图片描述

5.重定向功能

在这里插入图片描述

//增加一个分割指令和文件名的函数                                                                                                                               
commandstrtok(lineCommand);

分割的时候,我们需要知道重定向是什么类型,文件是什么名字,因此增加两个全局变量来记录。

  //重定向类型    
  //第一个为初始重定向     
  #define DEFAULT_REDIR 0    
  #define INPUT_REDIR 1  
  #define OUTPUT_REDIR 2    
  #define APPEND_REDIR 3    
      
  //重定向类型+文件名    
  int redirType=DEFAULT_REDIR;    
  char* redirFile=NULL;    

分割函数

  //这里找文件名没有写成函数,而写个宏
 #define trimSpace(start) do{\
      while(isspace(*start)) ++start;\
  }while(0)
  
  void commandstrtok(char* cmd)
  {
      assert(cmd);
      char* start=cmd;
      char* end=cmd+strlen(cmd);

      while(start < end)
      {
          if(*start == '<')
          {
              *start=0;
              ++start;
              //这里可能 ls -a -l >      log.txt,
              trimSpace(start);
              redirType=INPUT_REDIR;
              redirFile=start;
              break;
          }
          else if(*start == '>')                                                                                                                                 
          {                                                                                                                                                      
              *start=0;                                                                                                                                          
              ++start;                                                                                                                                           
              if(*start == '>')                                                                                                                                  
              {                                                                                                                                                  
                  redirType=APPEND_REDIR;                                                                                                                        
                  ++start;                                                                                                                                       
              }                                                                                                                                                  
              else                                                                                                                                               
              {                                                                                                                                                  
                  redirType=OUTPUT_REDIR;                                                                                                                        
              }                                                                                                                                                  
              trimSpace(start);                                                                                                                                  
              redirFile=start;                                                                                                                                       
              break;                                                                                                                                             
          }                                                                                                                                                      
          else
          {                                                                                                                                                      
              ++start;
          }
      }

因为命令是子进程执行的,真正重定向的功能一定是由子进程来完成的,
但是如何重定向是父进程要告知给子进程的。

//创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{             
   switch(redirType)
   {
    	case INPUT_REDIR:
         {
             int fd=open("log.txt",O_RDONLY);
             if(fd <0)
              {
                  perror("open");
                  return 1;
               }
               //重定向文件已经打开了
                dup2(fd,0);
           }
           	 break;
           case OUTPUT_REDIR:
           case APPEND_REDIR:
           {
                umask(0);
                int flags=O_WRONLY|O_CREAT;
                if(redirType == OUTPUT_REDIR) flags|=O_TRUNC;
                else flags|=O_APPEND;                                                                                                                     
                int fd=open("log.txt",flags,0666);
                if(fd < 0)
                {
                 	perror("open");
                    return 1;
                 }
                  dup2(fd,1);     
             }
               break;
             default:
             printf("bug?\n");
               break;
     }
      //程序替换
      execvp(myargv[0],myargv);
      exit(1);
}

在这里插入图片描述

6.myshell完整代码

    1 #include<stdio.h>
    2 #include<stdlib.h>
    3 #include<unistd.h>
    4 #include<assert.h>
    5 #include<sys/types.h>
    6 #include<sys/stat.h>
    7 #include<fcntl.h>
    8 #include<sys/wait.h>
    9 #include<string.h>
   10 #include<ctype.h>
   11 
   12 #define NUM 1024
   13 #define OPT_NUM 64
   14 
   15 //重定向类型
   16 //第一个为初始重定向 
   17 #define DEFAULT_REDIR 0
   18 #define INPUT_REDIR 1
   19 #define OUTPUT_REDIR 2
   20 #define APPEND_REDIR 3
   21 
   22 //重定向类型+文件名
   23 int redirType=DEFAULT_REDIR;
   24 char* redirFile=NULL;
   25 
   26 int lastCode=0;
   27 int lastsig=0;
   28 
   29 char lineCommand[NUM];
   30 char* myargv[OPT_NUM];//指针数组,存放分割的一个个字符串
   31 
   32 //这里找文件名没有写成函数,而写个宏
   33 #define trimSpace(start) do{\                                                                                                                                  
   34     while(isspace(*start)) ++start;\
   35 }while(0)
   36 
   37 void commandstrtok(char* cmd)
   38 {
   39	  assert(cmd);
   40     char* start=cmd;
   41     char* end=cmd+strlen(cmd);
   42     while(start < end)                                                                                                                                         
   43     {
   44         if(*start == '<')
   45         {
   46             *start=0;
   47             ++start;
   48             //这里可能 ls -a -l >      log.txt,
   49             trimSpace(start);
   50             redirType=INPUT_REDIR;
   51             redirFile=start;
   52             break;
   53         }
   54         else if(*start == '>')
   55         {
   56             *start=0;
   57             ++start;
   58             if(*start == '>')
   59             {
   60                 redirType=APPEND_REDIR;
   61                 ++start;
   62             }
   63             else
   64             {
   65                 redirType=OUTPUT_REDIR;
   66             }
   67             trimSpace(start);
   68             redirFile=start;
   69             break;
   70         }
   71         else
   72         {
   73             ++start;
   74         }
   75     }
   76 
   77 }
   78 
   79 int main()
   80 {
   81    while(1)
   82    {
   83        //防止输入ls -a -l > log.txt,在输入ls -a -l还大概在重定向来执行
   84          redirType=DEFAULT_REDIR;
   85          redirFile=NULL;
   86          //这里也可以使用环境变量来获取这些内容
   87          printf("用户名@主机名 当前路径# "); 
   88          fflush(stdout);
   89          //"ls -a -l"-----> "ls" "-a" "-l"
   90          //这里减1,是为了极端情况下放\0
W> 91          char* s=fgets(lineCommand,sizeof(lineCommand)-1,stdin);
   92          assert(s);
   93 
   94         //增加重定向功能
   95         //"ls -a -l > log.txt"      "ls -a -l"  "log.txt"
   96         //"ls -a -l >> log.txt"     "ls -a -l"  "log.txt"
   97         //"cat < log.txt"           "cat"       "log.txt"
   98         //分割指令和文件名的函数
   99         commandstrtok(lineCommand);
  100 
  101          //清除最后一个\n  abc\n
  102		  lineCommand[strlen(lineCommand)-1]=0;                                                                                                                 
  103          //测试一下
  104          //printf("test:%s\n",lineCommand);
  105          //这里以空格为分隔符
  106          myargv[0]=strtok(lineCommand," ");
  107          //分割的是同一个字符串,下一次第一个参数就置为NULL就可以了
  108          // myargv[1]=strtok(NULL,"");
  109          //这里需要实现循环,注意strtok到字符串结束,会返回NULL,myargv[end]=NULL
  110          int i=1;
  111          //这里对ls,特殊处理
  112          if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0)
  113          {
  114              myargv[i++]=(char*)"--color=auto";
  115          }
  116         
  117          while(myargv[i++]=strtok(NULL," "));
  118         //如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
  119         //像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
  120          if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0)
  121          {
  122              if(myargv[1] != NULL)
  123              {
  124                  chdir(myargv[1]);
  125                  continue;
  126              }
  127          }
  128 
  129          if(myargv[0] != NULL && strcmp(myargv[0],"echo") == 0)
  130          {
  131             if(strcmp(myargv[1],"$?") == 0)
  132             {
  133                 printf("%d,%d\n",lastCode,lastsig);
  134             }
  135             else                                                                                                                                               
  136             {
  137                 printf("%s\n",myargv[1]);
  138             }
  139             continue;
  140          }
  141          
  142 //测试分割是否成功
  143 #ifdef DEBUG
  144          for(int i=0;myargv[i];++i)
  145          {
  146              printf("myargv[%d]:%s\n",i,myargv[i]);
  147          }
  148 #endif
  149 
  150          //创建子进程
  151          pid_t id = fork();
  152          assert(id != -1);
  153          if(id == 0)
  154          {
  155              
  156              switch(redirType)
  157              {
  158                  case DEFAULT_REDIR:
  159                      //什么都不做
  160                      break;
  161                  case INPUT_REDIR:
  162                      {
  163                          int fd=open("log.txt",O_RDONLY);
  164                          if(fd <0)
  165                          {
  166                              perror("open");
  167                              return 1;
  168                          }                                                                                                                                     
  169                          //重定向文件已经打开了
  170                          dup2(fd,0);
  171                      }
  172                      break;
  173                  case OUTPUT_REDIR:
  174                  case APPEND_REDIR:
  175                      {
  176                          umask(0);
  177                          int flags=O_WRONLY|O_CREAT;
  178                          if(redirType == OUTPUT_REDIR) flags|=O_TRUNC;
  179                          else flags|=O_APPEND;
  180                          int fd=open("log.txt",flags,0666);
  181                          if(fd < 0)
  182                          {
  183                              perror("open");
  184                              return 1;
  185                          }
  186                          dup2(fd,1);
  187                      }
  188                      break;
  189                  default:
  190                     printf("bug?\n");
  191                     break;
  192 
  193              }
  194              //子进程
  195              execvp(myargv[0],myargv);
  196              exit(1);
  197          }
  198          //父进程
  199          int status=0;
W>200          pid_t ret= waitpid(id,NULL,0);
  201          assert(ret>0);
  202          lastCode=(status>>8)&0xFF;
  203          lastsig=status&0x7F;
  204    }
  205          return 0;
  206 }
本书共分五部分,详细介绍了shell编程技巧,各种UNIX命令及语法,还涉及了UNIX下的文字处理以及少量的系统管理问题。本书内容全面、文字简洁流畅,适合Shell编程人员学习、参考。 目 录 译者序 前言 第一部分 shell 第1章 文件安全与权限 1 1.1 文件 1 1.2 文件类型 2 1.3 权限 2 1.4 改变权限位 4 1.4.1 符号模式 4 1.4.2 chmod命令举例 5 1.4.3 绝对模式 5 1.4.4 chmod命令的其他例子 6 1.4.5 可以选择使用符号模式或绝对模式 7 1.5 目录 7 1.6 suid/guid 7 1.6.1 为什么要使用suid/guid 8 1.6.2 设置suid/guid的例子 8 1.7 chown和chgrp 9 1.7.1 chown举例 9 1.7.2 chgrp举例 9 1.7.3 找出你所属于的用户组 9 1.7.4 找出其他用户所属于的组 10 1.8 umask 10 1.8.1 如何计算umask值 10 1.8.2 常用的umask值 11 1.9 符号链接 12 1.9.1 使用软链接来保存文件的多个映像 12 1.9.2 符号链接举例 12 1.10 小结 13 第2章 使用find和xargs 14 2.1 find命令选项 14 2.1.1 使用name选项 15 2.1.2 使用perm选项 16 2.1.3 忽略某个目录 16 2.1.4 使用user和nouser选项 16 2.1.5 使用group和nogroup选项 16 2.1.6 按照更改时间查找文件 17 2.1.7 查找比某个文件新或旧的文件 17 2.1.8 使用type选项 17 2.1.9 使用size选项 18 2.1.10 使用depth选项 18 2.1.11 使用mount选项 18 2.1.12 使用cpio选项 18 2.1.13 使用exec或ok来执行shell命令 19 2.1.14 find命令的例子 20 2.2 xargs 20 2.3 小结 21 第3章 后台执行命令 22 3.1 cron和crontab 22 3.1.1 crontab的域 22 3.1.2 crontab条目举例 23 3.1.3 crontab命令选项 23 3.1.4 创建一个新的crontab文件 24 3.1.5 列出crontab文件 24 3.1.6 编辑crontab文件 24 3.1.7 删除crontab文件 25 3.1.8 恢复丢失的crontab文件 25 3.2 at命令 25 3.2.1 使用at命令提交命令或脚本 26 3.2.2 列出所提交的作业 27 3.2.3 清除一个作业 27 3.3 &命令 27 3.3.1 向后台提交命令 28 3.3.2 用ps命令查看进程 28 3.3.3 杀死后台进程 28 3.4 nohup命令 29 3.4.1 使用nohup命令提交作业 29 3.4.2 一次提交几个作业 29 3.5 小结 30 第4章 文件名置换 31 4.1 使用* 31 4.2 使用? 32 4.3 使用[...]和[!...] 32 4.4 小结 33 第5章 shell输入与输出 34 5.1 echo 34 5.2 read 35 5.3 cat 37 5.4 管道 38 5.5 tee 39 5.6 标准输入、输出和错误 40 5.6.1 标准输入 40 5.6.2 标准输出 40 5.6.3 标准错误 40 5.7 文件重定向 40 5.7.1 重定向标准输出 41 5.7.2 重定向标准输入 42 5.7.3 重定向标准错误 42 5.8 结合使用标准输出和标准错误 43 5.9 合并标准输出和标准错误 43 5.10 exec 44 5.11 使用文件描述符 44 5.12 小结 45 ... ...
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值