【Linux系统编程】第二十五弹---Shell编程入门:打造一个简易版Shell

news2024/12/25 14:23:50

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、简易的shell

1.1、输出一个命令行

 1.2、获取用户命令字符串

1.3、命令行字符串分割

1.4、检查命令是否是内建命令 

1.5、执行命令 

1.6、完整代码 


1、简易的shell

考虑下面这个与shell典型的互动:

[jkl@host shell]$ ls
makefile  myshell  myshell.c
[jkl@host shell]$ ps
  PID TTY          TIME CMD
20980 pts/0    00:00:00 bash
26709 pts/0    00:00:00 ps

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。 

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序并等待这个进程结束。
所以要写一个shell,需要循环以下过程:

  • 1. 输出一个命令行
  • 2. 获取命令行
  • 3. 解析命令行
  • 4. 检查是否为内建命令
  • 5. 建立一个子进程(fork)
  • 6. 替换子进程(execvp) 
  • 7. 父进程等待子进程退出(wait)

根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。

会用到的头文件

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

1.1、输出一个命令行

命令行的内容包含用户名,主机名,当前路径

环境变量中包含三种信息,因此我们可以使用获取环境变量的 getenv() 函数获取。

#include <stdlib.h>
char *getenv(const char *name);

获取到三个字符串的内容之后,我们需要将三个字符串合并成一个字符串,此时我们可以用到snprintf()函数。

#include<stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);

代码演示

#define SIZE 512
const char* GetUserName()
{
  const char* name = getenv("USER");
  if(name == NULL) return "None";
  return name;
}

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

const char* GetCwd()
{
  const char* cwd = getenv("PWD");
  if(cwd == NULL) return "None";
  return cwd;
}
void  MakeCommandLineAndPrint()
{
  char line[SIZE];
  // 用户名@主机名 当前路径
  const char* username = GetUserName();
  const char* hostname = GetHostName();
  const char* cwd = GetCwd();

  snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,cwd);
  printf("%s",line);
  sleep(2);
}
int main()
{
    // 1、自己输出一个命令行
    MakeCommandLineAndPrint();
    return 0;
}

运行结果 

 

两个问题:1、程序结束才刷新缓冲区(刷新缓冲区即可) 2、打印的是绝对路径(实现一个算法即可) 

优化

// 找最后一个/ ,宏是替换可以不用传二级指针,do while 不加分号,为了后面加分号
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)


// command : output
void  MakeCommandLineAndPrint()
{
  char line[SIZE];
  // 用户名@主机名 当前路径
  const char* username = GetUserName();
  const char* hostname = GetHostName();
  const char* cwd = GetCwd();

  SkipPath(cwd);// 处理绝对路径问题
  // 需要处理第一个/,长度为1用/
  snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,strlen(cwd) == 1 ? "/" : cwd + 1);
  printf("%s",line);
  fflush(stdout);// 处理缓冲区问题
}

运行结果 

 1.2、获取用户命令字符串

 从标准输入流中获取字符串,并存放在数组中。

此处需要用到 fgets() , strlen()函数。

#include<stdio.h>
char *fgets(char *s, int size, FILE *stream);

计算字符串长度 

#include <string.h>
size_t strlen(const char *s);

代码演示

#define ZERO '\0'
#define SIZE 512

int GetUserCommand(char command[],size_t n)
{
  char* s = fgets(command,n,stdin);
  if(s == NULL) return -1;// 字符串为空直接返回
  command[strlen(command)-1]=ZERO;// 设置结尾标志
  return strlen(command);// 返回字符串长度
}
int main()
{
  // 1、自己输出一个命令行
  MakeCommandLineAndPrint();
  2、获取用户命令字符串
  char usercommand[SIZE];
  int n = GetUserCommand(usercommand,sizeof(usercommand));
  if(n<=0) return 1;// 没有获取到字符串则结束程序
  printf("%s\n",usercommand);// 打印命令行字符串
  return 0;
}

运行结果 

