[Linux]编写一个极简版的shell(版本1)

[Linux]编写一个极简版的shell-version1

文章目录

本文能够帮助Linux系统学习者通过代码的角度更好地理解命令行解释器的实现原理。

命令行提示符打印

Linux操作系统运行时,就需要shell进程进行命令行解释,然后让系统完成对应的命令,因此打印命令行提示符时要采用死循环打印的方式,具体的代码逻辑如下:

c 复制代码
int main()
{
  while(1)
  {
    printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));
    fflush(stdout);
    //sleep(200); -- 当前阶段用于演示效果
  }
  • 为了和系统的shell进行区分因此打印两个$符号
  • 命令行提示符打印的信息都是通过环境变量得来的,因此只需要调用系统接口获取相应的环境变量即可
    • USER环境变量 -- 当前使用的用户名
    • HOSTNAME环境变量 -- 当前的主机名称
    • PWD环境变量 -- 当前用户所处绝对路径
  • 由于显示器采用的是行缓冲的策略,因此需要手动刷新缓冲区才能让命令行提示符显式到屏幕上

由于环境变量PWD是当前用户所处的绝对路径,因此需要编写一个函数getpath来获取当前所处的目录名称,具体的代码逻辑如下:

c 复制代码
const char* getpath(char* path)
{
  int length = strlen(path);
  if(length == 1) return "/"; //根目录
  int i = length - 1;
  while((path[i] != '/')) i--;
  return path+i+1;
}
  • 如果绝对路径的字符串长度为1,表示所处为根目录,返回"/"
  • 其余路径下都要将绝对路径里当前目录前的所有路径分割掉,包括路径分隔符,譬如当前记录绝对路径环境变量为PWD=/home/qxm/linux-warehouse/review/mybash/version1getpath的返回值为verson1字符串的首地址

效果演示:

接收命令行参数

接受命令行参数只需要设置一个字符串数组,将用户的输入接收即可,具体的代码逻辑如下:

c 复制代码
#define MAX 1024

int main()
{
  while(1)
  {
	char commandstr[MAX] = { 0 }; //接收命令行参数
    printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));
    fflush(stdout);
    char* s = fgets(commandstr, sizeof(commandstr), stdin);
    commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'
    //printf("%s\n", commandstr); -- 当前阶段用于演示效果
  }
  return 0;
}
  • 由于用户输入时会按下回车也就是会在输入时在末尾出现一个'\n',命令行参数使用时需要将其去除掉,譬如用户输入ls -a -l\n ,命令行参数只需要ls -a -l

效果演示:

将命令行参数进行解释

获取用户输入的命令行参数后,需要将命令行参数根据输入的空格将字符串分割为多个字串,譬如用户输入ls -a -l ,要分割为ls , -a , -l,具体的代码逻辑如下:

c 复制代码
#define MAX 1024
#define ARGC 64
#define SEP " "

int split(char commandstr[], char* argv[])//命令行参数解释
{
  argv[0] = strtok(commandstr, SEP);
  if (argv[0] == NULL) return -1; //字符串为空
  int i = 1;
    while(1)
    {
        argv[i] = strtok(NULL, SEP);
        if(argv[i] == NULL) break;
        i++;
    }
  return 0;
}

void debugPrint(char* argv[])//--当前阶段用于演示效果
{
  int i = 0;
  for (i = 0; argv[i] != NULL; i++)
  {
    printf("%s\n", argv[i]);
  }
}

int main()
{
  while(1)
  {
    char commandstr[MAX] = { 0 }; //接收命令行参数
    char* argv[ARGC] = { NULL }; //存储命令行参数
    printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));
    fflush(stdout);
    char* s = fgets(commandstr, sizeof(commandstr), stdin);
    commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'
    
    int n = split(commandstr, argv);
    if(n != 0) continue; //用户输入空串
    //debugPrint(argv);  -- 当前阶段用于演示效果
  }
  return 0;
}
  • 调用C语言库函数strtok获取分割空格字符后字串的首地址,并且将空格置为'\0',并将字串的首地址存储起来

效果演示:

执行用户命令

创建子进程,进程进程程序替换,让子进程完成用户所输入的命令,具体的代码逻辑如下:

c 复制代码
#define MAX 1024
#define ARGC 64
#define SEP " "

const char* getpath(char* path)
{
  int length = strlen(path);
  if(length == 1) return "/"; //根目录
  int i = length - 1;
  while((path[i] != '/')) i--;
  return path+i+1;
}

