第三十章:综合实战:从零构建一个跨平台命令行工具(文件搜索工具)

说实话,讲到这里,我觉得是时候来点真家伙了。

前面我们聊了那么多跨平台的理论、预处理器技巧、文件操作差异、路径处理……但这些东西如果不串起来用一次,你很难真正理解“跨平台”这三个字的分量。我自己带团队的时候,经常发现新人看了很多文档,一上手写工具,Windows 上跑得好好的,放到 Linux 就崩了——嗯,这种事我见得太多了。

所以这一章,我们直接动手:从零写一个跨平台的文件搜索工具。它要能在 Windows、Linux、macOS 上都能编译、运行,行为一致。

30.1 工具需求定义

先别急着敲代码。我个人的习惯是,先想清楚我们要做什么。

这个文件搜索工具,我们叫它 xfsearch(跨平台搜索),核心功能如下:

  • 用户输入一个目录路径和一个文件名模式(支持通配符 *?
  • 递归搜索该目录下所有匹配的文件
  • 输出每个匹配文件的完整路径、大小、最后修改时间
  • 支持 --help--version 参数
  • 在 Windows 和 Unix 系统上行为一致

说白了,就是一个简化版的 find 命令,但加上了一些友好的输出格式。

30.2 整体架构设计

我画了一张图,帮你理清这个工具的内部结构。你看一眼就明白了:

xfsearch 架构图 main() 主入口 parse_args() 参数解析 platform_list_dir() platform_file_stat() path_join() 路径拼接 search_recursive()

你看,核心思路就是:把平台相关的操作隔离到一层接口里。上层逻辑(递归搜索、模式匹配)完全不用关心底层是 Windows 还是 Linux。

30.3 跨平台文件操作封装

这是整个工具最关键的环节。我直接给你看代码,然后一句句解释。

// platform.h
#ifndef PLATFORM_H
#define PLATFORM_H

#include <stdio.h>
#include <time.h>

#ifdef _WIN32
    #include <windows.h>
    #include <direct.h>
    #define PATH_SEP '\\'
    #define PATH_SEP_STR "\\"
#else
    #include <dirent.h>
    #include <sys/stat.h>
    #include <unistd.h>
    #define PATH_SEP '/'
    #define PATH_SEP_STR "/"
#endif

typedef struct {
    char name[256];
    int is_dir;
    long long size;
    time_t mtime;
} FileEntry;

int platform_list_dir(const char *path, FileEntry *entries, int max_count);
int platform_file_stat(const char *path, FileEntry *entry);
void path_join(char *dest, const char *base, const char *name);

#endif

这里有个细节:PATH_SEPPATH_SEP_STR。我当年第一次写跨平台代码时,直接硬编码了 '/',结果在 Windows 上路径拼接出来全是 C:/Users/xxx——虽然 Windows 也能认,但有些老 API 不买账。所以,老老实实用宏定义

30.4 平台实现:Windows 版

Windows 的文件操作和 Unix 差别很大。它用 FindFirstFile/FindNextFile 这套 API,而不是 opendir/readdir

// platform_win.c
#ifdef _WIN32

int platform_list_dir(const char *path, FileEntry *entries, int max_count) {
    char pattern[512];
    WIN32_FIND_DATA ffd;
    HANDLE hFind;
    int count = 0;

    // 构造搜索模式:path\*
    snprintf(pattern, sizeof(pattern), "%s\\*", path);

    hFind = FindFirstFile(pattern, &ffd);
    if (hFind == INVALID_HANDLE_VALUE) {
        return -1;
    }

    do {
        if (count >= max_count) break;

        // 跳过 . 和 ..
        if (strcmp(ffd.cFileName, ".") == 0 || strcmp(ffd.cFileName, "..") == 0)
            continue;

        strncpy(entries[count].name, ffd.cFileName, 255);
        entries[count].name[255] = '\0';
        entries[count].is_dir = (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;

        if (!entries[count].is_dir) {
            entries[count].size = ((long long)ffd.nFileSizeHigh << 32) + ffd.nFileSizeLow;
        } else {
            entries[count].size = 0;
        }

        // 时间转换(Windows 的 FILETIME 转 time_t)
        ULARGE_INTEGER ull;
        ull.LowPart = ffd.ftLastWriteTime.dwLowDateTime;
        ull.HighPart = ffd.ftLastWriteTime.dwHighDateTime;
        entries[count].mtime = (time_t)((ull.QuadPart - 116444736000000000ULL) / 10000000ULL);

        count++;
    } while (FindNextFile(hFind, &ffd) != 0);

    FindClose(hFind);
    return count;
}

#endif
注意:Windows 的 FILETIME 转 time_t 那段代码,我当初调试了整整一个下午。那个基准时间 116444736000000000 是 1601年1月1日到1970年1月1日的100纳秒间隔数。记不住没关系,但你要知道有这回事。

30.5 平台实现:Unix 版

Unix 这边就清爽多了:

// platform_unix.c
#ifndef _WIN32

int platform_list_dir(const char *path, FileEntry *entries, int max_count) {
    DIR *dir;
    struct dirent *entry;
    struct stat st;
    int count = 0;
    char full_path[1024];

    dir = opendir(path);
    if (!dir) return -1;

    while ((entry = readdir(dir)) != NULL) {
        if (count >= max_count) break;

        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
            continue;

        strncpy(entries[count].name, entry->d_name, 255);
        entries[count].name[255] = '\0';

        // 获取完整路径的 stat 信息
        path_join(full_path, path, entry->d_name);
        if (stat(full_path, &st) == 0) {
            entries[count].is_dir = S_ISDIR(st.st_mode);
            entries[count].size = st.st_size;
            entries[count].mtime = st.st_mtime;
        }

        count++;
    }

    closedir(dir);
    return count;
}

#endif

你看,Unix 的 stat 直接返回 time_t,不用做任何转换。这就是为什么我常说:Unix 的设计哲学是“给程序员用的”,Windows 的设计哲学是“给应用开发者用的”

30.6 路径拼接:一个容易被忽视的坑

路径拼接看起来简单,但跨平台时要注意:

void path_join(char *dest, const char *base, const char *name) {
    size_t len = strlen(base);
    strcpy(dest, base);

    // 如果 base 末尾没有分隔符,加上
    if (len > 0 && dest[len-1] != '/' && dest[len-1] != '\\') {
        dest[len] = PATH_SEP;
        dest[len+1] = '\0';
    }

    strcat(dest, name);
}
小技巧:我习惯在 path_join 里同时检查 '/' 和 '\\',这样即使传入的路径风格混了,也能正确处理。你想想看,用户可能从配置文件里读到一个 Windows 路径,然后程序跑在 Linux 上——这种事我遇到过。

30.7 核心搜索逻辑

有了上面的基础,搜索逻辑就简单了:

#include "platform.h"
#include <string.h>
#include <stdio.h>

// 简单的通配符匹配(支持 * 和 ?)
int wildcard_match(const char *pattern, const char *text) {
    if (*pattern == '\0') return *text == '\0';

    if (*pattern == '*') {
        // * 匹配任意字符(包括空)
        while (*text) {
            if (wildcard_match(pattern + 1, text)) return 1;
            text++;
        }
        return wildcard_match(pattern + 1, text);
    }

    if (*pattern == '?' || *pattern == *text) {
        return wildcard_match(pattern + 1, text + 1);
    }

    return 0;
}

void search_recursive(const char *base_path, const char *pattern) {
    FileEntry entries[256];
    int count = platform_list_dir(base_path, entries, 256);
    char full_path[1024];

    if (count < 0) {
        fprintf(stderr, "无法打开目录: %s\n", base_path);
        return;
    }

    for (int i = 0; i < count; i++) {
        path_join(full_path, base_path, entries[i].name);

        if (entries[i].is_dir) {
            // 递归搜索子目录
            search_recursive(full_path, pattern);
        } else {
            // 匹配文件名
            if (wildcard_match(pattern, entries[i].name)) {
                struct tm *tm_info = localtime(&entries[i].mtime);
                char time_buf[64];
                strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", tm_info);

                printf("%-60s %10lld bytes  %s\n",
                       full_path, entries[i].size, time_buf);
            }
        }
    }
}

这个 wildcard_match 函数,说白了就是一个递归的模式匹配。我故意没用什么高级算法,因为对于文件搜索这种场景,目录深度通常不会超过几十层,递归完全够用。

30.8 主函数与参数处理

最后是主入口:

#include <stdio.h>
#include <string.h>

#define VERSION "1.0.0"

void print_help() {
    printf("用法: xfsearch <目录> <模式>\n");
    printf("递归搜索目录中匹配模式的文件\n\n");
    printf("选项:\n");
    printf("  --help        显示帮助信息\n");
    printf("  --version     显示版本号\n\n");
    printf("模式支持通配符:\n");
    printf("  *  匹配任意字符(包括空)\n");
    printf("  ?  匹配单个字符\n");
}

int main(int argc, char *argv[]) {
    if (argc == 2) {
        if (strcmp(argv[1], "--help") == 0) {
            print_help();
            return 0;
        }
        if (strcmp(argv[1], "--version") == 0) {
            printf("xfsearch version %s\n", VERSION);
            return 0;
        }
    }

    if (argc != 3) {
        fprintf(stderr, "错误: 参数数量不对\n");
        print_help();
        return 1;
    }

    const char *search_dir = argv[1];
    const char *pattern = argv[2];

    printf("搜索目录: %s\n", search_dir);
    printf("匹配模式: %s\n", pattern);
    printf("----------------------------------------\n");

    search_recursive(search_dir, pattern);

    return 0;
}

30.9 编译与测试

编译的时候,根据平台选择源文件:

平台 编译命令
Linux/macOS gcc -o xfsearch main.c platform_unix.c -Wall
Windows (MinGW) gcc -o xfsearch.exe main.c platform_win.c -Wall
Windows (MSVC) cl main.c platform_win.c /Fe:xfsearch.exe

测试一下:

$ ./xfsearch /home/user/docs "*.txt"
搜索目录: /home/user/docs
匹配模式: *.txt
----------------------------------------
/home/user/docs/readme.txt                                     1024 bytes  2024-01-15 14:30:00
/home/user/docs/notes/meeting.txt                               512 bytes  2024-01-14 09:15:00
/home/user/docs/notes/todo.txt                                  256 bytes  2024-01-13 18:00:00

核心要点回顾:

  • #ifdef _WIN32 隔离平台差异
  • 封装 platform_list_dirplatform_file_statpath_join 三个跨平台接口
  • 通配符匹配用递归实现,简单够用
  • 路径分隔符用宏定义,不要硬编码

嗯,这个工具虽然简单,但五脏俱全。你把它拿到任何支持 C 编译器的系统上,改一下源文件选择,就能跑起来。这就是跨平台开发的精髓——不是写一套代码到处兼容,而是把变化隔离在薄薄的一层里

我个人觉得,这个实战的价值不在于代码量,而在于你真正体会到了“平台差异”这四个字的分量。下次你再写跨平台工具,心里就有底了。


公众号:蓝海资料掘金营,微信deep3321