Linux自定义shell编写

这篇具有很好参考价值的文章主要介绍了Linux自定义shell编写。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


经过了创建进程,终止进程,进程等待和进程程序替换之后,
我们就可以借助这些知识实现一个简单的shell命令行解释器了

温馨提示:
建议大家自己写一遍,这些代码分块之后每一个函数都很简单,
不过实现过程中可能会有各种各样非常细枝末节的地方被我们所忽视
因此可能会发生一看就懂,一写就废的情况…

一.最终版本展示

输入命令行时想要删除字符时不能直接按backspace,而是要按ctrl+backspace才能成功删除

1.动图展示

Linux自定义shell编写,Linux学习之路,linux,shell,进程,cd,echo,export

2.代码展示

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//#define DEBUG 1
#define SEP " "

char cwd[1024]={'\0'};
int lastcode=0;//上一次进程退出时的退出码

char env[1024][1024]={'\0'};
int my_index=0;

const char* getUsername()
{
    const char* username=getenv("USER");
    if(username==NULL) return "none";
    return username;
}

const char* getHostname()
{

    const char* hostname=getenv("HOSTNAME");
    if(hostname==NULL) return "none";
    return hostname;
}

const char* getPwd()
{

    const char* pwd=getenv("PWD");
    if(pwd==NULL) return "none";
    return pwd;
}

//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{
    int i=0;
    usercommand[i++]=strtok(command,SEP);
    while(usercommand[i++]=strtok(NULL,SEP));
}

//解析命令行
void GetCommand(char* command,char* usercommand[])
{
    command[strlen(command)-1]='\0';//清理掉最后的'\0'
    CommandSplit(usercommand,command);
#ifdef DEBUG
    int i=0;
    while(usercommand[i]!=NULL)
    {
        printf("%d : %s\n",i,usercommand[i]);
        i++;
    }
#endif
}

//创建子进程,完成任务
void Execute(char* usercommand[])
{
    pid_t id=fork();
    if(id==0)
    {
        //子进程执行部分
        execvp(usercommand[0],usercommand);
        //如果子进程程序替换失败,已退出码为1的状态返回
        exit(1);
    }
    else
    {
        //父进程执行部分
        int status=0;
        //阻塞等待
        pid_t rid=waitpid(id,&status,0);
        if(rid>0)
        {
            lastcode=WEXITSTATUS(status);
        }
    }
}

void cd(char* usercommand[])
{
    chdir(usercommand[1]);
    char tmp[1024]={'\0'};
    getcwd(tmp,sizeof(tmp));
    sprintf(cwd,"PWD=%s",tmp);
    putenv(cwd);
    lastcode=0;
}   

int echo(char* usercommand[])
{
    //1.echo后面什么都没有,相当于'\n'
    if(usercommand[1]==NULL)
    {
        printf("\n");
        lastcode=0;
        return 1;
    }
    //2.echo $?  echo $PWD echo $
    char* cmd=usercommand[1];
    int len=strlen(cmd);
    if(cmd[0]=='$' && len>1)
    {
        //echo $?
        if(cmd[1]=='?')
        {
            printf("%d\n",lastcode);
            lastcode=0;
        }
        //echo $PWD
        else
        {
            char* tmp=cmd+1;
            const char* env=getenv(tmp);
            //找不到该环境变量,打印'\n',退出码依旧为0
            if(env==NULL)
            {
                printf("\n");
            }
            else
            {
                printf("%s\n",env);
            }
            lastcode=0;
        }
    }
    else
    {
        printf("%s\n",cmd);
    }
    return 1;
}

void export(char* usercommand[])
{
    //export
    if(usercommand[1]==NULL)
    {
        lastcode=0;
        return;
    }
    strcpy(env[my_index],usercommand[1]);
    putenv(env[my_index]);
    my_index++;
}

int doBuildIn(char* usercommand[])
{
    //cd
    if(strcmp(usercommand[0],"cd")==0)
    {
        if(usercommand[1]==NULL) return -1;
        cd(usercommand);
        return 1;
    }
    //echo
    else if(strcmp(usercommand[0],"echo")==0)
    {
        return echo(usercommand);
    }
    //export
    else if(strcmp(usercommand[0],"export")==0)
    {
        export(usercommand);
    }
    return 0;
}

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
        char* usercommand[1024]={NULL};
        //2.解析command字符串,放入usercommand指针数组当中
        GetCommand(command,usercommand);
        //3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
        int flag=doBuildIn(usercommand);
        //返回值!=0说明是内建命令,无需执行第4步
        if(flag!=0) continue;
        //4.创建子进程,交由子进程完成任务
        Execute(usercommand);
    }
    return 0;
}

二.具体步骤

1.打印提示符

Linux自定义shell编写,Linux学习之路,linux,shell,进程,cd,echo,export

