自定义Shell命令行解释器

发布于:2025-06-03 ⋅ 阅读:(26) ⋅ 点赞:(0)

目录

1、目标

2、显示命令提示符

2.1 getenv

2.2 getcwd

2.3 putenv

3、获取用户输入的命令

4、解析命令

5、处理内建命令

6、处理外部命令

7、完整代码

7.1 myshell.cpp

7.2 Makefile


1、目标

实现一个Linuxmyshell,有以下基本的功能。

  1. 显示命令提示符
  2. 获取用户输入的命令
  3. 解析命令
  4. 处理内建命令
  5. 处理外部命令

myshell有一张命令行参数表环境变量表(继承bash的,其实应该要从配置文件中获取,但比较麻烦)。

2、显示命令提示符

这里就要了解一下:getenv(),getcwd(),putenv()了。

2.1 getenv

获取环境变量

char *getenv(const char *name);
  • 根据环境变量名(如 "PATH")返回其对应的值(字符串)。

  • 如果变量不存在返回 NULL

2.2 getcwd

获取当前工作路径

char *getcwd(char *buf, size_t size);
  • 当前工作目录的绝对路径写入 buf,并返回 buf

  • 需确保 buf 足够大(否则返回 NULL,errno = ERANGE)。

注意:

进程的环境变量表中的PWD,是根据进程的CWD(进程当前的工作路径,在/proc/pid/下可以看到),进行更新的。 

2.3 putenv

设置环境变量

int putenv(char *string);
  • 设置环境变量,格式"KEY=VALUE" 的字符串。如果已存在相同的KEY,就覆盖VALUE

  • 成功返回 0失败返回非零

注意:

putenv()传的是指针要放在环境变量表里生命周期要和程序一样长,所以传全局变量

改变这个全局变量环境变量表中也会改变(当时不太理解,中坑了。)

const char* getUserName()
{
    const char* USER = getenv("USER");
    return USER == NULL?"None":USER;
}

const char* getHostName()
{
    const char* HOSTNAME = getenv("HOSTNAME");
    return HOSTNAME == NULL?"None":HOSTNAME;
}

std::string getCwd()
{
    char cwdenv[1024];
    char* cwd = getcwd(cwdenv,sizeof(cwdenv));
    
    return cwd == NULL? "None":cwdenv; // 因为返回的是局部变量,所以使用string,进行拷贝
}

char pwd[1024];
void cmdPrompt()
{
    std::string path = getCwd();

    // 更新环境变量PWD,不能putenv局部变量。
    snprintf(pwd,sizeof(pwd),"PWD=%s",path.c_str());
    putenv(pwd);

    if(path.size() != 1) // 不是/根目录
    {
        int index = path.rfind('/');
        path = path.substr(index+1);
    }

    std::cout<<"["<<getUserName()<<"@"<<getHostName()<<" "<<path<<"]"<<"$$";
}

3、获取用户输入的命令

bool getCmd(char cmd[],int size)
{
    char* s = fgets(cmd,size,stdin);
    if(s != NULL)
    {
        int len = strlen(cmd);
        cmd[len-1] = '\0'; // fgets会读取'\n',需要处理
    }
    return s != NULL;
}

获取成功,返回true,获取失败,返回false。 

4、解析命令

bool parseCmd(char cmd[],char* argv[])
{
    const char* delim = " ";
    char* token = strtok(cmd,delim);
    int i = 0;
    while(token != NULL)
    {
        argv[i] = token;
        token = strtok(NULL,delim);
        ++i;
    }
    argv[i] = NULL; // 进程替换,要求以NULL结尾

    return i != 0;
}
char *strtok(char *str, const char *delim);
  • str:要分割的字符串。第一次调用时传入原始字符串后续调用传入 NULL

  • delim:包含所有可能分隔符的字符串。

  • 返回分割出的子字符串的指针。如果没有更多子字符串,则返回 NULL

  • strtok 会在找到的分隔符位置插入 '\0' 字符,因此会修改原始字符串。

