LINUX系统-12-进程控制(三)-自定义shell

Linux-自定义shell

一、 几个基本使用

1、强制刷新输出缓冲区

在stdout流中,如果输出缓冲区没有满 、没有在输出行换行 或者进程没有终止运行(终止运行强制刷新缓冲区),输出缓冲区上的内容,是不会打印到屏幕上的。

例如:

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

void PrintCommandline()
{
	printf("[%s@%s %s]",getenv("USER"),getenv("HOSTNAME"),getenv("PWD"));
	fflush(stdout); 
	// 因为上面的 printf 没有以 \n 结尾,所以必须手动 fflush 才能看到输出
	// 强制刷新输出缓冲区
}

int main()
{
	while(1)
	{
		PrintCommandline(); 
		sleep(1);
	}
	return 0;
}

2、fgets函数使用

c 复制代码
#include <stdio.h>
char *fgets(char *str, int n, FILE *stream);
参数 含义 说明
str 字符数组(缓冲区) 用于存储读取到的字符串。函数会返回这个指针。
n 最大读取字符数 关键参数。函数最多读取 n-1 个字符(留 1 个给 \0)。
stream 文件流指针 指定数据来源,如 stdin(键盘)、fp(文件)。

fgets 的读取逻辑非常严谨,它会在以下三种情况发生时停止读取:

读到换行符 (\n):这是最常见的情况(读取一行)。注意:换行符\n会被包含在读取的结果字符串中。

读取了 n-1 个字符:为了防止缓冲区溢出,当读取到 n-1 个字符时,函数会停止,并自动在末尾添加 \0。

遇到文件结尾 (EOF):如果在读取任何字符之前遇到文件结尾,函数返回 NULL。

成功:返回指向字符数组 str 的指针。
失败或到达文件末尾:返回 NULL。

注意:如果在读取任何字符之前就遇到 EOF,返回 NULL;如果读取了部分字符后遇到 EOF,会先返回字符串,下一次调用才会返回 NULL。

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

int main() 
{
    char buffer[100];
    
    printf("请输入字符串: ");
    fgets(buffer, sizeof(buffer), stdin); // 读取一行
    
    // 去除可能存在的换行符
    int len = strlen(buffer);
    if (len > 0 && buffer[len-1] == '\n') {
        buffer[len-1] = '\0'; // 将换行符替换为字符串结束符
    }
    
    printf("处理后的字符串: %s", buffer); // 这里输出不会换行
    return 0;
}

这张图解释了2个关键点:

1、空格不作为结束符:

fgets 会读取包括空格在内的所有字符,直到遇到换行符 \n 或达到缓冲区限制。

这与 scanf("%s", ...) 不同,scanf 会在遇到空格时停止读取。

2、\n 会被记录在字符串中:

这是 fgets 最显著的特征。它会将换行符作为字符串的一部分存储。而且它在 \n 之后,自动添加一个空字符 \0 作为 C 语言字符串的结束标记。

因此,字符串的结尾是 \n\0,而不是 \0。

3、字符串解析

之前我们通过fgets读取到的是一行字符串,这一行字符串大大小取决于缓冲区的大小,然后我们得到了一个中间带有各种符号的字符串。

假如现在我们需要解析"ls -a -l",需要将其解析为"ls""-a""-l",换句话说就是需要用空格分割字符串,我们可以使用 strtok进行分割。

c 复制代码
char *strtok(char *str, const char *delim);
// 返回值是字符串指针
// 第一个参数是需要分割的字符串
// 第二个参数是分隔符

strtok 的工作方式非常巧妙,但也容易让人误解。它利用了静态局部变量来保存"当前解析位置"。

第一次调用:传入需要分割的字符串 str。

函数会跳过开头的分隔符。

从第一个非分隔符开始,直到遇到下一个分隔符为止,这一段就是第一个 Token。
函数会修改原字符串,将找到的分隔符替换为 \0(字符串结束符)。

返回指向第一个 Token 的指针。

c 复制代码
char Command_line[MAX_SIZE] = {0};
char* token;
token = strtok(Command_line," ");
// 当第一次调用的时候,token是返回的字符串首地址,即图中第一个token,结尾添加了\0
// 形成了第一个字符串
printf("Token: %s\n", token);

这样我们获得了第一个token:hello\0

再后续调用的时候,不需要再传入字符串首地址,只需要传入NULL和字符串分隔符即可。

因为如果字符串全部被遍历一遍,后续没有字符串了以后,strtok的返回值为NULL

所以我们使用循环对字符串进行遍历。

c 复制代码
while(token != NULL)
{	
	 printf("Token: %s\n", token);
	token = strtok(NULL," ");	
} 

这样我们就获得了第二个token:wo\0

第三个token:name\0。