const char* getUsername()
{
    const char* username=getenv("USER");
    if(username==NULL) return "none";
    return username;
}

const char* getHostname()
{
    const char* hostname=getenv("HOSTNAME");
    if(hostname==NULL) return "none";
    return hostname;
}

const char* getPwd()
{
    const char* pwd=getenv("PWD");
    if(pwd==NULL) return "none";
    return pwd;
}

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
    }
    return 0;
}

注意:
1.因为scanf默认读取到空格或者’\n’时就会停止继续读取,
可是我们命令行中要求读取到空格也不能停止,(否则我们的指令就无法带选项了,因为选项之间是用空格来分割的)
因此我们需要fgets函数

char command[1024]={'\0'};
fgets(command,sizeof(command),stdin);

从命令行当中读取一行字符串
Linux自定义shell编写,Linux学习之路,linux,shell,进程,cd,echo,export
2.因为我们用户输入的时候在最后的时候一定会输入一个’\n’,因此我们需要把’\n’置为’\0’

command[strlen(command)-1]='\0';//去除我们最后输入的'\n'

把’\n’置为’\0’的操作我们放到了下一个函数当中来完成

2.解析命令行

因为有些命令带有选项:例如 “ls -a -l”
我们在进行程序替换的时候需要分别传入"ls" “-a” "-l"这几个字符串,所以需要把用户输入的字符串分割为若干个字符串存放到一个指针数组当中,可以使用strtok字符串切割函数
Linux自定义shell编写,Linux学习之路,linux,shell,进程,cd,echo,export

#define DEBUG 1
#define SEP " "

//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{
    int i=0;
    usercommand[i++]=strtok(command,SEP);
    while(usercommand[i++]=strtok(NULL,SEP));
}

//解析命令行
void GetCommand(char* command,char* usercommand[])
{
    command[strlen(command)-1]='\0';//清理掉最后的'\0'
    CommandSplit(usercommand,command);
#ifdef DEBUG
    int i=0;
    while(usercommand[i]!=NULL)
    {
        printf("%d : %s\n",i,usercommand[i]);
        i++;
    }
#endif
}

我们可以使用条件编译来方便我们自由选择是否需要打印
解析命令行后的usercommand数组中的内容

3.分析是否是内建命令

1.shell对于内建命令的处理

Linux自定义shell编写,Linux学习之路,linux,shell,进程,cd,echo,export
下面我们就一起来实现一下
cd,echo,export这几个内建命令

2.cd命令

Linux自定义shell编写,Linux学习之路,linux,shell,进程,cd,echo,export
可是如果shell进行进程程序替换了,那么shell执行完之后不就没了吗?
因此shell执行内建命令时直接封装函数调用系统调用接口即可

内建命令的个数是有限的,所以shell是可以对内建命令进行穷举的

因此我们就能更好地理解内建命令了:

内建命令:不需要创建子进程执行,shell自己执行,本质就是调用系统调用接口

3.cd函数的实现

对于cd而言,我们可以调用chdir这个系统调用接口来改变当前进程的工作目录
Linux自定义shell编写,Linux学习之路,linux,shell,进程,cd,echo,export
同时也可以设置一个全局的char类型的数组cwd来保存当前路径
每次cd之后用cwd来记录新路径,然后通过putenv来修改环境变量PWD
让我们第一步打印的提示符中的PWD路径可以动态调整

//cwd:存放当前路径,是一个全局变量
char cwd[1024]={'\0'};

void cd(char* usercommand[])
{
	//1.chdir改变当前进程的工作目录
    chdir(usercommand[1]);
    //2.获取当前进程所在工作目录到tmp数组当中
    char tmp[1024]={'\0'};
    getcwd(tmp,sizeof(tmp));
    //3.把tmp数组中的内容格式化为"PWD=tmp数组中的内容"放到cwd数组当中
    sprintf(cwd,"PWD=%s",tmp);
    //4.导入环境变量
    putenv(cwd);
    //5.最后一次进程的退出码置为0(是为了echo $?获取最后一次进程的退出码的实现,跟cd无关)
    lastcode=0;
}  

注意:cwd数组必须是全局变量,如果cwd是局部变量,那么出了cd这个函数之后cwd数组就会销毁,其中的内容也将会消失
而我们putenv导入环境变量时其实是把cwd的地址放入了环境变量当中,
而不是拷贝一份这个cwd放入环境变量当中,因此cwd数组销毁时,对应导入的环境变量中的内容就不是原来的内容了

4.echo命令的实现

echo命令也是一个内建命令

echo 空串      打印换行
echo $ ?      打印上次进程退出时的退出码
echo $环境变量  打印环境变量
echo 字符串    打印字符串
注意:
echo $       打印$这个字符串
因此即使判断了$,还要继续判断$后面还有没有字符
//全局变量
int lastcode=0;//上一次进程退出时的退出码

