在学习了 Linux 系统编程和 C 语言之后,最好的巩固方式就是动手实践。今天,我们将从零开始,逐步实现一个简化版的 Shell ------ mybash。本系列将从最基础的命令行提示符开始,逐步深入,最终实现一个功能完整的命令行解释器。
整体项目的代码可以分为三大部分:输出命令行提示符、获取用户命令和参数、执行用户命令
在主函数中利用一个死循环实现重复响应
cpp
#include <stdio.h>
int main()
{
while (1)
{
// 1.命令行提示符输出
// 2.获取用户命令
// 3.执行用户命令
}
}
一、命令行提示符
1、系统提示符分析
当我们打开 Linux 终端时,会看到类似这样的提示符:

它的结构非常清晰:登录用户名@主机名: 当前工作目录/路径$
wuya:我当前登录的用户名wuya-virtual-machine:我的主机名~:当前工作目录(家目录的简写)$:普通用户权限标识(如果是#则为 root 用户)
我们的第一个目标就是用 C 语言复现这个提示符。
2、核心函数
要实现这个提示符,我们需要获取三部分信息:登录用户名、主机名和当前工作目录。下面我们逐一解析所需的系统函数。
(1) 获取登录用户名:getlogin()
getlogin() 函数用于获取当前终端的登录用户名。
下图是在终端用 man getlogin 命令获取到的信息

- 函数原型 :
char *getlogin(void); - 返回值 :成功时返回一个指向以
\0结尾的用户名字符串的指针;失败时返回NULL,并设置errno。 - 原理 :该函数通过读取系统记录登录信息的文件(如
/var/run/utmp)来获取用户名。 - 头文件:<unistd.h>
使用示例:
cpp
char *loginname = getlogin();
if (loginname == NULL) {
perror("getlogin failed");
exit(EXIT_FAILURE);
}
(2) 获取主机名:uname()
uname() 是一个系统调用,用于获取内核和系统的详细信息,其中就包括主机名。
下图为通过 man 2 uname 命令获取到的详细信息

- 函数原型 :
int uname(struct utsname *buf); - 返回值 :成功时返回非负整数;失败时返回
-1,并设置errno。 - 头文件:<sys/utsname.h>
- 结构体
struct utsname:
cpp
struct utsname {
char sysname[]; // 操作系统名称,如 "Linux"
char nodename[]; // 主机名,即我们需要的 hostname
char release[]; // 内核发行版号
char version[]; // 内核版本
char machine[]; // 硬件架构,如 "x86_64"
};
使用示例:
cpp
struct utsname sysinfo;
if (uname(&sysinfo) == -1) {
perror("uname failed");
exit(EXIT_FAILURE);
}
// 主机名存储在 sysinfo.nodename 中
(3) 获取路径
getcwd() 函数用于获取当前进程的工作目录的绝对路径。
通过 man 2 getcwd查看详细信息:

- 函数原型 :
char *getcwd(char *buf, size_t size); - 参数:
buf:指向用于存储路径的缓冲区。size:缓冲区的大小(字节数)。
- 返回值 :成功时返回指向路径字符串的指针;失败时返回
NULL,并设置errno。 - 错误处理 :如果路径长度(包括
\0)超过缓冲区大小size,函数会返回NULL并设置errno为ERANGE,表示缓冲区不足。 - 头文件:<unistd.h>
使用示例:
cpp
char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) == NULL) {
perror("getcwd failed");
exit(EXIT_FAILURE);
}
3、代码实现与优化
利用上面的三个函数,就可以实现基础的系统提示符的输出,代码如下:
为了让函数不一直输出,我们先加上简单的获取用户命令的fgets语句
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/utsname.h>
// 输出命令行提示符
void PrintPrompt()
{
// 获取主机名
struct utsname sysinfo; // 创建结构体
// 调用函数,获取内核和系统的详细信息
if (uname(&sysinfo) == -1)
{
// 获取失败
perror("get host info failed:");
printf("mybash> "); // 在提示符最前面加上mybash>,和系统终端区分开
fflush(stdout); // 刷新缓冲区
return;
}
// 获取目录/路径
char buf[128] = {0}; // 存储路径的缓冲区
char *ptr = getcwd(buf, 127); // 获取当前进程的工作目录的绝对路径
if (ptr == NULL) // 获取失败
{
perror("get current working directory failed:");
printf("mybash> ");
fflush(stdout);
return;
}
// 输出
printf("%s@%s:%s$ ", getlogin(), sysinfo.nodename, buf);
}
int main()
{
while (1)
{
// 1.系统提示符输出
PrintPrompt();
// 2.获取用户命令
char cmd[128] = {0};
char *s = fgets(cmd, 127, stdin); // 获取用户命令
if (s == NULL)
{
continue; // 获取失败,直接进行下一次获取
}
printf("cmd=%s\n", cmd);
// 3.执行用户命令
}
}
编译链接后的运行结果

优化改进
参照系统终端,我们可以做出以下优化,使得我们自己的终端更完善
- 直接显示目录,而非绝对路径
- root用户显示 # 而非 $
- 用户名不用 getlogin() 获取
- 家目录输出~
(1) 直接显示当前目录
不做特殊设置,系统终端的冒号后显示的是当前路径,若想要只显示当前目录,需要设置,设置的方法可以自行百度。
直接显示的目录(当前目录)就是当前路径的最后一个字符串,而路径是由 / 分割开的,所以直接获取最后一个 / 后的字符串即可。
cpp
// 获取目录
char *s = NULL;
s = buf + sizeof(buf); // 将s移动到buf末尾
// 将s向前移动至最后一个'/'位置
while (*s != '/')
{
s--;
}
// 输出
// s+1开始的字符串就是当前目录
printf("%s@%s:%s$ ", getlogin(), sysinfo.nodename, s + 1);
(2) 用户名不用 getlogin() 获取
如果是root用户,用getlogin()函数获取到的登录用户名还是原本的用户名,不会输出root,所以,需要改变获取用户名的方式------使用函数 getpwuid()

getpwuid() 会++根据你传入的用户 ID++ (uid),去系统的密码数据库(通常是 /etc/passwd)中++查找对应的用户记录++ ,并返回一个指向 struct passwd 结构体的指针,里面包含了该用户的详细信息。
函数 getuid() 可以获取到当前进程的用户ID