第四个token:le\n\0。

例子:

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

#define MAX_SIZE 100

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


int main()
{
	char Command_line[MAX_SIZE] = {0};
	char* token;
	int i = 0;
	
	fgets(Command_line,sizeof(Command_line),stdin);
	
	printf("原始字符串为:%s\n",Command_line);
	
	// 首次调用strtok,传入分隔符
	token = strtok(Command_line," ");
	
	// 循环调用strtok,传入NULL,直到返回NULL
	while(token != NULL)
	{	
		argv[i] = token;
		token = strtok(NULL," "); // 若token为空则退出,token不为空,argv记录地址
		i++;	
	} 
	
	argc = i;
	
	for(int i = 0; argv[i];i++)
	{
		printf("%s\n",argv[i]);
	}
	
	return 0;
}

这样一个很简单的代码实现了字符串的分割和命令行参数的模拟

ls -a -l

原始字符串为:ls -a -l 再存入argv中。

ls

-a

-l

这样我们就实现了字符串的分割,和模拟命令行参数存储。

二、自制shell

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


#define MAX_SIZE 128
#define MAXARGS 32
// shell 内部维护的第一张表:命令行参数表
char* gargv[100];
int gargc = 0;
const char* gsep = " "; // 注意此处必须设定为字符串,而不是字符

// 环境变量表
char* genv[MAX_SIZE];
int gennv = 0;

// 我们自己shell所处的工作路径
char cwd[MAX_SIZE];
// 最近一次命令执行完毕的退出码
int lastcode = 0;

void LoadEnv() //  正常情况环境变量表内部是从配置文件来的
{
    // 现在我们从父进程拷贝
    extern char** environ;
    for(int i = 0; environ[i];i++)
    {
        genv[i] = (char*)malloc(sizeof(char)*4096); // 每一个genv[i],都有100个字节的空间
        strcpy(genv[i],environ[i]); // 拷贝全局环境变量到genv中
    }

}

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");// 使用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()
{
    // 1、通过系统变量修改
    char* pwd = getenv("PWD"); 
    // 这样直接获取系统变量PWD的值,因为我们写的myshell是bash的子进程,继承至父进程bash
    // 我们通过系统调用chdir修改了myshell这个进程的环境变量中的PWD,所以不影响父进程的PWD
    // 换句话说,在cd .. 以后,bash中的PWD不能跟随修改
    // 也就是bash进程中的环境变量没有改变,而怎么修改bash的环境变量呢?只能bash自己修改
   
    // 2、通过系统调用getcwd获取当前路径
    // getcwd 函数用于获取当前工作目录的绝对路径
    // 使用系统调用 char* getpwd(char* buf,size_t size),return 值是当前pwd的字符串首地址
    // char *buf:这是一个指向缓冲区的指针,用于存放获取到的当前工作目录路径字符串。
    // size_t size:指定缓冲区 buf 的大小(以字节为单位),用于防止缓冲区溢出。
    // pwd 其实是cwd的首地址
    // char* pwd = getcwd(cwd,sizeof(cwd));
    
    // 截取绝对路径 /home/benjiangliu/lesson23/shell 只想要最后的那个shell
    //
    
    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 的意义是:确保 fgets 成功读取了一行输入,
    // 否则立即返回错误,避免后续操作出错。它是处理用户输入时必不可少的安全检查。
    if(NULL == fgets(commandline,size,stdin))
        return 0;
        
    // 3、用户在输入的时候,至少会按下一个回车\n
    // fgets 会收集回车键\n,打印的时候,会将\n输出
    // command_line = "hello\n\0",strlen(command_line) = 6,因为strlen不包括\0,
    // command_line[5] = '\n',所以将command_line[5]改为\0
    commandline[strlen(commandline)-1] = '\0'; // 如果只按下空格,fgets记录的只有一个\n,并会被替换为\0
    return strlen(commandline);  // 若缓冲区只有一个\0,则会自动返回0
}

int ParseCommand(char* commandline)
{
    // 每次进入函数,都会清空一次命令行参数表
    gargc = 0;
    memset(gargv,0,sizeof(gargv));

    char* token;
    token = strtok(commandline,gsep);
    while(token != NULL) // 这么写就算commandline 中只有一个字符串,也会进入while循环,进行记录
    {
        gargv[gargc++] = token; // 将分割好的命令传入命令行参数表
        token = strtok(NULL," ");
    }

   // printf("argc = %d\n",gargc);
   // for(int i = 0;i<gargc;i++)
   // {
   //     printf("gargv[%d] = %s\n",i,gargv[i]);
   // }
    gargv[gargc] = NULL;// 命令行参数表最后一个参数为NULL
    return gargc;
}

