Linux——shell程序的简单实现

shell程序的简单实现

本章思维导图:

注:本章思维导图对应的.xmind.png文件都已同步导入至资源,可免费查阅


在学习完有关进程的知识后,我们就可以开始尝试自己实现一个简单的shell程序了。

注:在编写简单的shell程序之前,你首先需要掌握:

👉进程控制

👉环境变量

👉进程替换

1. 实现交互 interact()

首先,和真正的shell程序一样,我们启动程序,shell就会打印出命令行提示符,并等待用户的输入

因此,我们首先要做的,就是要正确打印出命令行提示符,并等待接收用户输入的命令。

注:

命令行提示符的基本格式为:[用户名@主机名 当前路径]&

  • 需要注意,如果当前用户为root 用户,那么&就应该变为#

那么,我们该如何获取我们需要的有用户名、主机名和路径信息呢?答案便是通过环境变量来获取

  • 环境变量USER记录了当前的用户信息
  • 环境变量HOSTNAME记录了当前的主机信息
  • 环境变量PWD记录了当前的路径信息

可以利用系统调用getenv()来获取对应的信息,并进行打印

等待并接受用户的输入这一操作十分简单,定义一个字符数组,并用函数fgets()进行接收即可。

这样,我们就实现了第一部分的功能:

c 复制代码
//形参out为一个输出型参数,用于接收用户输入的命令
void interact(char* out)
{
  printf("[%s@%s %s]$ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
    
  fgets(out, SIZE, stdin);
  out[strlen(out) - 1] = '\0';	//fgets()会将用户输入的换行符读入,因此要将这个符号去除
}

2. 分割命令 split()

进程替换一节中我们提到,如果要将当前的进程替换为另一个程序,那么就需要使用exec系列函数来进行进程程序替换:

c 复制代码
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);

int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
  • 命令参数要么以参数列表的形式arg, ...传入,要么以字符串数组argv的方式传入

  • 但是我们在第一步interact()的过程中只接受了用户的一长串命令,这并不能直接作为参数传入程序替换函数中

  • 因此,我们就需要对之前输入的字符串以空格' '为分隔符进行分割

如何分割?------可以用库函数strtok解决

c 复制代码
char * strtok ( char * str, const char * delimiters );
  • delimiters分割符
  • 返回值即为被分割的字符串 ,分割结束返回NULL
  • 关于参数str,当要对用一个字符串多次调用时:
    • 第一次调用时,即为要被分割字符串str
    • 之后的所有调用,参数str都为NULL

如此,我们便可以实现功能分割功能了:

c 复制代码
//参数command为用户输入的命令
//参数out为输出型参数,用于存储被分割的字符串集合
void split(char* command, char** out)
{
  int i = 0;
  out[i++] = strtok(command, " ");
  while (out[i++] = strtok(NULL, " "));
}

3. 执行命令

获得了正确的命令参数后,我们就可以开始程序替换了。

但是应该注意,如果程序替换成功,那么原程序之后的所有代码便都不会再执行了。

因此,为了确保shell能够一直处理用户输入的命令,我们应该创建一个子进程来进行进程程序替换

我们可以很容易的写出这样的代码:

c 复制代码
//参数argv即为存储命令字符串的数组
void execute(char** argv)
{
    //创建子进程
    pid_t pid = fork();
    if (pid == 0)
    {
      //子进程进行进程程序替换
      execvp(argv[0], argv);
      exit(1);
    }
	
    //子进程退出后父进程进行等待,并获取子进程的退出码
    int status;
    pid_t rid = waitpid(pid, &status, 0);
    EXIT = WEXITSTATUS(status);
}

我们再对上面两部分代码进行整合,就可以得到我们shell的简单版本了:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

#define SIZE 1024
#define ARGC 64

int EXIT = 0;	//进程退出码