所以我们可以先用 getuid() 函数获取到用户id,再使用 getpwuid()函数获取到用户的详细信息,其中的pw_name就是对应的用户名
(getpwuid函数所需的头文件:<sys/type.h> 和 <pwd.h> )
cpp
#include <sys/types.h>
#include <pwd.h>
......
// 获取uid
uid_t uid = getuid();
// 获取用户名
struct passwd *p = getpwuid(uid); // p->pw_name为用户名
if (p == NULL)
{
perror("get passwd info faild:");
printf("mybash> ");
fflush(stdout);
return;
}
......
(3) root用户显示 # 而非 $
使用 sudo -i 命令输入密码后切换到 root的登录环境,可以看出,最后的符合是'#',而不是'$',所以我们可以针对这一点进行优化

基于获取用户名方式的改变,有两种方法可以实现 # 和 $ 的选择
**法一:**比较用户名和"root"
cpp
//$和#的选择
char ch = '$'; // 默认为'$'
// 如果登录用户名等于root,就输出'#'
if (strcmp(p->pw_name, "root") == 0)
{
ch = '#';
}
**法二:**root的uid为0
cpp
//$和#的选择
char ch = '$'; // 默认为'$'
// root的uid为0
if (uid == 0)
{
ch = '#';
}
最后的输出语句
cpp
printf("%s@%s:%s%c ", p->pw_name, sysinfo.nodename, s + 1, ch);
(4) 处理特殊情况:~和/
如图,当路径为家目录时,系统会用"~"代替,所以我们也可以对家目录的输出进行优化

使用 getpwuid() 函数获取到的passwd结构体里面有一个字段就是家目录

所以可以直接使用pw_dir获取即可,然后和获取到的路径比较即可
cpp
// 显示当前目录
char *s = NULL;
if (strcmp(buf, p->pw_dir) == 0)//当前路径为家目录
{
s = "~";//输出~
}
else //不是家目录,正常输出
{
s = buf + sizeof(buf); // 将s移动到buf末尾
// 将s向前移动至最后一个'/'位置
while (*s != '/')
{
s--;
}
// s+1开始的字符串就是当前目录
s++;
}
// 输出
printf("mybash> %s@%s:%s%c ", p->pw_name, sysinfo.nodename, s, ch);
fflush(stdout);

另外,如果所处的绝对路径为" / ",通过上面的代码 s++会出现越界的问题,所以这种情况也需要特殊处理:当当前路径为" / "时,直接输出s即可。
cpp
// 显示当前目录
char *s = NULL;
if (strcmp(buf, p->pw_dir) == 0)//当前路径为家目录
{
s = "~";//输出~
}
else //不是家目录,正常输出
{
s = buf + sizeof(buf); // 将s移动到buf末尾
// 将s向前移动至最后一个'/'位置
while (*s != '/')
{
s--;
}
//当前绝对路径为"/"时,直接输出s
// s+1开始的字符串就是当前目录
if(strlen(s)!=1){
s+=1;
}
}
// 输出
printf("mybash> %s@%s:%s%c ", p->pw_name, sysinfo.nodename, s, ch);
(5) 提示符高亮显示
系统终端的提示符有些部分是彩色高亮显示的,要实现这个功能,使用printf本身就可以实现。参考:https://www.cnblogs.com/GrootStudy/p/17025354.html
cpp
printf("mybash> \033[1;32m%s@%s\033[0m\033[1;34m:%s\033[0m%c ", p->pw_name, sysinfo.nodename, s, ch);

4、代码
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/utsname.h>
#include <string.h>
#include <sys/types.h>
#include <pwd.h>
// 输出命令行提示符
void PrintPrompt()
{
//$和#的选择
char ch = '$'; // 默认为'$'
// 获取uid
uid_t uid = getuid();
// 获取用户名
struct passwd *p = getpwuid(uid); // p->pw_name为用户名
if (p == NULL)
{
perror("get passwd info faild:");
printf("mybash> ");
fflush(stdout);
return;
}
// 法一
// 如果登录用户名等于root,就输出'#'
/*if (strcmp(p->pw_name, "root") == 0)
{
ch = '#';
}*/
// 法二
// root的uid为0
if (uid == 0)
{
ch = '#';
}
// 获取主机名
struct utsname sysinfo; // 创建结构体
// 调用函数,获取内核和系统的详细信息
if (uname(&sysinfo) == -1)
{
// 获取失败
perror("get host info failed:");
printf("mybash> "); // 在提示符最前面加上mybash>,和系统终端区分开
fflush(stdout); // 刷新缓冲区
return;
}
// 获取路径
char buf[128] = {0}; // 存储路径的缓冲区
char *ptr = getcwd(buf, 127); // 获取当前进程的工作目录的绝对路径
if (ptr == NULL) // 获取失败
{
perror("get current working directory failed:");
printf("mybash> ");
fflush(stdout);
return;
}
// 显示当前目录
char *s = NULL;
if (strcmp(buf, p->pw_dir) == 0)//当前路径为家目录
{
s = "~";//输出~
}
else //不是家目录,正常输出
{
s = buf + sizeof(buf); // 将s移动到buf末尾
// 将s向前移动至最后一个'/'位置
while (*s != '/')
{
s--;
}
//当前绝对路径为"/"时,直接输出s
// s+1开始的字符串就是当前目录
if(strlen(s)!=1){
s+=1;
}
}
// 输出
printf("mybash> \033[1;32m%s@%s\033[0m\033[1;34m:%s\033[0m%c ", p->pw_name, sysinfo.nodename, s, ch);
fflush(stdout);
}
int main()
{
while (1)
{
// 1.系统提示符输出
PrintPrompt();
// 2.获取用户命令
char cmd[128] = {0};
char *s = fgets(cmd, 127, stdin); // 获取用户命令
if (s == NULL)
{
continue; // 获取失败,直接进行下一次获取
}
printf("cmd=%s\n", cmd);
// 3.执行用户命令
}
}
二、分离用户命令和参数
在第一部分我们初步通过fgets()函数获取到用户输入的字符串cmd,实现了获取用户输入并输出的功能,接下来对该功能进行完善。
系统终端通过获取用户输入,针对输入的命令以及参数来实现功能,所以我们也需要将命令和参数分离开来,命令用来对应需要实现的功能,参数作为命令的补充实现。
1、核心函数 strtok()
通过strtok() 函数可以将获取到的cmd字符串分割。

