【Linux】:实现一个简易的shell

目录

1.命令行提示符

2.命令行参数 

2.1 获取命令行参数

2.2 解析命令行参数

3.判断指令类型

3.1 模拟cd命令

3.2 模拟export和echo

bash的环境变量来源

4.外部指令的执行

1.命令行提示符

在我们输入指令前,终端界面一般有一个命令行提示符, 我们先实现这个功能,

我们这里把当前工作路径的全路径打印出来了,如果只要打印当前工作目录名,可以分割路径提取。

2.命令行参数 

2.1 获取命令行参数

如果我们用scanf读取输入的命令行参数,只会读到一个不含空白符的字符串,因为scanf会以空白符为结尾。我们可以看看,

所以我们不能用scanf读取命令行参数,可以用fgets, fgets - cppreference.com

注意:

puts遇到空字符停止输出,在输出字符串时会自动在字符串末尾(\0)加一个换行符。
gets()丢弃输入中的换行符,puts()在输出中添加换行符。

fgets()保留输入中的换行符,fputs()不在输出中添加换行符。

 我们编写函数Interact,用来实现用户和命令行交互的功能,使程序获取用户输入的命令行参数,

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include <assert.h>
    4 #include <string.h>
    5 #define LEFT "["
    6 #define RIGHT "]"
    7 #define LABEL "#"//root用户是#,普通用户是@
    8 #define LINE_SIZE 1024
    9 
   10 char commandline[LINE_SIZE];
   11  
   12 //获取用户名
   13 const char* getUsername()          
   14 {
   15     return getenv("USER");                                                        
   16 }                                     
   17                                                                   
   18 //获取主机名                                            
   19 const char* getHostname()                            
   20 {                                             
   21     return getenv("HOSTNAME");    
   22 }   
   23                     
   24 //获取当前工作路径                                           
   25 const char* getPwd()                                
   26 {
   27     return getenv("PWD");
   28 }
   29                                 
   30 void Interact(char* cline,int size)
   31 {               
E> 32     printf(LEFT"%s@%s %s"RIGHT""LABEL" ",getUsername(),getHostname(),getPwd());
   33     char *s = fgets(cline,size,stdin);             
   34     assert(s);//assert只在debug模式下作用,release版本下会被优化掉
   35     //所以变量s在release版本下,可能只定义而没有被使用。
   36     //编译器对于定义了但没有使用的变量可能会报warning
   37     //所以为了让编译器编过,我们加下面一句代码
   38     (void)s;//抵消编译器的一些报警
   39     
   40     //"ls -a -l\n\0"
   41     //fgets会将最后用作结束的换行符保存,我们要删除这个换行符
   42     cline[strlen(cline) - 1] = '\0';//"ls -a -l\0\0"
   43 }
   44 int main()
   45 {
   46     char commandline[LINE_SIZE];
   47     int quit = 0;
   48     while(!quit)
   49     {
   50          Interact(commandline,sizeof(commandline));
   51          printf("echo:%s\n",commandline);
   52     }
   53     return 0;
   54 }

 运行

我们可以一直向命令行中输入参数。

2.2 解析命令行参数

 用户输入参数(指令)后,shell会对该行参数进行解析,一般会将字符串进行分割,我们这里用strtok函数分割字符串。

我们用splitstring接口实现字符串解析的功能, 同时用一个for循环检测解析后的结果。

   10 #define ARGC_SIZE 32
   11 #define DELIMIT " \t"
   //其余部分代码同上
   47 int splitstring(char cline[],char *argv[])
   48 {
   49     //分割字符串cline
   50     int i = 0;
   51     argv[i++] = strtok(cline,DELIMIT);
W> 52     while(argv[i++] = strtok(NULL,DELIMIT));
   53     return i - 1;//返回分割后字符串个数
   54 }
   55 int main()
   56 {
   57     char commandline[LINE_SIZE];//
   58     char *argv[ARGC_SIZE];
   59     int quit = 0;
   60     while(!quit)
   61     {
   62         //1.打印命令行标识符并等待参数输入
   63         Interact(commandline,sizeof(commandline));
   64         printf("echo:%s\n",commandline);
   65 
   66         //commandline -> "ls -a -l\0" -> "ls" "-a" "-l"
   67         //2.解析参数,对子串进行分割                                              
   68         int argc = splitstring(commandline,argv);
   69         if(argc == 0) continue;
   70         //检测分割后的结果
   71         for(int i = 0;argv[i];i++) printf("[%d]:%s\n",i,argv[i]);
   72     }
   73     return 0;
   74 }