void interact(char* out)
{
  printf("[%s@%s %s]$ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
  fgets(out, SIZE, stdin);
  out[strlen(out) - 1] = '\0';
}

void split(char* command, char** out)
{
  int i = 0;
  out[i++] = strtok(command, " ");
  while (out[i++] = strtok(NULL, " "));
}

void execute(char** argv)
{
    pid_t pid = fork();
    if (pid == 0)
    {
      execvp(argv[0], argv);
      exit(1);
    }

    int status;
    pid_t rid = waitpid(pid, &status, 0);
    EXIT = WEXITSTATUS(status);
}

int main()
{
  while(1)
  {
    //获取命令行参数
    char command[SIZE] = {0};
    interact(command);
    if (strlen(command) == 0)
      continue;

    //将命令行拆分成多个字符串
    char* argv[ARGC];
    split(command, argv);
    
   
    execute(argv);
  }

  return 0;
}

我么可以执行来看看:

可以发现我们执行catlsclear这些命令的时候没有出现问题,但是当我们执行cdechoexport这些命令的时候,却得不到正确的结果。这是为什么?

  • 应该清楚,我们是用子进程进行的进程替换,子进程执行完后便会退出终止。
  • 因此,子进程的改变不会影响到父进程,即不会影响到shell进程
  • 例如我们使用cd命令修改当前路径,我们修改的只是子进程的路径,而其父进程shell并未受任何影响
  • 同样,对于export,我们只是对子进程添加了环境变量,父进程的环境变量同样不会改变

所以,当遇到类似cd这种命令时,我们要对其进行特殊处理

3.1 执行内建命令

在Linux中,诸如echocdexport这样的命令我们称其为内建命令

我们可以利用枚举的方法来对内建命令进行处理

c 复制代码
//参数argv即为命令字符串集合
//返回值如果为0,说明不是内建命令;如果是1,说明是内建命令
int buildCommand(char** argv)
{
  int ret = 0;
    
  //处理"cd"
  if (strcmp(argv[0], "cd") == 0)
  {
    ret = 1;
    char* path = argv[1];	//命令cd后面跟的就是新的路径
    char put[SIZE];
    char absolutePath[SIZE];

    if (path == NULL)
      path = getenv("HOME");
    
    chdir(path);
    getcwd(absolutePath, SIZE);	//将新路径存入字符数组absolutePath
    
    snprintf(put, SIZE, "%s%s", "PWD=", absolutePath);	//修改环境变量PWD
    putenv(put);
  }
  //处理"export"
  else if (strcmp(argv[0], "export") == 0)
  {
    ret = 1;
    char env[SIZE];
    if (argv[1])
    {
      strcpy(env, argv[1]);
      putenv(env);
    }
      
    /*
    一定不能直接写成:putenv(argv[1]);
    否则当输入新的命令时,argv[1]的值就会改变,环境变量也会跟着变
    */
  }
  //处理"echo"
  else if (strcmp(argv[0], "echo") == 0)
  {
    ret = 1;
    if (argv[1] == NULL)
      printf("\n");
    else
    {  
      if (argv[1][0] == '$')
      {
        //"echo $?"即输出最近一个进程的退出码
        if (argv[1][1] == '?')
        {
          printf("%d\n", EXIT);
          EXIT = 0;
        }
        //否则输出对应的环境变量
        else 
        {
          char* env = getenv(argv[1] + 1);
          if (env == NULL)
            printf("\n");
          else 
            printf("%s\n", env);
        }
      } 
      //否则为向屏幕输出字符串
      else 
        printf("%s\n", argv[1]);                    
     }
  }
    
  return ret;
}

3.2 执行非内建命令

利用buildCommand()的返回值

  • 如果返回值为0,那么就说明该命令为非内建命令,开始创建子进程进行进程替换
  • 如果返回值为1,那么就说明该命令为内建命令,已经经过处理,直接等待下一条命令的输入即可
c 复制代码
//处理内建命令
int ret = buildCommand(argv);

//执行命令
if (!ret)
    execute(argv);

4. 实现代码

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

#define SIZE 1024
#define ARGC 64

int EXIT = 0;

void interact(char* out)
{
  printf("[%s@%s %s]$ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
  fgets(out, SIZE, stdin);
  out[strlen(out) - 1] = '\0';
}

void split(char* command, char** out)
{
  int i = 0;
  out[i++] = strtok(command, " ");
  while (out[i++] = strtok(NULL, " "));
}

int buildCommand(char** argv)
{
  int ret = 0;
  if (strcmp(argv[0], "cd") == 0)
  {
    ret = 1;
    char* path = argv[1];
    char put[SIZE];
    char absolutePath[SIZE];

    if (path == NULL)
      path = getenv("HOME");
    
    chdir(path);
    getcwd(absolutePath, SIZE);
    
    snprintf(put, SIZE, "%s%s", "PWD=", absolutePath);
    putenv(put);
  }
  else if (strcmp(argv[0], "export") == 0)
  {
    ret = 1;
    char env[SIZE];
    if (argv[1])
    {
      strcpy(env, argv[1]);
      putenv(env);
    }
  }
  else if (strcmp(argv[0], "echo") == 0)
  {
    ret = 1;
    if (argv[1] == NULL)
      printf("\n");
    else
    {  
      if (argv[1][0] == '$')
      {
        if (argv[1][1] == '?')
        {
          printf("%d\n", EXIT);
          EXIT = 0;
        }
        else 
        {
          char* env = getenv(argv[1] + 1);
          if (env == NULL)
            printf("\n");
          else 
            printf("%s\n", env);
        }
      } 
      else 
        printf("%s\n", argv[1]);                    
     }
  }


  return ret;
}

void execute(char** argv)
{
    pid_t pid = fork();
    if (pid == 0)
    {
      execvp(argv[0], argv);
      exit(1);
    }

    int status;
    pid_t rid = waitpid(pid, &status, 0);
    EXIT = WEXITSTATUS(status);
}

int main()
{
  while(1)
  {
    //获取命令行参数
    char command[SIZE] = {0};
    interact(command);
    if (strlen(command) == 0)
      continue;

    //将命令行拆分成多个字符串
    char* argv[ARGC];
    split(command, argv);
    
    //处理内建命令
    int ret = buildCommand(argv);

  	//执行命令
    if (!ret)
      execute(argv);
  }

  return 0;
}

本篇完

如果错误敬请指正

相关推荐
Yana.nice40 分钟前
openssl将证书从p7b转换为crt格式
java·linux
AI逐月44 分钟前
tmux 常用命令总结:从入门到稳定使用的一篇实战博客
linux·服务器·ssh·php
小白跃升坊1 小时前
基于1Panel的AI运维
linux·运维·人工智能·ai大模型·教学·ai agent
跃渊Yuey2 小时前
【Linux】线程同步与互斥
linux·笔记
杨江2 小时前
seafile docker安装说明
运维
舰长1152 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络
好好沉淀2 小时前
Docker开发笔记(详解)
运维·docker·容器
zmjjdank1ng2 小时前
Linux 输出重定向
linux·运维
路由侠内网穿透.2 小时前
本地部署智能家居集成解决方案 ESPHome 并实现外部访问( Linux 版本)
linux·运维·服务器·网络协议·智能家居
树℡独2 小时前
ns-3仿真之应用层(三)
运维·服务器·ns3