【Linux】自定义简易shell

发布于:2024-12-07 ⋅ 阅读:(112) ⋅ 点赞:(0)

【Linux】自定义简易shell

 

🥕个人主页:开敲🍉

🔥所属专栏:Linux🍊

🌼文章目录🌼

1. 实现思路

2. 实现代码

    2.1 输出命令行提示符

    2.2 获取用户输入信息

    2.3 解析用户输入命令

    2.4 执行用户输入命令

    2.5 小优化(可写可不写)

3. 实现源码

在学习完 Linux 进程控制之后,我们就可以用已有的知识来自己实现一个简易的 shell程序。

1. 实现思路

  仔细留意我们在 shell 中输入命令时,在每一行的最开始会打印这么一串信息:

  其中包含了:用户名、主机名、当前路径。这三个信息实际上分别对应的是环境变量的:USER、HOSTNAME、PWD。这就说明我们在实现自己的 shell 时需要获取这三个环境变量并打印。

  接着,在打印这一串信息后,光标就会卡在当前行的末尾:

  这是在等在用户输入信息,因此我们还需要获取用户输入的信息,在用户输入信息之前光标卡死不动,这里我们可以用 fgets 接口来帮助我们实现。

  随后,在我们输入完一串信息后,shell程序并不会直接退出,而是继续重复上面的过程:打印命令行提示符:用户名、主机名、当前路径;等待用户输入信息。

  因此我们可以把上面的过程写成一个死循环,只要程序不退出,就一直运行。

  最后当用户输入完命令后,我们就要执行用户所输入的命令,这里就用到了我们 进程控制 中所学的 程序替换

  综上,我们实现 shell 的大体思路就出来了,后面还有很多细节我们实现时作优化。

2. 实现代码
    2.1 输出命令行提示符

  输出命令行提示符:用户名、主机名、当前路径

snprintf:

第一个参数:要写入的空间的地址

第二个参数:写入的格式,类似 printf:%d %s 等格式

最后的 ...:可变参数,第二个参数中需要的参数格式及个数是什么这里就传什么

实现代码参考:

    2.2 获取用户输入信息

  使用 fgets 获取用户输入信息:

  获取用户输入信息就这一个函数,非常简单,但是我们光获取到了用户输入的信息还没法用于后面的执行:因为当前获取到的只是一串字符串 如:"ls -a -l"。我们需要对这个字符串进行解析操作,将 "ls"  "-a"  "-l" 全部剥离出来。

参考实现代码:

​ #include <iostream>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <unistd.h>
 using namespace std;

#define FORMAT "%s@%s %s # "

//命令行提示符
const char* GetUserName()
{
    const char* un = getenv("USER");
    return un == nullptr ? "None" : un;
}

 const char* GetHostName()
 {
    const char* hn = getenv("HOSTNAME");
    return hn == nullptr ? "None" : hn;
 }
 const char* GetPwd()
 {
     const char* pwd = getenv("PWD");
     return pwd == nullptr ? "None" : pwd;
 }

//输出命令行提示符
 void CmdPrompt()
 {
     char prompt[GETSIZE];
     snprintf(prompt,sizeof(prompt),FORMAT,GetUserName(),GetHostName(),GetPwd()));
     printf("%s",prompt);
 }

  下面就是解析用户输入信息。

    2.3 解析用户输入命令

  我们使用 strtok 函数来帮助我们解析:

第一个参数:需要解析的字符串

第二个参数:解析的格式,比如:传 "/",则会以 "/" 作为分隔符,将字符串一个个拆分

  因为我们解析完后需要执行解析后的命令,因此我们这里将解析出来的一个个字符串放入一个新的数组中:argv。看到这个名称相信你就能够恍然大悟:这不就是参数表吗?同时我们还需要一个参数表示 argv 中的参数个数:argc。看到这个名称也会让你再次恍然大悟。

  以空格为分隔符,将用户输入信息拆分存入 argv 中,argc 表示的就是参数个数。最后 argc-- 是因为我们最后还写入了一个 nullptr,这也会算进参数中,因此我们需要 --。因为 argc 是全局变量,因此为了保证我们每次输入命令能够正确拆分,每次调用都将 argc 置为0。

