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;
}
相关推荐
吠品8 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
XMAIPC_Robot8 小时前
软硬协同实时控制|RK3588业务调度+FPGA硬件时序,ethercat实现半导体设备微秒级响应(125us)
linux·arm开发·人工智能·fpga开发
AOwhisky9 小时前
Python 学习笔记(第十五期)——运维自动化(下·后篇):堡垒机实战——paramiko高阶篇
运维·python·学习·云原生·自动化·运维开发
重生的黑客9 小时前
Linux 进程优先级、切换与调度:从孤儿进程到 O(1) 调度模型
linux·运维·服务器·进程优先级·nice
Urbano9 小时前
夹克与商务单西口袋工序自动化技改研究报告
运维·自动化
骑上单车去旅行10 小时前
MD5校验对比脚本
linux·服务器·windows
平生幻10 小时前
Linux 常用命令
linux
期待着201310 小时前
docker 安装 ,在centos7.9
运维·docker·容器
踏月的造梦星球10 小时前
DM8 DSC 单机双实例部署
运维·开发语言·数据库
MXsoft61811 小时前
平台化是智能运维发展的必然趋势吗?
运维