1.3、命令行字符串分割

 将以空格分割的字符串,分割成全部是单独的字符串。

此处需要用到 strtok() 函数。

#include<string.h>
char *strtok(char *str, const char *delim);

代码演示 

#define SEP " "

char* gArgv[NUM];

void SplitCommand(char command[])
{
  // "ls -a -l -n" -> "ls" "-a" "-l" "-n"
  gArgv[0]=strtok(command,SEP);
  int index = 1;
  // 故意写成赋值,表示先赋值再判断,分割之后返回NULL,刚好让gArgv的最后一个元素为空,循环结束
  while((gArgv[index++] = strtok(NULL,SEP)));
}
int main()
{
  // 1、自己输出一个命令行
  MakeCommandLineAndPrint();
  //2、获取用户命令字符串
  char usercommand[SIZE];
  int n = GetUserCommand(usercommand,sizeof(usercommand));
  if(n<=0) return 1;// 没有获取到字符串则结束程序
  printf("%s\n",usercommand);
  // 3、命令行字符串分割
  SplitCommand(usercommand);
  // 打印分割之后的字符串
  for(int i=0;gArgv[i];i++)
  {
    printf("%s\n",gArgv[i]);
  }
  return 0;
}

运行结果 

1.4、检查命令是否是内建命令 

判断gArgv[0]是否是内建命令,是内建命令则做特殊处理。

此处需要用到 strcmp() chdir() getcwd() snprintf() putenv() 函数。

#include <string.h>
int strcmp(const char *s1, const char *s2);

 比较两个字符串是否相同,相同返回0。

#include <unistd.h>
int chdir(const char *path);

修改当前工作目录。 

#include <unistd.h>
char *getcwd(char *buf, size_t size);

获取当前工作目录。 

#include <stdlib.h>
int putenv(char *string);

添加环境变量。 

代码演示 

#define SIZE 512

char* gArgv[NUM];
char cwd[SIZE*2];
int lastcode = 0;

const char* GetHome()
{
  const char* home = getenv("HOME");
  if(home == NULL) return "None";
  return home;
}
void Cd()
{
  const char* path = gArgv[1];
  if(path == NULL) path = GetHome();
  // path 一定存在
  chdir(path);// 修改当前工作目录

  // 刷新环境变量
  char temp[SIZE*2];
  getcwd(temp,sizeof(temp));// 将工作目录保存到temp中
  snprintf(cwd,sizeof(cwd),"PWD=%s",temp);// 以格式化形式将PWD=目录保存到cwd中
  putenv(cwd);// 将cwd导入环境变量
}
int CheckBuildin()
{
  int yes = 0;
  const char* enter_cmd = gArgv[0];
  if(strcmp(enter_cmd,"cd") == 0)
  {
    yes = 1;
    Cd();
  }
  else if(strcmp(enter_cmd,"echo") == 0 && strcmp(gArgv[1],"$?") == 0)
  {
    yes = 1;
    printf("%d\n",lastcode);
    lastcode = 0;
  }
  return yes;
}
int main()
{
  // 1、自己输出一个命令行
  MakeCommandLineAndPrint();
  //2、获取用户命令字符串
  char usercommand[SIZE];
  int n = GetUserCommand(usercommand,sizeof(usercommand));
  if(n<=0) return 1;// 没有获取到字符串则结束程序
  printf("%s\n",usercommand);
  // 3、命令行字符串分割
  SplitCommand(usercommand);
  for(int i=0;gArgv[i];i++)
  {
    printf("%s\n",gArgv[i]);
  }
  // 4、检测命令是否是内建命令
  n = CheckBuildin();// 不是0则是内建命令
  printf("n = %d\n",n);
  return 0;
}

运行结果 

1.5、执行命令 

创建子进程,使用进程替换,让子进程执行命令。

此处需要用到 fork() execvp() exit() waitpid() strerror() 函数。

#include <unistd.h>
pid_t fork(void);

创建子进程,子进程返回0,父进程返回子进程pid。 

