当前位置:首页>Linux>OS28.【Linux】自制简单的Shell v.1.0版本

OS28.【Linux】自制简单的Shell v.1.0版本

  • 2026-09-10 04:25:40
OS28.【Linux】自制简单的Shell v.1.0版本

目录

1.知识回顾

2.看看bash显示的格式

3.打印命令行提示符代码

用户名

主机名

当前目录(后续处理比较复杂)

打印命令提示符

完整代码

读取命令字符串

切割命令

回顾strtok切割字符串的方法

Shell切割字符串

执行命令

执行常规命令

内建命令

cd

env

export

echo

封装为一个函数

4.总结

5.参考资料


1.知识回顾

参见以下文章复习:

OS24.【Linux】进程等待 (上)[1]

OS25.【Linux】进程等待 (下) 和 进程程序替换(上)[2]

OS26.【Linux】进程程序替换(下)[3]

shell是外壳程序,当用户通过SSH远程登录到服务器时,服务器会启动shell,方便用户和操作系统交互,shell也是一个进程,执行命令(非内建命令)的时,本质就是自己创建子进程执行的

2.看看bash显示的格式

对于root用户: root@主机名:当前目录**#**

其中~表示表示当前用户的主目录

对于非root用户: 用户名@主机名:当前目录**$**

用户名、当前目录和的结束均可以通过环境变量获取

