C++/Linux小项目:自主shell命令解释器

之前我们已经初步了解了Linux系统中shell命令的原理,本期我们就来模拟实现一下Linux系统中的shell命令解释器

相关的代码已经上传至作者的个人gitee:楼田莉子/Linux学习喜欢请点个赞谢谢

目录

目标

实现原理

模拟实现(C语言版本)

初始准备

获取命令行

解析命令

执行命令

项目总结

源码(Cy语言版本)

源码(C++版本)

[scanf 与 fgets 函数对比](#scanf 与 fgets 函数对比)

应用举例

[1. 读取带空格的字符串](#1. 读取带空格的字符串)

[2. 缓冲区溢出风险](#2. 缓冲区溢出风险)

[3. 换行符处理问题](#3. 换行符处理问题)

[4. fgets 包含换行符的问题](#4. fgets 包含换行符的问题)

[5. 最佳实践:fgets + sscanf 组合](#5. 最佳实践:fgets + sscanf 组合)

总结表格对应的代码验证

getenv函数和putenv函数解析

代码示例说明

[1. 基本用法对比](#1. 基本用法对比)

[2. 内存管理危险示例](#2. 内存管理危险示例)

[3. 实际应用场景对比](#3. 实际应用场景对比)

[4. 综合工具函数示例](#4. 综合工具函数示例)

[5. 错误处理对比](#5. 错误处理对比)

关键总结

[getenv 特点:](#getenv 特点:)

[putenv 特点:](#putenv 特点:)

snprintf函数的解析

函数原型

基本作用

参数说明

返回值

核心特性:安全截断

[与 sprintf 的对比](#与 sprintf 的对比)

详细用法示例

[1. 基本字符串格式化](#1. 基本字符串格式化)

[2. 缓冲区大小测试](#2. 缓冲区大小测试)

[3. 返回值的重要性](#3. 返回值的重要性)

与相关函数对比

Linux内建命令表


目标

• 要能处理普通命令

• 要能处理内建命令

• 要能帮助我们理解内建命令/本地变量/环境变量这些概念

• 要能帮助我们理解shell的运行原理

实现原理

shell本质上是一个死循环

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。所以要写一个shell,需要循环以下过程:

  1. 获取命令行

  2. 解析命令行

  3. 建立一个子进程(fork)

  4. 替换子进程(execvp)

  5. 父进程等待子进程退出(wait)

    根据这些思路,和我们前面学的技术,就可以自己来实现一个shell了

模拟实现(C语言版本)

初始准备

shell本质上是一个死循环所以我们先准备一个框架

cpp 复制代码
#include<stdio.h>

int main()
{
    //shell本质是一个死循环
    while(1)
    {
        sleep(1);
    }
    return 0;
}

获取命令行

对于命令行我们需要获取当前主机的用户名、主机号、路径名。我们可以通过系统调用和从环境变量中获取。但是我们这里只做模拟所以从环境变量中获取对应的信息

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>

//这里我们不做系统调用,只做模拟从环境变量中获取
const char* GetUserName()
{
    char*name=getenv("USER");
    if(name==NULL)
        return "None";
    return name;
}

const char* GetHostName()
{
    char*hostname=getenv("HOSTNAME");
    if(hostname==NULL)
        return "None";
    return hostname;
}
const char* GetPwd()
{
    char*pwdname=getenv("PWD");
    if(pwdname==NULL)
        return "None";
    return pwdname;
}
void PrintCommandLine()
{
    printf("[%s@%s %s]#",GetUserName(),GetHostName(),GetPwd());//用户名@主机名 当前路径
    fflush(stdout);
}
int main()
{
    //shell本质是一个死循环
    while(1)
    {
        //打印命令行
        PrintCommandLine();
        sleep(1);
    }
    return 0;
}

随后我们要从命令行从获取对应的命令。

因此我们需要先定义一个字符串数组

cpp 复制代码
#define MAXSIZE 128

char command_line[MAXSIZE]={0};

输入我们可能会想到scanf,但是这里是不行的。因为scanf会把空格当作分隔符,而真正的shell命令解释器必须有空格(我们输入选项的时候以空格为分隔符),所以用scanf不合适。这里我们使用fgets函数

这两个函数的区别我放好后面解释,这里不做赘述

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#define MAXSIZE 128
//这里我们不做系统调用,只做模拟从环境变量中获取
//之前的代码在这里省略:...
int main()
{
    char command_line[MAXSIZE]={0};
    //shell本质是一个死循环
    while(1)
    {
        //打印命令行
        PrintCommandLine();
        //获取用户输入
        if(fgets(command_line,sizeof(command_line),stdin)==NULL)//不用scanf是因为它会默认以空格为分隔符,无法全部截取
            continue;
        //用户在输入的时候至少按一下回车
        command_line[strlen(command_line)-1]='\0';
        if(command_line[0]=='0') continue;//如果用户输入的是空串,继续输入
        // printf("%s\n",command_line);//测试用,现在不需要了
        sleep(1);
    }

    return 0;
}

如果以上内容通过,那么就不需要printf的测试函数了。

不过这么写显然很麻烦,所以我们要先做一个封装,修改判定条件

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#define MAXSIZE 128
//这里我们不做系统调用,只做模拟从环境变量中获取
//之前的代码在这里省略:...
int GetCommand(char*command_line,int size)
{
    if(fgets(command_line,size,stdin)==NULL)//不用scanf是因为它会默认以空格为分隔符,无法全部截取
        return 0;
    //用户在输入的时候至少按一下回车
    command_line[strlen(command_line)-1]='\0';
    //如果用户输入的是空串,继续输入
    return strlen(command_line);
    // printf("%s\n",command_line);//测试用,现在不需要了
}
int main()
{
    char command_line[MAXSIZE]={0};
    //shell本质是一个死循环
    while(1)
    {
        //打印命令行
        PrintCommandLine();
        //获取用户输入
        if(GetCommand(command_line,sizeof(command_line))==0)//要么输入失败要么字符串为空
            continue;
        printf("%s\n",command_line);
        sleep(1);
    }
    return 0;
}

结果为:

解析命令

对于shell来说,我们更需要对用户输入的命令做解释,就是对于像这样的命令

bash 复制代码
ls -a -l

将其打散,重新组合在一起。

命令行解析命令: bash来解析命令行,string -> argv && argc

对此我们需要先写出框架

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#define MAXSIZE 128
//这两个作用等同于main函数参数
char*myargv[MAXSIZE];//将打散的命令存储于此
int myargc;
//之前的代码在这里省略了
int main()
{
    char command_line[MAXSIZE]={0};
    //shell本质是一个死循环
    while(1)
    {
        //1、打印命令行
        PrintCommandLine();
        //2、获取用户输入
        if(GetCommand(command_line,sizeof(command_line))==0)//要么输入失败要么字符串为空
            continue;
        printf("%s\n",command_line);
        //3、解析字符串。就是比如"ls -a -l"就要转化为"ls""-a""-l"命令解析器,对用户输入的命令做处理
        
        sleep(1);
    }
    return 0;
}

相关思路如下:

接下来我们对字符串切割函数strtok函数做简单使用讲解。具体的深入内容可以参考这个文章:https://blog.csdn.net/2401_89119815/article/details/147765173?fromshare=blogdetail&sharetype=blogdetail&sharerId=147765173&sharerefer=PC&sharesource=2401_89119815&sharefrom=from_link

以下用代码示例:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
int main()
{
    char str[]="aaa bbb ccc ddd";
    const char* sep=" #: ";
    //切割第一次
    char *p=strtok(str,sep);
    printf("%s\n",p);
    while(p)
    {
        printf("%s\n",p);
        p=strtok(NULL,sep);
        if(p==NULL)
            break;
    }
    //以下内容为使用示例
    //char *p1=strtok(str,sep);
    //printf("%s\n",p1);
    ////如果要继续上一次的切割第一个参数要为NULL
    //char *p2=strtok(NULL,sep);
    //printf("%s\n",p2);
    return 0;
}

结果为:

那么我就用这个代码来实现这个功能

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>
#define MAXSIZE 128
//这两个作用等同于main函数参数
//shell自己维护的第一张表:命令行参数表
char*myargv[MAXSIZE];//将打散的命令存储于此
int myargc=0;
const char*sep=" ";
//其余代码在这里:...
int ParseCommand(char*command_line)
{
    myargc = 0;  
    memset(myargv,0,sizeof(myargv));
    //写法1
    //while((myargv[myargc++]=strtok(command_line,sep)));
    //写法2
    myargv[0]=strtok(command_line,sep);
    while((myargv[++myargc]=strtok(NULL,sep)));
    //测试代码
  //  printf("myargc:%d\n", myargc);
  //  for(int i = 0; i < myargc; ++i)
  //      printf("myargv[%d]:%s\n", i, myargv[i]);
  //  
    return myargc;
}
int main()
{
    char command_line[MAXSIZE]={0};
    //shell本质是一个死循环
    while(1)
    {
        //1、打印命令行
        PrintCommandLine();
        //2、获取用户输入
        if(GetCommand(command_line,sizeof(command_line))==0)//要么输入失败要么字符串为空
            continue;
        printf("%s\n",command_line);
        //3、解析字符串。就是比如"ls -a -l"就要转化为"ls""-a""-l"命令解析器,对用户输入的命令做处理
        ParseCommand(command_line);
        sleep(1);
        //4、执行该命令
        ExectuCommand();        
    }
    return 0;
}

执行命令

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>
#define MAXSIZE 128
int ExectuCommand()
{
    //不能让bash自己执行命令,必须创建子进程进行替换
    pid_t id=fork();
    if(id<0) return -1;
    else if(id==0)
    {
        //子进程
        execvp(myargv[0],myargv);
        exit(0);        
    }
    else
    {
        int status=0;
        //父进程
        pid_t rid =waitpid(id,&status,0);
        if(rid>0)
        {
            printf("等待成功!");
        }
    }
    return 0;
}
int main()
{
    char command_line[MAXSIZE]={0};
    //shell本质是一个死循环
    while(1)
    {
        //1、打印命令行
        PrintCommandLine();
        //2、获取用户输入
        if(GetCommand(command_line,sizeof(command_line))==0)//要么输入失败要么字符串为空
            continue;
        printf("%s\n",command_line);
        //3、解析字符串。就是比如"ls -a -l"就要转化为"ls""-a""-l"命令解析器,对用户输入的命令做处理
        ParseCommand(command_line);
        sleep(1);
        //4、执行该命令
        ExectuCommand();        
    }
    return 0;
}

但是我们会发现cd命令无效

这是因为cd切换的是父进程(bash的路径)

命令是bash的子进程,所有的子进程,会继承父进程当前工作路径!!!

如果更改了bash的工作路径,就是更改了后续执行的所有指令(进程)的工作路径!!

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>
#define MAXSIZE 128
//这两个作用等同于main函数参数
//shell自己维护的第一张表:命令行参数表
char*myargv[MAXSIZE];//将打散的命令存储于此
int myargc=0;
const char*sep=" ";
//其余代码在这里
int ExecuteCommand()
{
    //不能让bash自己执行命令,必须创建子进程进行替换
    pid_t id=fork();
    if(id<0) return -1;
    else if(id==0)
    {
        //子进程
        execvp(myargv[0],myargv);
        exit(0);        
    }
    else
    {
        int status=0;
        //父进程
        pid_t rid =waitpid(id,&status,0);
        if(rid>0)
        {
            printf("等待成功!");
        }
    }
    return 0;
}
//1是内建命令或者已经执行完毕
//0不是内建命令
int CheckBuiltinExecute()
{
    if(strcmp(myargv[0],"cd")==0)
    {
        //内建命令
        if(myargc==2)
        {
            //新的路径为myargv[1]
            chdir(myargv[1]);
        }
        return 1;
    }
    return 0;
}
int main()
{
    char command_line[MAXSIZE]={0};
    //shell本质是一个死循环
    while(1)
    {
        //1、打印命令行
        PrintCommandLine();
        //2、获取用户输入
        if(GetCommand(command_line,sizeof(command_line))==0)//要么输入失败要么字符串为空
            continue;
        printf("%s\n",command_line);
        //3、解析字符串。就是比如"ls -a -l"就要转化为"ls""-a""-l"命令解析器,对用户输入的命令做处理
        ParseCommand(command_line);
        sleep(1);
        //4、判断该命令由哪个进程执行
        if(CheckBuiltinExecute())
            continue;
        //5、子进程执行该命令
        ExecuteCommand();        
    }
    return 0;
}

但是我们发现即使这样cd也没办法 以以下命令切换路径

bash 复制代码
 cd ..

是因为我们的路径是根据环境变量来的(父进程)。操作系统不会自动更改环境变量

cpp 复制代码
const char* GetHostName()
{
    char*hostname=getenv("HOSTNAME");
    if(hostname==NULL)
        return "None";
    return hostname;
}

因此我们还要自行更新环境变量

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>
#define MAXSIZE 128

//我们shell自己的工作路径
char cwd[MAXSIZE];
//这里我们不做系统调用,只做模拟从环境变量中获取
const char* GetPwd()
{
    //char*pwdname=getenv("PWD");
    char*pwdname=getcwd(cwd,sizeof(cwd));
    if(pwdname==NULL)
        return "None";
    return pwdname;
}
int main()
{
    char command_line[MAXSIZE]={0};
    //shell本质是一个死循环
    while(1)
    {
        //1、打印命令行
        PrintCommandLine();
        //2、获取用户输入
        if(GetCommand(command_line,sizeof(command_line))==0)//要么输入失败要么字符串为空
            continue;
        printf("%s\n",command_line);
        //3、解析字符串。就是比如"ls -a -l"就要转化为"ls""-a""-l"命令解析器,对用户输入的命令做处理
        ParseCommand(command_line);
        sleep(1);
        //4、判断该命令由哪个进程执行
        if(CheckBuiltinExecute())
            continue;
        //5、子进程执行该命令
        ExecuteCommand();        
    }
    return 0;
}

可以发现路径已经发生了变化

如果我们相对环境变量进行修改来让其发生变化我们可以这样修改

cpp 复制代码
const char* GetPwd()
{
    char*pwdname=getenv("PWD");
    //char*pwdname=getcwd(cwd,sizeof(cwd));
    if(pwdname==NULL)
        return "None";
    return pwdname;
}

//1是内建命令或者已经执行完毕
//0不是内建命令
int CheckBuiltinExecute()
{
    if(strcmp(myargv[0],"cd")==0)
    {
        //内建命令
        if(myargc==2)
        {
            //新的路径为myargv[1]
            chdir(myargv[1]);
            char pwd[1024];
            getcwd(pwd,sizeof(pwd));//获取当前路径
            snprintf(cwd,sizeof(cwd),"PID:%s",pwd);
            putenv(cwd);
        }
        return 1;
    }
    return 0;
}

项目总结

在继续学习新知识前,我们来思考函数和进程之间的相似性

exec/exit就像call/return

一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。

这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下图

源码(Cy语言版本)

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>
#define MAXSIZE 128
//这两个作用等同于main函数参数
//shell自己维护的第一张表:命令行参数表
char*myargv[MAXSIZE];//将打散的命令存储于此
int myargc=0;
//分隔符
const char*sep=" ";
//我们shell自己的工作路径
char cwd[MAXSIZE];
//这里我们不做系统调用,只做模拟从环境变量中获取
const char* GetUserName()
{
    char*name=getenv("USER");
    if(name==NULL)
        return "None";
    return name;
}

const char* GetHostName()
{
    char*hostname=getenv("HOSTNAME");
    if(hostname==NULL)
        return "None";
    return hostname;
}
const char* GetPwd()
{
    char*pwdname=getenv("PWD");
    //char*pwdname=getcwd(cwd,sizeof(cwd));
    if(pwdname==NULL)
        return "None";
    return pwdname;
}
void PrintCommandLine()
{
    printf("[%s@%s %s]#",GetUserName(),GetHostName(),GetPwd());//用户名@主机名 当前路径
    fflush(stdout);
}
int GetCommand(char*command_line,int size)
{
    if(fgets(command_line,size,stdin)==NULL)//不用scanf是因为它会默认以空格为分隔符,无法全部截取
        return 0;
    //用户在输入的时候至少按一下回车
    command_line[strlen(command_line)-1]='\0';
    //如果用户输入的是空串,继续输入
    return strlen(command_line);
    // printf("%s\n",command_line);//测试用,现在不需要了
}
int ParseCommand(char*command_line)
{
    myargc = 0;  
    memset(myargv,0,sizeof(myargv));
    //写法1
    //while((myargv[myargc++]=strtok(command_line,sep)));
    //写法2
    myargv[0]=strtok(command_line,sep);
    while((myargv[++myargc]=strtok(NULL,sep)));
    
  //  printf("myargc:%d\n", myargc);
  //  for(int i = 0; i < myargc; ++i)
  //      printf("myargv[%d]:%s\n", i, myargv[i]);
  //  
    return myargc;
}
int ExecuteCommand()
{
    //不能让bash自己执行命令,必须创建子进程进行替换
    pid_t id=fork();
    if(id<0) return -1;
    else if(id==0)
    {
        //子进程
        execvp(myargv[0],myargv);
        exit(0);        
    }
    else
    {
        int status=0;
        //父进程
        pid_t rid =waitpid(id,&status,0);
        if(rid>0)
        {
            printf("等待成功!");
        }
    }
    return 0;
}
//1是内建命令或者已经执行完毕
//0不是内建命令
int CheckBuiltinExecute()
{
    if(strcmp(myargv[0],"cd")==0)
    {
        //内建命令
        if(myargc==2)
        {
            //新的路径为myargv[1]
            chdir(myargv[1]);
            char pwd[1024];
            getcwd(pwd,sizeof(pwd));//获取当前路径
            snprintf(cwd,sizeof(cwd),"PID:%s",pwd);
            putenv(cwd);
        }
        return 1;
    }
    return 0;
}
int main()
{
    char command_line[MAXSIZE]={0};
    //shell本质是一个死循环
    while(1)
    {
        //1、打印命令行
        PrintCommandLine();
        //2、获取用户输入
        if(GetCommand(command_line,sizeof(command_line))==0)//要么输入失败要么字符串为空
            continue;
        printf("%s\n",command_line);
        //3、解析字符串。就是比如"ls -a -l"就要转化为"ls""-a""-l"命令解析器,对用户输入的命令做处理
        ParseCommand(command_line);
        sleep(1);
        //4、判断该命令由哪个进程执行
        if(CheckBuiltinExecute())
            continue;
        //5、子进程执行该命令
        ExecuteCommand();        
    }
    return 0;
}

源码(C++版本)

cpp 复制代码
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <iostream>
#include <string>

#define MAXSIZE 128
#define MAXARGS 32
// shell自己内部维护的第一张表: 命令行参数表
// 故意设计成为全局的
// 命令行参数表
char *gargv[MAXARGS];
int gargc = 0;
const char *gsep = " ";

// 环境变量表
char *genv[MAXARGS];
int genvc = 0;

// 我们shell自己所处的工作路径
char cwd[MAXSIZE];

// 最近一个命令执行完毕,退出码
int lastcode = 0;

// vector<std::string> cmds; // 1000


// ls -a -l > XX.txt -> "ls -a -l" && "XX.txt" && 重定向的方式
// 表明重定向的信息
#define NoneRedir   0
#define InputRedir  1
#define AppRedir    2
#define OutputRedir 3

int redir_type = NoneRedir; // 记录正在执行的执行,重定向方式
char *filename = NULL;      // 保存重定向的目标文件

// 空格空格空格filename.txt
#define TrimSpace(start) do{\
    while(isspace(*start)) start++;\
}while(0)

void LoadEnv()
{
    // 正常情况,环境变量表内部是从配置文件来的
    // 今天我们从父进程拷贝
    extern char **environ;
    for(; environ[genvc]; genvc++)
    {
        genv[genvc] = (char*)malloc(sizeof(char)*4096);
        strcpy(genv[genvc], environ[genvc]);
    }
    genv[genvc] = NULL;

    printf("Load env: \n");
    for(int i = 0; genv[i]; i++)
        printf("genv[%d]: %s\n", i, genv[i]);
}
static std::string rfindDir(const std::string &p)
{
    if(p == "/")
        return p;
    const std::string psep = "/";
    auto pos = p.rfind(psep);
    if(pos == std::string::npos)
        return std::string();
    return p.substr(pos+1); // /home/whb
}

const char *GetUserName()
{
    char *name = getenv("USER");
    if(name == NULL)
        return "None";
    return name;
}

const char *GetHostName()
{
    char *hostname = getenv("HOSTNAME");
    if(hostname == NULL)
        return "None";
    return hostname;
}
const char *GetPwd()
{
    char *pwd = getenv("PWD");
    //char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd == NULL)
        return "None";
    return pwd;
}

void PrintCommandLine()
{
    printf("[%s@%s %s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); // 用户名 @ 主机名 当前路径
    fflush(stdout);
}

int GetCommand(char commandline[], int size)
{
    if(NULL == fgets(commandline, size, stdin))
        return 0;
    // 2.1 用户输入的时候,至少会摁一下回车\n abcd\n ,\n '\0'
    commandline[strlen(commandline)-1] = '\0';
    return strlen(commandline);
}

// ls -a -l >> filenamel.txt -> ls -a -l \0\0 filename.txt
// ls -a -l > XX.txt || ls -a -l >> XX.txt || cat < log.txt || ls -a -l
void ParseRedir(char commandline[])
{
    redir_type = NoneRedir;
    filename = NULL;
    char *start = commandline;
    char *end = commandline+strlen(commandline);
    while(start < end)
    {
        if(*start == '>')
        {
            if(*(start+1) == '>')
            {
                // 追加重定向
                *start = '\0';
                start++;
                *start = '\0';
                start++;
                TrimSpace(start); // 去掉左半部分的空格
                redir_type = AppRedir;
                filename = start;
                break;
            }
            // 输出重定向
            *start = '\0';
            start++;
            TrimSpace(start);
            redir_type = OutputRedir;
            filename = start;
            break;
        }
        else if(*start == '<')
        {
            // 输入重定向
            *start = '\0';
            start++;
            TrimSpace(start);
            redir_type = InputRedir;
            filename = start;
            break;
        }
        else
        {
            // 没有重定向
            start++;
        }
    }
}

int ParseCommand(char commandline[])
{
    gargc = 0;
    memset(gargv, 0, sizeof(gargv));
    // ls -a -l
    // 故意 commandline : ls
    gargv[0] = strtok(commandline, gsep);
    while((gargv[++gargc] = strtok(NULL, gsep)));

//    printf("gargc: %d\n", gargc); // ?
//    int i = 0;
//    for(; gargv[i]; i++)
//        printf("gargv[%d]: %s\n", i, gargv[i]);
    return gargc;
}

// retunr val:
// 0 : 不是内建命令
// 1 : 内建命令&&执行完毕
int CheckBuiltinExecute()
{
    if(strcmp(gargv[0], "cd") == 0)
    {
        // 内建命令
        if(gargc == 2)
        {
            // 新的目标路径: gargv[1]
            // 1. 更改进程内核中的路径
            chdir(gargv[1]);
            // 2. 更改环境变量
            char pwd[1024];
            getcwd(pwd, sizeof(pwd)); // /home/whb
            snprintf(cwd, sizeof(cwd), "PWD=%s", pwd); // cwd: PWD=/home/home
            putenv(cwd);
            lastcode = 0;
        }
        return 1;
    }
    else if(strcmp(gargv[0], "echo") == 0) // cd , echo , env , export 内建命令
    {
        if(gargc == 2)
        {
            if(gargv[1][0] == '$')
            {
                // $? ? : 看做一个变量名字
                if(strcmp(gargv[1]+1, "?") == 0)
                {
                    printf("lastcode: %d\n", lastcode);
                }
                else if(strcmp(gargv[1]+1, "PATH") == 0)
                {
                    // 不准你用getenv和putenv
                    printf("%s\n", getenv("PATH")); // putenv 和 getenv 究竟是什么, 访问环境变量表!
                }
                lastcode = 0;
            }
            return 1;
            // echo helloworld
            // echo $?
        }
    }

    return 0;
}

int ExecuteCommand()
{
    // 能不能让你的bash自己执行命令:ls -a -l
    pid_t id = fork();
    if(id < 0)
        return -1;
    else if(id == 0)
    {
        //printf("我是子进程,我是exec启动前: %dp\n", getpid());
        // 子进程: 如何执行, gargv, gargc
        // ls -a -l
        int fd = -1;
        if(redir_type == NoneRedir)
        {
            // Do Nothing
        }
        else if(redir_type == OutputRedir)
        {
            // 子进程要进行输出重定向
            fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == AppRedir)
        {
            fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
            dup2(fd, 1);
        }
        else if(redir_type == InputRedir)
        {
            fd = open(filename, O_RDONLY);
            dup2(fd, 0);
        }
        else{
            //bug??
        }
        execvpe(gargv[0], gargv, genv);
        exit(1);
    }
    else
    {
        // 父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
            //printf("wait child process success!\n");
        }
    }
    return 0;
}

int main()
{
    // 0. 从配置文件中获取环境变量填充环境变量表的
    //LoadEnv();
    char command_line[MAXSIZE] = {0};
    while(1)
    {
        // 1. 打印命令行字符串
        PrintCommandLine();
        // 2. 获取用户输入
        if(0 == GetCommand(command_line, sizeof(command_line)))
            continue;

        //printf("%s\n", command_line);
        // ls -a -l > XX.txt || ls -a -l >> XX.txt || cat < log.txt || ls -a -l
        // ls -a -l > XX.txt -> "ls -a -l" && "XX.txt" && 重定向的方式
        ParseRedir(command_line);
        //printf("command: %s\n", command_line);
        //printf("redir type: %d\n", redir_type);
        //printf("filename: %s\n", filename);

        // 4. 解析字符串 -> "ls -a -l" -> "ls" "-a" "-l" 命令行解释器,就要对用户输入的命令字符串首先进行解析!
        ParseCommand(command_line);

        // 5. 这个命令,到底是让父进程bash自己执行(内建命令)?还是让子进程执行
        if(CheckBuiltinExecute()) // > 0
        {
            continue;
        }
    
        // 6. 让子进程执行这个命令
        ExecuteCommand();
    }

    return 0;
}

scanffgets 函数对比

对比角度 scanf 函数 fgets 函数 说明与影响
基本功能与设计目的 格式化输入,用于从标准输入(或文件流)中读取特定类型的数据(如整数、浮点数、字符串等)。 行输入,用于从标准输入(或文件流)中读取一整行文本,包括空格。 scanf 是"数据抽取器",fgets 是"行收集器"。这是它们最根本的区别。
读取字符串时的区别 使用 %s 格式符,遇到空白字符(空格、制表符、换行)即停止读取。 读取字符直到遇到换行符('\n') 或达到指定数量减一(为'\0'留空间)或文件结尾(EOF)。 scanf%s 无法读取带空格的句子(如 "Hello World"),而 fgets 可以。
缓冲区溢出风险 高危 。如果使用 %s 而没有指定宽度,无法限制读取的字符数,极易导致缓冲区溢出,是严重的安全漏洞。 安全 。必须显式指定最大读取字符数 n,函数最多读取 n-1 个字符,并自动在末尾添加空字符('\0')。 这是最重要的安全区别 。在生产代码中,应绝对避免使用不指定宽度的 scanf("%s", buf)
换行符的处理 留在输入缓冲区中scanf 在读取数字或字符串(%s)后,如果遇到换行符,会将其视为分隔符并留在输入缓冲区中。 存入目标缓冲区中fgets 会将读取到的换行符 '\n' 作为字符串的一部分存入缓冲区。 scanf 留下的换行符常导致后续的 getcharfgets 读取到空行,需要手动清空缓冲区。fgets 读取后,可能需要手动去除末尾的换行符。
返回值 返回一个整数 ,表示成功匹配并赋值的输入项的数量。如果失败或到达文件尾,则返回 EOF。 成功时返回指向目标缓冲区的指针 ,失败或到达文件尾时返回 NULL 指针。 通过检查 scanf 的返回值可以判断输入的数据类型是否正确。通过检查 fgets 是否为 NULL 可以判断是否发生错误或到达文件尾。
输入失败时的行为 如果输入与指定格式不匹配,失败的数据会"放回"输入缓冲区,导致后续读取继续失败,造成"无限循环"等问题。 通常是由于流错误或到达文件尾,不会因为数据格式不匹配而失败。 scanf 输入失败后的清理工作比较麻烦,通常需要循环读取并丢弃错误数据。
灵活性与适用场景 ,适用于读取结构化的、类型已知的数据。例如,从文件或输入中读取"123, 3.14, John"这样的数据。 较低但专注 ,最适合读取一整行文本,尤其是当文本内容未知或包含空格时。例如,读取用户的名字、地址、文件的一行等。 对于混合输入(如先读数字再读字符串),使用 fgets 读取整行再用 sscanf 进行解析是更安全、更可控的做法。
典型用法示例 scanf("%d", &num); scanf("%4s", str); // 相对安全 scanf("%d %f", &a, &b); fgets(buffer, sizeof(buffer), stdin); // 然后手动去除可能的换行符: buffer[strcspn(buffer, "\n")] = 0; scanf 需要传递变量的地址(&),而 fgets 直接使用数组名(地址)。

对于复杂的或要求健壮性的输入,推荐采用 fgets + sscanf 的组合:

  • 先用 fgets 安全地读取一整行到一个缓冲区。

  • 再用 sscanf 从这个缓冲区中安全地解析出需要的数据。

cpp 复制代码
char buffer[100];
int age;
float salary;

// 安全且健壮的输入方式
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
    // 尝试从缓冲区中解析数据
    if (sscanf(buffer, "%d %f", &age, &salary) == 2) {
        printf("Age: %d, Salary: %.2f\n", age, salary);
    } else {
        printf("Invalid input format.\n");
    }
}

应用举例

1. 读取带空格的字符串

使用 scanf (遇到空格停止)

cpp 复制代码
#include <stdio.h>

int main() {
    char name[50];

    printf("请输入您的全名(包含空格): ");
    scanf("%s", name);

    printf("您输入的是: %s\n", name);
    return 0;
}

运行结果:

cpp 复制代码
请输入您的全名(包含空格): 张三 丰
您输入的是: 张三

scanf 遇到空格就停止了,只读取了"张三"

使用 fgets (读取整行包含空格)

cpp 复制代码
#include <stdio.h>

int main() {
    char name[50];

    printf("请输入您的全名(包含空格): ");
    fgets(name, sizeof(name), stdin);

    printf("您输入的是: %s\n", name);
    return 0;
}

运行结果:

cpp 复制代码
请输入您的全名(包含空格): 张三 丰
您输入的是: 张三 丰

fgets 成功读取了包含空格的完整姓名

2. 缓冲区溢出风险

危险的 scanf 用法

cpp 复制代码
#include <stdio.h>

int main() {
    char buffer[5]; // 只能容纳4个字符 + '\0'

    printf("请输入长字符串: ");
    scanf("%s", buffer); // 没有限制长度!

    printf("缓冲区内容: %s\n", buffer);
    return 0;
}

运行结果:

bash 复制代码
请输入长字符串: 这是一个很长的字符串
缓冲区内容: 这是一个很长的字符串

💥 缓冲区溢出! 程序可能崩溃或产生不可预测行为

安全的 fgets 用法

cpp 复制代码
#include <stdio.h>

int main() {
    char buffer[5]; // 只能容纳4个字符 + '\0'

    printf("请输入长字符串: ");
    fgets(buffer, sizeof(buffer), stdin); // 自动限制长度

    printf("缓冲区内容: %s\n", buffer);
    return 0;
}

运行结果:

cpp 复制代码
请输入长字符串: 这是一个很长的字符串
缓冲区内容: 这是

安全! fgets 自动限制读取的字符数,防止溢出

3. 换行符处理问题

scanf 留下的换行符陷阱

cpp 复制代码
#include <stdio.h>

int main() {
    int age;
    char name[50];

    printf("请输入年龄: ");
    scanf("%d", &age);

    printf("请输入姓名: ");
    fgets(name, sizeof(name), stdin); // 问题所在!

    printf("年龄: %d, 姓名: %s\n", age, name);
    return 0;
}

运行结果:

cpp 复制代码
请输入年龄: 25
请输入姓名: 年龄: 25, 姓名: 

fgets 立即读取了 scanf 留下的换行符,看起来像是被"跳过"了

解决方案:清空输入缓冲区

cpp 复制代码
#include <stdio.h>

int main() {
    int age;
    char name[50];

    printf("请输入年龄: ");
    scanf("%d", &age);

    // 清空输入缓冲区中的换行符
    while (getchar() != '\n');

    printf("请输入姓名: ");
    fgets(name, sizeof(name), stdin);

    printf("年龄: %d, 姓名: %s\n", age, name);
    return 0;
}

运行结果:

cpp 复制代码
请输入年龄: 25
请输入姓名: 李四
年龄: 25, 姓名: 李四

✅ 正常工作了!

4. fgets 包含换行符的问题

fgets 会包含换行符

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char city[50];

    printf("请输入城市: ");
    fgets(city, sizeof(city), stdin);

    printf("城市长度: %zu\n", strlen(city));
    printf("城市内容: ");
    for(int i = 0; i < strlen(city); i++) {
        printf("%d ", city[i]); // 打印ASCII码
    }
    printf("\n");

    return 0;
}

运行结果(输入"北京"):

cpp 复制代码
请输入城市: 北京
城市长度: 5
城市内容: 229 140 151 228 186 172 10 

🔍 可以看到最后一个字符是10(换行符 \n

去除 fgets 的换行符

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char city[50];

    printf("请输入城市: ");
    fgets(city, sizeof(city), stdin);

    // 去除换行符的方法
    city[strcspn(city, "\n")] = 0;
    // 或者:if (city[strlen(city)-1] == '\n') city[strlen(city)-1] = 0;

    printf("处理后长度: %zu\n", strlen(city));
    printf("城市: %s\n", city);

    return 0;
}

运行结果:

cpp 复制代码
请输入城市: 北京
处理后长度: 4
城市: 北京

✅ 换行符被成功移除

5. 最佳实践:fgets + sscanf 组合

安全可靠的输入方法

cpp 复制代码
#include <stdio.h>

int main() {
    char input[100];
    int age;
    float height;
    char name[50];

    printf("请输入年龄、身高和姓名(用空格分隔): ");

    if (fgets(input, sizeof(input), stdin) != NULL) {
        // 从缓冲区安全解析数据
        int items = sscanf(input, "%d %f %49s", &age, &height, name);

        if (items == 3) {
            printf("解析成功:\n");
            printf("年龄: %d, 身高: %.2f, 姓名: %s\n", age, height, name);
        } else {
            printf("输入格式错误!期望3个数据,实际得到%d个\n", items);
        }
    } else {
        printf("读取输入失败!\n");
    }

    return 0;
}

运行结果:

cpp 复制代码
请输入年龄、身高和姓名(用空格分隔): 25 1.75 王五
解析成功:
年龄: 25, 身高: 1.75, 姓名: 王五

总结表格对应的代码验证

区别点 scanf 代码表现 fgets 代码表现
空格处理 scanf("%s") 遇到空格停止 fgets 读取整行包含空格
安全性 scanf("%s", buf) 可能溢出 fgets(buf, size, stdin) 自动限制
换行符 留在缓冲区,影响后续输入 包含在读取的字符串中
适用场景 适合读取结构化数据 适合读取整行文本

getenv函数和putenv函数解析

对比角度 getenv 函数 putenv 函数
基本功能 获取环境变量的值 设置或修改环境变量
函数原型 char *getenv(const char *name) int putenv(char *string)
参数形式 环境变量名称字符串 "name=value" 格式的完整字符串
返回值 成功:指向环境变量值的指针 失败:NULL 成功:0 失败:非0值
内存管理 安全:返回只读指针,用户无需管理内存 危险:直接使用传入的字符串指针,不复制内容
线程安全 是(只读操作) 否(修改全局环境变量表)
对原始数据影响 无影响,只读操作 会修改进程的环境变量表
字符串格式 单纯的名字,如 "PATH" 必须为 "name=value" 格式
使用复杂度 简单直接 需要特别注意内存管理
典型应用场景 读取配置、检查运行环境、获取系统信息 临时修改环境、传递配置给子进程、动态设置参数

代码示例说明

1. 基本用法对比

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void basic_usage_demo() {
    printf("=== getenv 基本用法 ===\n");
    
    // getenv: 简单安全的读取
    char *home = getenv("HOME");
    char *user = getenv("USER");
    char *path = getenv("PATH");
    
    printf("HOME: %s\n", home ? home : "(未设置)");
    printf("USER: %s\n", user ? user : "(未设置)");
    printf("PATH: %s\n", path ? path : "(未设置)");
    
    printf("\n=== putenv 基本用法 ===\n");
    
    // putenv: 必须使用持久存储的字符串
    static char my_var[] = "MY_APP=test_application";
    printf("设置前 MY_APP: %s\n", getenv("MY_APP") ? getenv("MY_APP") : "(未设置)");
    
    if (putenv(my_var) == 0) {
        printf("设置成功\n");
        printf("设置后 MY_APP: %s\n", getenv("MY_APP"));
    } else {
        printf("设置失败\n");
    }
}

int main() {
    basic_usage_demo();
    return 0;
}

2. 内存管理危险示例

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void dangerous_putenv() {
    printf("=== putenv 内存危险演示 ===\n");
    
    // 危险示例:使用局部变量
    char local_var[50] = "DANGEROUS_VAR=local_value";
    
    printf("设置前: %s\n", getenv("DANGEROUS_VAR") ? getenv("DANGEROUS_VAR") : "(未设置)");
    
    putenv(local_var);  // 危险!传入局部变量地址
    
    printf("设置后立即读取: %s\n", getenv("DANGEROUS_VAR"));
    
    // 如果函数返回,local_var 栈内存可能被重用
    // 环境变量指向的内存内容变得不可预测
}

void safe_putenv() {
    printf("\n=== 安全的 putenv 用法 ===\n");
    
    // 方法1:使用静态存储
    static char static_var[] = "SAFE_VAR_STATIC=static_value";
    putenv(static_var);
    printf("静态存储: %s\n", getenv("SAFE_VAR_STATIC"));
    
    // 方法2:使用堆内存(但注意不要释放)
    char *heap_var = strdup("SAFE_VAR_HEAP=heap_value");
    putenv(heap_var);
    printf("堆内存: %s\n", getenv("SAFE_VAR_HEAP"));
    
    // 注意:heap_var 不能 free,因为 putenv 还在使用它
}

int main() {
    dangerous_putenv();
    safe_putenv();
    return 0;
}

3. 实际应用场景对比

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// getenv 应用:配置读取器
void read_app_config() {
    printf("=== getenv 应用:读取配置 ===\n");
    
    char *db_host = getenv("DB_HOST");
    char *db_port = getenv("DB_PORT"); 
    char *debug_mode = getenv("DEBUG");
    char *log_level = getenv("LOG_LEVEL");
    
    printf("数据库配置:\n");
    printf("  主机: %s\n", db_host ? db_host : "localhost (默认)");
    printf("  端口: %s\n", db_port ? db_port : "5432 (默认)");
    printf("  调试模式: %s\n", debug_mode ? "开启" : "关闭");
    printf("  日志级别: %s\n", log_level ? log_level : "INFO (默认)");
}

// putenv 应用:环境配置器  
void setup_development_environment() {
    printf("\n=== putenv 应用:环境配置 ===\n");
    
    // 开发环境配置
    static char dev_vars[][100] = {
        "APP_ENV=development",
        "LOG_LEVEL=DEBUG", 
        "CACHE_ENABLED=false",
        "API_TIMEOUT=30"
    };
    
    for (int i = 0; i < 4; i++) {
        putenv(dev_vars[i]);
        printf("设置: %s\n", dev_vars[i]);
    }
    
    printf("\n验证配置:\n");
    printf("APP_ENV: %s\n", getenv("APP_ENV"));
    printf("LOG_LEVEL: %s\n", getenv("LOG_LEVEL"));
    printf("CACHE_ENABLED: %s\n", getenv("CACHE_ENABLED"));
    printf("API_TIMEOUT: %s\n", getenv("API_TIMEOUT"));
}

int main() {
    read_app_config();
    setup_development_environment();
    return 0;
}

4. 综合工具函数示例

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

// 安全的 getenv 包装器,提供默认值
char *getenv_safe(const char *name, const char *default_value) {
    char *value = getenv(name);
    return value ? value : (char *)default_value;
}

// 安全的 putenv 包装器,自动处理内存
bool putenv_safe(const char *name, const char *value) {
    // 计算需要的内存大小
    size_t len = strlen(name) + strlen(value) + 2; // +2 用于 '=' 和 '\0'
    char *env_string = malloc(len);
    
    if (!env_string) {
        return false;
    }
    
    // 构建 "name=value" 格式
    snprintf(env_string, len, "%s=%s", name, value);
    
    // 设置环境变量
    if (putenv(env_string) != 0) {
        free(env_string);
        return false;
    }
    
    // 注意:不要 free(env_string),putenv 会继续使用它
    return true;
}

// 环境变量管理器
void environment_manager_demo() {
    printf("=== 环境变量管理器演示 ===\n");
    
    // 使用安全的 getenv
    printf("读取配置(带默认值):\n");
    printf("  数据库主机: %s\n", getenv_safe("DB_HOST", "localhost"));
    printf("  数据库端口: %s\n", getenv_safe("DB_PORT", "5432"));
    printf("  日志级别: %s\n", getenv_safe("LOG_LEVEL", "INFO"));
    
    // 使用安全的 putenv
    printf("\n设置新的环境变量:\n");
    if (putenv_safe("APP_NAME", "MyApplication")) {
        printf("  成功设置 APP_NAME\n");
    }
    if (putenv_safe("APP_VERSION", "1.0.0")) {
        printf("  成功设置 APP_VERSION\n");
    }
    
    printf("\n验证设置:\n");
    printf("  APP_NAME: %s\n", getenv("APP_NAME"));
    printf("  APP_VERSION: %s\n", getenv("APP_VERSION"));
}

int main() {
    environment_manager_demo();
    return 0;
}

5. 错误处理对比

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void error_handling_demo() {
    printf("=== 错误处理对比 ===\n");
    
    // getenv 的错误处理:检查返回值
    printf("getenv 错误处理:\n");
    char *nonexistent = getenv("NON_EXISTENT_VARIABLE_12345");
    if (nonexistent == NULL) {
        printf("  环境变量不存在,返回 NULL\n");
    } else {
        printf("  环境变量值: %s\n", nonexistent);
    }
    
    // putenv 的错误处理:检查返回值
    printf("\nputenv 错误处理:\n");
    
    // 测试无效格式
    static char invalid_format[] = "INVALID_FORMAT"; // 缺少 '='
    if (putenv(invalid_format) != 0) {
        printf("  错误:无效的环境变量格式\n");
    }
    
    // 测试正常设置
    static char valid_var[] = "TEST_VAR=valid_value";
    if (putenv(valid_var) == 0) {
        printf("  成功设置 TEST_VAR\n");
    } else {
        printf("  设置 TEST_VAR 失败\n");
    }
    
    // 验证设置结果
    printf("  TEST_VAR: %s\n", getenv("TEST_VAR") ? getenv("TEST_VAR") : "(未设置)");
}

int main() {
    error_handling_demo();
    return 0;
}

关键总结

getenv 特点:

  • 只读操作,安全简单

  • 适合读取配置、检查环境

  • 返回值指向的环境变量值不应被修改

  • 线程安全

putenv 特点:

  • 写入操作,需要特别小心

  • 参数必须是 "name=value" 格式

  • 不会复制字符串,必须保证传入的字符串持久存在

  • 适合临时修改环境、配置传递

  • 非线程安全

snprintf函数的解析

snprintf 函数是 C 语言标准库中一个非常重要的安全字符串格式化函数,它解决了 sprintf 的缓冲区溢出问题。

函数原型

cpp 复制代码
int snprintf(char *str, size_t size, const char *format, ...);

基本作用

将格式化的数据写入字符串,但限制最大写入字符数,防止缓冲区溢出。

参数说明

  • str:目标字符串缓冲区

  • size:缓冲区大小(包括结尾的 \0

  • format:格式化字符串

  • ...:可变参数列表

返回值

  • 成功时:返回本应写入 的字符数(不包括结尾的 \0

  • 失败时:返回负值

核心特性:安全截断

sprintf 的对比

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char buffer[10];
    
    // 危险的 sprintf - 可能缓冲区溢出
    // sprintf(buffer, "这是一个很长的字符串"); // 危险!
    
    // 安全的 snprintf
    int result = snprintf(buffer, sizeof(buffer), "这是一个很长的字符串");
    
    printf("缓冲区内容: '%s'\n", buffer);
    printf("snprintf 返回值: %d\n", result);
    printf("实际写入字符数: %zu\n", strlen(buffer));
    
    return 0;
}

输出:

cpp 复制代码
缓冲区内容: '这是一个很'
snprintf 返回值: 24
实际写入字符数: 9

详细用法示例

1. 基本字符串格式化

cpp 复制代码
#include <stdio.h>

int main() {
    char buffer[50];
    char name[] = "张三";
    int age = 25;
    double salary = 8000.50;
    
    int needed = snprintf(buffer, sizeof(buffer), 
                         "姓名: %s, 年龄: %d, 工资: %.2f", 
                         name, age, salary);
    
    printf("格式化结果: %s\n", buffer);
    printf("需要空间: %d 字符\n", needed);
    printf("实际使用: %zu 字符\n", strlen(buffer));
    printf("剩余空间: %zu 字符\n", sizeof(buffer) - strlen(buffer) - 1);
    
    return 0;
}
复制代码

输出:

cpp 复制代码
格式化结果: 姓名: 张三, 年龄: 25, 工资: 8000.50
需要空间: 36 字符
实际使用: 35 字符
剩余空间: 14 字符

2. 缓冲区大小测试

cpp 复制代码
#include <stdio.h>
#include <string.h>

void test_buffer_size(const char *format, ...) {
    char small_buf[10];
    char large_buf[50];
    
    // 测试小缓冲区
    int result_small = snprintf(small_buf, sizeof(small_buf), format);
    printf("小缓冲区[%zu]: '%s'\n", sizeof(small_buf), small_buf);
    printf("需要字符数: %d\n", result_small);
    
    // 测试大缓冲区  
    int result_large = snprintf(large_buf, sizeof(large_buf), format);
    printf("大缓冲区[%zu]: '%s'\n", sizeof(large_buf), large_buf);
    printf("需要字符数: %d\n\n", result_large);
}

int main() {
    test_buffer_size("Hello, World!");
    test_buffer_size("数字: %d, 字符串: %s", 12345, "这是一个测试");
    return 0;
}

输出:

cpp 复制代码
小缓冲区[10]: 'Hello, Wo'
需要字符数: 13
大缓冲区[50]: 'Hello, World!'
需要字符数: 13

小缓冲区[10]: '数字: 123'
需要字符数: 30
大缓冲区[50]: '数字: 12345, 字符串: 这是一个测试'
需要字符数: 30
复制代码

3. 返回值的重要性

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void safe_format(const char *format, ...) {
    char initial_buf[100];
    char *final_buf;
    int needed;
    
    // 第一次调用,确定需要多大空间
    needed = snprintf(initial_buf, sizeof(initial_buf), format);
    
    if (needed < 0) {
        printf("格式化错误!\n");
        return;
    }
    
    if (needed < sizeof(initial_buf)) {
        // 初始缓冲区足够大
        printf("结果: %s\n", initial_buf);
    } else {
        // 需要分配更大的缓冲区
        printf("初始缓冲区不足,需要 %d 字符\n", needed);
        
        final_buf = malloc(needed + 1); // +1 给 '\0'
        if (final_buf == NULL) {
            printf("内存分配失败!\n");
            return;
        }
        
        snprintf(final_buf, needed + 1, format);
        printf("最终结果: %s\n", final_buf);
        
        free(final_buf);
    }
}

int main() {
    safe_format("短消息");
    safe_format("这是一个非常长的消息,需要比初始缓冲区大得多的空间来存储完整的格式化结果");
    return 0;
}
复制代码

与相关函数对比

函数 安全性 缓冲区大小检查 返回值 使用场景
sprintf 不安全 写入字符数 已知输出不会溢出时
snprintf 安全 需要的字符数 推荐:通用字符串格式化
vsnprintf 安全 需要的字符数 可变参数已为 va_list

Linux内建命令表

命令类别 命令名称 功能描述 使用示例
目录操作 cd 切换当前工作目录 cd /home
pwd 显示当前工作目录 pwd
dirs 显示目录栈 dirs
pushd 将目录压入栈并切换 pushd /tmp
popd 从目录栈弹出目录 popd
变量操作 set 设置shell变量和选项 set -o vi
unset 删除变量或函数 unset VAR
export 设置环境变量 export PATH=$PATH:/newdir
readonly 设置只读变量 readonly PI=3.14
declare 声明变量属性 declare -i number=5
local 在函数内声明局部变量 local var=value
作业控制 jobs 显示当前作业 jobs
fg 将作业切换到前台 fg %1
bg 将作业切换到后台 bg %1
wait 等待作业完成 wait %1
disown 从作业表中移除作业 disown %1
历史命令 history 显示命令历史 history
fc 编辑并重新执行历史命令 fc 100 105
别名管理 alias 创建命令别名 alias ll='ls -l'
unalias 删除别名 unalias ll
流程控制 exit 退出shell exit
return 从函数返回 return 0
break 退出循环 break
continue 继续循环下一次迭代 continue
条件测试 test 条件测试 test -f file.txt
[ ] 条件测试(同test) [ -d /home ]
[[ ]] 扩展条件测试(bash) [[ $var == pattern ]]
输入输出 echo 输出文本 echo "Hello"
printf 格式化输出 printf "Name: %s\n" $name
read 从标准输入读取 read name
readarray 读取输入到数组 readarray lines
mapfile 同readarray mapfile lines
命令执行 exec 执行命令并替换当前shell exec bash
eval 执行参数作为命令 eval "ls $dir"
command 执行命令忽略函数 command ls
builtin 执行内建命令忽略函数 builtin cd
type 显示命令类型 type cd
信号处理 trap 设置信号处理程序 trap 'echo Exit' EXIT
suspend 暂停shell执行 suspend
资源限制 ulimit 设置或显示资源限制 ulimit -n 1024
umask 设置文件创建掩码 umask 022
其他功能 source 在当前shell执行脚本 source script.sh
. 同source . script.sh
times 显示shell及子进程时间 times
help 显示内建命令帮助 help cd
shopt 设置shell选项 shopt -s extglob
enable 启用/禁用内建命令 enable -n echo
bind 设置键盘绑定 bind '"\C-l":clear-screen'
compgen 生成补全匹配 compgen -c
complete 设置命令补全 complete -F _cd cd
compopt 修改补全选项 compopt -o filenames
caller 返回子程序调用上下文 caller 0

本期关于Linux的shell解释器的项目到此结束了。如果您喜欢这个项目,请点个赞谢谢

封面图自取:

相关推荐
用户298698530142 小时前
Java: 为PDF批量添加图片水印实用指南
java·后端·api
杜子不疼.2 小时前
【Linux】网络编程入门:从一个小型回声服务器开始
linux·服务器·网络
EXtreme352 小时前
C语言指针深度剖析(2):从“数组名陷阱”到“二级指针操控”的进阶指南
c语言·开发语言·算法
昨天的猫2 小时前
《拒绝重复代码!模板模式教你优雅复用算法骨架》
后端·设计模式
一起养条鱼吧2 小时前
🧩 Argon2 密码哈希
人工智能·后端
shizhan_cloud2 小时前
收集系统资源使用情况
linux·运维
多喝开水少熬夜2 小时前
SlaugFL论文阅读学习
论文阅读·学习
QZQ541882 小时前
使用C++实现一个简易的线程池
后端
shark_chili2 小时前
基于魔改Nightingale源码浅谈go语言包模块管理
后端