(1) 函数作用
strtok() 会把一个字符串按照指定的分隔符,切割成一系列非空的 token(子串)。
(2) 核心用法规则
- 首次调用:必须传入要分割的字符串和分隔符
- 后续调用:要继续分割同一个字符串,字符串内容必须传NULL,函数会自动从上次结束的位置继续
- 分隔符:可以是一个包含多个字符的字符串,函数会把其中任意一个字符都当作分隔符。在后续调用中,可以更换不同的分隔符
(3) 返回值
- 每次调用返回一个以\0结尾的token字符串的指针,这个token不包含分隔符
- 当没有更多token时,返回NULL
(4) 内部机制
- 函数内部维护了一个静态指针,用来记录下一次分割的起始位置
- 首次调用时,这个指针指向字符串的开头
- 后续调用时,函数会从这个指针开始扫描,跳过所有分隔符,找到下一个非分隔符字节作为新token的起点,直到再次遇到分隔符
- 如果扫描到字符串末尾都没有找到新token,返回NULL
2、代码实现
cpp
// 命令和参数总数
#define CMD_NUM 10
// 分割用户命令和参数
int SplitCmd(char *cmd, char *cmdArr[])
{
// 首次分割:传递字符串
// 以空格为标准进行分割
char *token = strtok(cmd, " ");
int count = 0; // 数组索引
while (token != NULL)
{
// 将分割得到的字符串保存在cmdArr数字中
cmdArr[count++] = token;
// 继续分割,传递的字符串为NULL
token = strtok(NULL, " ");
}
// 返回字符串个数
return
}
cpp
int main()
{
while (1)
{
// 1.系统提示符输出
PrintPrompt();
// 2.获取用户命令
char cmd[128] = {0};
char *s = fgets(cmd, 127, stdin); // 获取用户命令
if (s == NULL)
{
continue; // 获取失败,直接进行下一次获取
}
cmd[strlen(cmd) - 1] = 0;
char *cmdArr[CMD_NUM] = {0}; // 记录分割后的输入
// 进行分割
int count = SplitCmd(cmd, cmdArr);
// 没有输入,直接进行下一次获取
if (count == 0)
{
continue;
}
// 输出命令和参数------验证
for (int i = 0; i < count; i++)
{
printf("cmd:%s\n", cmdArr[i]);
}
// 3.执行用户命令
}
}
3、运行结果

三、执行用户命令
内置命令 VS 外置命令
首先需要了解一下系统命令分为两种:内置bash的命令和外置命令。外置命令可以通过 which + 命令查到命令所在的文件/目录,如 ls 命令,而内置bash命令不行,如cd。

外置命令一般放在bin目录内,所以我们自己实现时写可以创建一个 mybin 目录装外置命令实现函数。内置命令直接放在 mybash.c 中即可。
具体区别:
| 维度 | 内置命令(Built-in) | 外置命令(External Command) |
|---|---|---|
| 本质 | Shell 程序本身的一部分(函数 / 指令) | 独立的可执行文件(二进制 / 脚本) |
| 执行方式 | 由 Shell 直接执行,不创建子进程 | Shell 会 fork 子进程,再 exec 加载执行 |
| 速度 | 极快(无需磁盘 IO、无进程创建开销) | 较慢(需要加载文件、创建子进程) |
| 作用域 | 影响当前 Shell 进程(如 cd) |
仅影响子进程(无法改变父 Shell 状态) |
| 查找方式 | 无需 PATH,Shell 内部直接识别 |
依赖 PATH 环境变量查找可执行文件 |
我们先实现cd和exit两个内置命令。
1、运行命令函数+exit命令的实现
编写一个函数,在函数中根据用户输入的命令进行不同的操作
在主函数中已经获取到了用户输入,且对输入进行了分割,cmdArr[0]就是命令,将cmdArr[0]和exit等命令对比,就能针对不同情况进行处理。
可以先得出一个大致框架如下:exit命令直接调用系统exit函数即可。
cpp
#include<stdlib.h> //exit需要的头文件
// 执行系统命令
void RunCmd(char *cmdArr[], int count)
{
char* cmd = cmdArr[0];
if (strcmp(cmd, "cd") == 0)
{
printf("cd->%s\n", cmd);
}
else if (strcmp(cmd, "exit") == 0)
{
exit(0);
}
else
{
printf("other cmd:%s\n", cmd);
}
}
主函数调用该函数
cpp
int main(){
while(1){
......
//3.执行用户命令
RunCmd(cmdArr,count);
}
return 0;
}
2、cd 命令的实现
我们先实现基础的cd命令,后续在完善。
假设用户输入"cd 参数1",该命令实现的功能:从当前路径切换到参数1路径下,所以我们的主要工作是切换路径。
(1) 核心函数 chdir()
通过函数 chdir() 可以实现路径的切换,切换成功,函数返回0;失败,返回-1。


(2) 代码实现
情况分析
首先来分析cd命令的几种常见情况:
1.没有参数
如果没有参数,cd命令会将路径切换到家目录

所以目的路径为家目录,家目录的获取在命令行提示符实现部分已经说过:结构体的pw_dir字段对应的就是家目录,所以切换到 pw_dir即可。
2.参数为 ~
~代表家目录,和没有参数的情况一样,将路径切换为 pw_dir字段即可。
若 ~ 后还有参数,目录路径为:家目录/后面的参数。
3.一般情况
参数为一般的路径,直接切换即可
代码
cpp
// cd命令的实现
void RunCd(char *path)//参数为传递的参数
{
char buf[128]={0};
struct passwd* p=getpwuid(getuid());
if(p==NULL){
printf("get pwuid info failed\n");
return;
}
//没有参数
if(path==NULL){
path=p->pw_dir;//目的路径为家目录
}
//参数为~ + ~后面还有参数
else if(strncmp(path,"~",1)==0){
strcpy(buf,p->pw_dir);//令buf为家目录
strcat(buf,path+1);//把剩余参数连接到buf上
path=buf;//令path保存目的路径
}
//一般情况,直接切换路径
//切换路径
if (chdir(path) == -1)
{
perror("cd failed\n");
return;
}
}
......
if (strcmp(cmd, "cd") == 0)
{
RunCd(cmdArr[1]);//将参数传递给函数
}
......
运行结果

