第一版面世
文件结构
程序文件的组织划分,从一定程度上反映了我们对于程序结构的理解。ccleaner按逻辑将代码组织到三个文件中,其中:
- ccleaner.h:程序功能接口文件,负责对外提供提供服务。
- ccleaner.c:程序功能实现文件,负责实现目标功能。
- main.c:程序驱动文件,调用接口将整体逻辑组织起来。
一对命名为ccleaner的文件,可以看作是一个模块。从复用的角度讲,我们完全可以提供源代码或库级别的代码复用。如果没有进行这样的物理划分,将所有的代码集中到一个.c文件中,在维护、复用方面都是不利的。
接口函数
ccleaner.h提供了以下两个接口函数:
/**
* 配置清理选项
* suffix:要清理的文件后缀名列表,后缀名以空格分隔,如 ".obj .log";
* rmdir:是否删除目录,1(是),0(否)
*/
void ccleaner_config(const char *suffix, int rmdir);
/**
* 根据配置执行指定目录清理
* dir:要清理的目录
*/
void ccleaner_run(const char *dir);
配置函数ccleaner_config的suffix参数,允许我们针对清理操作,提供定制化服务。考虑到易用性,比如我们要删除指定目标下的所有文件,也支持传递"*.*",否则逐个输入要匹配的后缀,显然缺少了易用性。参数rmdir指示是否要删除空目录。这两个参数都是属于配置程序行为,所以函数的名字含有config,可以看出,为函数取一个确切的名称,能让代码更好理解。在ccleaner_config的实现中,如果没有传递后缀列表,则会使用内置的选项。这些选项参数随后被保存到全局变量中,供后续代码使用。
void ccleaner_config(const char *suffix, int rmdir)
{
const char *def_suffix = ".obj .log .tmp .exe";
if (suffix != NULL)
strcpy_s(g_suffix, BUFSIZ, suffix);
else
strcpy_s(g_suffix, BUFSIZ, def_suffix);
g_rmdir = rmdir;
}
清理操作是通过ccleaner_run函数发动的,参数dir指出了要清理的目录。函数内部直接将参数转发给clear_core函数,以实现递归操作。
void ccleaner_run(const char *dir)
{
int fcnt, dcnt;
clear_core(dir, &fcnt, &dcnt);
}
将清理操作划分为这两步,是考虑到了配置一次,清理多个文件夹的情形,如:
ccleaner_config(".obj .tmp .tlog", 1);
ccleaner_run("c:\\windows\\temp");
ccleaner_run("c:\\users\\default");
将两步合并到一起,提供一个接口函数也是可行的,如:
void ccleaner_run(const char *dir, const char *suffix, int rmdir);
针对上面的使用情景,可能的调用如下:
ccleaner_run("c:\\windows\\temp", ".obj .tmp .tlog", 1);
ccleaner_run("c:\\users\\default", ".obj .tmp .tlog", 1);
在设计上没有银弹,选择哪种方案都无可厚非。
内部函数
接口函数以ccleaner_开头,内部函数没有遵循这个规则。clear_core函数是整个程序的核心实现,它遍历文件夹,删除文件,同时记录计数信息。
/**
* 文件清理核心实现
* dir:要清理的目录
* foundcnt:在当前目录下已找到的文件计数(文件夹包括在内)
* delcnt:在当前目录下实际清理的文件计算
* bool:函数执行成功时返回true,否则返回false
*/
bool clear_core(const char *dir, int *foundcnt, int *delcnt)
{
*foundcnt = 0;
*delcnt = 0;
// 查找当前目录下的所有文件
char path[BUFSIZ];
sprintf_s(path, BUFSIZ, "%s\\*.*", dir);
// 开始查找文件
struct _finddata_t fd;
intptr_t handle = _findfirst(path, &fd);
if (handle != -1)
{
do
{
// 当前文件是目录
if (fd.attrib & _A_SUBDIR)
{
// 忽略".", ".."目录
if (strcmp(fd.name, ".") != 0 && strcmp(fd.name, "..") != 0)
{
char subdir[BUFSIZ];
sprintf_s(subdir, BUFSIZ, "%s\\%s", dir, fd.name);
*foundcnt += 1;
// 递归清理子目录
int fcnt, dcnt;
bool ret = clear_core(subdir, &fcnt, &dcnt);
// 删除目录
if (ret && g_rmdir && fcnt == dcnt)
{
if (_rmdir(subdir) == 0)
*delcnt += 1;
}
}
}
else
{
*foundcnt += 1;
if (delete_check(fd.name))
{
char path[BUFSIZ];
sprintf_s(path, BUFSIZ, "%s\\%s", dir, fd.name);
if (delete_file(path))
*delcnt += 1;
}
}
} while (_findnext(handle, &fd) == 0);
// 查找结束的清理操作
_findclose(handle);
}
return handle != -1;
}
遍历过程中,每找到一个文件,程序将调用delete_check检查当前文件是否应该被删除。方法是检测文件的后缀是,是否包含在配置的文件后缀列表中。
/**
* 根据文件后缀名判断当前文件是否属于被删除文件类型之一
* path:文件名
* bool:文件可被删除时返回true,否则返回false。
*/
bool delete_check(const char *path)
{
// 程序是否配置为删除所有文件
char *allfiles = strstr(g_suffix, "*.*");
if (allfiles)
return true;
// 取得文件后缀名
char *suffix = strrchr(path, '.');
if (suffix == NULL)
return false;
// 将后缀名转换为小写
for (size_t i = 1; i < strlen(suffix); i++)
suffix[i] = tolower(suffix[i]);
// 查找是否为需要删除的文件类型
char *found = strstr(g_suffix, suffix);
return found != NULL;
}
函数使用了C语言提供的字符串函数完成功能,其中:
// 在字符串str1中查找是否含有字符串str2,
// 如果存在,返回str2在str1中第一次出现的地址;否则返回NULL。
char* strstr( const char *str1, const char *str2);
// 在参数str所指向的字符串中搜索最后一次出现字符c的位置。
char* strrchr(const char *str, int c);
// 是把字母字符转换成小写,非字母字符不做出处理。
int tolower(int c);
文件通过检测后,就调用delete_file函数将其删除,这是对库函数remove的一个简单包装,方便依赖删除状态打印日志信息。
/**
* 删除文件
* path:文件绝对路径
* bool:删除是否成功,成功返回true,否则返回false
*/
bool delete_file(const char *path)
{
bool state = remove(path) == 0 ? true : false;
printf(state ? "[OK]: " : "[ER]: ");
printf("%s\n", path);
return state;
}
主程序
在main.c中,我们实现main函数,它简单的使用接口,将整个流程驱动起来,现在我们配置的是删除当前debug目录下的所有文件。
#include <stdio.h>
#include "ccleaner.h"
int main()
{
ccleaner_config("*.*", 1);
ccleaner_run(".\\debug");
return 0;
}

注意第一行输出,因为程序ccleaner.exe正在运行中,程序不能清理自身,所以删除失败。
至此,ccleaner的第一版算是诞生了。