运行, 命令行提示符、用户与命令行交互、解析参数的功能实现了,现在我们需要根据用户输入的参数(指令),执行程序。

但在执行程序之前,我们必须对指令进行判断。

3.判断指令类型

我们之前学习过指令,Linux系统的指令一般可以分为两类 ,

一类是内部指令(builtin shell command),内部指令是指内建在shell中的指令,但我们执行该类指令时,不需要额外创建进程,所以内部指令执行的效率高。

另一类是外部指令(external shell command)。外部指令是指非内建于shell的指令,我们执行该类指令时,会额外创建一个进程。

我们可以通过指令type判断一个指令是否是内建指令。

type命令来自英文单词“类型”,其功能是用于查看命令类型,如需区分某个命令是Shell内部指令还是外部命令,则可以使用type命令进行查看。
原文链接:type命令 – 查看命令类型 – Linux命令大全(手册)

显示出文件路径的一般是外部命令,显示“is a shell builtin”是内部命令。

我们判断是否是内建命令,是一条一条判断的,

3.1 模拟cd命令

拿“cd”举例,cd + 目标路径,我们可以通过chdir系统调用,将当前的工作路径换为“目标路径”,

 注意,chdir只会更改进程的当前工作目录,只会影响当前进程及其子进程的工作目录,不会影响环境变量中的PWD,PWD 是由 shell 自动维护,而不是由内核直接管理。

对于典型的 shell,PWD 在你使用命令(如 cd)改变目录时会被更新,但是,直接用系统调用 chdir 不会自动更新 PWD。linux环境 调用chdir函数虽然更改了工作目录,但是加载动态库还是之前的目录下 - CSDN文库

所以当我们调用chdir更改当前工作目录后,还要对环境变量PWD进行修改。先通过调用getcwd()使全局变量pwd获取当前工作目录的绝对路径,然后将全局变量pwd拷贝到环境变量PWD中,可以通过getenv()的返回值获取对应环境变量的地址,再将字符串pwd写入getenv返回的指针指向的内存区域。

返回值为1,表示内部指令的执行。

同样的,我们还可以实现几个内建命令,

3.2 模拟export和echo

我们检验一下内建命令export,没有问题,我们先把代码中的检测注释掉,再检验一下echo,

看上去没有问题,我们用export增加一个环境变量,然后用echo打印该环境变量, 

很奇怪,没有打印新增的环境变量val_env,我们输入env指令检查一下,发现也没有新增的环境变量,但我们之前export新增命令后输入env,可以查看到新增环境变量,这是为什么呢?为什么中间输入echo指令查看环境变量,env就不显示新增环境变量了?

这是因为,当进程启动时,会专门开辟一块空间存储命令行参数和环境变量,同时用一个字符串指针数组管理这些环境变量,这个管理环境变量的字符串指针数组就叫做环境变量表(char* envrion[]),

当我们用putenv新增一个环境变量_argv[1],这时环境变量表会分配一个元素,也就是字符串指针指向这个新增的环境变量,这个新增的环境变量并没有添加到专门存储环境变量的内存空间中,而是在栈区(因为_argv[1]是一个全局变量),

当我们重新在命令行输入指令时,会刷新命令行参数表_argv(char* argv[]),如果argv会分割成两个字符串,第二个字符串就会覆盖原来的_argv[1],“echo $val_env”中的“$val_env”,就会覆盖原来的“export val_env=1111111”中的“val_env=1111111”,这样环境变量表environ就找不到原来新增的环境变量了val_env=1111111,因为被覆盖了。​​​​​​​

 为了防止再次输入命令会覆盖环境变量的问题,我们要自己维护一个存放环境变量的空间myenv,将新增的环境变量存放在myenv中,

这样就不会覆盖了。 (但再添加一个新的环境变量会覆盖旧的环境变量,大家也可以把自己维护的环境变量设置成二维数组的形式,在堆上申请空间)

上面的myenv属于自定义的环境变量表,具有全局属性,我们还可以自定义一个本地变量表,【Linux】:环境变量-CSDN博客 当我们用echo检查最近一个进程的退出码时,可增加一个if语句, 

bash的环境变量来源

当我们成功登录linux系统时,会启动一个shell进程(bash进程),我们在命令行运行的程序,实例化的进程的环境变量都是继承bash进程。bash进程的环境变量保存在用户目录的".bash.profile"文件中,该文件中保存了导入环境变量的方式。