3、退格键功能的实现
在运行mybash可执行文件时,会存在无法删除以前输入的命令的情况,这时需要自己实现删除功能。
终端默认有两种模式:
| 模式 | 特点 | 能否删除字符 |
|---|---|---|
| 规范模式(Canonical Mode) | 终端会先缓存输入,直到按回车才把整行传给程序;同时终端内核会处理删除、退格、光标移动等编辑操作 | 可以删除(这是终端内核帮你做的,不是 fgets() 做的) |
| 非规范模式(Non-Canonical Mode) | 输入不缓存,字符直接传给程序;终端内核不处理任何编辑操作 | 无法删除(按删除键只会输入乱码 / 特殊字符) |
无法删除的原因就是系统处于非标准模式。
(1) 核心结构体 termios
termios 函数族提供了一套通用的终端接口,用于控制异步通信端口(比如终端)的各种行为,包括输入模式、输出模式、控制字符等。
书籍《Linux程序设计》中写到:

(标准模式是规范模式的口语化描述)
可以通过 tcgetattr() 获取当前终端属性,修改 c_lflag 字段,开启 ICANON (规范模式)和 ECHO (回显),然后用 **tcsetattr()**应用修改,从而恢复终端的正常编辑功能。
(2) 核心函数
tcgetattr()
作用:读取终端属性
int tcgetattr(int fd, struct termios *termios_p);
| 参数 | 含义 | 你的 mybash 场景 |
|---|---|---|
fd |
终端文件描述符(必须是终端设备,否则报错) | 用 STDIN_FILENO(值为 0),表示 "标准输入对应的终端" |
termios_p |
指向 struct termios 结构体的指针 |
用来存储读取到的终端属性(比如当前是否是规范模式、是否开启回显) |
返回值:成功返回0;失败返回-1,并设置error
tcsetattr()
作用:设置终端属性
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
| 参数 | 含义 | 你的 mybash 场景 |
|---|---|---|
fd |
同 tcgetattr(),终端文件描述符 |
同样用 STDIN_FILENO |
optional_actions |
生效时机 | 用 TCSANOW(立即生效) |
termios_p |
指向修改后的 struct termios 结构体 |
传入你修改过的属性(比如开启 ICANON 和 ECHO) |
返回值:成功返回0;失败返回-1,并设置error
(3) 代码实现
代码除了使用termios结构和相关函数,还使用到了printf函数

cpp
#include<termios.h>
// 手动获取用户输入
void GetCmd(char *cmd)
{
struct termios tcAttr, newtcAttr; // 创建两个结构体变量
if (tcgetattr(0, &tcAttr) == -1) // 获取当前终端属性
{
perror("get tcAttr failed\n");
return;
}
newtcAttr = tcAttr; // 将属性复制给newtcAttr
// 改变newtcAttr的信息
newtcAttr.c_lflag &= ~ICANON; // 关闭规范模式
newtcAttr.c_lflag &= ~ECHO; // 取消回显功能
// 应用newtcAttr的属性
if (tcsetattr(0, TCSANOW, &newtcAttr) == -1)
{
perror("set tcAttr failed\n");
return;
}
int index = 0;
char ch = 0;
while ((ch = getchar()) != '\n') // 注意符号的优先级
{
if (ch == 8) // 对应Backspace(退格)
{
if (index >= 1) // 边界处理
{
// 如果用户按下Backspace,则舍弃命令的最后一个字符
cmd[--index] = 0;
// 实现删除操作的显示
printf("\033[1D"); // 光标左移1列
printf("\033[K"); // 删除光标到行尾的内容
fflush(stdout);
}
}
else
{
// 反之,显示按下的字符
// 这里属于手动实现回显功能
cmd[index++] = ch; // 将输入的字符记录到cmd中
printf("%c", ch);
fflush(stdout);
}
}
// 改为终端的设置
if (tcsetattr(0, TCSANOW, &tcAttr) == -1)
{
perror("set tcAttr failed\n");
return;
}
// 输出换行符,让提示符在下一行显示
printf("\n");
}
int main()
{
while (1)
{
......
char cmd[128] = {0};
GetCmd(cmd); // 获取用户输入
char *cmdArr[CMD_NUM] = {0}; // 记录分割后的输入
......
}
}
4、pwd 命令的实现
下面的命令都是外置命令,为了和系统终端类似,我们需要自建一个 mybin 目录,用来存储外置命令函数及可执行文件(一般把自定义命令和 mybash 主程序放在同一项目目录下)
(1) 准备工作
在上文提到过外置命令的实现是:先 fork 子进程,再 exec 加载执行,所以需要在 RunCmd() 函数中创建子进程来执行 pwd 的命令。
execv() 函数
int execv(const char *pathname, char *const argv[]);
| 参数 | 含义 |
|---|---|
pathname |
指向要执行的可执行文件的完整路径 (如 /bin/ls 或 /home/wuya/mybin/pwd)。 |
argv |
一个以 NULL 结尾的字符串数组,作为新程序的命令行参数。格式要求:{程序名, 参数1, 参数2, ..., NULL}。这个数组会直接传递给新程序的 main 函数,作为 argv 参数。 |
这里我们传递的pathname需要提前定义一个 binPath,再连接上cmdArr[0],就组成了指定命令的可执行文件的路径;argv传递获取到的命令cmdArr即可,既包含命令又包含参数
代码
cpp
#include<sys/wait.h>// wait()函数需要的头文件
// 路径:设置为自己的mybin文件的路径
// 注意最后有一个 /
char *binPath = "/mnt/hgfs/LinuxShare/MyBash/mybin/"; // 需要保存不变
// 执行系统命令
void RunCmd(char *cmdArr[], int count)
{
char *cmd = cmdArr[0];
if (strcmp(cmd, "cd") == 0) // cd命令
{
RunCd(cmdArr[1]); // 将参数传递给函数
}
else if (strcmp(cmd, "exit") == 0) // exit命令
{
exit(0);
}
else // 外置命令
{
pid_t pid = fork(); // 创建子进程
if (pid == -1) // 创建失败
{
perror("fork failed\n");
return;
}
if (pid == 0) // 子进程执行
{
char cmdPath[128] = {0};
// 拼接自定义路径+命令名(如 /mybin/pwd
strcpy(cmdPath, binPath);
strcat(cmdPath, cmdArr[0]);
// 替换子进程代码,执行自定义路径下的 pwd
execv(cmdPath, cmdArr);
perror("exec failed\n"); // 子进程执行失败
// 如果execv失败,需要解释子进程,防止回到主函数的while循环,再次生成子进程
exit(0);
}
else
{
wait(NULL); // 等待子进程结束
}
}
}
(2) 命令分析+核心函数 getcwd()

