【Linux系统编程】shell程序编写

目录

介绍:

一,命令行的编写

二,命令的串的处理

三,命令的执行

四,shell程序总代码


介绍:

        Linux下,Shell是一个用户与操作系统之间的接口,而命令行则是这个接口的表现形式。具体来说Shell是一个命令行解释器,它读取用户输入的命令(或来自脚本的命令),然后解释并执行这些命令。这里需说明一下,Shell程序在处理用户输入的指令时很多时候都是创建子进程来实现,但有些指令功能子进程无法对其做出相应功能的处理,必须依靠父进程完成,比如:cd、export、alias等,这种命令称为内建命令。内建命令是Shell程序的一部分,这种命令程序是嵌入到Shell的源码中,运用时直接由shell解释并执。

        Shell程序的实现,我们可以分为三大步骤:

                1,命令行的编写以及获取用户输入的命令串。这里需考虑特殊情况——指令串为空串。

                2,指令串的处理。这里要考虑指令中的选项参数。

                3,指令的执行。这里的指令依靠子进程来完成。执行指令时要考虑特殊指令,比如内                    建指令。


一,命令行的编写

        当编写shell程序时,首要考虑的是打印命令行。命令行中有用户名、主机名、当前的工作目录消息。由于用户名和工作目录会根据需要而变化,因此这里需使用系统下的环境变量来获取这些信息。这里我们分别设置三个函数接口来调用环境变量实现。

//以下的获取中,通过调用获取指定Shell环境变量的getenv接口来获取我们需要的信息,一旦获取失败同一用 None 表示

const char* HostName()  //获取主机名
{
    char* hostname = getenv("HOSTNAME");
    if (hostname) 
        return hostname;
    else
        return "None";
}
const char* UserName()  //获取用户名
{
    char* username = getenv("USER");
    if (username) 
        return username;
    else
        return "None";
}
const char* WorkDir()   //获取当前目录
{
    char* workdir = getenv("PWD");
    if (workdir)
        return workdir;
    else 
        return "None";
}

        命令行输出完之后下面就要获取输入流中的命令串。这里需注意输入的指令串为空串的情况。当输入的指令串不为空串时进行下一步操作,当输入的指令串为空串时不做任何处理。

int Interact(char* out, int size)  //out:存储的指令串    size:指令串的长度大小
{
    // 输出命令行提示符并获取用户输入的命令字符串
    fprintf(stdout, "[%s@%s %s]$ ", UserName(), HostName(), WorkDir());
    fgets(out, size, stdin);   // 从输入流中获取命令串,要考虑空格字符
    out[strlen(out) - 1] = NULL; // fgets也会把'\n'字符存储,这里去掉此多余字符
    return strlen(out);  // 这里要在后面执行中处理空串的问题
}

        当函数返回值为0时,表明输入的串为空串,此时Shell程序需要从头开始,即输入命令行,存储用户输入的指令,指令的处理。


二,命令的串的处理

        由于输入的命令存在选项参数,为了后面指令程序方便实现,这里需要用指针数组把指令以及参数分别存储下来。

#define MAX_SIZE 64
#define SEP " "   //以空格作为分割符

char* argv[MAX_SIZE];   //存储处理后的指令串
 

void Split(char* in)   // in:存储的指令串
{
    int i = 0;
    //strtok接口函数对输入的字符串指令进行切割
    argv[i++] = strtok(in, SEP);
    while (argv[i++] = strtok(NULL, SEP))
    {   }

    // 下面是针对于 ls 指针的处理,让ls指令显示的目录和文件添加上该有的颜色
    if (strcmp(argv[0], "ls") == 0)
    {
        argv[i - 1] = (char*)"--color";  // 这里把ls指令的颜色选项功能带上
        argv[i] = NULL;
    }
}