int echo(char* usercommand[])
{
      //1.echo后面什么都没有,相当于'\n'
      if(usercommand[1]==NULL)
      {
          printf("\n");
          lastcode=0;
          return 1;
      }
      //2.echo $?  echo $PWD echo $
      char* cmd=usercommand[1];
      int len=strlen(cmd);
      if(cmd[0]=='$' && len>1)
      {
          //echo $?
          if(cmd[1]=='?')
          {
              printf("%d\n",lastcode);
              lastcode=0;
          }
          //echo $PWD
          else
          {
              char* tmp=cmd+1;
              const char* env=getenv(tmp);
              //找不到该环境变量,打印'\n',退出码依旧为0
              if(env==NULL)
              {
                  printf("\n");
              }
              else
              {
                  printf("%s\n",env);
              }
              lastcode=0;
          }
      }
      else
      {
          printf("%s\n",cmd);
      }
      return 1;
}

5.export命令的实现

export导入环境变量
注意:
1.刚才介绍cd函数的时候.我们说明了环境变量导入时其实是导入的对应字符串的地址
因此我们环境变量字符串必须要保证全局有效

2.由于我们可以导入很多环境变量,因此env需要是一个二维数组,同时还需要一个index下标来标记该数组当中已经导入过的环境变量

char env[1024][1024]={'\0'};
int my_index=0;

void export(char* usercommand[])
{
	//1.export后面什么都没跟,什么都不执行,直接返回即可
    if(usercommand[1]==NULL)
    {
        lastcode=0;
        return;
    }
    //2.要导入的环境变量拷贝到env数组当中
    strcpy(env[my_index],usercommand[1]);
    //3.将env数组当中的环境变量导入该进程当中
    putenv(env[my_index]);
    my_index++;
}

6.内建命令函数的实现

写好了cd,echo,export这几个函数之后,我们只需要在内建命令函数当中调用这几个函数即可

//返回值=0,说明不是内建命令
//返回值=1,说明是内建命令并且执行成功
int doBuildIn(char* usercommand[])
{
    //cd
    if(strcmp(usercommand[0],"cd")==0)
    {
        if(usercommand[1]==NULL) return -1;
        cd(usercommand);
        return 1;
    }
    //echo
    else if(strcmp(usercommand[0],"echo")==0)
    {
        return echo(usercommand);
    }
    //export
    else if(strcmp(usercommand[0],"export")==0)
    {
        export(usercommand);
    }
    return 0;
}

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
        char* usercommand[1024]={NULL};
        
        //2.解析command字符串,放入usercommand指针数组当中
        GetCommand(command,usercommand);
        
        //3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
        int flag=doBuildIn(usercommand);
        //返回值!=0说明是内建命令,无需执行第4步
        if(flag!=0) continue;
        
        //4.创建子进程,交由子进程完成任务
        Execute(usercommand);
    }
    return 0;
}

4.创建子进程通过程序替换执行命令

使用execvp这个函数来进行程序替换
带上v:因为我们的命令都是放在数组当中的
带上p:因为我们输入的都是系统指令,带上p才可以自动在环境变量中查找我们的命令
否则就要显式传入路径

注意lastcode的设置

//创建子进程,完成任务
void Execute(char* usercommand[])
{
    pid_t id=fork();
    if(id==0)
    {
        //子进程执行部分
        execvp(usercommand[0],usercommand);
        //如果子进程程序替换失败,已退出码为1的状态返回
        exit(1);
    }
    else
    {
        //父进程执行部分
        int status=0;
        //阻塞等待
        pid_t rid=waitpid(id,&status,0);
        if(rid>0)
        {
            lastcode=WEXITSTATUS(status);
        }
    }
}

5.循环往复即可

main函数加上while(1)死循环即可

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
        char* usercommand[1024]={NULL};
        //2.解析command字符串,放入usercommand指针数组当中
        GetCommand(command,usercommand);
        //3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
        int flag=doBuildIn(usercommand);
        //返回值!=0说明是内建命令,无需执行第4步
        if(flag!=0) continue;
        //4.创建子进程,交由子进程完成任务
        Execute(usercommand);
    }
    return 0;
}

三.shell运行原理

shell内部提取用户输入的命令行进行解析
判断是否是内建命令,
1.如果是内建命令的话,shell自己通过调用自己封装的函数来执行该命令
2.如果不是内建命令,shell创建子进程,通过程序替换来然子进程执行该命令
shell进程阻塞等待回收子进程的退出状态
然后循环往复

以上就是Linux自定义shell编写的全部内容,希望能对大家有所帮助!文章来源地址https://www.toymoban.com/news/detail-764986.html