pwd命令的作用是:打印当前工作目录的绝对路径。
其功能实现也比较简单,直接使用到 **getcwd()**函数即可。
需要头文件 <unistd.h>


(3) 代码实现
pwd.c
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
char buf[128] = {0};
if (getcwd(buf, 127) == NULL)
{
perror("get cwd failed\n");
return;
}
printf("%s\n", buf);
return 0;
}
(4) 执行命令
注意:执行前,需要先将 pwd.c 编译链接为可执行文件 pwd

5、ls 命令的实现
ls命令的核心功能是:列出指定目录下的文件 / 目录信息
(1) 核心函数与结构
opendir()

目录流(directory stream):这是一个抽象的概念,本质上是一个 DIR* 类型的指针,代表了对某个目录的 "读取会话"。它内部维护了读取位置、文件信息等状态。
DIR *opendir(const char *name); //
name是要打开的目录路径(可以是相对路径或绝对路径)
函数返回值 :成功时返回一个非空的 DIR* 指针;失败时返回 NULL,并设置 errno(如目录不存在、权限不足等)。
readdir()
readdir() 会从目录流中读取下一个条目,直到没有更多条目为止。

closedir()

dirent 结构

(2) ls.c 基础功能实现
首先实现 ls 不带参数(如-l、-a等)的功能
ShowPathFile()函数
cpp
void ShowPathFile(char *path)
{
DIR *dirp = opendir(path); // 打开目录并建立目录流dirp
// 只有多个文件,才显示路径
if (pathNum > 1)
{
printf("%s:\n", path);
}
struct dirent *pfile = NULL; // 创建dirent结构体
while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
{
if (strncmp(pfile->d_name, ".", 1) == 0)// 过滤隐藏文件(. 和 ..)
{
continue;
}
// 打印文件名称
printf("%s ", pfile->d_name);
}
printf("\n");
// 关闭目录流并释放资源
closedir(dirp);
}
- 若指定多个路径,先打印路径名,再列出对应文件
cpp
void ShowPathFile(char *path)
{
......
// 只有多个文件,才显示路径
if (pathNum > 1)
{
printf("%s:\n", path);
}
.......
closedir(dirp);
}
主函数
- 统计用户输入的非参数路径个数:过滤掉以 - 开头的参数
cpp
#include <stdio.h>
#include <string.h>
static int pathNum = 0; // ls命令后的非参数路径个数
//argv[0]:ls
//argv[1]:-i 选项
//argv[2]:路径
int main(int argc, char *argv[])
{
// 统计输入的路径个数,注意需要跳过 argv[0]
for (int i = 1; i < argc; i++)
{
if (strncmp(argv[i], "-", 1) == 0)//略过-开头的参数
{
continue;
}
pathNum++;
}
return 0;
}
- 遍历每个路径,调用 ShowPathFile() 函数,列出该目录下的非隐藏文件
cpp
int main(...)
{
......
int count = 0;// 记录非-开头的参数个数
for (int i = 1; i < argc; i++)
{
if (strncmp(argv[i], "-", 1) == 0)
{
continue;
}
ShowPathFile(argv[i]);
count++;
}
return 0;
}
- 如果用户未指定任何路径,默认列出当前目录下的文件
cpp
int main(int argc, char *argv[])
{
......
if (count == 0) // 无指定路径时,默认列出当前目录
{
ShowPathFile("."); // 显示当前路径下的文件
}
return 0;
}
完整代码
cpp
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
static int pathNum = 0; // ls命令后的非参数路径个数
void ShowPathFile(char *path)
{
DIR *dirp = opendir(path); // 打开目录并建立目录流dirp
// 只有多个文件,才显示路径
if (pathNum > 1)
{
printf("%s:\n", path);
}
struct dirent *pfile = NULL; // 创建dirent结构体
while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
{
if (strncmp(pfile->d_name, ".", 1) == 0)// 过滤隐藏文件(. 和 ..)
{
continue;
}
// 打印文件名称
printf("%s ", pfile->d_name);
}
printf("\n");
// 关闭目录流并释放资源
closedir(dirp);
}
//argv[0]:ls
//argv[1]:-i 选项
//argv[2]:路径
int main(int argc, char *argv[])
{
// 统计输入的路径个数
for (int i = 1; i < argc; i++)// 跳过argv[0]
{
if (strncmp(argv[i], "-", 1) == 0) // 略过-开头的参数
{
continue;
}
pathNum++;
}
int count = 0; // 记录非-开头的参数个数
for (int i = 1; i < argc; i++)// 跳过argv[0]
{
if (strncmp(argv[i], "-", 1) == 0) // 略过-开头的参数
{
continue;
}
ShowPathFile(argv[i]);
count++;
}
if (count == 0) // 参数为0
{
ShowPathFile("."); // 显示当前路径下的文件
}
return 0;
}
运行结果
注意:执行前,需要先将 ls.c 编译链接为可执行文件 ls