5、处理内建命令

内建命令需要父进程自己执行(如:cd,需要改变自己的路径),或父进程自己执行效率更高(如:echo)。 

std::string getCwd()
{
    char cwdenv[1024];
    char* cwd = getcwd(cwdenv,sizeof(cwdenv));
    
    return cwd == NULL? "None":cwdenv; // 因为返回的是局部变量,所以使用string,进行拷贝
}

const char* getHome()
{
    const char* HOME = getenv("HOME");
    return HOME == NULL?"None":HOME;
}

const char* getOldPwd()
{
    const char* OLDPWD = getenv("OLDPWD");
    return OLDPWD == NULL?"None":OLDPWD;
}

char oldPwd[1024];
void cd(char* argv[])
{ 
    std::string cwdenv = getCwd();

    if(argv[1] == NULL || strcmp(argv[1],"~") == 0)
    {
        snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
        putenv(oldPwd); // 更新OLDPWD

        chdir(getHome());
    }
    else if(strcmp(argv[1],"-") == 0)
    {
        chdir(getOldPwd());
        
        snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());

        putenv(oldPwd);
    }
    else
    {
        snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
        putenv(oldPwd); // 更新OLDPWD
       
        chdir(argv[1]);
    }
}

int lastExitno = 0;
void echo(char* argv[])
{
    if(argv[1] == NULL)
        return;
   
    std::string cmd = argv[1];
    // echo $?
    // echo $PATH
    // echo "hello world"

    if(cmd[0] == '$')
    {
        if(cmd.substr(1) == "?")
        {
            std::cout<<lastExitno<<std::endl;
            lastExitno = 0;
        }
        else
        {
            if(getenv(cmd.substr(1).c_str()))
            std::cout<<getenv(cmd.substr(1).c_str())<<std::endl;
        }
    }
    else
    {
        std::cout<<cmd<<std::endl;
    }
}

bool executeBuiltIn(char* argv[])
{
    std::string cmd = argv[0];
    if(cmd == "cd")
    {
        cd(argv);
        return true;
    }
    else if(cmd == "echo")
    {
        echo(argv);
        return true;
    }
    // ...
    else
    {

    }

    return false;
}

注意:

    else if(strcmp(argv[1],"-") == 0)
    {
        chdir(getOldPwd());
        
        snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());

        putenv(oldPwd);
    }

如果之前putenv(oldPwd),传的是指针,

若先snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());

那么,环境变量表中的OLDPWD就已经是CWD了,那么chdir(getOldPwd());就出错了。

6、处理外部命令

外部命令,防止父进程挂了,所以创建子进程,进行程序替换(可执行任意程序)。 

int executeExternal(char* argv[])
{
    pid_t id = fork();
    if(id == -1)
        return 1;
    if(id == 0)
    {
        execvp(argv[0],argv);
        exit(2);
    }
    int status = 0;

    pid_t wid = waitpid(id,&status,0);

    if(wid == id && WIFEXITED(status)) // 子进程退出,且是正常退出
        lastExitno = WEXITSTATUS(status);

    return 0;
}

7、完整代码

7.1 myshell.cpp

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

const char* getUserName()
{
    const char* USER = getenv("USER");
    return USER == NULL?"None":USER;
}

const char* getHostName()
{
    const char* HOSTNAME = getenv("HOSTNAME");
    return HOSTNAME == NULL?"None":HOSTNAME;
}

const char* getHome()
{
    const char* HOME = getenv("HOME");
    return HOME == NULL?"None":HOME;
}

std::string getCwd()
{
    char cwdenv[1024];
    char* cwd = getcwd(cwdenv,sizeof(cwdenv));
    
    return cwd == NULL? "None":cwdenv; // 因为返回的是局部变量,所以使用string,进行拷贝
}

const char* getOldPwd()
{
    const char* OLDPWD = getenv("OLDPWD");
    return OLDPWD == NULL?"None":OLDPWD;
}

