Linux——shell程序的简单实现

简介: Linux——shell程序的简单实现

shell程序的简单实现

本章思维导图:

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


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

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

👉进程控制

👉环境变量

👉进程替换

1. 实现交互 interact()

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

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

注:

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

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

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

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

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

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

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

//形参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系列函数来进行进程程序替换:

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解决

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

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

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

3. 执行命令

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

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

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

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

//参数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的简单版本了:

#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这样的命令我们称其为内建命令

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

//参数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,那么就说明该命令为内建命令,已经经过处理,直接等待下一条命令的输入即可
//处理内建命令
int ret = buildCommand(argv);
//执行命令
if (!ret)
    execute(argv);

4. 实现代码

#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;
}

本篇完

如果错误敬请指正

相关文章
|
6天前
|
Shell Linux
Linux shell编程学习笔记30:打造彩色的选项菜单
Linux shell编程学习笔记30:打造彩色的选项菜单
|
1月前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
|
2月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
145 2
|
2月前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
47 2
|
6天前
|
Shell Linux
Linux shell编程学习笔记82:w命令——一览无余
Linux shell编程学习笔记82:w命令——一览无余
|
9天前
|
消息中间件 分布式计算 Java
Linux环境下 java程序提交spark任务到Yarn报错
Linux环境下 java程序提交spark任务到Yarn报错
18 5
|
11天前
|
人工智能 监控 Shell
常用的 55 个 Linux Shell 脚本(包括基础案例、文件操作、实用工具、图形化、sed、gawk)
这篇文章提供了55个常用的Linux Shell脚本实例,涵盖基础案例、文件操作、实用工具、图形化界面及sed、gawk的使用。
27 2
|
1月前
|
Shell Linux 开发工具
linux shell 脚本调试技巧
【9月更文挑战第3天】在Linux中调试shell脚本可采用多种技巧:使用`-x`选项显示每行命令及变量扩展情况;通过`read`或`trap`设置断点;利用`echo`检查变量值,`set`显示所有变量;检查退出状态码 `$?` 进行错误处理;使用`bashdb`等调试工具实现更复杂调试功能。
|
2月前
|
NoSQL Linux C语言
嵌入式GDB调试Linux C程序或交叉编译(开发板)
【8月更文挑战第24天】本文档介绍了如何在嵌入式环境下使用GDB调试Linux C程序及进行交叉编译。调试步骤包括:编译程序时加入`-g`选项以生成调试信息;启动GDB并加载程序;设置断点;运行程序至断点;单步执行代码;查看变量值;继续执行或退出GDB。对于交叉编译,需安装对应架构的交叉编译工具链,配置编译环境,使用工具链编译程序,并将程序传输到开发板进行调试。过程中可能遇到工具链不匹配等问题,需针对性解决。
|
2月前
|
Linux Windows Python
最新 Windows\Linux 后台运行程序注解
本文介绍了在Windows和Linux系统后台运行程序的方法,包括Linux系统中使用nohup命令和ps命令查看进程,以及Windows系统中通过编写bat文件和使用PowerShell启动隐藏窗口的程序,确保即使退出命令行界面程序也继续在后台运行。
下一篇
无影云桌面