三,命令的执行

        在指令的指令前,我们要先考虑内建命令。由于内建命令的在Shell程序中处理的方式各种各样,所以这里我们只实现对cd、export、echo的处理。

        除了内建命令外,其它命令进程的执行是依靠子进程来实现。系统一旦处理到内建命令时,就不能正常当作普通命令来处理,需要单独进行。

       cd指令切换目录时,若让子进程让子进程处理,发现cd指令无法正常切换,因为此时执行指令的不是当前父进程的shell/bash,而是子进程的bash/shell。每当子进程执行完相应的进程后就退出了,无法正常进行切换,所以这里必须看成内建命令进行处理。当处理cd指令时,我们可先使用函数 chdir() 来改变当前的工作路径(目前环境变量中的所在路径没有变换。chidir不会影响环境变量),然后 getcwd 和 putenv  来获取当前进程的绝对路径改变环境变量。这样,再一次进行命令行的输出时就会正常得到工作路径的切换。

        export导入环境变量时,若正常让子进程处理,只会导入到子进程的环境变量表中,父进程存储的环境变量表中根本没有此数据,导致处理失败,因此,需要将export导入环境变量的操作在父进程中执行。当export没有任何参数时不做任何处理,后面一旦跟环境变量串时,我们只需利用putenv导入环境变量表中即可。

        echo命令形式复杂多样,这里我们只对 echo [信息] 和 echo $[环境变量] 进行处理。处理的方式很简单,当使用 echo [信息] 时只需输出信息即可;当 echo $[环境变量] 时只需输出获取的环境变量即可。

        在实现时,我们先认识一下有关系统和C库中的专有函数接口。

chdir接口:更改当前工作目录时,但不会影响任何环境变量

头文件

#include <unistd.h>  

接口形式

int chdir(const char *path);

参数 path 是一个指向要切换到的目录路径的字符串。如果函数执行成功,它将返回0。如果执行失败,它将返回-1

getcwd接口:获取当前工作目录的完整路径

头文件

#include <unistd.h>

接口形式

char *getcwd(char *buf, size_t size);

参数buf指向一个字符数组的指针,该数组用于存储当前工作目录的路径。如果此参数为NULL,则getcwd会动态分配所需的内存。size是数组buf的大小。如果成功,getcwd返回指向buf的指针。如果失败,返回NULL,并设置全局变量errno以指示错误类型。

putenv接口:用于添加或修改环境变量

头文件

#include <stdlib.h>  
接口形式
int putenv(char *string);
putenv接受一个指向以name=value格式表示的字符串的指针,其中name是环境变量的名称,value是该变量的值。如果环境变量已经存在,putenv会修改它的值;如果不存在,它会添加新的环境变量。如果putenv成功,它会返回0;如果失败(比如内存不足),则返回非零值。

getenv:从环境中取字符串,获取环境变量的值

头文件

#include <stdlib.h>

接口形式

char *getenv(const char *name);

name参数是一个指向要检索的环境变量名称的字符串的指针。getenv函数返回的指针指向一个字符串,这个字符串就是环境变量的值。如果getenv找不到指定的环境变量,它会返回NULL

snprintf接口:将格式化的数据写入一个字符串中

头文件

#include <stdio.h>

接口形式

int snprintf(char *str, size_t size, const char *format, ...);

此函数跟printf效果类似,将指定的数据串输出到大小为size的str字符数组中

        下面是对内建命令做出的处理函数。

char* Home()
{
    return getenv("HOME");
}

int BuildCommand()
{
    int ret = 0;
    //检测输入的指令程序是否是内建指令,ret ==1表示是,ret == 0 表示不是
    if (strcmp("cd", argv[0]) == 0)
    {

        ret = 1;
        char* target = argv[1];  // 这里cd内建指令可能存在选项参数
        if (!target) target = Home(); // cd无参形式的情况
        chdir(target);  // 改变当前的工作路径,但目前环境变量中的所在路径没有变换。chidir不会影响环境变量
        // 这里要注意类似于cd ..的情况
        char temp[1024];
        getcwd(temp, 1024); // 获取当前进程的绝对路径
        snprintf(pwd, SIZE, "PWD=%s", temp); //将获得的路径存放到pwd中
        putenv(pwd); //添加或修改当前进程PCB存储中的环境变量
    }
    // 处理导入系统下的环境变量
    else if(strcmp("export", argv[0]) == 0)
    {
        ret = 1;
        if (argv[1]) // export使用时,参数可能为空
        {
            strcpy(env, argv[1]);
            putenv(env);
        }
    }
    else if(strcmp("echo", argv[0]) == 0)
    {
        ret = 1;
        if (argv[1] == NULL)
        {
            cout << endl;
        }
        else 
        {
            if (argv[1][0] == '$')
            {
                if (argv[1][1] == '?')  //输出进程退出状态
                {
                    cout << lastcode << endl;
                    lastcode = 0;
                }
                else  //输出指定环境变量
                {
                    char* e = getenv(argv[1] + 1);
                    if (e) cout << e << endl;
                }
            }
            else  //输出指定信息串
            {
                cout << argv[1] << endl;
            }
        }
    }
    return ret;
}

        系统下的普通命令,我们只需创建子进程,运用进程替换去实现即可。代码如下:

void Execute()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 让子进程执行命令
        execvp(argv[0], argv);
        exit(1);
    }

    //为防止僵尸进程,这里需进行进程等待
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid == id) lastcode = WEXITSTATUS(status); // 保存子进程的退出码
}


四,shell程序总代码

        具体的接口功能已经全部实现,下面我们需进行逻辑的关联。这里面的逻辑很简单,由于shell程序总是存在的,因此这里我们只需分别套用无限循环对以上三大步骤进行实现即可。若是发现步骤一中的空串情况或步骤三中内建命令,就不在进行以下步骤。

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;

#define SIZE 1024
#define MAX_SIZE 64
#define SEP " "

char* argv[MAX_SIZE];
char pwd[SIZE];
char env[SIZE]; 
int lastcode = 0;

const char* HostName()
{
    char* hostname = getenv("HOSTNAME");
    if (hostname) 
        return hostname;
    else
        return "None";
}
const char* UserName()
{
    char* username = getenv("USER");
    if (username) 
        return username;
    else
        return "None";
}
const char* WorkDir()
{
    char* workdir = getenv("PWD");
    if (workdir)
        return workdir;
    else 
        return "None";
}

char* Home()
{
    return getenv("HOME");
}

int Interact(char* out, int size)
{
    // 输出命令行提示符并获取用户输入的命令字符串
    fprintf(stdout, "[%s@%s %s]$ ", UserName(), HostName(), WorkDir());
    fgets(out, size, stdin);
    out[strlen(out) - 1] = NULL; // fgets也会把'\n'字符存储,这里去掉getcwd此多余字符
    return strlen(out);
}

void Split(char* in)
{
    int i = 0;
    //strtok 对输入的字符串指令进行切割
    argv[i++] = strtok(in, SEP);
    while (argv[i++] = strtok(NULL, SEP))
    {   }
    // 让ls指令显示的目录和文件添加上该有的颜色
    if (strcmp(argv[0], "ls") == 0)
    {
        argv[i - 1] = (char*)"--color";  // 这里把ls指令的颜色选项功能带上
        argv[i] = NULL;
    }
}

void Execute()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 让子进程执行命令
        execvp(argv[0], argv);
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid == id) lastcode = WEXITSTATUS(status); // 保存子进程的退出码
}

int BuildCommand()
{
    int ret = 0;
    //1, 检测输入的指令程序是否是内建指令,1表示是,0表示不是
    if (strcmp("cd", argv[0]) == 0)
    {
        ret = 1;
        char* target = argv[1];  // 这里cd内建指令可能存在选项参数
        if (!target) target = Home(); // cd无参形式的情况
        chdir(target);  // 改变当前的工作路径,但目前环境变量中的所在路径没有变换。chidir不会影响环境变量
        // 这里要注意类似于cd ..的情况

        char temp[1024];
        getcwd(temp, 1024); // 获取当前进程的绝对路径
        snprintf(pwd, SIZE, "PWD=%s", temp); //将获得的路径存放到pwd中
        putenv(pwd); //添加或修改当前进程PCB存储中的环境变量
    }
    // 处理导入系统下的环境变量
    else if(strcmp("export", argv[0]) == 0)
    {
        ret = 1;
        if (argv[1]) // export使用时,参数可能为空
        {
            strcpy(env, argv[1]);
            putenv(env);
        }
    }
    else if(strcmp("echo", argv[0]) == 0)
    {
        ret = 1;
        if (argv[1] == NULL) cout << endl;
        else 
        {
            if (argv[1][0] == '$')
            {
                if (argv[1][1] == '?')
                {
                    cout << lastcode << endl;
                    lastcode = 0;
                }
                else 
                {
                    char* e = getenv(argv[1] + 1);
                    if (e) cout << e << endl;
                }
            }
            else  cout << argv[1] << endl;
        }
    }
    return ret;
}

int main()
{
    while (true)
    {
        //1, 输出命令行提示符并获取用户输入的命令字符串                                                        
        char commandline[SIZE];                                                                                              
        int n = Interact(commandline, SIZE);                                                                            
        if (n == 0) continue; // 处理空串的问题                                                                           
       

        //2, 对命令行字符串进行切割                                                                                            
        Split(commandline);                                                                                                       
       

        //3, 处理内建命令 
        n = BuildCommand();                                                                                                              if (n) continue;                                                                                                                                                               
        //4, 开始执行输入的程序指令                                                                                            
        Execute();
    }
    return 0;
}

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值