到了这里,关于Linux自定义shell编写的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【运维工程师学习三】Linux中Shell脚本编写

    Shell程序有很多, 如 Korn shell(ksh)、Bourne Again shell(bash)、C shell(包括csh与tcsh) 等等, 各主要操作系统下缺省的shell: AIX下是 Korn Shell Solaris缺省的是 Bourne shell FreeBSD缺省的是 C shell HP-UX缺省的是 POSIX shell Linux缺省的是 Bourne Again shell 但这种在命令行中的命令是即时输出结果的,不

    2024年02月11日
    浏览(67)
  • Linux shell编程学习笔记14:编写和运行第一个shell脚本hello world!

     * 20231020 写这篇博文断断续续花了好几天,为了说明不同shell在执行同一脚本文件时的差别,我分别在csdn提供线上Linux环境 (使用的shell是zsh)和自己的电脑上(使用的shell是bash)做测试。功夫不负有心人,在其中一些实例中可以体现出zsh和bash的对脚本文件支持的差别,收

    2024年02月07日
    浏览(54)
  • Linux shell编程学习笔记24:函数定义和使用

    *20231113 补充了实例使用for循环来简化代码的内容 为了实现模块化设计和代码重用,很多编程语言支持函数或过程,Linux shell也支持函数定义和调用。 Linux shell中的函数与其它编程语言很多有相似之处,也有自己独特之处。 其中: function是 Shell 中的,专门用来定义函数

    2024年02月05日
    浏览(60)
  • Linux shell练习题——cd、ls、mkdir、mv、rm

    1. 使⽤ cd 切换到桌⾯ 2. 使⽤ touch 在桌⾯创建 a.txt b.txt c.txt 三个⽂件 3. 使⽤ mkdir 在桌⾯创建 hello ⽬录. 4. 使⽤ mv命令 b.txt c.txt 两个⽂件移动到 hello ⽬录下 5. 双击打开 a.txt, 在⽂件中填写内容 hello world. 6. 使⽤ cp 命令拷⻉ a.txt 到 abc.txt 中. 7. 使⽤ tree 查看下当前⽬录的⽬录

    2024年02月11日
    浏览(50)
  • Linux shell编程学习笔记15:定义数组、获取数组元素值和长度

     * 20231103 增加了 五、数组拼接或合并 数组是一种常见的数据结构。跟大多数编程语言一样,大多数Linux shell脚本支持数组,但对数组的支持程度各不相同,比如数组的维度,是支持一维数组还是多维数组?再如,数组元素的下标是从 0 开始还是从1开始?则因shell而异,下面

    2024年02月06日
    浏览(66)
  • 【Linux升级之路】3_Linux进程概念

    🌟hello,各位读者大大们你们好呀🌟 🍭🍭系列专栏:【Linux升级之路】 ✒️✒️本篇内容:认识冯诺依曼系统,操作系统概念与定位,深入理解进程概念(了解PCB),学习进程状态(创建进程、僵尸进程和孤儿进程),进程优先级进程切换(进程竞争性与独立性、并行与并

    2024年02月05日
    浏览(32)
  • Linux编写简易shell

    思路:​ ​ ​ 所以要写一个shell,需要循环以下过程:​ 获取命令行 解析命令行 建立一个子进程(fork) 替换子进程(execvp) 父进程等待子进程退出(wait) 实现代码:​         以上就是本文的全部内容,如果对你有帮助,欢迎点赞收藏转发评论! 

    2024年01月20日
    浏览(34)
  • Linux shell脚本编写

    一、常用shell脚本指令 echo: 输出指定的文本或变量值到标准输出。 read: 从标准输入读取用户输入,并将其保存到指定的变量中。 if: 执行条件语句,如果满足指定条件则执行特定操作,否则执行其他操作。 for: 循环执行特定操作,每次迭代更新变量值。 while: 循环执行

    2024年02月16日
    浏览(50)
  • 【Linux】Linux进程控制 --- 进程创建、终止、等待、替换、shell派生子进程的理解…

    柴犬: 你好啊,屏幕前的大帅哥or大美女,和我一起享受美好的今天叭😃😃😃 1. 在调用fork函数之后, 当执行的程序代码转移到内核中的fork代码后 ,内核需要分配 新的内存块 和 内核数据结构 给子进程, 内核数据结构包括PCB、mm_struct和页表,然后构建起映射关系 ,同时

    2024年01月16日
    浏览(56)
  • 【Linux】编写一个 shell 脚本&执行

    在Linux中编写和执行脚本相对简单。下面是一个基本的步骤指南,帮助你创建一个简单的bash脚本并运行它: 1. 创建脚本文件 首先,你需要使用文本编辑器创建一个新的文件。这个文件通常会有 .sh 的扩展名,以表明它是一个shell脚本。例如,你可以创建一个名为 myscript.sh 的文

    2024年04月26日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包