#include<unistd.h>
int execvp(const char *file, char *const argv[]);

进程替换(只需传文件名,argv为命令行参数表)。 

#include <stdlib.h>
void exit(int status);

 退出进程。

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

等待子进程。

#include <string.h>
char *strerror(int errnum);

根据错误码打印错误信息。 

errno为全局的错误码变量。

代码演示

char* gArgv[NUM];
int lastcode = 0;

void Die()
{
  exit(1);
}

void  ExecuteCommand()
{
  pid_t id = fork();
  if(id < 0) Die();
  else if(id == 0)
  {
    // child 程序替换
    execvp(gArgv[0],gArgv);
    exit(errno);
  }
  else 
  {
    // father
    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    if(rid > 0)
    {
      lastcode = WEXITSTATUS(status);
      if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode);
    }
  }
}

运行结果 

命令行是能够一直解释命令的,因此我们还需要加一个循环,在完整代码里面。 

1.6、完整代码 

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

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
// 找最后一个/ ,宏是替换可以不用传二级指针,do while 不加分号,为了后面加分号
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)

char* gArgv[NUM];
int lastcode = 0;
char cwd[SIZE*2];

const char* GetUserName()
{
  const char* name = getenv("USER");
  if(name == NULL) return "None";
  return name;
}

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

const char* GetCwd()
{
  const char* cwd = getenv("PWD");
  if(cwd == NULL) return "None";
  return cwd;
}

const char* GetHome()
{
  const char* home = getenv("HOME");
  if(home == NULL) return "None";
  return home;
}

// command : output
void  MakeCommandLineAndPrint()
{
  char line[SIZE];
  // 用户名@主机名 当前路径
  const char* username = GetUserName();
  const char* hostname = GetHostName();
  const char* cwd = GetCwd();

  SkipPath(cwd);
  // 需要处理第一个/,长度为1用/
  snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,strlen(cwd) == 1 ? "/" : cwd + 1);
  printf("%s",line);
  fflush(stdout);
}


int GetUserCommand(char command[],size_t n)
{
  char* s = fgets(command,n,stdin);
  if(s == NULL) return -1;
  command[strlen(command)-1]=ZERO;
  return strlen(command);
}

void SplitCommand(char command[])
{
  // "ls -a -l -n" -> "ls" "-a" "-l" "-n"
  gArgv[0]=strtok(command,SEP);
  int index = 1;
  // 故意写成赋值,表示先赋值再判断,分割之后返回NULL,刚好让gArgv的最后一个元素为空,循环结束
  while((gArgv[index++] = strtok(NULL,SEP)));
}

void Die()
{
  exit(1);
}

void  ExecuteCommand()
{
  pid_t id = fork();
  if(id < 0) Die();
  else if(id == 0)
  {
    // child 程序替换
    execvp(gArgv[0],gArgv);
    exit(errno);
  }
  else 
  {
    // father
    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    if(rid > 0)
    {
      lastcode = WEXITSTATUS(status);
      if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode);
    }
  }
}

void Cd()
{
  const char* path = gArgv[1];
  if(path == NULL) path = GetHome();
  // path 一定存在
  chdir(path);

  // 刷新环境变量
  char temp[SIZE*2];
  getcwd(temp,sizeof(temp));
  snprintf(cwd,sizeof(cwd),"PWD=%s",temp);
  putenv(cwd);
}
int CheckBuildin()
{
  int yes = 0;
  const char* enter_cmd = gArgv[0];
  if(strcmp(enter_cmd,"cd") == 0)
  {
    yes = 1;
    Cd();
  }
  else if(strcmp(enter_cmd,"echo") == 0 && strcmp(gArgv[1],"$?") == 0)
  {
    yes = 1;
    printf("%d\n",lastcode);
    lastcode = 0;
  }
  return yes;
}
int main()
{
  int quit = 0;
  while(!quit)
  {
    // 1、自己输出一个命令行
    MakeCommandLineAndPrint();
    // 2、获取用户命令字符串
    char usercommand[SIZE];
    int n = GetUserCommand(usercommand,sizeof(usercommand));
    if(n<=0) return 1;
    // 3、命令行字符串分割
    SplitCommand(usercommand);
    // 4、检测命令是否是内建命令
    n = CheckBuildin();
    if(n) continue;
    // 5、执行命名
    ExecuteCommand();
  }
  return 0;
}