4.外部指令的执行

 我们用externalExecute接口实现外部指令的执行,

5.完整代码

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define LEFT "["
#define RIGHT "]"
#define LABEL "#"//root用户是#,普通用户是@
#define LINE_SIZE 1024//命令行标识符的大小
#define ARGC_SIZE 32//字符串指针数组的字符串个数
#define DELIMIT " \t"//strtok()的分割符
#define EXIT_CODE 303//子进程程序替换失败后的退出码

extern char **environ;//环境变量
char commandline[LINE_SIZE];//命令行标识符
int lastcode = 0;//退出码
int quit = 0;//充当bash进程结束条件的判断
char *argv[ARGC_SIZE];//存储分割后的参数序列
char pwd[LINE_SIZE];//获取当前目录的工作路径
char myenv[LINE_SIZE];//环境变量存放空间

//获取用户名
const char* getUsername()
{
    return getenv("USER");
}

//获取主机名
const char* getHostname()
{
    return getenv("HOSTNAME");
}

//获取当前工作目录路径
void getpwd()
{
    getcwd(pwd,1024);
}

void Interact(char* cline,int size)
{
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABEL" ",getUsername(),getHostname(),pwd);
    char *s = fgets(cline,size,stdin);
    assert(s);//assert只在debug模式下作用,release版本下会被优化掉
    //所以变量s在release版本下,可能只定义而没有被使用。
    //编译器对于定义了但没有使用的变量可能会报warning
    //所以为了让编译器编过,我们加下面一句代码
    (void)s;//抵消编译器的一些报警
    
    //"ls -a -l\n\0"
    //fgets会将最后用作结束的换行符保存,我们要删除这个换行符
    cline[strlen(cline) - 1] = '\0';//"ls -a -l\0\0"
}

int splitstring(char cline[],char *_argv[])
{
    //分割字符串cline
    int i = 0;
    _argv[i++] = strtok(cline,DELIMIT);
    while(_argv[i++] = strtok(NULL,DELIMIT));
    return i - 1;//返回分割后字符串个数
}

void externalExecute(char* _argv[])
{

    pid_t id = fork();
    if(id < 0)
    {
        //子进程创建失败
        perror("fork");
        return;
    }
    else if(id == 0)
    {
        //子进程创建成功,开始执行子进程
        //采用程序替换执行命令
        //execvpe(_argv[0],_argv,environ);
        execvp(_argv[0],_argv);
        //程序替换成功,子进程的旧代码(也就是下一句代码)不会执行,
        //如果执行了,说明程序替换失败
        exit(EXIT_CODE);
    }
    else
    {
        //执行父进程,父进程等待回收子进程
        int status = 0;
        pid_t rid = waitpid(id,&status,0);
        if(rid == id) 
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}

int buildCmd(char* _argv[],int _argc)
{

    if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
    {
        printf("检验环境变量PWD是否改变:%s\n",getenv("PWD"));
        chdir(_argv[1]);
        getpwd();
        sprintf(getenv("PWD"),"%s",pwd);//修改环境变量PWD
        printf("检验环境变量PWD是否改变:%s\n",getenv("PWD"));
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
    {
        strcpy(myenv,_argv[1]);
        putenv(myenv);
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
    {
        if(strcmp(_argv[1],"$?") == 0)
        {
            printf("%d\n",lastcode);
            lastcode = 0;
        }
        else if(*_argv[1] == '$')
        {
            //如果是字符串打印环境变量
            char *val = getenv(_argv[1] + 1);
            if(val) printf("%s\n",val);//要判断一下环境变量是否存在
        }
        else
        {
            printf("%s\n",_argv[1]);
        }
        return 1;
    }

    // 特殊处理一下ls
    if(strcmp(_argv[0], "ls") == 0)
    {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }
    return 0;
}

int main()
{
    while(!quit)
    {
        //1.
        //2.获取命令行参数(指令)
        Interact(commandline,sizeof(commandline));
        //printf("echo:%s\n",commandline);

        //commandline -> "ls -a -l\0" -> "ls" "-a" "-l"
        //3.解析参数,对子串进行分割
        int argc = splitstring(commandline,argv);
        if(argc == 0) continue;
        //检测分割后的结果
        //for(int i = 0;argv[i];i++) printf("[%d]:%s\n",i,argv[i]);

        //4.判断指令
        int n = buildCmd(argv,argc);

        //5.外部指令的执行
        if(!n) externalExecute(argv);
    }
    return 0;
}

  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值