Linux之简易Linux Shell的实现过程
作者:码完就睡
本文通过C语言实现一个简易命令行解释器,解析用户输入的指令,处理内置命令,并创建子进程执行外部命令,文章详细介绍了命令提示符、命令解析及主函数实现逻辑
一、引言
在使用 Linux 终端时,我们输入一条命令(例如 ls -l)后,终端就会解析并执行对应的指令,最终将结果展示出来。
为了理解这一过程背后的实现原理,本文将通过C语言代码,手动实现一个简易的命令行解释器,模拟 Shell 的核心执行逻辑。

二、核心模块分析
(一)打印命令提示符
1.区分普通用户和管理员
普通用户使用 $ 作为提示符;管理员使用 # 作为提示符
若 用户ID = 0 ,则说明当前是管理员。

/*区分普通用户和管理员*/ 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.打印

注意:获取到的用户信息都在 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); //父进程,等待子进程结束
}
}
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