运行结果 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2179911.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode24. 两两交换链表中的节点(2024秋季每日一题 32)

给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4] 输出&#xff1a;[2,1,…

Llama微调以及Ollama部署

1 Llama微调 在基础模型的基础上&#xff0c;通过一些特定的数据集&#xff0c;将具有特定功能加在原有的模型上。 1.1 效果对比 特定数据集 未使用微调的基础模型的回答 使用微调后的回答 1.2 基础模型 基础大模型我选择Mistral-7B-v0.3-Chinese-Chat-uncensored&#x…

Label-Studio ML利用yolov8模型实现自动标注

引言 Label Studio ML 后端是一个 SDK&#xff0c;用于包装您的机器学习代码并将其转换为 Web 服务器。Web 服务器可以连接到正在运行的 Label Studio 实例&#xff0c;以自动执行标记任务。我们提供了一个示例模型库&#xff0c;您可以在自己的工作流程中使用这些模型&#x…

[Cocoa]_[初级]_[绘制文本如何设置断行方式]

场景 在开发Cocoa程序时&#xff0c;表格NSTableView是经常使用的控件。其基于View Base的视图单元格模式就是使用NSCell或其子类来控制每个单元格的呈现。当一个单元格里的文字过多时&#xff0c;需要截断超出宽度的文字&#xff0c;怎么实现&#xff1f; 说明 Cocoa下的文本…

演讲干货整理:泛能网能碳产业智能平台基于 TDengine 的升级之路

在 7 月 26 日的 TDengine 用户大会上&#xff0c;新奥数能 / 物联和数据技术召集人袁文科进行了题为《基于新一代时序数据库 TDengine 助力泛能网能碳产业智能平台底座升级》的主题演讲。他从泛能网能碳产业智能平台的业务及架构痛点出发&#xff0c;详细分享了在数据库选型、…

【多线程奇妙屋】能把进程和线程讲的这么透彻的,没有20年功夫还真不行【0基础也能看懂】

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

OpenGL ES 顶点缓冲区和布局(3)

OpenGL ES 顶点缓冲区和布局(3) 简述 顶点缓冲区的本质就是一段GPU上的显存&#xff0c;我们通过绑定顶点缓冲区的方式来将数据从CPU传到GPU。 我们之前在绘制三角形的例子中&#xff0c;我们往顶点缓冲区只传入了坐标&#xff0c;但是其实顶点是可以包含很多数据的&#xff…

指定PDF或图片多个识别区域,识别区域文字,并导出到Excel文件中

常见场景 用户有大量图片/PDF文件&#xff0c;期望能将图片/PDF中的多个区域中的文字批量识别出来&#xff0c;并导入到Excel文件中。期望工具可以批量处理、离线识别&#xff08;保证数据安全性&#xff09;。手工操作麻烦。具体场景&#xff1a;用户有工程现场照片&#xff…

xgboost cross validation

在R中使用xgboost 假设X为训练数据&#xff0c;y为label&#xff0c;为0或者1.用xgboost建立分类模型代码如下 调用caret包中的createFolds方法&#xff0c;进行10倍交叉验证 最后画出AUC曲线 library(xgboost) library(caret) library(caTools) library(pROC)set.seed(123) …

【北京迅为】《STM32MP157开发板嵌入式开发指南》- 第十一章 Linux 帮助手册讲解

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

3DGS中Densification梯度累计策略的改进——绝对梯度策略(Gaussian Opacity Fields)

在学习 StreetGS 代码中发现了其中的 Densification 策略与原 3DGS 不太一样&#xff0c;其是使用的 Gaussian Opacity Fields 中的一个的策略 我们先来回忆一下 3DGS 中一个比较重要 contribution&#xff1a;自适应密度控制 1 自适应密度控制 其具体步骤如下&#xff1a; …