命令提示符的结束字符(#或者$)在PS1变量中,但它不是环境变量,而是本地变量

Linux有些版本可以通过环境变量获取主机名,有些不能,可以通过系统调用gethostname()获取

int gethostname(char *name, size_t len);的name是输出型参数,gethostname函数会将主机名(含\0)写入name数组中,其中len为name数组的字节数,注意空间要足够写入主机名

当然用户名和当前目录也可以通过系统调用获取:

用户名:

先调用uid_t getuid(void)获取当前用户的UID(每个用户的UID是独一无二的),再调用struct passwd *getpwuid(uid_t uid)获取当前用户的passwd结构体,里面含有用户名

struct passwd{char   *pw_name;       /* username */char   *pw_passwd;     /* user password */uid_t   pw_uid;        /* user ID */gid_t   pw_gid;        /* group ID */char   *pw_gecos;      /* user information */char   *pw_dir;        /* home directory */char   *pw_shell;      /* shell program */};

pw_dir为当前用户的主目录

合在一起可以写成: 用户名 = getpwuid(getuid()) -> pw_name

当前目录:

调用 char *getcwd(char buf[.size], size_t size)获取当前目录,当前目录的字符串存在buf数组中

3.打印命令行提示符代码

模块化设计

分别处理用户名、当前目录、命令提示符的结束字符

用户名

char* usr_name设置为 全局变量

void set_usr_name()//直接修改全局变量,不用传参{  usr_name = getpwuid(getuid()) -> pw_name;}

主机名

char hostname[HOST_NAME_SIZE]设置为全局变量

void write_hostname(char* hostname){if (gethostname(hostname,HOST_NAME_SIZE))  {perror("gethostname failed");exit(-1);  }}

当前目录(后续处理比较复杂)

如果直接调用getcwd(pwd,PWD_SIZE)而不做后续处理,设计出来的命令提示符和系统的bash有一点区别:

系统会使用''来表示当前用户的主目录的路径,如果切换到主目录中的目录,系统会以"/xxx"的形式显示

如果切换到主目录外,系统会显示完整的路径:

那就要分类讨论得到的pwd指针指向的字符串,主目录的路径可以通过getpwuid(getuid()) -> pw_dir系统调用获取,其次:

1.如果pwd的有效字符串的大小<主目录的路径的字符串的大小,不用处理pwd

2.如果pwd的有效字符串的大小==主目录的路径的字符串的大小,pwd应该指向"~"字符串

3.如果pwd的有效字符串的大小>主目录的路径的字符串的大小,应该将pwd的有效字符串的多余部分去除换成~字符

void write_pwd(char* pwd){if (!getcwd(pwd,PWD_SIZE))  {perror("getcwd failed");exit(-1);  }if (strlen(home_dir)==strlen(pwd))  {    pwd[0]='~';    pwd[1]='\0';  }else if (strlen(home_dir)<strlen(pwd))  {size_t new_len=strlen(pwd)-strlen(home_dir)+1;    pwd[0]='~';memmove(pwd+1,pwd+strlen(home_dir),new_len);  }else  {//什么都不做  }}

打印命令提示符

将各个部分融合到一起

void print_command_prompt(char* usr_name,char* hostname,char* pwd){printf("%s@%s:%s%c\n",usr_name,hostname,pwd,END_CHAR);}

完整代码

#include <stdio.h>#include <unistd.h>#include <string.h>#include <sys/types.h>#include <pwd.h>#include <stdlib.h>#define END_CHAR (strcmp(usr_name,"root") == 0 ? '#' : '$')#define HOST_NAME_SIZE 20#define PWD_SIZE 50char* usr_name;char* home_dir;void write_hostname(char* hostname){if (gethostname(hostname,HOST_NAME_SIZE))  {perror("gethostname failed");exit(-1);  }}void write_pwd(char* pwd){if (!getcwd(pwd,PWD_SIZE))  {perror("getcwd failed");exit(-1);  }if (strlen(home_dir)==strlen(pwd))  {    pwd[0]='~';    pwd[1]='\0';  }else if (strlen(home_dir)<strlen(pwd))  {size_t new_len=strlen(pwd)-strlen(home_dir)+1;    pwd[0]='~';memmove(pwd+1,pwd+strlen(home_dir),new_len);  }else  {//什么都不做  }}void print_command_prompt(char* usr_name,char* hostname,char* pwd){printf("%s@%s:%s%c ",usr_name,hostname,pwd,END_CHAR);}void set_usr_name()//直接修改全局变量,不用传参{  usr_name = getpwuid(getuid()) -> pw_name;}void set_home_dir()//直接修改全局变量,不用传参{  home_dir = getpwuid(getuid()) -> pw_dir;}int main(){char hostname[HOST_NAME_SIZE];char pwd[PWD_SIZE];set_home_dir();set_usr_name();write_pwd(pwd);write_hostname(hostname);while (1)  {print_command_prompt(usr_name,hostname,pwd);  }return 0;}

各个路径下的运行结果:

接下来需要读取命令字符串

读取命令字符串

对于shell而言,只有一种数据格式: 字符串!!!

不能用scanf读取字符串,scanf会忽略空格后面的所有内容

之前在CC18.【C++ Cont】OJ测试用例的各种输入情况汇总[4]文章讲过解决方法:使用getline或者fgets

ssize_t getline(char **restrict lineptr, size_t *restrict n, FILE *restrict stream);
char *fgets(char s[restrict .size], int size, FILE *restrict stream);

size给出s占有的字节数,stream指向流

这里使用fgets,先解释restrict关键字:

restrict关键字是C99标准引入的类型限定符(type qualifiers)

简明作用在C99标准草案的第109页中:

即restrict关键字确保对该对象的所有访问都必须直接或间接地使用该指针的值,否则结果不可预测,这只是一个承诺,不是规则

GNU的21.3 restrict-Qualified Pointers有写到:

即不应该这样写:

int val = 1;;int* restrict p = val;//已经承诺了只用p指针访问valint* q = p;//不应该再用q指针去访问

在fgets中,确保s和stream指向不同的内存区域,否则结果是不可预测的:

例如以下代码结果不可预测,运行结果直接报段错误'Segmentation fault (core dumped)"

#include <stdio.h>int main(){char buffer[10];fgets(buffer,10,(FILE*)buffer);//违反restrict的规则return 0;}

restricts 参考资料 :

C99标准草案

https://www.gnu.org/software/c-intro-and-ref/manual/html\_node/restrict-Pointers.html[5]

https://en.cppreference.com/w/c/language/restrict.html[6]

https://learn.microsoft.com/en-us/cpp/c-language/type-qualifiers?view=msvc-170[7]

fgets如果执行成功,返回commandline的值;如果执行失败返回NULL

可以这样读取命令字符串,访问stdin即可:

#define COMMANDLINE_SIZE 50char commandline[COMMANDLINE_SIZE];char* fgets_ret=fgets(commandline,COMMANDLINE_SIZE,stdin);assert(fgets_ret != NULL);

测试读取效果:

当然字符串结尾的\n可以去掉:

fgets_ret[strlen(fgets_ret)-1]='\0';

上述操作封装为一个函数:

void get_commandline(){char* fgets_ret=fgets(commandline,COMMANDLINE_SIZE,stdin);assert(fgets_ret != NULL);    fgets_ret[strlen(fgets_ret)-1]='\0';//去掉换行符}

切割命令

执行命令必须要拆分字符串,可以使用strtok函数

(之前在56.【C语言】字符函数和字符串函数(strtok函数)[8]文章讲过基本用法).这里直接用

回顾strtok切割字符串的方法

这个在56.【C语言】字符函数和字符串函数(strtok函数)[9]文章没有详细讲解,看看C89的标准文档的第167页的"7.11.5.8 The strstr generic function"

(高清图见http://zhangcoder.ysepan.com/[10]上的/CSDN上的资料/C89的strtok函数的解读.bmp)

第一次使用strtok时,第一个参数传要的切割字符串的地址,剩下几次切割要想让strtok切分原来的字符串,必须要传NULL,例如以下代码:

#include <stdio.h>#include <string.h>int main(){char arr[] = { "user@csdn.net" };char delimiters[20] = { "@." };char* p[10];int i = 0;  p[i] = strtok(arr, "@.");while (p[i] != NULL)  {printf("%s\n", p[i]);    p[++i] = strtok(NULL, "@.");  }return 0;}

运行结果:

回到自制Shell的编写:

Shell切割字符串

写一个切割字符串的函数split_commandline,传入的参数为argv,而commandline直接从全局变量中读取,函数返回参数的个数

split_commandline的作用是将参数一个一个拆分出来,然后写入到argv指针数组

#define DELIMITER " "int split_commandline(char* argv[]){int num=0;  argv[num++]=strtok(commandline,DELIMITER);while (argv[num++]=strtok(NULL,DELIMITER));return num-1;  }

while (argv[num++]=strtok(NULL,DELIMITER));写法能一遍执行赋值语句,一边判断赋值语句的值的真假,while退出时,argv的结尾为NULL,不用再单独设置

测试代码:

int main(int argc,char* argv[],char* envp[]){set_home_dir();set_usr_name();write_hostname();while (1)  {print_command_prompt();get_commandline();int argc=split_commandline(argv);printf("命令参数的个数为:%d\n",argc);int i=0;while(argv[i])printf("%s\n",argv[i++]);  }return 0;}

运行结果:

执行命令

回顾之前在OS21.【Linux】环境变量[11]文章讲过的命令的分类:系统命令分为常规命令和内建命令

1.常规命令: 通过创建子进程;来执行常规命令2.内建命令(也称内置命令,英文名为builtin): bash 不创建子进程,而是由自己亲自执行,类似于 bash 调用了自己写的或者系统提供的函数

执行常规命令

使用fork()创建子进程,有关fork的使用和父进程的等待方法参见以下文章:

OS18.【Linux】进程基础知识(2)[12]

OS23.【Linux】进程终止[13]

OS24.【Linux】进程等待 (上)[14]

OS25.【Linux】进程等待 (下) 和 进程程序替换(上)[15]

框架代码:

pid_t fork_ret_id=fork();id (fork_ret_id<0){perror("fork failed");continue;}else if (fork_ret_id==0)//子进程执行命令{//......}else //Shell获取到了子进程的PID{//......}

父进程的等待代码:

int status;pid_t waitpid_ret_id=waitpid(fork_ret_id,&status,0);//阻塞等待if (waitpid_ret_id<0){perror("waitpid failed");continue;}if (fork_ret_id==waitpid_ret_id){  lastcode = WEXITSTATUS(status);}

注意保留最近一个子进程的退出码

子进程执行代码:

因为子进程执行的是不带路径的命令,所以用execvp或execvpe,需要去环境变量中找

execvpe(argv[0],argv,envp);printf("%s: command not found\n",argv[0]);exit(-1);

注:execvpe不是POSIX 标准函数,在 Linux 上属于GNU扩展,使用前需要将#define _GNU_SOURCE放在所有#include之前,或者编译时加 -D_GNU_SOURCE,否则会报警告:

如果不

其次如果没找到命令,execvpe就无法执行,也就无法进行进程的替换了,就会执行下面的printf和exit,报告没有找到命令

运行结果:

再将常规命令的执行封装为一个函数:execute_program

void execute_program(int argc,char* argv[],char* envp[]){pid_t fork_ret_id=fork();if (fork_ret_id<0)  {perror("fork failed");return;  }else if (fork_ret_id==0)//子进程执行命令  {execvpe(argv[0],argv,envp);printf("%s: command not found\n",argv[0]);exit(-1);  }else //Shell获取到了子进程的PID  {int status;pid_t waitpid_ret_id=waitpid(fork_ret_id,&status,0);//阻塞等待if (waitpid_ret_id<0)    {perror("waitpid failed");return;    }if (fork_ret_id==waitpid_ret_id)    {      lastcode = WEXITSTATUS(status);    }  }}

内建命令

cd

cd是内建命令,如果让Shell的子进程执行cd,虽然子进程的理解改了,但是不影响Shell父进程,因此内建命令必须让Shell自己执行,内建命令的本质是Shell内的一个函数

为了简单起见,这里写if判断,可以直接调用系统调用chdir来改变进程当前所处的目录,注意判断返回值

以下代码添加到execute_program函数执行前:

if (argc==2&&strcmp(argv[0],"cd")==0){if (chdir(argv[1])<0)perror("change working directory failed");continue;}

运行结果:cd .和cd ..特殊情况也可以处理

当然print_command_prompt也可以再优化,不用每次调用都要修改pwd,只要在修改所处路径时修改

void print_command_prompt(){//write_pwd();printf("%s@%s:%s%c ",usr_name,hostname,pwd,END_CHAR);}

注:如果chdir不支持cd .和cd ..操作可以这样处理:

通过环境变量OLDPWD获取上次所处的目录

if (strcmp(argv[1],"."))continue;if (strcmp(argv[1],"..")){chdir(getenv("OLDPWD"));}
env

单独的env命令,直接打印环境变量:

if (argc==1&&strcmp(argv[0],"env")){for (int i=0;envp[i];i++)printf("%s\n",envp[i]);continue;}
export

用于导入环境变量,可直接使用系统调用putenv,注意处理返回值

如果这样写会有问题:

if (argc==2&&strcmp(argv[0],"export")==0){if (putenv(argv[1])!=0)perror("change or add an environment variable failed");continue;}

根据?文章的**结论:putenv只修改环境变量表的指针,不会拷贝形如"name=val"的字符串,**比如插入export   VAL=1,VAL=1字符串是临时的,使用putenv(argv[0])插入,putenv会设置指针指向,VAL=1,但argv[0]的值会被下一次命令覆盖,因此需要自行维护添加的环境变量

#define MY_ENVP_SIZE 50char* my_envp[MY_ENVP_SIZE];int my_envp_index;//在main函数中初始化if (argc==2&&strcmp(argv[0],"export")==0){char* tmp=(char*)malloc(strlen(argv[1])+1);strcpy(tmp,argv[1]);  my_envp[my_envp_index]=tmp;if (putenv(my_envp[my_envp_index])!=0)perror("change or add an environment variable failed");  my_envp_index++;continue;}

运行结果:

echo

S?打印退出码,$VAL打印VAL的值

if (strcmp(argv[1],"$?")==0){printf("%d\n",lastcode);  lastcode=0;//echo $?正常执行continue;}else//argv[1]=="$"{printf("%s\n",getenv((char*)argv[1]+1));continue;}

运行结果:

封装为一个函数

当然可以将以上这些内建命令封装为一个函数execute_buildin_command

bool execute_buildin_command(int argc,char* argv[]){if (argc==2&&strcmp(argv[0],"cd")==0)  {if (chdir(argv[1])<0)perror("change working directory failed");return true;  }if (argc==1&&strcmp(argv[0],"env")==0)  {for (int i=0;environ[i];i++)printf("%s\n",environ[i]);return true;  }if (strcmp(argv[0],"about")==0)  {printf("DIY Shell v1.0 Written By zhangcoder~\n");return true;  }if (argc==2&&strcmp(argv[0],"export")==0)  {char* tmp=(char*)malloc(strlen(argv[1])+1);strcpy(tmp,argv[1]);//putenv("A=aaa");    my_envp[my_envp_index]=tmp;if (putenv(my_envp[my_envp_index])!=0)perror("change or add an environment variable failed");    my_envp_index++;return true;  }if (argc==2&&strcmp(argv[0],"echo")==0)  {if (strcmp(argv[1],"$?")==0)    {printf("%d\n",lastcode);      lastcode=0;//echo $?正常执行return true;    }else//argv[1]=="$"    {printf("%s\n",getenv((char*)argv[1]+1));return true;    }  }return false;}

则while循环内改成:

while (1){print_command_prompt();get_commandline();int argc=split_commandline(argv);if (argv[0]==NULL)//不加会报段错误continue;if (!strcmp(argv[0],"exit"))break;if (strcmp(argv[0],"ls")==0)//为ls加上颜色选项  {    argv[argc++]="--color";    argv[argc]=NULL;  }bool is_buildin=execute_buildin_command(argc, argv);if (!is_buildin)execute_program(argc,argv);}

4.总结

1.用户进行登陆的时,系统会启动一个Shell 进程

2.Shell 本身的环境变量是从用户目录下的 .bash_profile文件中读取的,,面保存了导入环境变量的方式

5.参考资料

Stephen Brennan的博客:

https://brennan.io/2015/01/16/write-a-shell-in-c/[16]

翻译版:https://zhuanlan.zhihu.com/p/83623613?share\_code=UIZy2moM6aAV&utm\_psn=1944181466322961448[17]

南京大学Shell作业+答案:

https://cs.nju.edu.cn/zhangl/linux/lab01.pdf?sharetype=link[18]

https://github.com/NJU-Linux/linux-systems-lab01-161220117[19]

GNU官方bash源代码仓库:

https://ftp.gnu.org/gnu/bash/[20]

参考资料

[1] 

OS24.【Linux】进程等待 (上): https://zhangcoder.blog.csdn.net/article/details/150157854?spm=1011.2415.3001.5331

[2] 

OS25.【Linux】进程等待 (下) 和 进程程序替换(上): https://zhangcoder.blog.csdn.net/article/details/150472197?spm=1011.2415.3001.5331

[3] 

OS26.【Linux】进程程序替换(下): https://zhangcoder.blog.csdn.net/article/details/150534985?spm=1011.2415.3001.5331

[4] 

CC18.【C++ Cont】OJ测试用例的各种输入情况汇总: https://blog.csdn.net/2401_85828611/article/details/145107565

[5] 

https://www.gnu.org/software/c-intro-and-ref/manual/html_node/restrict-Pointers.html: https://www.gnu.org/software/c-intro-and-ref/manual/html_node/restrict-Pointers.html

[6] 

https://en.cppreference.com/w/c/language/restrict.html: https://en.cppreference.com/w/c/language/restrict.html

[7] 

https://learn.microsoft.com/en-us/cpp/c-language/type-qualifiers?view=msvc-170: https://learn.microsoft.com/en-us/cpp/c-language/type-qualifiers?view=msvc-170

[8] 

56.【C语言】字符函数和字符串函数(strtok函数): https://blog.csdn.net/2401_85828611/article/details/142313699

[9] 

56.【C语言】字符函数和字符串函数(strtok函数): https://blog.csdn.net/2401_85828611/article/details/142313699

[10] 

http://zhangcoder.ysepan.com/: http://zhangcoder.ysepan.com/

[11] 

OS21.【Linux】环境变量: https://blog.csdn.net/2401_85828611/article/details/149641629

[12] 

OS18.【Linux】进程基础知识(2): https://zhangcoder.blog.csdn.net/article/details/149428230

[13] 

OS23.【Linux】进程终止: https://zhangcoder.blog.csdn.net/article/details/149980273

[14] 

OS24.【Linux】进程等待 (上): https://zhangcoder.blog.csdn.net/article/details/150157854

[15] 

OS25.【Linux】进程等待 (下) 和 进程程序替换(上): https://zhangcoder.blog.csdn.net/article/details/150472197

[16] 

https://brennan.io/2015/01/16/write-a-shell-in-c/: https://brennan.io/2015/01/16/write-a-shell-in-c/

[17] 

https://zhuanlan.zhihu.com/p/83623613?share_code=UIZy2moM6aAV&utm_psn=1944181466322961448: https://zhuanlan.zhihu.com/p/83623613?share_code=UIZy2moM6aAV&utm_psn=1944181466322961448

[18] 

https://cs.nju.edu.cn/zhangl/linux/lab01.pdf: https://cs.nju.edu.cn/zhangl/linux/lab01.pdf

[19] 

https://github.com/NJU-Linux/linux-systems-lab01-161220117: https://github.com/NJU-Linux/linux-systems-lab01-161220117

[20] 

https://ftp.gnu.org/gnu/bash/: https://ftp.gnu.org/gnu/bash/

最新文章

随机文章