linux shell

关注公众号 jb51net

关闭
首页 > 脚本专栏 > linux shell > Linux之简易Linux Shell实现

Linux之简易Linux Shell的实现过程

作者:码完就睡

本文通过C语言实现一个简易命令行解释器,解析用户输入的指令,处理内置命令,并创建子进程执行外部命令,文章详细介绍了命令提示符、命令解析及主函数实现逻辑

一、引言

在使用 Linux 终端时,我们输入一条命令(例如 ls -l)后,终端就会解析并执行对应的指令,最终将结果展示出来。

为了理解这一过程背后的实现原理,本文将通过C语言代码,手动实现一个简易的命令行解释器,模拟 Shell 的核心执行逻辑。

引言

二、核心模块分析

(一)打印命令提示符

1.区分普通用户和管理员

普通用户使用 $ 作为提示符;管理员使用 # 作为提示符

用户ID = 0 ,则说明当前是管理员。

1.区分普通用户和管理员

/*区分普通用户和管理员*/
char *str = "$"; //初始默认为普通用户,使用 $
int uid = getuid(); //获取当前用户ID
if(uid == 0) str = "#"; //若 用户ID = 0, 则为管理员, 使用 #

2.获取用户信息、主机名称、当前路径

/*获取用户信息、主机名称、当前路径*/
struct passwd *ptr = getpwuid(uid); //获取用户信息

//如果获取用户信息失败,就打印一个简单的提示符 mybash $,直接退出函数
if(ptr == NULL)
{
    printf("mybash $");
    fflush(stdout);
    return;
}

char host[128] = {0};
gethostname(host, 128); //获取主机名称
    
char path[128] = {0};
getcwd(path, 128); //获取当前路径

3.打印

3.打印

注意:获取到的用户信息都在 ptr 这个结构体中,pw_name 就是用户名

/*打印*/
printf("%s@%s:%s%s", ptr->pw_name, host, path, str);
fflush(stdout); //刷新输出缓冲区

4.完整代码

/*打印命令提示符*/
void printf_info()
{
    /*区分普通用户和管理员*/
    char *str = "$"; //初始默认为普通用户,使用 $
    int uid = getuid(); //获取当前用户ID
    if(uid == 0) str = "#"; //若 用户ID = 0, 则为管理员, 使用 #

    /*获取用户信息、主机名称、当前路径*/
    struct passwd *ptr = getpwuid(uid); //获取用户信息

    //如果获取用户信息失败,就打印一个简单的提示符 mybash $,直接退出函数
    if(ptr == NULL)
    {
        printf("mybash $");
        fflush(stdout);
        return;
    }

    char host[128] = {0};
    gethostname(host, 128); //获取主机名称
    
    char path[128] = {0};
    getcwd(path, 128); //获取当前路径

    /*打印*/
    printf("%s@%s:%s%s", ptr->pw_name, host, path, str);
    fflush(stdout); //刷新输出缓冲区
}

(二)命令解析

在Shell中,用户输入的一条完整指令通常由命令、选项、参数共同组成。

为了让程序能够识别并执行指令,我们需要对输入的整行字符串按空格进行分割,将其拆解为独立的命令与参数列表,供后续执行使用。

strtok():字符串分割函数;可以按照规定的方式分割字符串

/*命令解析*/
char *get_cmd(char buff[], char *myargv[])
{
    int i = 0;
    char *s = strtok(buff, " "); //按空格分隔第一个单词
    while(s != NULL) //当分割出来的字符串为NULL时,说明分割完成,循环结束
    {
        myargv[i++] = s; //将分割出来的字符串放进myargv数组中
        s = strtok(NULL, " "); //NULL会继续分割剩余的字符串
    }
    
    myargv[i] = NULL; //让myargv以NULL结尾,避免后续使用execvp错误

    return myargv[0]; //返回第一个单词,即命令名(如 ls、cd)
}

(三)主函数

1.打印命令提示符

/*打印命令提示符*/
printf_info(); 

2.读取用户输入

/*读取用户输入*/
char buff[128] = {0};
fgets(buff, 128, stdin); //读取用户输入
buff[strlen(buff) - 1] = '\0'; //去掉末尾换行

3.解析命令

/*解析命令*/
char *myargv[MAX_SIZE] = {0};
char *cmd = get_cmd(buff, myargv); //分割命令,cmd是返回的是输入的命令名(如 ls、cd)

4.处理空命令和内置命令

cd 是 Shell 内置命令,不能通过创建子进程执行;

chdir()是 Linux 系统调用,用于切换当前工作目录

/*处理空命令和内置命令*/
if(cmd == NULL) continue; //输入为空,重新循环

if(strcmp(cmd, "exit") == 0) exit(0); //输入exit,退出

if(strcmp(cmd, "cd") == 0) 
{
    chdir(myargv[1]); //需要使用chdir()函数进行跳转
    continue;
}

