=Linux=一步步自己写一个shell程序


title: =Linux=一步步自己写一个shell程序
date: 2024-06-04 00:13:55
tags: Linux C
cover: https://picbed0521.oss-cn-shanghai.aliyuncs.com/blogpic/craftTerminal.png

原文见我的网站: www.supdriver.top


系统:阿里云服务器Linux CentOs 7

编辑器: vim

编译器: gcc (支持C99)


文件

本次写的程序较为简单,所以只使用一个源文件

所以在shell中touch一个makefile和一个myshell.c

shell

touch makefile
touch myshell.c

然后编辑makefile文件

makefile

1 myshell:myshell.c                         
  gcc -o $@ $^ -std=c99

.PHONY:clean
clean:
   rm -f myshell

头文件

本程序因函数较杂,会include较多头文件

myshell.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>

宏定义

为了统一修改部分参数,以及使参数更易读,这里使用部分宏定义

myshell.c

#define LEFT "["
#define RIGHT "]"
#define LABEL "# "//注意有个空格

#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define DLIM " \t" //可能有多个分隔符
#define EXIT_CODE 446 //特殊的退出码,表示程序未正常退出,具体数字目前没有约定

全局变量

我们需要用全局的变量来存储命令行(command line)和参数包

myshell.c

char cline[LINE_SIZE];
char* arg[ARGC_SIZE];
int last_code = 0;

Interact函数实现交互功能

打印命令行头部

为了打印命令行头部,我们需要知道三样东西:用户,主机,工作路径,这里包装了三个函数来分别调用getenv函数

myshell.c

const char* getusername()//获取用户名
{
    return getenv("USER");
}

const char* gethostname()//获取主机名
{
    return getenv("HOSTNAME");
}

const char* getpwd()//获取工作路径
{
    return getenv("PWD");
}

因此打印的代码为

printf(LEFT "%s@%s %s" RIGHT LABLE,getusername(),gethostname(),getpwd());

获取命令行

使用Linux的终端时,我们会打命令+空格+参数...,因此我们的myshell程序也要支持连空格一起读入,读入一整行命令

所以scanf并不适合用来读入命令,这次我们使用fgets函数,这个函数可以从文件流中整行读入,而正好在终端输入的字符都储存在标准输入流,即stdin中,因此可以用一行代码获取命令行

为安全考虑,这里使用一个临时变量s来接受fgets的返回值并用assert判空,但在release版本中assert不被编译,导致变量s未被调用,而报警告(甚至报错),所以还要再加一句(void) s,只为了调用一下s,没有更多用处

之后便完成了文件流的读取

但此时获得的命令行在\0前以\n结尾,所以要把\n替换为\0

char *s = fgets(cline,size,stdin);
assert(s);//s为空时报错
(void) s;//防止因未调用s而报警告

cline[strlen(cline) - 1] = '\0';

整个函数体

myshell.c

void Interact(char* cline,int size)
{
    printf(LEFT "%s@%s %s" RIGHT LABLE,getusername(),gethostname(),getpwd());//打印头部
    fgets(cline,size,stdin);//获取命令行
    assert(s);//s为空时报错
    (void) s;//防止因未调用s而报警告
    cline[strlen(cline) - 1] = '\0';

    printf("echo: %s\n");//写一段测一段的测试代码,输出获取的命令行,测完可删
}

测试

先在main函数里调用一次Interact函数测试一下

我的测试结果如下

可以看到达到了预期效果,但是工作路径太长了,还是学一学Linux的展示方式吧,我们来把getpwd函数重写一下

重写getpwd()

const char* getpwd()
{
    const char* pwd = getenv("PWD");
    int n = strlen(pwd);
    while(n)
    {
        if(pwd[n] == '/') break;
        n--;
    }
    return (pwd+n+1);
}

这样打印出的工作路径仅为当前文件夹,可以缩短很多长度

分割命令行

现在的cline中的命令行还是完整的一串,需要分割出命令和参数包,因此我们也封装一个函数Splitcline

这里使用的是string.h中的strtok函数,可以用特定的单个或多个字符将字符串分割

myshell.c

int Splitcline(char*cline,char** argv)
{
    memset(argv,0,sizeof(char*) * ARGV_SIZE);
    int i = 1;
    argv[0] = strtok(cline,DLIM);
    if(argv[0])
        while(argv[i++] == strtok(NULL,DLIM));

    *argv_n = i-1;//输出型参数
}

再写一段测试代码

myshell.c

int main()
{
    Interact(cline,sizeof(cline));

    Splitcline(cline,argv);
    for(int i = 0;argv[i];i++)//逐行打印输出argv的内容
    {
        printf("%s\n",argv[i]);
    }
    return 0;
}

执行外部命令

通过fork函数创建子进程,然后用execvp替换子进程,通过环境变量PATH找到外部命令并替换到子进程执行,同时父进程myshell调用waitpid函数等待子进程结束,保证myshell程序正常运行

myshell.c

void ExternalCommand()
{
    pid_t id = fork();
    if(id <0)
    {
        perror("fork");
        return;
    }
    else if(id == 0)//child
    {
        execvp(argv[0],argv+1);//+1之后才是参数列表
        exit(EXIT_CODE);
    }
    else
    {
        int status = 0;
        pid_t rid = waitpid(id,&status,0);
        if(rid == id)
        {
            last_code = WEXITSTATUS(status);
        }
        return;
    }
}

执行内建命令

shell中并不是所有的命令都由子进程完成的,比如用cd命令改变工作路径,就不能让子进程去执行(否则只是改了子进程的路径),因此我们还需要加一个内建命令接口

myshell.c

int BuildCommand(char* _argv[],int _argv_n)//处理内建命令
{
  if(_argv_n == 2 && strcmp(_argv[0],"cd")== 0)//特殊处理的命令1
  {
    chdir(argv[1]);
    getpwd();
    sprintf(getenv("PWD"),"%s",pwd);
    return 1;//完成执行返回1
  }
  //还可以继续else uf 加特殊处理的命令2,3,4,,,n
  return 0;//未执行内建命令。返回0
}

完成框架

至此,把main函数组织好后,一个简单的shell代码框架就搭好了,可以根据需要继续扩展内建命令的内容,比如导出环境变量,实现echo指令等(略写)。

myshell.c

int main()
{
   int quit = 0;
   while(!quit)
   {
     Interact(cline,sizeof(cline));
     int argv_n;
     Splitcline(cline,argv,&argv_n);
 
     if(argv_n == 0 )continue;
     
     int flag = BuildCommand(argv,argv_n);
     if(!flag) ExeternalCommand();
   }
 
    return 0;                            
}          

拓展

这里的命令行处理并没有考虑输入/输出重定向,所以仍有较大的需要完善的地方

源代码

点我去往github仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值