(3) 增加对不同参数的处理
整体操作还是只针对与 ls.c 文件,在这里先实现两个常见参数 -a 和 -i:
| 参数 | 核心作用 |
|---|---|
-a |
显示目录下所有文件 / 目录 (包括以 . 开头的隐藏文件 / 目录,如 .bashrc、..) |
-i |
显示文件 / 目录对应的 inode 编号(inode 是文件系统中标识文件的唯一数字 ID) |
可以定义全局bool类型变量来表示某个参数是否存在
cpp
#include<stdbool.h>
// 参数存在标识:默认不存在
static bool haveA = false; // 参数a
static bool haveI = false; // 参数i
OptionAns()
统计参数个数,并判断以-开头的参数是否存在
cpp
//分析选项函数
void OptionAns(int argc,char* argv[])
{
for(int i=1;i<argc;i++)
{
if(strncmp(argv[i],"-",1)==0)
{
if(strstr(argv[i],"a")!=NULL)
{
haveA=true;
}
if(strstr(argv[i],"i")!=NULL)
{
haveI=true;
}
continue;
}
pathNum++;
}
}
ShowPathFile()
cpp
void ShowPathFile(char *path)
{
DIR *dirp = opendir(path); // 打开目录并建立目录流dirp
// 只有多个文件,才显示路径
if (pathNum > 1)
{
printf("%s:\n", path);
}
struct dirent *pfile = NULL; // 创建dirent结构体
while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
{
//没有参数 -a 时过滤隐藏文件(. 和 ..)
if (!haveA && strncmp(pfile->d_name, ".", 1) == 0)
{
continue;
}
// 显示结点索引
if (haveI)
{
printf("%d ", pfile->d_ino);
}
// 打印文件名称
printf("%s ", pfile->d_name);
}
printf("\n");
// 关闭目录流并释放资源
closedir(dirp);
}
(4) 不同类型文件不同颜色输出
目录:蓝色;可执行文件:绿色
lstat()函数
获取文件属性(包括类型、权限),存入 struct stat 结构体;
int lstat(const char *restrict pathname, struct stat *restrict statbuf);
| 类型 | 名称 | 含义 |
|---|---|---|
| 入参 | pathname |
要获取属性的文件 / 目录路径(相对路径 / 绝对路径均可) |
| 出参 | statbuf |
指向 struct stat 结构体的指针,用于存储文件属性(需提前定义) |
| 返回值 | 0 |
成功:文件属性已存入 statbuf |
| 返回值 | -1 |
失败:设置 errno(如路径不存在、权限不足、路径过长等) |
struct stat 结构体
cpp
struct stat {
ino_t st_ino; // inode 编号(对应 ls -i)
mode_t st_mode; // 核心:文件类型 + 权限(判断目录/可执行文件)
nlink_t st_nlink; // 硬链接数(ls -l 显示的数字)
uid_t st_uid; // 文件所有者的 UID
gid_t st_gid; // 文件所属组的 GID
off_t st_size; // 文件大小(字节)
time_t st_mtime; // 最后修改时间
// 其他次要字段(如块大小、设备号等)
};
相关宏定义
| 宏名 | 含义 | 对应数值(八进制) | 作用 |
|---|---|---|---|
S_IXUSR |
User execute(所有者可执行) | 0100 | 判断文件是否对所有者有可执行权限 |
S_IXGRP |
Group execute(组用户可执行) | 0010 | 判断文件是否对组用户有可执行权限 |
S_IXOTH |
Other execute(其他用户可执行) | 0001 | 判断文件是否对其他用户有可执行权限 |
代码
cpp
#include <sys/stat.h>
void ShowPathFile(char *path)
{
......
while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
{
//没有参数 -a 时过滤隐藏文件(. 和 ..)
if (!haveA && strncmp(pfile->d_name, ".", 1) == 0)
{
continue;
}
// 显示结点索引
if (haveI)
{
printf("%ld ", pfile->d_ino);
}
//不同类型文件不同颜色输出
//目录:蓝色;可执行文件:绿色
struct stat st;
char fullpath[1028] = {0};
// 拼接路径:防止ls命令找不到文件
//如ls /home,pfile->d_name为test.txt,不拼接lstat会会去当前目录找test/txt,而非/home/test.txt
snprintf(fullpath, sizeof(fullpath), "%s/%s", path, pfile->d_name);
if (lstat(fullpath, &st) == -1) { // 判断是否失败
perror(fullpath); // 打印错误原因
printf("%s ", pfile->d_name); // 默认颜色显示
continue; // 跳过当前文件,处理下一个
}
if (S_ISDIR(st.st_mode))
{
printf("\033[1;34m%s\033[0m ", pfile->d_name);
}
else if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
//等同于:if(st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_name & S_IXOTH)
{
printf("\033[1;32m%s\033[0m ", pfile->d_name);
}
else
{
printf("%s ", pfile->d_name);
}
}
printf("\n");
// 关闭目录流并释放资源
closedir(dirp);
}
(5) 完整代码
cpp
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdbool.h>
#include <sys/stat.h>
static int pathNum = 0; // ls命令后的非参数路径个数
// 参数存在标识:默认不存在
static bool haveA = false; // 参数a
static bool haveI = false; // 参数i
void ShowPathFile(char *path)
{
DIR *dirp = opendir(path); // 打开目录并建立目录流dirp
// 只有多个文件,才显示路径
if (pathNum > 1)
{
printf("%s:\n", path);
}
struct dirent *pfile = NULL; // 创建dirent结构体
while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
{
//没有参数 -a 时过滤隐藏文件(. 和 ..)
if (!haveA && strncmp(pfile->d_name, ".", 1) == 0)
{
continue;
}
// 显示结点索引
if (haveI)
{
printf("%ld ", pfile->d_ino);
}
//不同类型文件不同颜色输出
//目录:蓝色;可执行文件:绿色
struct stat st;
char fullpath[1028] = {0};
// 拼接路径:防止ls命令找不到文件
//如ls /home,pfile->d_name为test.txt,不拼接lstat会会去当前目录找test/txt,而非/home/test.txt
snprintf(fullpath, sizeof(fullpath), "%s/%s", path, pfile->d_name);
if (lstat(fullpath, &st) == -1) { // 判断是否失败
perror(fullpath); // 打印错误原因
printf("%s ", pfile->d_name); // 默认颜色显示
continue; // 跳过当前文件,处理下一个
}
if (S_ISDIR(st.st_mode))
{
printf("\033[1;34m%s\033[0m ", pfile->d_name);
}
else if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
//等同于:if(st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_name & S_IXOTH)
{
printf("\033[1;32m%s\033[0m ", pfile->d_name);
}
else
{
printf("%s ", pfile->d_name);
}
}
printf("\n");
// 关闭目录流并释放资源
closedir(dirp);
}
// 分析选项函数
void OptionAns(int argc, char *argv[])
{
for (int i = 1; i < argc; i++)
{
if (strncmp(argv[i], "-", 1) == 0)
{
if (strstr(argv[i], "a") != NULL)
{
haveA = true;
}
if (strstr(argv[i], "i") != NULL)
{
haveI = true;
}
continue;
}
pathNum++;
}
}
// argv[0]:ls
// argv[1]:-i 选项
// argv[2]:路径
int main(int argc, char *argv[])
{
// 统计参数个数,并判断以-开头的参数是否存在
OptionAns(argc, argv);
int count = 0; // 记录非-开头的参数个数
for (int i = 1; i < argc; i++) // 跳过argv[0]
{
if (strncmp(argv[i], "-", 1) == 0) // 略过-开头的参数
{
continue;
}
ShowPathFile(argv[i]);
count++;
}
if (count == 0) // 参数为0
{
ShowPathFile("."); // 显示当前路径下的文件
}
return 0;
}
(6)运行结果