int split(char commandstr[], char* argv[])
{
  argv[0] = strtok(commandstr, SEP);
  if (argv[0] == NULL) return -1; //字符串为空
  int i = 1;
    while(1)
    {
        argv[i] = strtok(NULL, SEP);
        if(argv[i] == NULL) break;
        i++;
    }
  return 0;
}

int main()
{
  while(1)
  {
    char commandstr[MAX] = { 0 }; //接收命令行参数
    char* argv[ARGC] = { NULL };  //存储命令行参数
    printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));
    fflush(stdout);
    char* s = fgets(commandstr, sizeof(commandstr), stdin);
    commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'
    int n = split(commandstr, argv);
    if(n != 0) continue; //用户输入空串
      
    pid_t id = fork();//创建子进程完成命令执行
    if (id == 0)
    {
      //子进程
      execvp(argv[0], argv); //进程程序替换
      exit(0);
    }
    
    int status = 0;
    waitpid(id, &status, 0);//回收子进程
  }
  return 0;
}
  • 由于实现的shell用于执行系统命令,并且获取了记录命令行参数的数组,因此采用execvp进程程序替换

效果演示:

完整代码

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

#define MAX 1024
#define ARGC 64
#define SEP " "

const char* getpath(char* path)
{
  int length = strlen(path);
  if(length == 1) return "/"; //根目录
  int i = length - 1;
  while((path[i] != '/')) i--;
  return path+i+1;
}

int split(char commandstr[], char* argv[])
{
  assert(commandstr);
  assert(argv);
    
  argv[0] = strtok(commandstr, SEP);
  if (argv[0] == NULL) return -1; //字符串为空
  int i = 1;
    while(1)
    {
        argv[i] = strtok(NULL, SEP);
        if(argv[i] == NULL) break;
        i++;
    }
  return 0;
}

int main()
{
  while(1)
  {
    char commandstr[MAX] = { 0 }; //接收命令行参数
    char* argv[ARGC] = { NULL };  //存储命令行参数
    printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));
    fflush(stdout);
    char* s = fgets(commandstr, sizeof(commandstr), stdin);
    assert(s); //对fgets函数的结果断言
    (void)s;//保证在release方式发布的时候,因为去掉assert了,所以s就没有被使用,而带来的编译告警, 什么都没做,但是充当一次使用
    commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'
    int n = split(commandstr, argv);
    if(n != 0) continue; //用户输入空串
    pid_t id = fork();
    assert(id >= 0);
    (void)id;
    if (id == 0)
    {
      //子进程
      execvp(argv[0], argv); 
      exit(0);
    } 
    int status = 0;
    waitpid(id, &status, 0);
  }
  return 0;
}
);
    if(n != 0) continue; //用户输入空串
    pid_t id = fork();
    assert(id >= 0);
    (void)id;
    if (id == 0)
    {
      //子进程
      execvp(argv[0], argv); 
      exit(0);
    } 
    int status = 0;
    waitpid(id, &status, 0);
  }
  return 0;
}

说明: 该版本shell只能够执行系统命令,并且不能够支持所有系统命令比如cd命令。

相关推荐
搬码临时工6 分钟前
如何把本地服务器变成公网服务器?内网ip网址转换到外网连接访问
运维·服务器·网络·tcp/ip·智能路由器·远程工作·访问公司内网
vortex510 分钟前
探索 Shell:选择适合你的命令行利器 bash, zsh, fish, dash, sh...
linux·开发语言·bash·shell·dash
GalaxyPokemon34 分钟前
LeetCode - 148. 排序链表
linux·算法·leetcode
Guheyunyi44 分钟前
监测预警系统重塑隧道安全新范式
大数据·运维·人工智能·科技·安全
懒羊羊大王呀1 小时前
Ubuntu20.04中 Redis 的安装和配置
linux·redis
鳄鱼杆1 小时前
服务器 | Centos 9 系统中,如何部署SpringBoot后端项目?
服务器·spring boot·centos
杰哥技术分享1 小时前
在 CentOS 上安装 Docker 和 Docker Compose 并配置使用国内镜像源
linux·docker·centos
知更鸟呆呆1 小时前
【Linux操作系统】基础开发工具(yum、vim、gcc/g++)
linux·运维·vim
xiangyong581 小时前
ubuntu系统文件误删(/lib/x86_64-linux-gnu/libc.so.6)修复方案 [成功解决]
linux·ubuntu·gnu
Gold Steps.2 小时前
Docker容器部署elasticsearch8.*与Kibana8.*版本使用filebeat采集日志
运维·docker·云原生·es