int ExecuteCommand()
{
    pid_t id = fork(); // 创建子进程执行命令

    if(id == 0) // child
    {
        // 因为需要从环境变量搜索可执行文件
        execvp(gargv[0],gargv);// 命令行第一个参数是命令的名字
        perror("execvp fault!");
        exit(-1); // 防止exec出问题1
    }
    else if(id > 0)
    {
        int status = 0;
        waitpid(id,&status,0);
        if(WIFEXITED(status) && WEXITSTATUS(status) != 0)
        {
            printf("child process is exit ,exit code is %d\n",WEXITSTATUS(status));
        }

        lastcode = WEXITSTATUS(status);

       // else if(WIFSIGNALED(status))
       // {
       //     printf("child process is exit ,ignel code is %d\n",WTERMSIG(status));
       // }
    }
    else
    {
        return -1;
    }

}

// 0:不是内建命令
// 1:  是内建命令 && 执行内建命令
// 内建命令其实就是bash执行了一个函数
int CheckBuiltinExecute()
{
    if(strcmp(gargv[0],"cd") == 0 )
    {
        // 是内建命令,准备执行
        if(gargc == 2) // 说明是由参数的
        {
            // 1、先修改当前myshell进程的工作路径
            // cd 时候新的目标路径 在gargv[1]中
            // int chdir(const char* path) // 更改当前进程的工作路径
            chdir(gargv[1]);
            
            // 2、更改刷新环境变量,让命令行直接可以调用PWD显示
            // 将当前的工作路径加上PWD,构建一个自定义环境变量:PWD=/home/benjiangliu/Bite这样的结构
            // 然后修改子进程自定义环境变量,替换继承自父进程bash的环境变量
            char pwd[1024];
            getcwd(pwd,sizeof(pwd));// 获取当前工作路径
            snprintf(cwd,sizeof(cwd),"PWD=%s",pwd); // 将cwd传输到PWD中,修改的是当前进程
            putenv(cwd);
            lastcode = 0;//因为内建命令也有退出码
        }
        return 1;
    }
    else if(strcmp(gargv[0],"echo") == 0)
    {
        if(gargc == 2)
        {
            if(gargv[1][0] == '$')
            {
                if(strcmp(gargv[1]+1,"?") == 0)
                {
                    printf("%d\n",lastcode);
                }
                else if(strcmp(gargv[1]+1,"PATH") == 0)
                {
                    printf("%s\n",getenv("PATH"));// putenv 和 getenv 是什么??
                }
            }
                lastcode = 0;
        }
        return 1;
    }
        return 0;
}

int main()
{
    // 0、从配置文件中获取环境变量填充环境变量表
    LoadEnv();
    char Command_line[MAX_SIZE] = {0};
    while(1)
    {
        // 1、打印用户名 主机名 当前路径
        PrintCommandline();

        // 2、从键盘获取输入命令行
        if(0 == GetCommand(Command_line,sizeof(Command_line)))
            continue;
        // printf("%s\n",Command_line);// 打印输入从命令行,测试使用因为在之前就过滤了\n
        
        // 3、解析字符串:"ls -a -l",-> "ls" "-a" "-l"
        // 命令行解释器,就要对命令行字符串进行解析
        ParseCommand(Command_line);
       
        // 4、这个命令,到底让父进程执行(内建命令),还是让子进程执行?
        // 更改了父进程的路径,所有的子进程都会继承父进程已经改变的路径
        // 但是改变子进程的路径,父进程路径不会改变
        if(CheckBuiltinExecute()) // > 0
        {
            continue; // 不执行第五步
        }        
        // 5、让子进程执行命令
        ExecuteCommand();
    }

    return 0;
}
相关推荐
lzhailb1 小时前
nginx
运维·nginx
learndiary1 小时前
Deepin国产系统搭建B站桌面直播环境要点
linux·直播·deepin·b站
好好学习天天向上~~1 小时前
14_Linux学习总结_进程等待
linux·学习
Pretend° Ω2 小时前
抢占优先级 vs 响应优先级:任务调度的双刃剑
linux·c语言·抢占优先级·响应优先级
17(无规则自律)2 小时前
你对 argc 和 argv 的理解有多深?
linux·c语言·嵌入式硬件·考研
The Open Group2 小时前
开放流程自动化™标准:不是“更好的控制系统”,而是一次工业自动化协作方式的重构
运维·重构·自动化
The️2 小时前
Linux驱动开发之Open_Close函数
linux·运维·驱动开发·mcu·ubuntu
my_styles2 小时前
window系统安装/配置Nginx
服务器·前端·spring boot·nginx
feathered-feathered2 小时前
测试实战【用例设计】自己写的项目+功能测试(1)
java·服务器·后端·功能测试·jmeter·单元测试·压力测试