张银峰的编程课堂

第一版面世

文件结构

程序文件的组织划分,从一定程度上反映了我们对于程序结构的理解。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;
}

glimix.com

注意第一行输出,因为程序ccleaner.exe正在运行中,程序不能清理自身,所以删除失败。

至此,ccleaner的第一版算是诞生了。