四、项目总结
通过从零实现自定义命令行解释器 mybash ,我们完整打通了 Linux 系统编程的核心知识链,从进程管理、文件系统、终端交互 到字符串处理、错误处理,全方位理解了 Shell 的底层运行机制。
本项目涉及的核心技术点包括:
- Linux 进程模型 :熟练使用
fork创建子进程、exec系列函数加载外部程序、wait/waitpid回收子进程,理解父子进程、孤儿进程与僵尸进程原理。 - 文件与目录操作 :掌握
opendir/readdir/closedir目录遍历,使用stat/lstat获取文件属性,通过文件类型与权限位实现文件颜色高亮。 - 终端交互控制 :使用
termios结构体修改终端属性,实现规范 / 非规范模式切换,手动处理退格、光标移动与回显逻辑。 - 命令解析与字符串处理 :使用
strtok完成命令行分割,实现多参数、组合参数解析,支持-a/-i等选项解析。 - 系统调用与错误处理 :对
getcwd、uname、chdir、lstat等系统调用做完整返回值判断,使用perror/strerror输出友好错误信息,提升程序健壮性。 - 内置命令与外置命令 :区分并实现
cd/exit等内置命令,以及pwd/ls等外置命令,理解 Shell 执行命令的完整流程。
可扩展方向
在当前 mybash 基础上,可继续扩展以下功能,进一步接近原生 bash:
- 实现
ls -l长格式输出,展示文件权限、硬链接数、用户、大小、时间等信息。 - 支持输入输出重定向
>、<、>>,实现文件读写功能。 - 支持管道命令
|,实现多命令间数据传递。 - 增加环境变量、
PATH搜索、cd -返回上一级目录等功能。 - 支持命令历史记录、Tab 补全、光标上下左右移动。
- 扩展更多内置命令,如
echo、export、umask、jobs、fg/bg等。 - 支持信号处理,如
Ctrl+C中断、Ctrl+Z挂起、Ctrl+D退出。
完整代码
mybash.c
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/utsname.h>
#include <string.h>
#include <sys/types.h>
#include <pwd.h>
#include <stdlib.h>
#include <termios.h>
#include <sys/wait.h>
// 路径:设置为自己的mybin文件的路径
char *binPath = "/mnt/hgfs/LinuxShare/MyBash/mybin/"; // 需要保存不变
// 命令和参数总数
#define CMD_NUM 10
// 输出命令行提示符
void PrintPrompt()
{
//$和#的选择
char ch = '$'; // 默认为'$'
// 获取uid
uid_t uid = getuid();
// 获取用户名
struct passwd *p = getpwuid(uid); // p->pw_name为用户名
if (p == NULL)
{
perror("get passwd info faild:");
printf("mybash> ");
fflush(stdout);
return;
}
// 法一
// 如果登录用户名等于root,就输出'#'
/*if (strcmp(p->pw_name, "root") == 0)
{
ch = '#';
}*/
// 法二
// root的uid为0
if (uid == 0)
{
ch = '#';
}
// 获取主机名
struct utsname sysinfo; // 创建结构体
// 调用函数,获取内核和系统的详细信息
if (uname(&sysinfo) == -1)
{
// 获取失败
perror("get host info failed:");
printf("mybash> "); // 在提示符最前面加上mybash>,和系统终端区分开
fflush(stdout); // 刷新缓冲区
return;
}
// 获取路径
char buf[128] = {0}; // 存储路径的缓冲区
char *ptr = getcwd(buf, 127); // 获取当前进程的工作目录的绝对路径
if (ptr == NULL) // 获取失败
{
perror("get current working directory failed:");
printf("mybash> ");
fflush(stdout);
return;
}
// 显示当前目录
char *s = NULL;
if (strcmp(buf, p->pw_dir) == 0) // 当前路径为家目录
{
s = "~"; // 输出~
}
else // 不是家目录,正常输出
{
s = buf + sizeof(buf); // 将s移动到buf末尾
// 将s向前移动至最后一个'/'位置
while (*s != '/')
{
s--;
}
// 当前绝对路径为"/"时,直接输出s
// s+1开始的字符串就是当前目录
if (strlen(s) != 1)
{
s += 1;
}
}
// 输出
printf("mybash> \033[1;32m%s@%s\033[0m\033[1;34m:%s\033[0m%c ", p->pw_name, sysinfo.nodename, s, ch);
fflush(stdout);
}
// 分割用户命令和参数
int SplitCmd(char *cmd, char *cmdArr[])
{
// 首次分割:传递字符串
// 以空格为标准进行分割
char *token = strtok(cmd, " ");
int count = 0; // 数组索引
while (token != NULL)
{
// 将分割得到的字符串保存在cmdArr数字中
cmdArr[count++] = token;
// 继续分割,传递的字符串为NULL
token = strtok(NULL, " ");
}
// 返回字符串个数
return count;
}
// cd命令的实现
void RunCd(char *path) // 参数为传递的参数
{
char buf[128] = {0};
struct passwd *p = getpwuid(getuid());
if (p == NULL)
{
printf("get pwuid info failed\n");
return;
}
// 没有参数
if (path == NULL)
{
path = p->pw_dir; // 目的路径为家目录
}
// 参数为~ + ~后面还有参数
else if (strncmp(path, "~", 1) == 0)
{
strcpy(buf, p->pw_dir); // 令buf为家目录
strcat(buf, path + 1); // 把剩余参数连接到buf上
path = buf; // 令path保存目的路径
}
// 一般情况,直接切换路径
// 切换路径
if (chdir(path) == -1)
{
perror("cd failed\n");
return;
}
}
// 执行系统命令
void RunCmd(char *cmdArr[], int count)
{
char *cmd = cmdArr[0];
if (strcmp(cmd, "cd") == 0) // cd命令
{
RunCd(cmdArr[1]); // 将参数传递给函数
}
else if (strcmp(cmd, "exit") == 0) // exit命令
{
exit(0);
}
else // 外置命令
{
pid_t pid = fork(); // 创建子进程
if (pid == -1) // 创建失败
{
perror("fork failed\n");
return;
}
if (pid == 0) // 子进程执行
{
char cmdPath[128] = {0};
// 拼接自定义路径+命令名(如 /mybin/pwd
strcpy(cmdPath, binPath);
strcat(cmdPath, cmdArr[0]);
// 替换子进程代码,执行自定义路径下的 pwd
execv(cmdPath, cmdArr);
perror("exec failed\n"); // 子进程执行失败
// 如果execv失败,需要解释子进程,防止回到主函数的while循环,再次生成子进程
exit(0);
}
else
{
wait(NULL); // 等待子进程结束
}
}
}
// 手动获取用户输入
void GetCmd(char *cmd)
{
struct termios tcAttr, newtcAttr; // 创建两个结构体变量
if (tcgetattr(0, &tcAttr) == -1) // 获取当前终端属性
{
perror("get tcAttr failed\n");
return;
}
newtcAttr = tcAttr; // 将属性复制给newtcAttr
// 改变newtcAttr的信息
newtcAttr.c_lflag &= ~ICANON; // 关闭规范模式
newtcAttr.c_lflag &= ~ECHO; // 取消回显功能
// 应用newtcAttr的属性
if (tcsetattr(0, TCSANOW, &newtcAttr) == -1)
{
perror("set tcAttr failed\n");
return;
}
int index = 0;
char ch = 0;
while ((ch = getchar()) != '\n') // 注意符号的优先级
{
if (ch == 8) // 对应Backspace(退格)
{
if (index >= 1) // 边界处理
{
// 如果用户按下Backspace,则舍弃命令的最后一个字符
cmd[--index] = 0;
// 实现删除操作的显示
printf("\033[1D"); // 光标左移1列
printf("\033[K"); // 删除光标到行尾的内容
fflush(stdout);
}
}
else
{
// 反之,显示按下的字符
// 这里属于手动实现回显功能
cmd[index++] = ch; // 将输入的字符记录到cmd中
printf("%c", ch);
fflush(stdout);
}
}
// 改为终端的设置
if (tcsetattr(0, TCSANOW, &tcAttr) == -1)
{
perror("set tcAttr failed\n");
return;
}
// 输出换行符,让提示符在下一行显示
printf("\n");
}
int main()
{
while (1)
{
// 1.系统提示符输出
PrintPrompt();
// 2.获取用户命令
char cmd[128] = {0};
//GetCmd(cmd); // 获取用户输入
char* s = fgets(cmd,127,stdin);
if(s == NULL){
continue;
}
cmd[strlen(cmd)-1]=0;
char *cmdArr[CMD_NUM] = {0}; // 记录分割后的输入
// 进行分割
int count = SplitCmd(cmd, cmdArr);
// 没有输入,直接进行下一次获取
if (count == 0)
{
continue;
}
// 3.执行用户命令
RunCmd(cmdArr, count);
}
}
pwd.c
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
char buf[128] = {0};
if (getcwd(buf, 127) == NULL)
{
perror("get cwd failed\n");
return 0;
}
printf("%s\n", buf);
return 0;
}
ls.c
cpp
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdbool.h>
#include <sys/stat.h>
static int pathNum = 0; // ls命令后的非参数路径个数
// 参数存在标识:默认不存在
static bool haveA = false; // 参数a
static bool haveI = false; // 参数i
void ShowPathFile(char *path)
{
DIR *dirp = opendir(path); // 打开目录并建立目录流dirp
// 只有多个文件,才显示路径
if (pathNum > 1)
{
printf("%s:\n", path);
}
struct dirent *pfile = NULL; // 创建dirent结构体
while ((pfile = readdir(dirp)) != NULL) // 读取目录流drip的目录项的资料
{
//没有参数 -a 时过滤隐藏文件(. 和 ..)
if (!haveA && strncmp(pfile->d_name, ".", 1) == 0)
{
continue;
}
// 显示结点索引
if (haveI)
{
printf("%ld ", pfile->d_ino);
}
//不同类型文件不同颜色输出
//目录:蓝色;可执行文件:绿色
struct stat st;
char fullpath[1028] = {0};
// 拼接路径:防止ls命令找不到文件
//如ls /home,pfile->d_name为test.txt,不拼接lstat会会去当前目录找test/txt,而非/home/test.txt
snprintf(fullpath, sizeof(fullpath), "%s/%s", path, pfile->d_name);
if (lstat(fullpath, &st) == -1) { // 判断是否失败
perror(fullpath); // 打印错误原因
printf("%s ", pfile->d_name); // 默认颜色显示
continue; // 跳过当前文件,处理下一个
}
if (S_ISDIR(st.st_mode))
{
printf("\033[1;34m%s\033[0m ", pfile->d_name);
}
else if (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
//等同于:if(st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_name & S_IXOTH)
{
printf("\033[1;32m%s\033[0m ", pfile->d_name);
}
else
{
printf("%s ", pfile->d_name);
}
}
printf("\n");
// 关闭目录流并释放资源
closedir(dirp);
}
// 分析选项函数
void OptionAns(int argc, char *argv[])
{
for (int i = 1; i < argc; i++)
{
if (strncmp(argv[i], "-", 1) == 0)
{
if (strstr(argv[i], "a") != NULL)
{
haveA = true;
}
if (strstr(argv[i], "i") != NULL)
{
haveI = true;
}
continue;
}
pathNum++;
}
}
// argv[0]:ls
// argv[1]:-i 选项
// argv[2]:路径
int main(int argc, char *argv[])
{
// 统计参数个数,并判断以-开头的参数是否存在
OptionAns(argc, argv);
int count = 0; // 记录非-开头的参数个数
for (int i = 1; i < argc; i++) // 跳过argv[0]
{
if (strncmp(argv[i], "-", 1) == 0) // 略过-开头的参数
{
continue;
}
ShowPathFile(argv[i]);
count++;
}
if (count == 0) // 参数为0
{
ShowPathFile("."); // 显示当前路径下的文件
}
return 0;
}