概率论——随机分布

离散型——二项分布 X ~ B&#xff08;n , p&#xff09; 例题&#xff1a; 例题二&#xff1a; 离散型——泊松分布 例题 注意&#xff1a;记住题二的结论&#xff01;&#xff01;&#xff01; 连续性——均匀分布 例题&#xff1a;求解概率密度&#xff08;具体方法见随机…

请问PMP英文报名被审查该怎么通过?

审核抽查是随机进行的&#xff08;一般概率约为30%&#xff09;&#xff0c;并非所有人都会接受资料抽查。如果您报考了机构&#xff0c;他们会协助您解决这个问题。 一、资料审查&#xff1a; 如果被PMI选中进行审查&#xff0c;这是正常情况&#xff0c;不必惊慌。如果你参…

要洞察数字化本质,才能形成破局之道...

在深入探索数字化转型的突破路径之前&#xff0c;首要之务是构筑对数字化及其转型过程的深刻而准确的认知体系。唯有透彻理解数字化的内在本质&#xff0c;精准把握数字化转型的演变规律&#xff0c;方能引领数字化转型的航向&#xff0c;确保数字化工具与策略得以高效、精准地…

(一)万字详解G1垃圾收集器 —G1的设计目标是什么?G1的分区是什么?卡表的作用和工作原理?如何解决漏标问题?

一、G1垃圾收集器简介 G1 GC&#xff08;Garbage-First Garbage Collector&#xff09;是一款先进的垃圾收集器&#xff0c;通过 -XX:UseG1GC 参数启用。它首次亮相于JDK 6u14版本&#xff0c;并在JDK 7u4中正式发布。对于熟悉JVM的开发者而言&#xff0c;G1已是一个广为人知的…

测试卡(1)灰卡

#灵感# 灰卡为什么是18%&#xff1f;文章分为三部分&#xff0c;前部分&#xff0c;解释灰卡的定义&#xff0c;后部分是 市场买的18%灰卡的说明书&#xff0c;其中穿插了网络上搜到的灰卡使用案例。 目录 18% 中性灰卡应用说明 1&#xff09; 曝光水平 例子&#xff1a;用灰…

ppt模板如何制作?建议试试这4招

在追求效率和简洁的现代办公环境中&#xff0c;简约风格的PPT模板因其清晰、直观的特点而备受青睐。制作一个简约的PPT模板不仅能提升演示的专业感&#xff0c;还能帮助观众快速抓住重点。 今天&#xff0c;我来告诉大家&#xff1a;ppt模板简约怎么制作&#xff1f;让你能够利…

Kubernetes 节点何时处于就绪状态?

在 Kubernetes 中&#xff0c;节点&#xff08;Node&#xff09;是一个工作负载的基本单元&#xff0c;容器被部署和运行在这些节点上。每个 Kubernetes 节点在加入集群后都需要经过一定的健康检查和状态评估&#xff0c;才能被集群标记为“就绪”状态。这一过程的关键是节点的…

Linux相关概念和重要知识点(9)(父进程、子进程、进程状态)

1.父进程、子进程 &#xff08;1&#xff09;父进程 CLI本质上是一款命令行界面的软件&#xff0c;是用户调用接口层面的程序&#xff08;上层&#xff0c;可以和系统调用接口做沟通&#xff09;&#xff0c;CLI和GUI是同级别的。用户的操作都是建立在CLI和GUI之上的。 但是…

奔三理工男适合转行做AI算法工程师吗?

奔三男生转行可以做什么&#xff1f; 干了几年开发程序员却面临降薪优化&#xff1f; 说实话&#xff0c;如果学历一般技术一般&#xff0c; 无法与时俱进的话&#xff0c;会容易面临尴尬情况…… 就业这件事&#xff0c;选对赛道方向至关重要&#xff01;&#xff01; 这…