1. 实现一个基础的 shell 程序,主要完成两个命令的功能 cp 和 ls
1.1.1. cp 命令主要实现:
- ⽂件复制
- ⽬录复制
1.1.2. ls 命令主要实现:
- ls -l 命令的功能
1.1. 在框架设计上,采⽤模块化设计思想,并具备⼀定的可扩展性, 具体框架如下:
- cmd_handle 模块: 用于解析命令相关信息,并进行命令的分发执行
- cmd_ls 模块 : 用于执行 ls 命令
- cmd_cp 模块 : 用于执行 cp 命令
- cmd_xxx 模块 : 用于扩展
- 编写MakeFile
OBJS :=main.o cmd_ls.o cmd_cp.o cdm_handle.o # 依赖的文件
CC :=gcc # 编译器
TARGET :=tinshell # 目标文件%.o: %.c$(CC) -c $< -o $@$(TARGET): $(OBJS)$(CC) $^ -o $@ @echo "目标文件编译成功." # 编译成功提示clean: #删除编译生成的文件rm -rf $(OBJS) $(TARGET)@echo "文件删除成功." # 删除成功提示
- main函数:
#include <stdio.h>
#include <string.h>#include "cmd_handle.h"
#define SZ_CMD 64
int main()
{char command[SZ_CMD] = {0};while (1){printf("请输入需要操作的shell指令: ");fgets(command, SZ_CMD, stdin); // 读取用户输入的指令 stdin标准输入command[strlen(command) - 1] = '\0'; // 去掉换行符if (strcmp(command, "exit") == 0) // 判断是否为退出指令{printf("退出程序\n");break;}else{printf("执行指令: %s\n", command); // 打印用户输入的指令cmd_execute(command); //调用cmd_handle模块中的函数,去执行指令}}return 0;
}
- cmd_handle.h
#ifndef __CMD_HANDLE_H__
#define __CMD_HANDLE_H__#include <stdio.h>
#include <stdlib.h>
#include <string.h>extern int cmd_execute(char *cmd_str); // 执行命令#endif
- cmd_handle.c
- 注意:一般在工作开发中 会有两个版本一个是DEBUG版本,还有一个是RELEASE版本,
-
- DEBUG版本是开发是调试用的,
- RELEASE版本是发布是用的
#include "cmd_handle.h"#define DEBUG
int cmd_execute(char *cmd_str)
{
#ifdef DEBUGprintf("[DEBUG]cmd_str:< %s >\n", cmd_str);#endifreturn 0;
}
逐行解释
#include "cmd_handle.h"
-
- 这行代码的意思是:把这个文件的内容包含进来。这个文件里可能有一些我们需要用到的定义,比如函数的声明等。
cmd_handle.h
- 这行代码的意思是:把这个文件的内容包含进来。这个文件里可能有一些我们需要用到的定义,比如函数的声明等。
#define DEBUG
-
- 这行代码的意思是:定义一个叫做的宏。宏是一种在编译前进行的文本替换。这里定义的目的是为了在代码中加入一些调试信息,帮助我们更好地理解程序的运行情况。
DEBUG
DEBUG
- 这行代码的意思是:定义一个叫做的宏。宏是一种在编译前进行的文本替换。这里定义的目的是为了在代码中加入一些调试信息,帮助我们更好地理解程序的运行情况。
int cdm_handle(char *cmd_str)
-
- 这行代码定义了一个函数,函数的名字叫。这个函数接受一个参数,参数的名字叫,是一个指向字符的指针,用来传递一个字符串。函数的返回值是一个整数()。
cdm_handle
cmd_str
int
- 这行代码定义了一个函数,函数的名字叫。这个函数接受一个参数,参数的名字叫,是一个指向字符的指针,用来传递一个字符串。函数的返回值是一个整数()。
#ifdef DEBUG
-
- 这行代码的意思是:如果定义了宏,那么接下来的代码块就会被编译。这是一种条件编译的指令,用于控制某些代码是否会被编译进最终的程序中。
DEBUG
- 这行代码的意思是:如果定义了宏,那么接下来的代码块就会被编译。这是一种条件编译的指令,用于控制某些代码是否会被编译进最终的程序中。
printf("cmd_str:%s\n", cmd_str);
-
- 这行代码的意思是:输出一个字符串,字符串的内容是变量的值。是一个占位符,用来表示字符串。是一个换行符,用来在输出后换行。这行代码的作用是打印出传入的命令字符串,帮助我们调试程序。
cmd_str
%s
\n
- 这行代码的意思是:输出一个字符串,字符串的内容是变量的值。是一个占位符,用来表示字符串。是一个换行符,用来在输出后换行。这行代码的作用是打印出传入的命令字符串,帮助我们调试程序。
#endif
-
- 这行代码的意思是:条件编译的结束标记。它表示前面的开始的代码块到此结束。
#ifdef DEBUG
- 这行代码的意思是:条件编译的结束标记。它表示前面的开始的代码块到此结束。
return 0;
-
- 这行代码的意思是:函数返回0。在C语言中,返回0通常表示函数执行成功。
总结
这段代码定义了一个函数,它接受一个字符串参数。如果定义了宏,函数会打印出这个字符串,然后返回0。这个函数的目的是处理一个命令字符串,并在调试模式下输出这个字符串。cdm_handle
cmd_str
DEBUG
示例
假设你有一个主函数,调用函数:main
cdm_handle
#include "cmd_handle.h"
#include <stdio.h>
int main() {
char *command = "ls -l";
cdm_handle(command);
return 0;}
int cmd_execute(char *cmd_str){
#ifdef DEBUG
printf("[DEBUG]cmd_str:< %s >\n", cmd_str);
#endif
return 0;}
编译并运行这个程序:
gcc -o main main.c cmd_handle.c
./main
如果宏被定义,你会看到输出:DEBUG
cmd_str:ls -l
这表示函数成功地打印了传入的命令字符串cdm_handle
1.2. strtok函数:
函数原型
char *strtok(char *str, const char *delim);
- 参数:
-
str
:要分割的字符串。第一次调用时传入要分割的字符串,之后传入 。NULL
delim
:分隔符字符串,包含所有用作分隔符的字符。
- 返回值:
-
- 返回下一个分割后的字符串。
- 如果没有更多的字符串可返回,则返回 。
NULL
使用方法
- 第一次调用:
-
- 传入要分割的字符串 和分隔符字符串 。
str
delim
strtok
会返回第一个分割后的字符串,并在内部保存下一个分割点的位置。
- 传入要分割的字符串 和分隔符字符串 。
- 后续调用:
-
- 传入 和分隔符字符串 。
NULL
delim
strtok
会从上次保存的位置继续分割字符串,返回下一个分割后的字符串。- 重复调用,直到返回 ,表示没有更多的字符串可分割。
NULL
- 传入 和分隔符字符串 。
demo:
#include <stdio.h> // 包含标准输入输出库,用于printf等函数
#include <string.h> // 包含字符串处理函数,用于strtok等函数int main() // 主函数,程序的入口点
{char str[100] = "ABC 123 XYZ"; // 定义一个长度为100的字符数组str,并初始化为"ABC 123 XYZ"char *first = NULL; // 定义一个指向字符的指针first,初始化为NULLchar *other = NULL; // 定义一个指向字符的指针other,初始化为NULLfirst = strtok(str, " "); // 使用strtok函数按空格分隔str,返回第一个分隔后的字符串,存储在first中if (NULL == first) // 检查first是否为NULL,如果是,表示没有找到任何分隔后的字符串{printf("No token found\n"); // 输出错误信息return -1; // 返回-1表示错误}printf("first:%s\n", first); // 输出第一个分隔后的字符串while ((other = strtok(NULL, " ")) != NULL) // 继续使用strtok函数按空格分隔字符串,返回后续的分隔后的字符串,存储在other中{printf("other:%s\n", other); // 输出每个后续的分隔后的字符串}return 0; // 返回0表示程序正常结束
}
运行结果:
1.3. cmd_handle.c 函数
#include "cmd_handle.h"#define DEBUG
int command_execute(char *cmd_str)
{cmd_t command; // 定义命令结构体int ret;if (NULL == cmd_str) return -1;#ifdef DEBUGprintf("[DEBUG]cmd_str:< %s >\n", cmd_str); // 打印命令字符串#endifinit_command_struct(&command); // 初始化命令结构体#ifdef DEBUGret = command_parse(cmd_str, &command); // 解析命令if (ret == -1) return -1;
#endif#ifdef DEBUGprint_command_table(&command); // 打印命令结构体#endifret = cmd_dispatch(&command); // 分发命令if (ret == -1) return -1;return 0;
}void init_command_struct(cmd_t *pcmd) // 初始化命令结构体
{memset(pcmd->cmd_name, 0, SZ_NAME); // 清空命令名称for (int i = 0; i < SZ_COUNT; i++){memset(pcmd->cmd_arg_list[i], 0, SZ_ARG); // 清空命令参数列表}pcmd->cmd_arg_count = 0; // 清空命令参数个数
}
// cp 1.txt 2.txt
int command_parse(char *cmd_str, cmd_t *pcmd) // 解析命令
{char *p_cmd_name = NULL;char *p_cmd_arg = NULL;int i = 0 ,j = 0;if (NULL == cmd_str || NULL == pcmd)return -1;p_cmd_name = strtok(cmd_str, " "); // 获取命令名称
#ifdef DEBUGstrcpy(pcmd->cmd_name, p_cmd_name);printf("[DEBUG]:命令名称 : %s\n", pcmd->cmd_name);
#endifif (NULL == p_cmd_name){printf("No token found\n");return -1;}while ((p_cmd_arg = strtok(NULL, " ")) != NULL) // 获取命令参数{printf("第%d个命令参数:%s\n",++j, p_cmd_arg);strcpy(pcmd->cmd_arg_list[i++], p_cmd_arg);pcmd->cmd_arg_count = i;}return 0;
}void print_command_table(cmd_t *pcmd) // 打印命令结构体
{printf("----------------\n");printf("[DEBUG]解析之后的命令名称: < %s >\n", pcmd->cmd_name);printf("[DEBUG]解析之后的命令参数个数: < %d >\n", pcmd->cmd_arg_count);printf("[DEBUG]解析之后的命令内容是: ");for (int i = 0; i < pcmd->cmd_arg_count; i++){printf("< %s > ", pcmd->cmd_arg_list[i]);}printf("\n-----------------\n");
}int cmd_dispatch(cmd_t *pcmd) // 分发命令
{if (pcmd == NULL) return -1;if (strcmp(pcmd->cmd_name, "ls") == 0){
#ifdef DEBUGprintf("[DEBUG]执行ls命令 \n");
#endif}else if (strcmp(pcmd->cmd_name, "cp") == 0){
#ifdef DEBUGprintf("[DEBUG]执行cp命令\n");cmd_cp_execute(pcmd);
#endif}return 0;
}
2. 命令处理框架设计 (⼀)
输入的命令是1个完整字符串,比如复制 ”cp test.txt test1.txt“,在实际实现业务逻辑时需要进⾏拆分
具体在解析字符串的步骤如下:
- step 1 : 设计⾃定义的数据结构存储拆分之后的命名信息
- step 2 : 使⽤ strtok 函数对命令字符串进⾏拆分, 并存储到⾃定义数据结构中
- step 3 : 按照命令名字分发到具体模块中执行
对于解析之后的字符串,需要保存到自定义的数据结构中 :
- 命令名称
- 参数个数
- 参数列表
- cmd_handle.h
#ifndef __CMD_HANDLE_H__
#define __CMD_HANDLE_H__#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define SZ_NAME 8 //
#define SZ_ARG 32
#define SZ_COUNT 2 typedef struct command{char cmd_name[SZ_NAME]; // 命令名称char cmd_arg_list[SZ_COUNT][SZ_ARG]; // 命令参数列表 二维数组int cmd_arg_count; // 命令参数个数
}cmd_t;extern void init_command_struct(cmd_t *pcmd); // 初始化命令表
extern void print_command_table(cmd_t *pcmd); // 打印命令表extern int command_execute(char *cmd_str); // 执行命令
extern int command_parse(char *cmd_str, cmd_t *pcmd); // 解析命令
extern int cmd_dispatch(cmd_t *pcmd); // 分发命令
#endif
- cmd_handle.c
#include "cmd_handle.h" // 包含命令处理相关的头文件
#include "cmd_cp.h" // 包含cp命令处理相关的头文件#define DEBUG // 定义DEBUG宏,用于控制调试信息的输出int command_execute(char *cmd_str) // 命令执行函数
{cmd_t command; // 定义一个命令结构体变量int ret; // 定义返回值变量if (NULL == cmd_str) // 检查输入字符串是否为NULLreturn -1; // 如果为NULL,返回-1表示错误#ifdef DEBUG // 如果定义了DEBUG宏printf("[DEBUG]cmd_str:< %s >\n", cmd_str); // 输出调试信息,显示输入的命令字符串
#endifinit_command_struct(&command); // 调用函数初始化命令结构体#ifdef DEBUG // 如果定义了DEBUG宏ret = command_parse(cmd_str, &command); // 调用函数解析命令字符串if (ret == -1) // 如果解析失败return -1; // 返回-1表示错误
#endif#ifdef DEBUG // 如果定义了DEBUG宏print_command_table(&command); // 调用函数打印命令结构体的内容
#endifret = cmd_dispatch(&command); // 调用函数分发命令if (ret == -1) // 如果分发失败return -1; // 返回-1表示错误return 0; // 返回0表示成功
}void init_command_struct(cmd_t *pcmd) // 初始化命令结构体的函数
{int i; // 定义循环变量memset(pcmd->cmd_name, 0, SZ_NAME); // 清空命令名称for (i = 0; i < SZ_COUNT; i++) // 循环清空命令参数列表{memset(pcmd->cmd_arg_list[i], 0, SZ_ARG); // 清空每个命令参数}pcmd->cmd_arg_count = 0; // 清空命令参数个数
}int command_parse(char *cmd_str, cmd_t *pcmd) // 命令解析函数
{char *p_cmd_name = NULL; // 定义指向命令名称的指针char *p_cmd_arg = NULL; // 定义指向命令参数的指针int i = 0; // 定义命令参数索引if (NULL == cmd_str || NULL == pcmd) // 检查输入字符串和命令结构体指针是否为NULLreturn -1; // 如果为NULL,返回-1表示错误p_cmd_name = strtok(cmd_str, " "); // 使用strtok分割命令字符串,获取命令名称#ifdef DEBUG // 如果定义了DEBUG宏printf("p_cmd_name:%s\n", p_cmd_name); // 输出调试信息,显示命令名称strcpy(pcmd->cmd_name, p_cmd_name); // 复制命令名称到命令结构体printf("[DEBUG]: cmd_name : %s\n", pcmd->cmd_name); // 输出调试信息,显示命令名称
#endifif (NULL == p_cmd_name) // 检查是否获取到命令名称{printf("No token found\n"); // 如果没有获取到,输出错误信息return -1; // 返回-1表示错误}while ((p_cmd_arg = strtok(NULL, " ")) != NULL) // 继续使用strtok分割命令字符串,获取命令参数{printf("p_cmd_arg:%s\n", p_cmd_arg); // 输出调试信息,显示命令参数strcpy(pcmd->cmd_arg_list[i++], p_cmd_arg); // 复制命令参数到命令结构体pcmd->cmd_arg_count = i; // 更新命令参数个数}return 0; // 返回0表示成功
}void print_command_table(cmd_t *pcmd) // 打印命令结构体内容的函数
{printf("----------------\n"); // 输出分隔线printf("[DEBUG]cmd_name: < %s >\n", pcmd->cmd_name); // 输出命令名称printf("[DEBUG]cmd_arg_count: < %d >\n", pcmd->cmd_arg_count); // 输出命令参数个数printf("[DEBUG]cmd_arg_list: "); // 输出命令参数列表前缀for (int i = 0; i < pcmd->cmd_arg_count; i++) // 循环输出每个命令参数{printf("< %s > ", pcmd->cmd_arg_list[i]); // 输出命令参数}printf("\n-----------------\n"); // 输出分隔线
}int cmd_dispatch(cmd_t *pcmd) // 命令分发函数
{if (pcmd == NULL) // 检查命令结构体指针是否为NULLreturn -1; // 如果为NULL,返回-1表示错误if (strcmp(pcmd->cmd_name, "ls") == 0) // 检查命令名称是否为"ls"{
#ifdef DEBUG // 如果定义了DEBUG宏printf("[DEBUG]执行ls命令 \n"); // 输出调试信息,表示执行ls命令
#endif}else if (strcmp(pcmd->cmd_name, "cp") == 0) // 检查命令名称是否为"cp"{
#ifdef DEBUG // 如果定义了DEBUG宏printf("[DEBUG]执行cp命令\n"); // 输出调试信息,表示执行cp命令cmd_cp_execute(pcmd); // 调用cp命令执行函数
#endif}return 0; // 返回0表示成功
}
- main.c
#include <stdio.h>
#include <string.h>#include "cmd_handle.h"
#define SZ_CMD 64int main()
{char command[SZ_CMD] = {0};while (1){printf("请输入需要操作的shell指令: ");fgets(command, SZ_CMD, stdin); // 读取用户输入的指令 stdin标准输入command[strlen(command) - 1] = '\0'; // 去掉换行符if (strcmp(command, "exit") == 0) // 判断是否为退出指令{printf("退出程序\n");break;}else{printf("执行指令: %s\n", command); // 打印用户输入的指令command_execute(command); //调用cmd_handle模块中的函数,去执行指令}}return 0;
}
- 运行结果:
2.1. cp 命令设计与实现 (⼀) _保持路径
- 完成⼀个⽬录的复制,具体要求如下:
-
- 实现⽂件复制
-
-
- cp 1.txt 2.txt
-
-
- 实现⽬录复制
-
-
- cp src_dir dest_dir
-
- 总体思路 :
-
- 根据⽂件类型进⾏判断,如果是普通⽂件,则直接进⾏复制,
- 如果是⽬录,则递归复制⽬录
- 基本思路如下:
-
- 判断⽂件类型
-
-
- 是普通⽂件, 则直接进⾏复制
- 是⽬录,则递归进⾏⽬录复制
-
-
- 复制⽬录
-
-
- 在⽬标路径创建新的同名⽬录
- 打开⽬录
- 遍历⽬录
-
-
-
-
- 获取⽂件名,并合成源⽬录绝对路径以及⽬标⽬录绝对路径
- 根据路径判断源⽂件类型
-
-
-
-
-
-
- 是⽂件,则直接进⾏复制
- 是⽬录,则继续进⾏递归复制
-
-
-
- cp 的命令的总的⼊⼝函数为 cmd_cp_execute 函数, 具体逻辑如下:
- 解析路径就是将 cp 命令参数的参数信息存储到 ⽂件信息结构中
- cmd_cp.h
#ifndef __CMD_CP_H__
#define __CMD_CP_H__
#include <stdio.h> // 包含标准输入输出库
#include <stdlib.h> // 包含标准库,用于退出程序等
#include <string.h> // 包含字符串处理函数
#include <unistd.h> // 包含 POSIX 操作系统 API
#include <sys/types.h> // 包含系统数据类型
#include <sys/stat.h> // 包含文件状态处理函数
#include <dirent.h> // 包含目录操作函数
#include <errno.h>
#include "cmd_handle.h"#define SZ_PATH 1024
#define SZ_BUFFER 1024typedef enum file_type // enum 文件类型枚举
{ FT_DIR = 0, // 目录FT_FILE = 1, // 文件FT_ERROR = 2, // 错误FT_UNKNOWN = 3 // 文件类型未知
} file_type_t;typedef struct cp_file_info
{file_type_t src_ftype; // 源文件类型char src_path[SZ_PATH]; // 源文件路径char dest_path[SZ_PATH]; // 目标文件路径
} cp_file_info_t;extern int cmd_cp_execute(cmd_t *pcmd);
#endif
- cmd_cp.c
#include "cmd_cp.h"#define DEBUGint cmd_cp_execute(cmd_t *pcmd)
{if (NULL == pcmd) return -1;int ret = 0;
#ifdef DEBUG#endifcp_file_info_t fileninfo; // 定义一个cp_file_info_t结构体// 解析路径并且保存到 cp_file_info_t结构体中;ret = cmd_cp_parse_path(pcmd, &fileninfo);if (ret == -1)return -1;return 0;
}int cmd_cp_parse_path(cmd_t *pcmd, cp_file_info_t *pcpinfo)
{if (NULL == pcmd || NULL == pcpinfo)return -1;strcpy(pcpinfo->src_path, pcmd->cmd_arg_list[0]);strcpy(pcpinfo->dest_path, pcmd->cmd_arg_list[1]);#ifdef DEBUGprintf("[DEBUG] 源路径是 :%s\n", pcpinfo->src_path);printf("[DEBUG] 目标路径是:%s\n", pcpinfo->dest_path);
#endifreturn 0;
}
2.2. cp 命令设计与实现 (⼆)_解析文件属性
基本思路:
step 1 : 调⽤ stat 函数,获取⽂件属性, 并会保存到 struct stat 结构体中
step 2 : 将⽂件属性中的⽂件类型信息保存到⾃定义结构体中 struct cp_file_info
获取⽂件属性⼀般使⽤ stat 函数 与 lstat 函数
stat 函数适⽤于通⽤⽂件
stat 专⻔针对 链接⽂件
stat 函数
函数头⽂件
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
函数功能
获取⽂件属性,并将⽂件属性信息保存到 struct stat 结构体中
函数原型
int stat(const char *pathname, struct stat *statbuf);
函数参数
pathname : ⽂件绝对路径
statbuf : ⽂件属性结构体的指针, 具体定义如下
struct stat {dev_t st_dev; /* 设备 ID,表示文件所在的设备 */ino_t st_ino; /* inode 编号,每个文件或目录在文件系统中都有一个唯一的 inode 编号 */mode_t st_mode; /* 文件类型和权限模式,可以通过 S_ISDIR、S_ISREG 等宏来判断文件类型 */nlink_t st_nlink; /* 硬链接数,表示有多少个文件名指向同一个 inode */uid_t st_uid; /* 文件所有者的用户 ID */gid_t st_gid; /* 文件所有者的组 ID */dev_t st_rdev; /* 设备 ID(如果文件是特殊文件,如字符设备或块设备) */off_t st_size; /* 文件的总大小,以字节为单位 */blksize_t st_blksize; /* 文件系统 I/O 的块大小 */blkcnt_t st_blocks; /* 分配给文件的 512 字节块的数量 */struct timespec st_atime; /* 最后访问时间 */struct timespec st_mtime; /* 最后修改时间 */struct timespec st_ctime; /* 最后状态改变时间(如权限或所有者更改) */
};
字段详细解释
st_dev:
类型:dev_t
描述:文件所在的设备的 ID。每个文件系统都有一个唯一的设备 ID,表示文件存储在哪个设备上。
st_ino:
类型:ino_t
描述:文件的 inode 编号。inode 是文件系统中用于存储文件元数据的数据结构,每个文件或目录都有一个唯一的 inode 编号。
st_mode:
类型:mode_t
描述:文件类型和权限。可以通过以下宏来检查文件类型:
S_ISDIR(st_mode):是否为目录。
S_ISREG(st_mode):是否为普通文件。
S_ISLNK(st_mode):是否为符号链接。
S_ISCHR(st_mode):是否为字符设备文件。
S_ISBLK(st_mode):是否为块设备文件。
S_ISFIFO(st_mode):是否为 FIFO(命名管道)。
S_ISSOCK(st_mode):是否为套接字。
权限部分可以通过位运算符来检查,例如:
S_IRUSR:用户读权限。
S_IWUSR:用户写权限。
S_IXUSR:用户执行权限。
st_nlink:
类型:nlink_t
描述:文件的硬链接数。硬链接是指向同一个 inode 的多个文件名,这个字段表示有多少个硬链接指向该文件。
st_uid:
类型:uid_t
描述:文件所有者的用户 ID。表示拥有该文件的用户。
st_gid:
类型:gid_t
描述:文件所有者的组 ID。表示拥有该文件的用户组。
st_rdev:
类型:dev_t
描述:如果文件是特殊文件(如设备文件),则存储设备 ID。对于普通文件和目录,这个字段通常没有意义。
st_size:
类型:off_t
描述:文件的大小,以字节为单位。对于普通文件,表示文件的实际大小;对于目录,通常为 0。
st_blksize:
类型:blksize_t
描述:文件系统 I/O 的块大小。这是文件系统建议的 I/O 操作的块大小。
st_blocks:
类型:blkcnt_t
描述:分配给文件的 512 字节块的数量。表示文件实际占用的磁盘块数。
st_atime:
类型:struct timespec
描述:文件的最后访问时间。struct timespec 包含秒和纳秒两个字段,表示时间的高精度。
st_mtime:
类型:struct timespec
描述:文件的最后修改时间。表示文件内容最后一次被修改的时间。
st_ctime:
类型:struct timespec
描述:文件的最后状态改变时间。表示文件的元数据(如权限、所有者等)最后一次被修改的时间。
2.2.1. stat函数demo.c
#include <stdio.h> // 包含标准输入输出库,用于 printf 和 perror 等函数
#include <sys/types.h> // 包含系统类型定义,如 dev_t、ino_t 等
#include <sys/stat.h> // 包含 stat 结构体和相关宏定义
#include <unistd.h> // 包含标准符号常量和类型定义,如 stat 函数int main(int argc, char *argv[]) // 主函数,程序入口
{struct stat buf; // 定义一个 struct stat 类型的变量 buf,用于存储文件状态信息int ret; // 定义一个整型变量 ret,用于存储 stat 函数的返回值// 获取文件状态信息ret = stat("./text.txt", &buf); // 调用 stat 函数获取文件 "./text.txt" 的状态信息,存储到 buf 中if (ret == -1) // 检查 stat 函数是否成功{perror("stat"); // 如果失败,输出错误信息return -1; // 返回 -1 表示程序失败}// 打印文件大小printf("文件大小:%ld 字节\n", buf.st_size); // 打印文件的大小,单位为字节// 判断并打印文件类型printf("文件类型:"); // 提示输出文件类型if (S_ISREG(buf.st_mode)) // 检查文件是否为普通文件{printf("普通文件\n"); // 如果是普通文件,输出 "普通文件"}else if (S_ISDIR(buf.st_mode)) // 检查文件是否为目录{printf("目录\n"); // 如果是目录,输出 "目录"}else if (S_ISLNK(buf.st_mode)) // 检查文件是否为符号链接{printf("符号链接\n"); // 如果是符号链接,输出 "符号链接"}else if (S_ISCHR(buf.st_mode)) // 检查文件是否为字符设备文件{printf("字符设备文件\n"); // 如果是字符设备文件,输出 "字符设备文件"}else if (S_ISBLK(buf.st_mode)) // 检查文件是否为块设备文件{printf("块设备文件\n"); // 如果是块设备文件,输出 "块设备文件"}else if (S_ISFIFO(buf.st_mode)) // 检查文件是否为 FIFO(命名管道){printf("FIFO(命名管道)\n"); // 如果是 FIFO,输出 "FIFO(命名管道)"}else if (S_ISSOCK(buf.st_mode)) // 检查文件是否为套接字{printf("套接字\n"); // 如果是套接字,输出 "套接字"}else{printf("未知类型\n"); // 如果文件类型未知,输出 "未知类型"}return 0; // 程序成功结束,返回 0
}
运行结果:
S_ISREG(buf.st_mode)
宏定义:S_ISREG 是一个标准的宏,用于检查文件是否为普通文件。
作用:如果 buf.st_mode 表示的是一个普通文件,S_ISREG(buf.st_mode) 返回非零值(通常是 1),否则返回 0。
if (S_ISREG(buf.st_mode))
{
printf("普通文件\n");
}
如果文件是一个普通文件,程序会输出 "普通文件"。
2. S_ISDIR(buf.st_mode)
宏定义:S_ISDIR 是一个标准的宏,用于检查文件是否为目录。
作用:如果 buf.st_mode 表示的是一个目录,S_ISDIR(buf.st_mode) 返回非零值(通常是 1),否则返回 0。
else if (S_ISDIR(buf.st_mode))
{
printf("目录\n");
}
如果文件是一个目录,程序会输出 "目录"。
- demo
在具体业务逻辑实现时,步骤如下 : step 1 : 获取⽂件类型, 并转换成枚举
file_type_t get_file_type(const char *src_path)
{int ret = 0;struct stat statbuf;ret = stat(src_path, &statbuf);if (ret == -1){perror("stat():");return FT_ERROR;}if (S_ISDIR(statbuf.st_mode)) // 目录return FT_DIR; if (S_ISREG(statbuf.st_mode)) // 文件return FT_FILE;return FT_UNKNOWN;// 未知
}
step 2 : 将获取的⽂件类型存储⾃定义存储结构中
int cmd_cp_parse_type(cp_file_info_t *pcpinfo)
{enum file_type ftpye;ftpye = get_file_type(pcpinfo->src_path); // 得到文件类型if (ftpye == FT_ERROR || ftpye == FT_UNKNOWN){perror("get_file_type():");return -1;}elsepcpinfo->src_ftype = ftpye; //保存文件类型至文件类型枚举结构体中
#ifdef DEBUGif (pcpinfo->src_ftype == FT_DIR)printf("[DEBUG]src is dir\n"); // 目录else if (pcpinfo->src_ftype == FT_FILE)printf("[DEBUG]src is file\n"); // 文件
#endifreturn 0;
}
step 3 : 在主逻辑函数 cmd_cp_execute 中进⾏调⽤ // 主函数
int cmd_cp_execute(cmd_t *pcmd)
{if (NULL == pcmd) return -1;int ret = 0;
#ifdef DEBUG#endifcp_file_info_t fileninfo; // 定义一个cp_file_info_t结构体// 解析路径并且保存到 cp_file_info_t结构体中;ret = cmd_cp_parse_path(pcmd, &fileninfo);if (ret == -1)return -1;ret = cmd_cp_parse_type(&fileninfo); // 解析文件类型if (ret == -1)return -1;return 0;
}
2.3. cp 命令设计与实现 (三)
- 关于 cp 命令具体情形分析如下:
-
- 复制⽂件到⽬标⽬录中
- 复制⽬录到⽬标⽬录中
-
-
- 如果是第⼀种情形, 源⽂件是普通⽂件时,则直接复制
- 如果是第⼆种情形, 源⽂件是⽬录时,则需要复制⽬录中所有的⽂件以及⼦⽬录的内容
-
在获取⽂件类型之后,要分析具体情形,这⾥需要设计⼀个分发函数 cmd_cp_dispatch, 具体
实现如下:
- 主函数
int cmd_cp_execute(cmd_t *pcmd)
{if (NULL == pcmd)return -1;int ret = 0;
#ifdef DEBUG#endifcp_file_info_t fileninfo; // 定义一个cp_file_info_t结构体// 解析路径并且保存到 cp_file_info_t结构体中;ret = cmd_cp_parse_path(pcmd, &fileninfo);if (ret == -1)return -1;ret = cmd_cp_parse_type(&fileninfo); // 解析文件类型if (ret == -1)return -1;ret = cmd_cp_dispatch(&fileninfo); // 根据文件类型分发if (ret == -1)return -1;return 0;
}
- 分发函数
// 处理拷贝操作
int cmd_cp_dispatch(cp_file_info_t *pfileinfo)
{if (pfileinfo->src_ftype == FT_FILE)
{return cmd_cp_file(pfileinfo->src_path,pfileinfo->dest_path);} else if (pfileinfo->src_ftype == FT_DIR)
{return cmd_cp_directory(pfileinfo->src_path,pfileinfo->dest_path);}
}
- 文件复制直接采⽤ 标准 io ⼆进制读写函数接⼝,这样可以兼容 文件本文件与⼆进制文件:
-
- 使用fread /fwirte或者是fgets/fputs两种方式实现cp文件
// 复制文件
int cmd_cp_file(const char *src_path, const char *dest_path)
{FILE *src_fp = NULL, *dest_fp = NULL; // 声明两个文件指针size_t rbytes = 0, wbytes = 0; // 声明两个变量,用于记录读取和写入的字节数char buffer[SZ_BUFFER] = {0}; // 声明一个缓冲区,用于读取文件内容char buf[SZ_BUFFER] = {0}; // 声明另一个缓冲区,未在代码中使用,可以删除int ret1 = 0; // 声明一个变量,用于记录操作结果#ifdef DEBUGprintf(" [ DEBUG ] %s ----> %s\n", src_path, dest_path); // 调试信息,输出源文件路径和目标文件路径
#endifif (NULL == src_path || NULL == dest_path)return -1; // 检查输入路径是否为空// 打开源文件src_fp = fopen(src_path, "r");if (NULL == src_fp){perror("src fopen():"); // 输出错误信息return -1; // 返回-1表示失败}// 打开目标文件dest_fp = fopen(dest_path, "w+");if (NULL == dest_fp){perror("dest fopen():"); // 输出错误信息fclose(src_fp); // 关闭已打开的源文件return -1; // 返回-1表示失败}// 读取源文件内容并写入目标文件char *ret = NULL;while ((ret = fgets(buf, sizeof(buf), src_fp)) != NULL){if (fputs(buf, dest_fp) == EOF){perror("fputs():"); // 输出错误信息ret1 = -1; // 设置错误标志break; // 跳出循环}}
/*// 读取源文件内容并写入目标文件(使用fread和fwrite)for (;;){rbytes = fread(buffer, sizeof(char), SZ_BUFFER, src_fp); // 从源文件中读取内容到缓冲区if (rbytes != 0) {wbytes = fwrite(buffer, sizeof(char), rbytes, dest_fp); // 将缓冲区内容写入目标文件if (wbytes != rbytes){perror("fwrite():"); // 输出错误信息ret1 = -1; // 设置错误标志break; // 跳出循环}}elsebreak; // 如果没有读取到内容,跳出循环}
*/// 关闭文件fclose(src_fp); // 关闭源文件fclose(dest_fp); // 关闭目标文件// 输出结果if (ret1 == -1){printf("文件拷贝失败\n"); // 输出失败信息return -1; // 返回-1表示失败}else{printf("文件拷贝成功\n"); // 输出成功信息return 0; // 返回0表示成功}
}
2.3.1. 如果目标为一个目录+文件
在拷贝cmd_cp_file();函数中先判断一下。使用strrchr函数,查找目标路径是否存在有/,如果存在/就表示有目录,就调用create_directories(dest_dir)函数去创建它,在create_directories函数中会去判断这个目录是否存在,如果存在就不会创建,会直接进入fopen环节,如果不存在,就会使用mkdir函数创建目标目录,之后在进入fopen函数就行操作。
- mkdir 创建目录
- 函数头⽂件
#include <sys/stat.h>
#include <sys/types.h>
- 函数原型
int mkdir(const char *pathname, mode_t mode);
- 函数功能
在指定路径下创建⼀个⽬录
- 函数参数
pathname : 路径名
mode : 模式 【0777】
- 函数返回值
成功 : 返回 0
失败 : 返回 -1
int cmd_cp_file(const char *src_path, const char *dest_path)
{// 确保目标路径的父目录存在char dest_dir[SZ_FILENAME] = {0};strncpy(dest_dir, dest_path, sizeof(dest_dir));char *last_slash = strrchr(dest_dir, '/');if (last_slash){*last_slash = '\0'; // 截取目标路径的父目录if (create_directories(dest_dir) != 0) // 创建父目录{fprintf(stderr, "无法创建目标目录:%s\n", dest_dir);return -1;}}FILE *src_fp = NULL, *dest_fp = NULL;size_t rbytes = 0, wbytes = 0;cp_file_info_t pcpinfo;char buffer[SZ_BUFFER] = {0};char buf[SZ_BUFFER] = {0};int ret1 = 0;#ifdef DEBUGprintf(" [ DEBUG ] %s ----> %s\n",src_path, dest_path);
#endifif (NULL == src_path || NULL ==dest_path)return -1;// 打开源文件src_fp = fopen(src_path, "r");if (NULL == src_fp){perror("src fopen():");return -1;}// 打开目标文件dest_fp = fopen(dest_path, "w+");if (NULL == dest_fp){perror("dest fopen():");fclose(src_fp); // 关闭已打开的源文件return -1;}// 读取源文件内容并写入目标文件char *ret = NULL;while ((ret = fgets(buf, sizeof(buf), src_fp)) != NULL){if (fputs(buf, dest_fp) == EOF){perror("fputs():");ret1 = -1;break;}} // 关闭文件fclose(src_fp);fclose(dest_fp);// 输出结果if (ret1 == -1){printf("文件拷贝失败\n");return -1;}else{printf("文件拷贝成功\n");return 0;}
}
- 创建目录函数
int create_directories(const char *dest_path)
{char *dir = strdup(dest_path); // 复制目标路径字符串/*假设 dest_path 是一个指向字符串的指针,例如:const char *dest_path = "/home/lxl/imooc/two";调用 strdup:char *dir = strdup(dest_path);此时,dir 将指向一块新分配的内存,这块内存中存储了字符串 "/home/lxl/imooc/two" 的副本。独立副本:strdup 创建了一个独立的字符串副本,这意味着你可以自由修改 dir 指向的字符串,而不会影响原始的 dest_path。动态内存分配:strdup 使用动态内存分配(malloc)来存储副本,因此你需要在使用完毕后调用 free 来释放这块内存。*/if (dir == NULL){perror("strdup");return -1;}// 递归创建多级目录char *last_slash = dir;while ((last_slash = strchr(last_slash + 1, '/')) != NULL){ /*递归创建多级目录:char *dir = strdup("/home/lxl/imooc/two");第一次循环:last_slash 指向 /home 中的 /,路径变为 /home。创建 /home 目录(如果不存在)。第二次循环:last_slash 指向 /home/lxl 中的 /,路径变为 /home/lxl。创建 /home/lxl 目录(如果不存在)。第三次循环:last_slash 指向 /home/lxl/imooc 中的 /,路径变为 /home/lxl/imooc。创建 /home/lxl/imooc 目录(如果不存在)。创建最终目录:创建 /home/lxl/imooc/two 目录(如果不存在)。*/*last_slash = '\0'; // 去掉路径中的最后一个组件if (mkdir(dir, 0777) == -1){/*如果目录成功创建,mkdir 返回 0。如果目录已经存在,mkdir 返回 -1,并将 errno 设置为 EEXIST。如果发生其他错误,mkdir 返回 -1,并将 errno 设置为其他值。*/if (errno != EEXIST)// errno 不等于 EEXIST,说明错误类型不是 "File exists",而是其他类型的错误。// errno 等于 EEXIST,说明错误类型是 "File exists"。 [文件存在]{perror("mkdir1:");free(dir);return -1;}}*last_slash = '/'; // 恢复路径中的最后一个组件}// 创建最终目录if (mkdir(dir, 0777) == -1){if (errno != EEXIST){perror("mkdir2:");free(dir);return -1;}}free(dir);return 0;
}
2.4. cp 命令设计与实现 -(四)
- ⽬录复制到⽬录的基本思路如下:
-
- 遍历⽬录
- 判断是⽂件还是⽬录
-
-
- 是⽂件 则直接进⾏复制
- 是⽬录 则进⾏递归
-
- opendir 打开目录
- 函数头⽂件
#include <sys/types.h>
#include <dirent.h>
- 函数原型
DIR *opendir(const char *name);
- 函数功能
打开⼀个⽬录
- 函数参数
name : ⽬录路径名
- 函数返回值
成功 : 返回⽬录流的指针
失败 : 返回 NULL, 并设置错误编号到 errno
- readdir 读取目录内容
- 函数头⽂件
#include <dirent.h>
- 函数原型
struct dirent *readdir(DIR *dirp);
- 函数功能
读取⽬录中的⼀项,并将信息保存到 struct dirent 指针中, ⼀般⽤于遍历⽬录
- 函数参数
dirp : ⽬录流指针 【目录描述符指针】
- 函数返回值
成功 : 返回⽬录项信息结构体指针
失败 : 返回 NULL, 并设置错误编号到 errno
使用readdir()函数遍历目录中的子目录,并且输出目录名字,【是当前目录中的目录、文件名字,不包括子目录下面的文件名】
struct dirent {ino_t d_ino; // inode 号码(文件或目录的唯一标识符)off_t d_off; // 当前目录项在目录流中的位置unsigned short d_reclen; // 该目录项的长度unsigned char d_type; // 目录项的类型(文件、目录、链接等)char d_name[256]; // 目录项的名称(文件或目录名)
};
- demo
#include <stdio.h> // 包含标准输入输出库
#include <stdlib.h> // 包含标准库,用于退出程序等
#include <string.h> // 包含字符串处理函数
#include <unistd.h> // 包含 POSIX 操作系统 API
#include <sys/types.h> // 包含系统数据类型
#include <sys/stat.h> // 包含文件状态处理函数
#include <dirent.h> // 包含目录操作函数int main(int argc, char *argv[])
{if(argc != 2) // 检查命令行参数个数是否为2{printf("Usage: %s <dir>\n", argv[0]); // 输出使用说明exit(1); // 退出程序,返回值为1表示错误}DIR *dir = NULL; // 声明一个目录流指针struct dirent *pdirent = NULL; // 声明一个目录项指针,dirent 结构体是系统定义的结构体dir = opendir(argv[1]); // 打开目录,返回目录流if(dir == NULL) // 检查目录是否成功打开{perror("opendir"); // 输出错误信息exit(1); // 退出程序,返回值为1表示错误}while((pdirent = readdir(dir)) != NULL) // 读取目录项,返回目录项指针{ // readdir() 读取目录项,一次遍历一项if(strcmp(pdirent->d_name, ".") == 0 || strcmp(pdirent->d_name, "..") == 0)continue; // 跳过当前目录(.)和父目录(..)printf("%s\n", pdirent->d_name); // 输出目录项名称}closedir(dir); // 关闭目录流return 0; // 程序正常退出,返回值为0表示成功
}
运行结果:
demo
int cmd_cp_dir(const char *src_path, const char *dest_path)
{DIR *dir = NULL; // 声明一个目录流指针struct dirent *pdirent = NULL; // 声明一个目录项指针,dirent 结构体是系统定义的结构体cp_file_info_t info ; // 声明一个 cp_file_info_t 结构体变量,create_directories(dest_path); // 递归创建目标多级目录dir = opendir(src_path); // 打开源文件目录,返回目录流if (dir == NULL) // 检查目录是否成功打开{perror("opendir"); // 输出错误信息exit(-1); // 退出程序,返回值为-1表示错误}while ((pdirent = readdir(dir)) != NULL) // 读取目录项,返回目录项指针{ // readdir() 读取目录项,一次遍历一项if (strcmp(pdirent->d_name, ".") == 0 || strcmp(pdirent->d_name, "..") == 0)continue; // 跳过当前目录(.)和父目录(..)
#ifdef DEBUGprintf("[DEBUG]: pdirent->d_name %s\n", pdirent->d_name); // 输出目录项名称#endifmake_path(&info,src_path, dest_path,pdirent->d_name ); // 构造源文件路径
#ifdef DEBUGprintf("[DEBUG] info src_path :%s\n", info.src_path); // 输出源文件路径printf("[DEBUG] info dest_path :%s\n", info.dest_path); // 输出目标文件路径
#endif//获取源目录文件类型info.src_ftype = get_file_type(info.src_path); // 获取源文件类型if (info.src_ftype == FT_DIR) // 如果是目录{cmd_cp_dir(info.src_path, info.dest_path);// 递归调用 cmd_cp_dir() 函数}else if (info.src_ftype == FT_FILE) // 如果是文件{cmd_cp_file(info.src_path, info.dest_path);// 调用 cmd_cp_file() 函数}}closedir(dir); // 关闭目录流return 0;
}// cp test test1 test/1.txt test/2.txt test1/1.txt test1/2.txt
void make_path(cp_file_info_t *pinfo,const char *src_path,const char *dest_path,const char *filename)
{
memset(pinfo->src_path, 0, sizeof(pinfo->src_path));
memset(pinfo->dest_path, 0, sizeof(pinfo->dest_path));strcpy(pinfo->src_path, src_path);
strcat(pinfo->src_path, "/"); // 拼接路径
strcat(pinfo->src_path, filename); // 拼接路径strcpy(pinfo->dest_path, dest_path);
strcat(pinfo->dest_path, "/");
strcat(pinfo->dest_path, filename);}
2.4.1. 完整cp代码:
#include "cmd_cp.h"#define DEBUGint cmd_cp_execute(cmd_t *pcmd)
{if (NULL == pcmd)return -1;int ret = 0;
#ifdef DEBUG#endifcp_file_info_t fileninfo; // 定义一个cp_file_info_t结构体// 解析路径并且保存到 cp_file_info_t结构体中;ret = cmd_cp_parse_path(pcmd, &fileninfo);if (ret == -1)return -1;ret = cmd_cp_parse_type(&fileninfo); // 解析文件类型if (ret == -1)return -1;ret = cmd_cp_dispatch(&fileninfo); // 根据文件类型分发if (ret == -1)return -1;return 0;
}int cmd_cp_parse_path(cmd_t *pcmd, cp_file_info_t *pcpinfo)
{if (NULL == pcmd || NULL == pcpinfo)return -1;strcpy(pcpinfo->src_path, pcmd->cmd_arg_list[0]);strcpy(pcpinfo->dest_path, pcmd->cmd_arg_list[1]);#ifdef DEBUGprintf("[DEBUG] 源路径是 :%s\n", pcpinfo->src_path);printf("[DEBUG] 目标路径是:%s\n", pcpinfo->dest_path);
#endifreturn 0;
}file_type_t get_file_type(const char *path)
{int ret = 0;struct stat statbuf;ret = stat(path, &statbuf);if (ret == -1){perror("stat():");return FT_ERROR;}if (S_ISDIR(statbuf.st_mode))return FT_DIR;if (S_ISREG(statbuf.st_mode))return FT_FILE;return FT_UNKNOWN;
}int cmd_cp_parse_type(cp_file_info_t *pcpinfo)
{enum file_type ftpye;ftpye = get_file_type(pcpinfo->src_path);if (ftpye == FT_ERROR || ftpye == FT_UNKNOWN){perror("get_file_type():");return -1;}elsepcpinfo->src_ftype = ftpye;
#ifdef DEBUGif (pcpinfo->src_ftype == FT_DIR)printf("[DEBUG]src is dir\n"); // 目录else if (pcpinfo->src_ftype == FT_FILE)printf("[DEBUG]src is file\n"); // 文件
#endifreturn 0;
}// 处理拷贝操作
int cmd_cp_dispatch(cp_file_info_t *pcpinfo)
{if (pcpinfo->src_ftype == FT_FILE){printf("cp is file\n");return cmd_cp_file(pcpinfo->src_path, pcpinfo->dest_path);}else if (pcpinfo->src_ftype == FT_DIR){printf("cp is dir\n");return cmd_cp_dir(pcpinfo->src_path, pcpinfo->dest_path); // 如果需要支持目录复制,加入此功能}return -1;
}int cmd_cp_file(const char *src_path, const char *dest_path)
{// 确保目标路径的父目录存在char dest_dir[SZ_FILENAME] = {0};strncpy(dest_dir, dest_path, sizeof(dest_dir));char *last_slash = strrchr(dest_dir, '/');if (last_slash){*last_slash = '\0'; // 截取目标路径的父目录if (create_directories(dest_dir) != 0) // 创建父目录{fprintf(stderr, "无法创建目标目录:%s\n", dest_dir);return -1;}}FILE *src_fp = NULL, *dest_fp = NULL;size_t rbytes = 0, wbytes = 0;cp_file_info_t pcpinfo;char buffer[SZ_BUFFER] = {0};char buf[SZ_BUFFER] = {0};int ret1 = 0;#ifdef DEBUGprintf(" [ DEBUG ] %s ----> %s\n",src_path, dest_path);
#endifif (NULL == src_path || NULL ==dest_path)return -1;// 打开源文件src_fp = fopen(src_path, "r");if (NULL == src_fp){perror("src fopen():");return -1;}// 打开目标文件dest_fp = fopen(dest_path, "w+");if (NULL == dest_fp){perror("dest fopen():");fclose(src_fp); // 关闭已打开的源文件return -1;}// 读取源文件内容并写入目标文件char *ret = NULL;while ((ret = fgets(buf, sizeof(buf), src_fp)) != NULL){if (fputs(buf, dest_fp) == EOF){perror("fputs():");ret1 = -1;break;}} // 关闭文件fclose(src_fp);fclose(dest_fp);// 输出结果if (ret1 == -1){printf("文件拷贝失败\n");return -1;}else{printf("文件拷贝成功\n");return 0;}
}int cmd_cp_dir(const char *src_path, const char *dest_path)
{DIR *dir = NULL; // 声明一个目录流指针struct dirent *pdirent = NULL; // 声明一个目录项指针,dirent 结构体是系统定义的结构体cp_file_info_t info ; // 声明一个 cp_file_info_t 结构体变量,create_directories(dest_path); // 递归创建目标多级目录dir = opendir(src_path); // 打开目录,返回目录流if (dir == NULL) // 检查目录是否成功打开{perror("opendir"); // 输出错误信息exit(-1); // 退出程序,返回值为-1表示错误}while ((pdirent = readdir(dir)) != NULL) // 读取目录项,返回目录项指针{ // readdir() 读取目录项,一次遍历一项if (strcmp(pdirent->d_name, ".") == 0 || strcmp(pdirent->d_name, "..") == 0)continue; // 跳过当前目录(.)和父目录(..)
#ifdef DEBUGprintf("[DEBUG]: pdirent->d_name %s\n", pdirent->d_name); // 输出目录项名称#endifmake_path(&info,src_path, dest_path,pdirent->d_name ); // 构造源文件路径
#ifdef DEBUGprintf("[DEBUG] info src_path :%s\n", info.src_path); // 输出源文件路径printf("[DEBUG] info dest_path :%s\n", info.dest_path); // 输出目标文件路径
#endif//获取源目录文件类型info.src_ftype = get_file_type(info.src_path); // 获取源文件类型if (info.src_ftype == FT_DIR) // 如果是目录{cmd_cp_dir(info.src_path, info.dest_path);// 递归调用 cmd_cp_dir() 函数}else if (info.src_ftype == FT_FILE) // 如果是文件{cmd_cp_file(info.src_path, info.dest_path);// 调用 cmd_cp_file() 函数}}closedir(dir); // 关闭目录流return 0;
}int create_directories(const char *dest_path)
{char *dir = strdup(dest_path); // 复制目标路径字符串/*假设 dest_path 是一个指向字符串的指针,例如:const char *dest_path = "/home/lxl/imooc/two";调用 strdup:char *dir = strdup(dest_path);此时,dir 将指向一块新分配的内存,这块内存中存储了字符串 "/home/lxl/imooc/two" 的副本。独立副本:strdup 创建了一个独立的字符串副本,这意味着你可以自由修改 dir 指向的字符串,而不会影响原始的 dest_path。动态内存分配:strdup 使用动态内存分配(malloc)来存储副本,因此你需要在使用完毕后调用 free 来释放这块内存。*/if (dir == NULL){perror("strdup");return -1;}// 递归创建多级目录char *last_slash = dir;while ((last_slash = strchr(last_slash + 1, '/')) != NULL){ /*递归创建多级目录:char *dir = strdup("/home/lxl/imooc/two");第一次循环:last_slash 指向 /home 中的 /,路径变为 /home。创建 /home 目录(如果不存在)。第二次循环:last_slash 指向 /home/lxl 中的 /,路径变为 /home/lxl。创建 /home/lxl 目录(如果不存在)。第三次循环:last_slash 指向 /home/lxl/imooc 中的 /,路径变为 /home/lxl/imooc。创建 /home/lxl/imooc 目录(如果不存在)。创建最终目录:创建 /home/lxl/imooc/two 目录(如果不存在)。*/*last_slash = '\0'; // 去掉路径中的最后一个组件if (mkdir(dir, 0777) == -1){/*如果目录成功创建,mkdir 返回 0。如果目录已经存在,mkdir 返回 -1,并将 errno 设置为 EEXIST。如果发生其他错误,mkdir 返回 -1,并将 errno 设置为其他值。*/if (errno != EEXIST)// errno 不等于 EEXIST,说明错误类型不是 "File exists",而是其他类型的错误。// errno 等于 EEXIST,说明错误类型是 "File exists"。 [文件存在]{perror("mkdir1:");free(dir);return -1;}}*last_slash = '/'; // 恢复路径中的最后一个组件}// 创建最终目录if (mkdir(dir, 0777) == -1){if (errno != EEXIST){perror("mkdir2:");free(dir);return -1;}}free(dir);return 0;
}// cp test test1 test/1.txt test/2.txt test1/1.txt test1/2.txt
void make_path(cp_file_info_t *pinfo,const char *src_path,const char *dest_path,const char *filename)
{memset(pinfo->src_path, 0, sizeof(pinfo->src_path));memset(pinfo->dest_path, 0, sizeof(pinfo->dest_path));strcpy(pinfo->src_path, src_path);strcat(pinfo->src_path, "/"); // 拼接路径strcat(pinfo->src_path, filename); // 拼接路径strcpy(pinfo->dest_path, dest_path);strcat(pinfo->dest_path, "/");strcat(pinfo->dest_path, filename);}
2.4.2. 对整个cp代码的整体分析
这段代码实现了一个简单的文件和目录复制功能,类似于Linux中的cp命令。代码分为多个函数,每个函数都有特定的功能。以下是逐行解释:
1. cmd_cp_execute 函数
这是主函数,负责执行复制操作。它调用其他辅助函数来完成路径解析、文件类型解析和实际的复制操作。
int cmd_cp_execute(cmd_t *pcmd)
{if (NULL == pcmd) return -1; // 如果传入的命令结构体为空,返回错误int ret = 0;
#ifdef DEBUG// 调试信息(如果定义了DEBUG宏)
#endifcp_file_info_t fileninfo; // 定义一个cp_file_info_t结构体
// 解析路径并且保存到 cp_file_info_t结构体中;ret = cmd_cp_parse_path(pcmd, &fileninfo); // 解析源路径和目标路径if (ret == -1)return -1; // 如果解析失败,返回错误ret = cmd_cp_parse_type(&fileninfo); // 解析文件类型if (ret == -1) return -1; // 如果解析失败,返回错误ret = cmd_cp_dispatch(&fileninfo); // 根据文件类型分发if (ret == -1) return -1; // 如果分发失败,返回错误
return 0; // 成功返回0
2.cmd_cp_parse_path 函数:
使用了 strcpy函数 将路径,拷贝到CP函数中对应的结构体中
这个函数负责解析命令行参数中的源路径和目标路径,并将它们保存到cp_file_info_t结构体中。
int cmd_cp_parse_path(cmd_t *pcmd, cp_file_info_t *pcpinfo)
{if (NULL == pcmd || NULL == pcpinfo) return -1; // 如果传入的指针为空,返回错误strcpy(pcpinfo->src_path, pcmd->cmd_arg_list[0]); // 复制源路径strcpy(pcpinfo->dest_path, pcmd->cmd_arg_list[1]); // 复制目标路径
#ifdef DEBUGprintf("[DEBUG] 源路径是 :%s\n", pcpinfo->src_path); // 打印源路径printf("[DEBUG] 目标路径是:%s\n", pcpinfo->dest_path); // 打印目标路径
#endifreturn 0; // 成功返回0
}
3. get_file_type 函数:
使用stat函数 获取源文件的各种信息,[文件类型,大小,用户id,用户组id等等]
这个函数通过stat系统调用获取文件的类型(文件、目录或其他类型)。file_type_t get_file_type(const char *path)
{int ret = 0;struct stat statbuf; // 定义一个stat结构体ret = stat(path, &statbuf); // 获取文件状态if (ret == -1){perror("stat():"); // 如果失败,打印错误信息return FT_ERROR; // 返回错误类型}if (S_ISDIR(statbuf.st_mode)) // 如果是目录return FT_DIR; // 返回目录类型if (S_ISREG(statbuf.st_mode)) // 如果是普通文件return FT_FILE; // 返回文件类型return FT_UNKNOWN; // 其他类型
}
4.cmd_cp_parse_type 函数
这个函数模块主要是,调度get_file_type()函数,去获取文件类型,获取结果之后,保存到cp_file_info_t结构体中
这个函数调用get_file_type函数来获取源路径的文件类型,并将其保存到cp_file_info_t结构体中。
int cmd_cp_parse_type(cp_file_info_t *pcpinfo)
{enum file_type ftpye;ftpye = get_file_type(pcpinfo->src_path); // 获取源路径的文件类型if (ftpye == FT_ERROR || ftpye == FT_UNKNOWN){perror("get_file_type():"); // 如果类型未知或出错,打印错误信息return -1; // 返回错误}elsepcpinfo->src_ftype = ftpye; // 保存文件类型
#ifdef DEBUGif (pcpinfo->src_ftype == FT_DIR)printf("[DEBUG]src is dir\n"); // 如果是目录,打印调试信息else if (pcpinfo->src_ftype == FT_FILE)printf("[DEBUG]src is file\n"); // 如果是文件,打印调试信息
#endifreturn 0; // 成功返回0
}
5. cmd_cp_dispatch 函数
这个函数是将保存到cp_file_info_t结构体中的文件类型调取出来,
按照如果是文件调用文件函数去操作,
如果是目录,调用目录函数去操作
这个函数根据文件类型调用不同的复制函数(文件或目录)。
int cmd_cp_dispatch(cp_file_info_t *pcpinfo)
{if (pcpinfo->src_ftype == FT_FILE) // 如果是文件{printf("cp is file\n");return cmd_cp_file(pcpinfo->src_path, pcpinfo->dest_path); // 调用文件复制函数}else if (pcpinfo->src_ftype == FT_DIR) // 如果是目录{printf("cp is dir\n");return cmd_cp_dir(pcpinfo->src_path, pcpinfo->dest_path); // 调用目录复制函数}return -1; // 如果类型未知,返回错误
}
6. cmd_cp_file 函数
这个函数负责复制文件。它打开源文件和目标文件,逐行读取源文件内容并写入目标文件。
int cmd_cp_file(const char *src_path, const char *dest_path)
{// 确保目标路径的父目录存在char dest_dir[SZ_FILENAME] = {0};strncpy(dest_dir, dest_path, sizeof(dest_dir)); // 复制目标路径char *last_slash = strrchr(dest_dir, '/'); // 找到最后一个斜杠if (last_slash){*last_slash = '\0'; // 截取目标路径的父目录if (create_directories(dest_dir) != 0) // 创建父目录{fprintf(stderr, "无法创建目标目录:%s\n", dest_dir); // 如果失败,打印错误信息return -1; // 返回错误}}FILE *src_fp = NULL, *dest_fp = NULL; // 定义文件指针size_t rbytes = 0, wbytes = 0; // 定义读取和写入的字节数char buffer[SZ_BUFFER] = {0}; // 定义缓冲区char buf[SZ_BUFFER] = {0}; // 定义缓冲区int ret1 = 0; // 定义返回值
#ifdef DEBUGprintf(" [ DEBUG ] %s ----> %s\n",src_path, dest_path); // 打印调试信息
#endifif (NULL == src_path || NULL ==dest_path)return -1; // 如果路径为空,返回错误
// 打开源文件src_fp = fopen(src_path, "r");if (NULL == src_fp){perror("src fopen():"); // 如果失败,打印错误信息return -1; // 返回错误}// 打开目标文件dest_fp = fopen(dest_path, "w+");if (NULL == dest_fp){perror("dest fopen():"); // 如果失败,打印错误信息fclose(src_fp); // 关闭已打开的源文件return -1; // 返回错误}// 读取源文件内容并写入目标文件char *ret = NULL;while ((ret = fgets(buf, sizeof(buf), src_fp)) != NULL) // 逐行读取{if (fputs(buf, dest_fp) == EOF) // 写入目标文件{perror("fputs():"); // 如果失败,打印错误信息ret1 = -1; // 设置返回值为错误break; // 跳出循环}} // 关闭文件fclose(src_fp);fclose(dest_fp);// 输出结果if (ret1 == -1){printf("文件拷贝失败\n"); // 如果失败,打印失败信息return -1; // 返回错误}elseprintf("文件拷贝成功\n"); // 如果成功,打印成功信息return 0; // 成功返回0}
7. cmd_cp_dir 函数
这个函数负责递归复制目录。它打开源目录,遍历目录中的每个文件或子目录,并调用相应的复制函数。int cmd_cp_dir(const char *src_path, const char *dest_path)
{DIR *dir = NULL; // 声明一个目录流指针struct dirent *pdirent = NULL; // 声明一个目录项指针cp_file_info_t info; // 声明一个cp_file_info_t结构体变量create_directories(dest_path); // 递归创建目标多级目录
dir = opendir(src_path); // 打开目录
if (dir == NULL) // 如果打开失败
{perror("opendir"); // 打印错误信息exit(-1); // 退出程序
}while ((pdirent = readdir(dir)) != NULL) // 遍历目录
{if (strcmp(pdirent->d_name, ".") == 0 || strcmp(pdirent->d_name, "..") == 0)continue; // 跳过当前目录(.)和父目录(..)
#ifdef DEBUGprintf("[DEBUG]: pdirent->d_name %s\n", pdirent->d_name); // 打印目录项名称
#endifmake_path(&info, src_path, dest_path, pdirent->d_name); // 构造源文件路径和目标文件路径
#ifdef DEBUGprintf("[DEBUG] info src_path :%s\n", info.src_path); // 打印源路径printf("[DEBUG] info dest_path :%s\n", info.dest_path); // 打印目标路径
#endif// 获取源目录文件类型info.src_ftype = get_file_type(info.src_path); // 获取文件类型if (info.src_ftype == FT_DIR) // 如果是目录{cmd_cp_dir(info.src_path, info.dest_path); // 递归调用目录复制函数}else if (info.src_ftype == FT_FILE) // 如果是文件{cmd_cp_file(info.src_path, info.dest_path); // 调用文件复制函数}}
closedir(dir); // 关闭目录流
return 0; // 成功返回0
}
8. create_directories 函数
这个函数递归创建多级目录。它使用mkdir系统调用创建目录。
int create_directories(const char *dest_path)
{char *dir = strdup(dest_path); // 复制目标路径字符串if (dir == NULL){perror("strdup"); // 如果复制失败,打印错误信息return -1; // 返回错误}
// 递归创建多级目录
char *last_slash = dir;
while ((last_slash = strchr(last_slash + 1, '/')) != NULL) // 遍历路径中的每个斜杠
{*last_slash = '\0'; // 去掉路径中的最后一个组件if (mkdir(dir, 0777) == -1) // 创建目录{if (errno != EEXIST) // 如果目录已经存在,忽略错误{perror("mkdir1:"); // 如果其他错误,打印错误信息free(dir); // 释放内存return -1; // 返回错误}}*last_slash = '/'; // 恢复路径中的最后一个组件
}// 创建最终目录
if (mkdir(dir, 0777) == -1) // 创建最终目录
{if (errno != EEXIST) // 如果目录已经存在,忽略错误{perror("mkdir2:"); // 如果其他错误,打印错误信息free(dir); // 释放内存return -1; // 返回错误}
}free(dir); // 释放内存
return 0; // 成功返回0
}
9. make_path 函数
这个函数用于构造源路径和目标路径。它将文件名拼接到路径中。
void make_path(cp_file_info_t *pinfo,const char *src_path,const char *dest_path,const char *filename)
{memset(pinfo->src_path, 0, sizeof(pinfo->src_path)); // 清空源路径memset(pinfo->dest_path, 0, sizeof(pinfo->dest_path)); // 清空目标路径strcpy(pinfo->src_path, src_path); // 复制源路径strcat(pinfo->src_path, "/"); // 拼接路径strcat(pinfo->src_path, filename); // 拼接文件名strcpy(pinfo->dest_path, dest_path); // 复制目标路径strcat(pinfo->dest_path, "/"); // 拼接路径strcat(pinfo->dest_path, filename); // 拼接文件名
}
这段代码实现了一个简单的文件和目录复制功能。它通过解析路径、判断文件类型,并根据类型调用相应的复制函数来完成任务。代码中使用了多个辅助函数来分解任务,使代码更加清晰和模块化。
3. ls 命令设计与实现 (⼀)
- 编程实现 ls -l 命令功能,具体要求如下:
-
- 显示⽂件类型
- 显示⽂件权限
- 显示⽂件所属⽤户名
- 显示⽂件所属⽤户组名
- 显示⽂件最后⼀次修改时间
- 显示⽂件名
-
- step 1 : 遍历⽬录, 并获取⽬录下的⽂件名
- step 2 : 通过系统 stat 函数来获取每个⽂件的属性
- step 3 : 解析⽂件属性信息,进⾏显示
目的:保存获取的⽂件属性信息,并进⾏输出
- 基本框架 : 在系统的 struct stat 结构体上进⾏扩展, 产⽣新的结构体, 具体设计如下:
#ifndef __CMD_LS_H__
#define __CMD_LS_H__#include <stdio.h> // 包含标准输入输出库
#include <stdlib.h> // 包含标准库,用于退出程序等
#include <string.h> // 包含字符串处理函数
#include <unistd.h> // 包含 POSIX 操作系统 API
#include <sys/types.h> // 包含系统数据类型
#include <sys/stat.h> // 包含文件状态处理函数
#include <dirent.h> // 包含目录操作函数
#include <errno.h>#include "cmd_handle.h"#define SZ_LS_NAME 64
#define SZ_LS_PERMISSION 10
#define SZ_LS_TIME 32
#define SZ_LS_LINK_CONTENT 64#define DEBUGstruct file_attribute
{struct stat f_attr_stat_info; // 保留stat的原本文件属性信息char f_attr_type; // 文件类型char f_attr_uname[SZ_LS_NAME]; // 文件所有者char f_attr_gname[SZ_LS_NAME]; // 文件所属组char f_attr_mtime[SZ_LS_TIME]; // 文件修改时间char f_attr_permission[SZ_LS_PERMISSION]; // 文件权限char f_attr_name[SZ_LS_NAME]; // 文件名char f_attr_link_content[SZ_LS_LINK_CONTENT]; // 链接文件内容
};extern int cmd_ls_execute(cmd_t *pcmd);
#endif // __CMD_LS_H__
- 当cmd_handle.c模块中解析出来,如果是ls命令就会调用 cmd_ls模块中的cmd_ls_execute函数。去执行相关指令
#include "cmd_ls.h"// ls -l <path>
int cmd_ls_execute(cmd_t *pcmd) // pcmd 得到ls命令的内容
{if (NULL == pcmd) return -1; // 判断自定义结构体是否为空if (pcmd->cmd_arg_count != 2) // 判断命令参数个数是否为2{fprintf(stderr, "Command argument Error.\n");return -1;}if (pcmd->cmd_arg_list[1] != NULL) // 判断命令参数1是否为空return cmd_list_directory(pcmd->cmd_arg_list[1]); // 调用遍历目录函数elsereturn -1;int ret = 0;
#ifdef DEBUGprintf("cmd_ls_execute\n");
#endifreturn 0;
}int cmd_list_directory(const char *dirpath) // 遍历目录
{DIR *pdir = NULL; // 目录指针struct dirent *pdirent = NULL; // 目录项指针file_attribute_t f_attr; // 自定义文件属性结构体char path[128] = {0};pdir = opendir(dirpath); // 打开目录if (pdir == NULL){perror("open(): ");return -1;}while ((pdirent = readdir(pdir)) != NULL) // 读取目录项,返回目录项指针{ // readdir() 读取目录项,一次遍历一项if (strcmp(pdirent->d_name, ".") == 0 || strcmp(pdirent->d_name, "..") == 0)continue; // 跳过当前目录(.)和父目录(..)continue;
#ifdef DEBUGprintf("[DEBUG]: pdirent->d_name %s\n", pdirent->d_name); // 输出目录项名称#endif}closedir(pdir); // 关闭目录return 0;
}
3.1. ls 命令设计与实现 (⼆)获取文件类型
- 基本思路:
-
- 通过 stat 函数获取⽂件信息,并保存到 struct stat 结构体中
- struct stat 结构体 st_mode 成员则可以解析到⽂件类型
注意点:
对于链接⽂件本身需要通过 lstat 函数来获取,否则获取的是链接⽂件所指向的内容
获取属性的主要接⼝为 get_file_attr , 并调⽤
#include "cmd_ls.h"// ls -l <path>
int cmd_ls_execute(cmd_t *pcmd) // pcmd 得到ls命令的内容
{if (NULL == pcmd) return -1; // 判断自定义结构体是否为空if (pcmd->cmd_arg_count != 2) // 判断命令参数个数是否为2{fprintf(stderr, "Command argument Error.\n");return -1;}if (pcmd->cmd_arg_list[1] != NULL) // 判断命令参数1是否为空return cmd_list_directory(pcmd->cmd_arg_list[1]); // 调用遍历目录函数elsereturn -1;int ret = 0;
#ifdef DEBUGprintf("cmd_ls_execute\n");
#endifreturn 0;
}int cmd_list_directory(const char *dirpath) // 遍历目录
{DIR *pdir = NULL; // 目录指针struct dirent *pdirent = NULL; // 目录项指针file_attribute_t f_attr; // 自定义文件属性结构体char path[128] = {0};pdir = opendir(dirpath); // 打开目录if (pdir == NULL){perror("open(): ");return -1;}while ((pdirent = readdir(pdir)) != NULL) // 读取目录项,返回目录项指针{ // readdir() 读取目录项,一次遍历一项if (strcmp(pdirent->d_name, ".") == 0 || strcmp(pdirent->d_name, "..") == 0)continue; // 跳过当前目录(.)和父目录(..)continue;
#ifdef DEBUGprintf("[DEBUG]: pdirent->d_name %s\n", pdirent->d_name); // 输出目录项名称#endifmemset(&f_attr, 0, sizeof(f_attr)); // 清空自定义文件属性结构体make_path_ls(path , dirpath, pdirent->d_name); // 合成具体文件的路径if( pdirent->d_type == DT_LNK) // 如果是软连接文件get_file_attr(&f_attr, path, pdirent->d_name, true); // 获取链接文件属性elseget_file_attr(&f_attr, path, pdirent->d_name, false); // 获取普通文件属性show_file_attrbutes(&f_attr); // 打印文件属性}closedir(pdir); // 关闭目录return 0;
}/* attr 自定义文件属性结构体指针path 文件路径filename 文件名islink 是否是链接文件
*/
// 获取文件属性
int get_file_attr(file_attribute_t *attr,const char *path,const char *filename,bool islink)
{ if (NULL == attr || NULL == path || NULL == filename) return -1;int ret ;if (islink) // 如果是链接文件// lstat() 获取链接文件属性函数ret = lstat(path, &attr->f_attr_stat_info); // 获取链接文件属性保存到自定义文件属性结构体中elseret = stat(path, &attr->f_attr_stat_info); // 获取普通文件属性保存到自定义文件属性结构体中if (ret == -1 ){perror("[DEEPR]stat(): ") ;return -1;} return 0;
}void show_file_attrbutes(file_attribute_t *attr)// 打印文件属性
{printf("File Name: %c\n", attr->f_attr_type); // 文件类型}// ls -l test test/1.txt 合成具体文件的路径
int make_path_ls(char *path , const char *dirpath, const char *filename)
{if (NULL == dirpath || NULL == filename || NULL == path)return -1;strcpy(path, dirpath);strcat(path, "/");strcat(path, filename);return 0;
}
获取⽂件类型 是通过解析 struct stat 结构体中的 st_mode 的第 [15:12] bit 来判断
通过与 S_IFMT 进⾏按位与操作,则可以得到 [15:12] 的值
在与定义的⼋进制的值进⾏⽐对即可,具体定义如下
#define S_IFMT 00170000 // 文件类型位掩码
#define S_IFSOCK 0140000 // 套接字
#define S_IFLNK 0120000 // 符号链接
#define S_IFREG 0100000 // 普通文件
#define S_IFBLK 0060000 // 块设备
#define S_IFDIR 0040000 // 目录
#define S_IFCHR 0020000 // 字符设备
#define S_IFIFO 0010000 // FIFO
获取⽂件类型的接⼝设计为 get_file_type_ls , 将获取的⽂件属性信息保存到结构体相应的成员
中,在 get_file_attr函数中进⾏调⽤
// 获取文件类型
int get_file_type_ls(file_attribute_t *pfile_attr)
{if (NULL == pfile_attr)return -1;mode_t mode = pfile_attr->f_attr_stat_info.st_mode; // 获取文件类型switch (mode & S_IFMT) // S_IFMT 是一个宏,用于获取文件类型; 按位与{case S_IFBLK: // 块设备文件pfile_attr->f_attr_type = 'b';break;case S_IFCHR: // 字符设备文件pfile_attr->f_attr_type = 'c';break;case S_IFDIR: // 目录文件pfile_attr->f_attr_type = 'd';break;case S_IFIFO: // 管道文件pfile_attr->f_attr_type = 'p';break;case S_IFLNK: // 链接文件pfile_attr->f_attr_type = 'l';break;case S_IFREG: // 普通文件pfile_attr->f_attr_type = '-';break;case S_IFSOCK: // 套接字文件pfile_attr->f_attr_type = 's';break;default:break;}return 0;
}
3.2. ls 命令设计与实现 (三)_获取文件权限
3.2.1. 设计⼀个获取⽂件权限的接⼝ get_file_permission, 具体如下:
int get_file_permission(file_attribute_t *pattr)
{// 获取文件的模式(st_mode),其中包括文件类型和权限信息mode_t mode = pattr->f_attr_stat_info.st_mode;// 循环变量 i 用来从 8 到 0 逐位检查文件的权限int i; // index 用来指示当前权限字符存储在 f_attr_permission 数组的位置int index = 0;// perm[] 数组存储权限字符,对应 'r' -> read,'w' -> write,'x' -> executechar perm[] = {'r', 'w', 'x'};// 循环从第 8 位到第 0 位(共 9 位,分别对应三个权限:rwx,rwx,rwx)for (i = 8; i >= 0; i--){// 右移 i 位后,按位与 0x1,检查该位是否为 1// (mode >> i) 将 mode 的第 i 位移到最低位,& 0x1 只保留最低位,用来检查该位是否为 1if ((mode >> i) & 0x1){// 如果该位是 1(表示有该权限),则将对应的权限字符(r、w 或 x)赋值给 f_attr_permission[index]// index % 3 用来选择 'r', 'w', 'x' 中的一个字符,确保每三个字符循环一次pattr->f_attr_permission[index] = perm[index % 3];}else{// 如果该位是 0(表示没有该权限),则在 f_attr_permission[index] 位置存储 '-'pattr->f_attr_permission[index] = '-';}// index 每次递增,表示下一个权限字符存储到 f_attr_permission 数组的下一个位置index++;}// 返回 0 表示函数执行完毕return 0;
}
3.2.1.1. 代码解释:
mode_t mode = pattr->f_attr_stat_info.st_mode;
这行代码从文件的属性结构体 中获取 ,这个 值是一个包含文件权限信息的整数。pattr
st_mode
mode
接下来,我们要通过这个 来提取文件的每一位权限,最终转换成字符(、、 或 )。mode
r
w
x
-
int i;
int index = 0;
char perm[] = {'r', 'w', 'x'};
perm[]
数组保存了权限字符。它有三个元素:(读)、(写)和 (执行)。r
w
x
index
用来指示当前权限字符应该存储到 数组的哪个位置。f_attr_permission
i
用作循环变量。
for (i = 8; i >= 0; i--) {
if ((mode >> i) & 0x1) {
pattr->f_attr_permission[index] = perm[index % 3];
} else {
pattr->f_attr_permission[index] = '-';
}
index++;
}
3.2.1.2. for (i = 8; i >= 0; i--)
- 这个循环从 开始,逐步递减到 ,总共 9 次。因为文件权限有 9 位(),每次循环处理一位权限。
i = 8
i = 0
rwxrwxrwx
3.2.1.3. (mode >> i) & 0x1
mode >> i
:将 中的第 位移到最低位。比如:mode
i
-
- 当 时, 会把 中的第 8 位移到最低位。
i = 8
mode >> 8
mode
- 当 时, 会把第 7 位移到最低位,以此类推。
i = 7
mode >> 7
- 当 时, 会把 中的第 8 位移到最低位。
& 0x1
:通过与运算,检查最低位是否为 1。如果是 1,表示该权限位启用,否则表示该权限位禁用。
-
- 如果 的第 位是 1,表示该权限有效(比如,文件可以被读、写或执行),否则表示没有该权限。
mode
i
- 如果 的第 位是 1,表示该权限有效(比如,文件可以被读、写或执行),否则表示没有该权限。
3.2.1.4. pattr->f_attr_permission[index] = perm[index % 3];
index % 3
: 会循环递增,表示当前正在处理第几个权限字符(、 或 )。 用来从 数组中选择对应的字符:index
r
w
x
index % 3
perm[]
-
index % 3 == 0
时,选择 (读权限)。r
index % 3 == 1
时,选择 (写权限)。w
index % 3 == 2
时,选择 (执行权限)。x
- 如果该位是 1(即权限启用),则把对应的权限字符(、 或 )存储到 中。
r
w
x
f_attr_permission[index]
3.2.1.5. pattr->f_attr_permission[index] = '-';
- 如果该位是 0(即权限未启用),则在对应的位置存储 ,表示没有该权限。
-
3.2.1.6. index++
index
每次增加 1,表示将下一个权限字符存储到数组的下一个位置。
3.2.2. 在 get_file_attribute 函数中进⾏调⽤
// 获取文件属性
int get_file_attr(file_attribute_t *attr,const char *path,const char *filename,bool islink)
{if (NULL == attr || NULL == path || NULL == filename)return -1;int ret;if (islink) // 如果是链接文件// lstat() 获取链接文件属性函数ret = lstat(path, &attr->f_attr_stat_info); // 获取链接文件属性保存到自定义文件属性结构体中elseret = stat(path, &attr->f_attr_stat_info); // 获取普通文件属性保存到自定义文件属性结构体中if (ret == -1){perror("[DEEPR]stat(): ");return -1;}get_file_type_ls(attr); // 获取文件类型get_file_permission(attr); // 获取文件权限return 0;
}
3.3. ls 命令设计与实现 (四)_获取⽤户名与⽤户组名
获取⽤户名与⽤户组名需要调⽤ getpwuid 函数与 getgrgid 函数, 根据 uid 与 gid 来获取对应
的⽤户名与⽤户组的名字
- getpwuid 函数信息如下:
- 函数头⽂件
#include <sys/types.h>
#include <pwd.h>
- 函数原型
struct passwd *getpwuid(uid_t uid);
- 函数功能
获取 ⽤户相关信息
- 函数参数
uid : ⽤户 id
- 函数返回值
成功 : 返回 struct passwd 指针
失败 : 返回 NULL,并设置 errno
struct passwd {
char *pw_name; /* 用户名 */
char *pw_passwd; /* 用户密码 */
uid_t pw_uid; /* 用户 ID */
gid_t pw_gid; /* 用户组 ID */
char *pw_gecos; /* 用户信息(通常是全名或者其他描述信息) */
char *pw_dir; /* 用户的主目录 */
char *pw_shell; /* 用户的默认 shell 程序 */
};
- getgrgid函数信息如下:
- 函数头⽂件
#include <sys/types.h>
#include <pwd.h>
- 函数原型
struct group *getgrgid(gid_t gid);
- 函数功能
获取 ⽤户组相关信息
函数参数
gid : ⽤户组 id
- 函数返回值
成功 : 返回 struct group 指针
失败 : 返回 NULL,并设置 errno
struct group 结构体定义如下:
sstruct group {
char *gr_name; /* 组名 */
char *gr_passwd; /* 组密码 */
gid_t gr_gid; /* 组 ID */
char **gr_mem; /* 成员列表,NULL 终止的指针数组 */
};
- 根据 uid 与 gid 来获取对应的⽤户名与⽤户组的名字
在stat函数中,f_attr_stat_info的结构体中保存着uid和gid,使用f_attr_stat_info.st_uid和f_attr_stat_info.st_gid 即可获取uid与gid
- 设计接⼝ get_file_uname 并保存到 struct file_attribute 结构体中
int get_file_uname(file_attribute_t *pattr) // 获取文件所有者
{struct passwd *pwd = getpwuid(pattr->f_attr_stat_info.st_uid); // 获取文件所有者的用户信息strcpy(pattr->f_attr_uname, pwd->pw_name); // 将用户名复制到结构体的 f_attr_uname 字段
}
- 设计接⼝ get_file_group 并保存到 struct file_attribute 结构体中
int get_file_group(file_attribute_t *pattr) // 获取文件所属组
{struct group *grp = getgrgid(pattr->f_attr_stat_info.st_gid); // 获取文件所属组的组信息strcpy(pattr->f_attr_gname, grp->gr_name); // 将组名复制到结构体的 f_attr_gname 字段
}
- 在接⼝ get_file_attr 函数中进⾏调⽤
// 获取文件属性
int get_file_attr(file_attribute_t *attr,const char *path,const char *filename,bool islink)
{if (NULL == attr || NULL == path || NULL == filename)return -1; // 检查输入参数是否为空,如果为空则返回错误int ret;if (islink) // 如果是链接文件// lstat() 获取链接文件属性函数ret = lstat(path, &attr->f_attr_stat_info); // 获取链接文件属性保存到自定义文件属性结构体中elseret = stat(path, &attr->f_attr_stat_info); // 获取普通文件属性保存到自定义文件属性结构体中if (ret == -1){perror("[DEEPR]stat(): "); // 如果stat或lstat失败,打印错误信息return -1; // 返回错误}get_file_type_ls(attr); // 获取文件类型get_file_permission(attr); // 获取文件权限get_file_uname(attr); // 获取文件所有者get_file_group(attr); // 获取文件所属组return 0; // 成功完成,返回0
}
在 show_file_attribute 函数中打印
void show_file_attrbutes(file_attribute_t *attr) // 打印文件属性
{printf("File Name: %c ", attr->f_attr_type); // 文件类型printf("%s ", attr->f_attr_permission); // 文件权限printf(" %ld ", attr->f_attr_stat_info.st_nlink); // 硬链接数printf("%s ", attr->f_attr_uname); // 文件所有者printf("%s ", attr->f_attr_gname); // 文件所属组putchar('\n'); // 空格
}
3.4. ls 命令设计与实现 (五)_获取文件大小
- 获取⽂件⼤⼩可以直接通过 struct stat 中的 st_size 成员
- 具体可以在 show_file_attribute 函数中进⾏打印即可
void show_file_attributes(struct file_attribute *pattr)
{
printf("File Name: %c ", attr->f_attr_type); // 文件类型
printf("%s ", attr->f_attr_permission); // 文件权限
printf(" %ld ", attr->f_attr_stat_info.st_nlink); // 硬链接数
printf("%s ", attr->f_attr_uname); // 文件所有者
printf("%s ", attr->f_attr_gname); // 文件所属组
printf(" %ld ",attr->f_attr_stat_info.st_size); // 文件大小
putchar('\n'); // 空格
}
获取最后修改时间需要在 struct stat 结构体中获取时间戳 ,通过 ctime () 或者 localtime ()
进⾏转换,具体实现如下:
- 设计接⼝ get_file_last_modify_time 函数
void get_file_last_modify_time(file_attribute_t *pattr) // 获取文件最后修改时间
{char time_str[80];struct tm *local_time = localtime(&pattr->f_attr_stat_info.st_mtime); // 获取文件最后修改时间snprintf(time_str, sizeof(time_str), "%2d月 %02d日 %02d:%02d",local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, local_time->tm_min);// 将时间格式化为字符串strcpy(pattr->f_attr_mtime, time_str); // 将时间字符串复制到结构体中}
- 在 show_file_attribute 函数中进⾏调⽤
void show_file_attrbutes(file_attribute_t *attr) // 打印文件属性
{printf("File Name: %c ", attr->f_attr_type); // 文件类型printf("%s ", attr->f_attr_permission); // 文件权限printf(" %ld ", attr->f_attr_stat_info.st_nlink); // 硬链接数printf("%s ", attr->f_attr_uname); // 文件所有者printf("%s ", attr->f_attr_gname); // 文件所属组printf(" %ld ", attr->f_attr_stat_info.st_size); // 文件大小printf("%s ", attr->f_attr_mtime); // 文件修改时间putchar('\n'); // 空格
}
3.5. ls 命令设计与实现 (六)_获取软连接
软链接⽂件⽐较特殊,需要进⾏特殊处理, 具体处理如下:
当需要获取链接⽂件的信息时,不能直接通过 stat 函数,需要调⽤ lstat 函数
在 get_file_attr 函数中要进⾏分开处理
int get_file_attr(struct file_attribute *pattr,const char *path,const cha
{
int ret;
if(islink)
ret = lstat(path,&pattr->f_attr_stat_info);
else
ret = stat(path,&pattr->f_attr_stat_info);
if(ret == -1)
{
perror("stat(): ");
return -1;
}
}
需要读取软链接⽂件所指向的⽂件,则需要使⽤ readlink 函数进⾏读取, 因为系统 ls 显示软链接是会显示具体内容
- readlink 函数具体信息如下:
函数头⽂件
#include <unistd.h>
函数原型
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
函数功能
读取链接⽂件的内容,就是所指向的⽂件的⽂件名
函数参数
pathname : 路径名
buf : 缓冲区地址
bufsiz : 读取的最⼤⼤⼩
函数返回值
成功 : 读取的字节数
失败 : -1, 并设置 errno
在 get_file_attr 函数中的实现如下:
if(pattr->f_attr_type == 'l')
{
ret = readlink(path,pattr->f_attr_link_content,sizeof(pattr->f_attr_link_content);
if(ret == -1)
{
perror("readlink(): ");
return -1;
}
}
- ⽂件名的输出,将命令输⼊的参数保存到相应 file_attribute_t 结构体中即可
- 在 get_file_attr 中的输出:
int get_file_attr(file_attribute_t *attr,const char *path,const char *filename,bool islink)
{if (NULL == attr || NULL == path || NULL == filename)return -1;int ret;if (islink) // 如果是链接文件// lstat() 获取链接文件属性函数ret = lstat(path, &attr->f_attr_stat_info); // 获取链接文件属性保存到自定义文件属性结构体中elseret = stat(path, &attr->f_attr_stat_info); // 获取普通文件属性保存到自定义文件属性结构体中if (ret == -1){perror("[DEEPR]stat(): ");return -1;}if(attr->f_attr_type == 'l') // 如果是软连接文件{ret = readlink(path,attr->f_attr_link_content,sizeof(attr->f_attr_link_content));if(ret == -1){perror("readlink(): ");return -1;}}get_file_type_ls(attr); // 获取文件类型get_file_permission(attr); // 获取文件权限get_file__uname(attr); // 获取文件所有者get_file_group(attr); // 获取文件所属组get_file_last_modify_time(attr); // 获取文件修改时间strcpy(attr->f_attr_name,filename); // 获取文件名return 0;
}
- 在 show_file_attribute 函数中进⾏打印
void show_file_attrbutes(file_attribute_t *attr) // 打印文件属性
{printf("File Name: %c ", attr->f_attr_type); // 文件类型printf("%s ", attr->f_attr_permission); // 文件权限printf(" %ld ", attr->f_attr_stat_info.st_nlink); // 硬链接数printf("%s ", attr->f_attr_uname); // 文件所有者printf("%s ", attr->f_attr_gname); // 文件所属组printf(" %ld ", attr->f_attr_stat_info.st_size); // 文件大小printf("%s ", attr->f_attr_mtime); // 文件修改时间if(attr->f_attr_type == 'l') // 如果是软连接文件printf(" %s->%s \n",attr->f_attr_name,attr->f_attr_link_content); // 打印链接文件内容printf(" %s \n",attr->f_attr_name); // 打印文件名putchar('\n'); // 空格
}
- 完整ls代码:
#include "cmd_ls.h"// ls -l <path>
int cmd_ls_execute(cmd_t *pcmd) // pcmd 得到ls命令的内容
{if (NULL == pcmd)return -1; // 判断自定义结构体是否为空if (pcmd->cmd_arg_count != 2) // 判断命令参数个数是否为2{fprintf(stderr, "Command argument Error.\n");return -1;}if (pcmd->cmd_arg_list[1] != NULL) // 判断命令参数1是否为空return cmd_list_directory(pcmd->cmd_arg_list[1]); // 调用遍历目录函数elsereturn -1;int ret = 0;
#ifdef DEBUGprintf("cmd_ls_execute\n");
#endifreturn 0;
}// 遍历目录
int cmd_list_directory(const char *dirpath)
{DIR *pdir = NULL; // 目录指针struct dirent *pdirent = NULL; // 目录项指针char path[128] = {0}; // 用于存储文件的完整路径file_attribute_t f_attr; // 自定义文件属性结构体pdir = opendir(dirpath); // 打开目录if (pdir == NULL){perror("open(): "); // 如果打开目录失败,打印错误信息return -1;}// 读取目录中的每个条目while ((pdirent = readdir(pdir)) != NULL) { // 跳过当前目录(.)和父目录(..)if (strcmp(pdirent->d_name, ".") == 0 || strcmp(pdirent->d_name, "..") == 0)continue;#ifdef DEBUGprintf("[DEBUG]: pdirent->d_name %s\n", pdirent->d_name); // 输出目录项名称
#endifmemset(&f_attr, 0, sizeof(f_attr)); // 清空自定义文件属性结构体make_path_ls(path, dirpath, pdirent->d_name); // 合成具体文件的路径// 根据文件类型获取文件属性if (pdirent->d_type == DT_LNK) // 如果是软链接文件get_file_attr(&f_attr, path, pdirent->d_name, true); // 获取链接文件属性elseget_file_attr(&f_attr, path, pdirent->d_name, false); // 获取普通文件属性show_file_attrbutes(&f_attr); // 打印文件属性}closedir(pdir); // 关闭目录return 0;
}// 获取文件属性
int get_file_attr(file_attribute_t *attr,const char *path,const char *filename,bool islink)
{if (NULL == attr || NULL == path || NULL == filename)return -1; // 参数检查int ret;// 根据是否是链接文件选择不同的函数获取文件属性if (islink) // 如果是链接文件ret = lstat(path, &attr->f_attr_stat_info); // 使用 lstat 获取链接文件的属性elseret = stat(path, &attr->f_attr_stat_info); // 使用 stat 获取普通文件的属性if (ret == -1){perror("[DEEPR]stat(): "); // 如果获取失败,打印错误信息return -1;}// 如果是链接文件,获取链接的目标内容if(attr->f_attr_type == 'l') {ret = readlink(path, attr->f_attr_link_content, sizeof(attr->f_attr_link_content));if(ret == -1){perror("readlink(): "); // 如果获取链接内容失败,打印错误信息return -1;}}// 获取文件的各种属性get_file_type_ls(attr); // 获取文件类型get_file_permission(attr); // 获取文件权限get_file__uname(attr); // 获取文件所有者get_file_group(attr); // 获取文件所属组get_file_last_modify_time(attr); // 获取文件修改时间strcpy(attr->f_attr_name, filename); // 获取文件名return 0;
}// 打印文件属性
void show_file_attrbutes(file_attribute_t *attr)
{// 按照 ls -l 的格式打印文件属性printf("File Name: %c ", attr->f_attr_type); // 文件类型printf("%s ", attr->f_attr_permission); // 文件权限printf(" %ld ", attr->f_attr_stat_info.st_nlink); // 硬链接数printf("%s ", attr->f_attr_uname); // 文件所有者printf("%s ", attr->f_attr_gname); // 文件所属组printf(" %ld ", attr->f_attr_stat_info.st_size); // 文件大小printf("%s ", attr->f_attr_mtime); // 文件修改时间// 如果是软链接文件,打印链接的目标if(attr->f_attr_type == 'l') printf(" %s->%s \n", attr->f_attr_name, attr->f_attr_link_content); printf(" %s \n", attr->f_attr_name); // 打印文件名putchar('\n'); // 换行
}// 合成具体文件的路径
int make_path_ls(char *path, const char *dirpath, const char *filename)
{strcpy(path, dirpath); // 复制目录路径strcat(path, "/"); // 添加路径分隔符strcat(path, filename); // 添加文件名#ifdef DEBUGprintf("[DEBUG] make_path_ls: %s\n", path); // 调试信息,检查合成的路径
#endifreturn 0;
}// 获取文件类型
int get_file_type_ls(struct file_attribute *pattr)
{mode_t mode = pattr->f_attr_stat_info.st_mode; // 获取文件模式// 根据文件模式判断文件类型switch (mode & S_IFMT) {case S_IFBLK:pattr->f_attr_type = 'b'; // 块设备文件break;case S_IFCHR:pattr->f_attr_type = 'c'; // 字符设备文件break;case S_IFDIR:pattr->f_attr_type = 'd'; // 目录break;case S_IFIFO:pattr->f_attr_type = 'p'; // FIFO 文件break;case S_IFLNK:pattr->f_attr_type = 'l'; // 符号链接文件break;case S_IFREG:pattr->f_attr_type = '-'; // 普通文件break;case S_IFSOCK:pattr->f_attr_type = 's'; // 套接字文件break;default:break;}return 0;
}// 获取文件权限
int get_file_permission(file_attribute_t *pattr)
{mode_t mode = pattr->f_attr_stat_info.st_mode; // 获取文件模式int i;int index = 0;char perm[] = {'r', 'w', 'x'}; // 权限字符数组// 遍历文件模式的每一位,判断权限for (i = 8; i >= 0; i--){if ((mode >> i) & 0x1) // 将高位的值移动到最底位,在与0x1进行与运算,如果结果为1,则表示该位为1pattr->f_attr_permission[index] = perm[index % 3]; // 将权限字符保存到数组中elsepattr->f_attr_permission[index] = '-'; // 如果没有权限,用 '-' 表示index++;}return 0;
}// 获取文件所有者
int get_file__uname(file_attribute_t *pattr)
{struct passwd *pwd = getpwuid(pattr->f_attr_stat_info.st_uid); // 根据用户ID获取用户信息strcpy(pattr->f_attr_uname, pwd->pw_name); // 将用户名复制到结构体中
}// 获取文件所属组
int get_file_group(file_attribute_t *pattr)
{struct group *grp = getgrgid(pattr->f_attr_stat_info.st_gid); // 根据组ID获取组信息strcpy(pattr->f_attr_gname, grp->gr_name); // 将组名复制到结构体中
}// 获取文件最后修改时间
void get_file_last_modify_time(file_attribute_t *pattr)
{char time_str[80]; // 用于存储时间字符串struct tm *local_time = localtime(&pattr->f_attr_stat_info.st_mtime); // 获取文件最后修改时间// 将时间格式化为字符串snprintf(time_str, sizeof(time_str), "%02d月 %02d日 %02d:%02d",local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, local_time->tm_min);strcpy(pattr->f_attr_mtime, time_str); // 将时间字符串复制到结构体中
}
书写不易,点个赞吧!