char pwd[1024];
void cmdPrompt()
{
    std::string path = getCwd();

    // 更新环境变量PWD,不能putenv局部变量。
    snprintf(pwd,sizeof(pwd),"PWD=%s",path.c_str());
    putenv(pwd);

    if(path.size() != 1) // 不是/根目录
    {
        int index = path.rfind('/');
        path = path.substr(index+1);
    }

    std::cout<<"["<<getUserName()<<"@"<<getHostName()<<" "<<path<<"]"<<"$$";
}

bool getCmd(char cmd[],int size)
{
    char* s = fgets(cmd,size,stdin);
    if(s != NULL)
    {
        int len = strlen(cmd);
        cmd[len-1] = '\0'; // fgets会读取'\n',需要处理
    }
    return s != NULL;
}

bool parseCmd(char cmd[],char* argv[])
{
    const char* delim = " ";
    char* token = strtok(cmd,delim);
    int i = 0;
    while(token != NULL)
    {
        argv[i] = token;
        token = strtok(NULL,delim);
        ++i;
    }
    argv[i] = NULL; // 进程替换,要求以NULL结尾

    return i != 0;
}

char oldPwd[1024];
void cd(char* argv[])
{ 
    std::string cwdenv = getCwd();

    if(argv[1] == NULL || strcmp(argv[1],"~") == 0)
    {
        snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
        putenv(oldPwd); // 更新OLDPWD

        chdir(getHome());
    }
    else if(strcmp(argv[1],"-") == 0)
    {
        chdir(getOldPwd());
        
        snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());

        putenv(oldPwd);
    }
    else
    {
        snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
        putenv(oldPwd); // 更新OLDPWD
       
        chdir(argv[1]);
    }
}

int lastExitno = 0;
void echo(char* argv[])
{
    if(argv[1] == NULL)
        return;
   
    std::string cmd = argv[1];
    // echo $?
    // echo $PATH
    // echo "hello world"

    if(cmd[0] == '$')
    {
        if(cmd.substr(1) == "?")
        {
            std::cout<<lastExitno<<std::endl;
            lastExitno = 0;
        }
        else
        {
            if(getenv(cmd.substr(1).c_str()))
            std::cout<<getenv(cmd.substr(1).c_str())<<std::endl;
        }
    }
    else
    {
        std::cout<<cmd<<std::endl;
    }
}

bool executeBuiltIn(char* argv[])
{
    std::string cmd = argv[0];
    if(cmd == "cd")
    {
        cd(argv);
        return true;
    }
    else if(cmd == "echo")
    {
        echo(argv);
        return true;
    }
    // ...
    else
    {

    }

    return false;
}

int executeExternal(char* argv[])
{
    pid_t id = fork();
    if(id == -1)
        return 1;
    if(id == 0)
    {
        execvp(argv[0],argv);
        exit(2);
    }
    int status = 0;

    pid_t wid = waitpid(id,&status,0);

    if(wid == id && WIFEXITED(status)) // 子进程退出,且是正常退出
        lastExitno = WEXITSTATUS(status);

    return 0;
}

int main()
{
    while(true)
    {
        // 命令提示符
        cmdPrompt();

        // 获取用户输入命令
        char cmd[1024] = {0};
        if(!getCmd(cmd,sizeof(cmd)))
            continue;

        // 解析命令
        char* argv[1024] = {0};
        if(!parseCmd(cmd,argv))
            continue;

        // 内建命令
        if(executeBuiltIn(argv))
            continue;

        // 执行命令
        executeExternal(argv);
    }

    return 0;
}

7.2 Makefile

TARGET := myshell
SRCS := myshell.cpp
SUFFIX := .cpp
OBJS := $(SRCS:$(SUFFIX)=.o)
CC := g++

$(TARGET): $(OBJS)
	$(CC) -o $@ $^

%.o: %$(SUFFIX)
	$(CC) -c $<

.PHONY: clean
clean:
	rm -f $(OBJS) $(TARGET)

网站公告

今日签到

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