参考实现代码:

#define MAXSIZE 128
char* argv[MAXSIZE]
int argc = 0;

//解析用户输入的命令
 void ParseCommand(char* command)
 {
 #define SEP " "
     argc = 0;
     argv[argc++] = strtok(command,SEP);
     while(argv[argc++] = strtok(nullptr,SEP));
     argc--;
 }
    2.4 执行用户输入命令

  这里就要用到我们 进程控制 中学的 exec 系列函数,来进行进程替换。但是这里我们不能直接替换当前进程,我们创建一个子进程,让子进程执行进程替换,替换子进程不影响父进程。

参考实现代码:

pid_t id = fork();
if(id==0)
{
     execvp(argv[0],argv);
     exit(1);
}
 
pid_t pid = waitpid(-1,nullptr,0);
    2.5 小优化(可写可不写)

  实现完前面的功能,我们的自定义简易shell就已经成型了,不过还有个小问题:我们在 输出命令行提示符 里输出当前路径时直接用的是 PWD,这会直接输出当前路径,非常长,而系统的 shell 输出的只是当前目录,因此我们这里可以对路径的打印做个小优化。

参考实现代码:

//优化命令行路径
 string OptimizePrompt(const char* pwd)
 {
 #define SLASH "/"
     string s = pwd;
     if(s==SLASH) return "/";
     auto pos = s.rfind(SLASH);
     if(pos==string::npos) return "BUG?";
     return s.substr(pos+1);
 }


//输出命令行提示符
 void CmdPrompt()
 {
     char prompt[GETSIZE];
     snprintf(prompt,sizeof(prompt),FORMAT,GetUserName(),GetHostName(),OptimizePrompt(GetPwd()).c_str());
     printf("%s",prompt);
 }
3. 实现源码
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;

#define GETSIZE 1024
#define FORMAT "%s@%s %s # "
#define MAXSIZE 128

char* argv[MAXSIZE];
int argc = 0;

//输出命令行提示符
const char* GetUserName()
{
    const char* un = getenv("USER");
    return un==nullptr?"None":un;
}

const char* GetHostName()
{
    const char* hn = getenv("HOSTNAME");
    return hn==nullptr?"None":hn;
}

const char* GetPwd()
{
    const char* pwd = getenv("PWD");
    return pwd==nullptr?"None":pwd;
}


//优化命令行路径
string OptimizePrompt(const char* pwd)
{
#define SLASH "/"
    string s = pwd;
    if(s==SLASH) return "/";
    auto pos = s.rfind(SLASH);
    if(pos==string::npos) return "BUG?";
    return s.substr(pos+1);
}

//输出命令行提示符
void CmdPrompt()
{
    char prompt[GETSIZE];
    snprintf(prompt,sizeof(prompt),FORMAT,GetUserName(),GetHostName(),OptimizePrompt(GetPwd()).c_str());
    printf("%s",prompt);
}

//获取用户输入的命令
bool GetCommand(char* command,int size)
{
    char* ret = fgets(command,size,stdin);
    if(!ret) return false;
    command[strlen(command)-1] = 0;
    if(!strlen(command)) return false;
    return true;
}

//解析用户输入的命令
void ParseCommand(char* command)
{
#define SEP " "
    argc = 0;
    argv[argc++] = strtok(command,SEP);
    while(argv[argc++] = strtok(nullptr,SEP));
    argc--;
}
int main()
{
    while(1)
    {
        //1. 输出命令行提示符
        CmdPrompt();
        //2. 获取用户输入命令
        char command[GETSIZE];
        if(!GetCommand(command,sizeof(command)))
            continue;
        //3. 解析用户命令
        ParseCommand(command);
        //DeBugPC();
        //4. 执行用户命令
        pid_t id = fork();
        if(id==0)
        {
            execvp(argv[0],argv);
            exit(1);
        }

        pid_t pid = waitpid(-1,nullptr,0);

    }
    return 0;
}

                                                 创作不易,点个赞呗,蟹蟹啦~ 


网站公告

今日签到

点亮在社区的每一天
去签到