5.创建子进程

/*创建子进程*/
pid_t pid = fork();
if(pid == -1) exit(0); //创建失败,直接退出

6.子进程执行外部命令

/*子进程执行外部命令*/
if(pid == 0)  //子进程
{
    execvp(cmd, myargv); //执行命令
    //执行成功后,子进程会完全变成目标命令,不会再往下执行

    printf("exec err!!!\n");
    exit(1);
}

7.父进程等待子进程

/*父进程等待子进程*/
else wait(1); //父进程,等待子进程结束

8.完整代码

/*主函数*/
int main()
{
    while(1) 
    {
        /*打印命令提示符*/
        printf_info(); 

        /*读取用户输入*/
        char buff[128] = {0};
        fgets(buff, 128, stdin); //读取用户输入
        buff[strlen(buff) - 1] = '\0'; //去掉末尾换行
        
        /*解析命令*/
        char *myargv[MAX_SIZE] = {0};
        char *cmd = get_cmd(buff, myargv); //分割命令,cmd是返回的是输入的命令名(如 ls、cd)
        
        /*处理空命令和内置命令*/
        if(cmd == NULL) continue; //输入为空,重新循环

        if(strcmp(cmd, "exit") == 0) exit(0); //输入exit,退出

        if(strcmp(cmd, "cd") == 0) 
        {
            chdir(myargv[1]); //需要使用chdir()函数进行跳转
            continue;
        }

        /*创建子进程*/
        pid_t pid = fork();
        if(pid == -1) exit(0); //创建失败,直接退出

        /*子进程执行外部命令*/
        if(pid == 0)  //子进程
        {
            execvp(cmd, myargv); //执行命令
            //执行成功后,子进程会完全变成目标命令,不会再往下执行

            printf("exec err!!!\n");
            exit(1);
        }

        /*父进程等待子进程*/
        else wait(1); //父进程,等待子进程结束
    }
}

三、测试

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

#define MAX_SIZE 100

/*打印命令提示符*/
void printf_info()
{
    /*区分普通用户和管理员*/
    char *str = "$"; //初始默认为普通用户,使用 $
    int uid = getuid(); //获取当前用户ID
    if(uid == 0) str = "#"; //若 用户ID = 0, 则为管理员, 使用 #

    /*获取用户信息、主机名称、当前路径*/
    struct passwd *ptr = getpwuid(uid); //获取用户信息

    //如果获取用户信息失败,就打印一个简单的提示符 mybash $,直接退出函数
    if(ptr == NULL)
    {
        printf("mybash $");
        fflush(stdout);
        return;
    }

    char host[128] = {0};
    gethostname(host, 128); //获取主机名称
    
    char path[128] = {0};
    getcwd(path, 128); //获取当前路径

    /*打印*/
    printf("%s@%s:%s%s ", ptr->pw_name, host, path, str);
    fflush(stdout); //刷新输出缓冲区
}



/*命令解析*/
char *get_cmd(char buff[], char *myargv[])
{
    int i = 0;
    char *s = strtok(buff, " "); //按空格分隔第一个单词
    while(s != NULL) //当分割出来的字符串为NULL时,说明分割完成,循环结束
    {
        myargv[i++] = s; //将分割出来的字符串放进myargv数组中
        s = strtok(NULL, " "); //NULL会继续分割剩余的字符串
    }
    
    myargv[i] = NULL; //让myargv以NULL结尾,避免后续使用execvp错误

    return myargv[0]; //返回第一个单词,即命令名(如 ls、cd)
}


/*主函数*/
int main()
{
    while(1) 
    {
        /*打印命令提示符*/
        printf_info(); 

        /*读取用户输入*/
        char buff[128] = {0};
        fgets(buff, 128, stdin); //读取用户输入
        buff[strlen(buff) - 1] = '\0'; //去掉末尾换行
        
        /*解析命令*/
        char *myargv[MAX_SIZE] = {0};
        char *cmd = get_cmd(buff, myargv); //分割命令,cmd是返回的是输入的命令名(如 ls、cd)
        
        /*处理空命令和内置命令*/
        if(cmd == NULL) continue; //输入为空,重新循环

        if(strcmp(cmd, "exit") == 0) exit(0); //输入exit,退出

        if(strcmp(cmd, "cd") == 0) 
        {
            chdir(myargv[1]); //需要使用chdir()函数进行跳转
            continue;
        }

        /*创建子进程*/
        pid_t pid = fork();
        if(pid == -1) exit(0); //创建失败,直接退出

        /*子进程执行外部命令*/
        if(pid == 0)  //子进程
        {
            execvp(cmd, myargv); //执行命令
            //执行成功后,子进程会完全变成目标命令,不会再往下执行

            printf("exec err!!!\n");
            exit(1);
        }

        /*父进程等待子进程*/
        else wait(1); //父进程,等待子进程结束
    }
}

测试

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文