Linux之简易Linux Shell的实现过程

 更新时间:2026年04月22日 09:04:33   作者:码完就睡  
本文通过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.子进程执行外部命令

  • int execvp(const *file, char *const argv[]);
  • file:要执行的程序名
  • argv:参数数组,第一个元素通常是程序名,最后一个元素必须是NULL
/*子进程执行外部命令*/
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); //父进程,等待子进程结束
    }
}

测试

总结

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

相关文章

  • Linux 删除和替换文件中某一行的方法【推荐】

    Linux 删除和替换文件中某一行的方法【推荐】

    这篇文章给大家介绍了Linux 删除和替换文件中某一行的方法,非常不错,具有一定的参考借鉴价值,需要的朋友参考下吧
    2018-05-05
  • shell命令行参数用法简介

    shell命令行参数用法简介

    本文介绍了shell命令行参数的用法,对于普通脚本语言诸如perl python等,写一个脚本程序,包装命令行参数时,一般都是用getopt之类的;c语言也类似
    2014-04-04
  • Linux Bash脚本中的IFS的作用

    Linux Bash脚本中的IFS的作用

    ​​在处理文本数据时,Bash 将文本分割成多个字段,这些字段之间由 IFS 指定的字符进行分隔,本文主要介绍了Linux Bash脚本中的IFS的作用,具有一定的参考价值,感兴趣的可以了解一下
    2024-05-05
  • Linux命令之free命令使用详解

    Linux命令之free命令使用详解

    在Linux系统中,free命令用于显示系统内存的使用情况,它提供了系统内存总量、已使用内存、空闲内存以及其他与内存相关的统计信息,本文将给大家详细的介绍一下Linux free命令的使用方法,需要的朋友可以参考下
    2023-08-08
  • Linux 使用grep筛选多个条件及grep常用过滤命令

    Linux 使用grep筛选多个条件及grep常用过滤命令

    这篇文章主要介绍了Linux 使用grep筛选多个条件及grep常用过滤命令,需要的朋友可以参考下
    2018-07-07
  • setsid 命令工作原理和使用案例介绍

    setsid 命令工作原理和使用案例介绍

    setsid命令在Linux中创建独立会话,使进程脱离终端运行,适用于守护进程和后台任务,通过重定向输出和确保权限,可有效管理长时间运行的进程,本文给大家介绍setsid 命令工作原理和使用案例介绍,感兴趣的朋友一起看看吧
    2025-08-08
  • Shell中整数计算的几种方式

    Shell中整数计算的几种方式

    今天小编就为大家分享一篇关于Shell中整数计算的几种方式,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-03-03
  • 数值运算shell脚本

    数值运算shell脚本

    这次的shell案例比较简单,但有其特点,脚本整体简洁明了,但功能强大,可以实现带自定义数值运算
    2016-08-08
  • Shell脚本一次读取文件中一行的2种写法

    Shell脚本一次读取文件中一行的2种写法

    这篇文章主要介绍了Shell脚本一次读取文件中一行的2种写法,本文还同时讲解了Shell读取文本文件的2种方法,需要的朋友可以参考下
    2015-04-04
  • shell字符串操作详解

    shell字符串操作详解

    这篇文章主要介绍了shell字符串操作详解的相关资料,需要的朋友可以参考下
    2014-02-02

最新评论