文件操作:file命令详解

CMake 里的 file 命令,说白了就是你的瑞士军刀。读写文件、下载东西、打包压缩、路径处理、glob 匹配…… 几乎所有的文件系统操作,它都能干。我最早接触 CMake 时,觉得这命令就是个「文件搬运工」,后来才发现,它其实是构建系统里最容易被低估的利器。

今天我们就把它彻底拆开,一个一个讲清楚。

1. 文件读写:最基础的操作

先看最简单的——读写文件。我个人习惯把项目版本号、构建时间这些信息写到一个头文件里,然后让 C++ 代码去引用。怎么做?用 file(WRITE)file(READ)

# 写入文件(覆盖)
file(WRITE version.h.in "#define VERSION_MAJOR 1\n")
file(APPEND version.h.in "#define VERSION_MINOR 0\n")

# 读取文件
file(READ version.h.in VERSION_CONTENT)
message("版本内容:${VERSION_CONTENT}")

注意 WRITE 会覆盖已有内容,APPEND 则是追加。我在项目中遇到过一个问题:用 WRITE 写了一个配置文件,结果每次 CMake 重新运行都会覆盖掉用户手动修改的内容。后来我改用 APPEND + 条件判断,才解决了。

避坑指南: 我曾经在 CI 流水线里用 file(WRITE) 写临时文件,忘了加 ${CMAKE_CURRENT_BINARY_DIR} 路径,结果文件写到了源码目录里,导致 git 仓库被污染。记住:写文件时,目标路径一定要明确。

2. 文件下载:远程资源获取

如果你的项目需要从网上下载第三方库或数据文件,file(DOWNLOAD) 就是你的好帮手。它支持 HTTP/HTTPS、FTP,还能设置超时、校验哈希。

file(DOWNLOAD
    "https://example.com/data.zip"
    "${CMAKE_CURRENT_BINARY_DIR}/data.zip"
    SHOW_PROGRESS
    EXPECTED_MD5 1234567890abcdef1234567890abcdef
    TIMEOUT 30
    STATUS DOWNLOAD_STATUS
    LOG DOWNLOAD_LOG
)

if(NOT DOWNLOAD_STATUS EQUAL 0)
    message(FATAL_ERROR "下载失败:${DOWNLOAD_LOG}")
endif()

这里有个小技巧:STATUS 变量会返回一个列表,第一个元素是状态码(0 表示成功),第二个是错误信息。我建议每次都检查一下状态,别指望网络永远可靠。

提示: 如果你在公司内网,可能需要配置代理。CMake 会读取环境变量 http_proxyhttps_proxy,所以提前设好就行。

3. 文件打包:压缩与解压

CMake 3.18 之后,file(ARCHIVE) 命令可以直接创建和提取压缩包。支持 zip、tar、7z 等格式。我在项目中用它来打包构建产物,省去了调用外部工具的麻烦。

# 创建压缩包
file(ARCHIVE_CREATE
    OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/output.zip"
    PATHS "${CMAKE_CURRENT_BINARY_DIR}/bin"
    FORMAT zip
    COMPRESSION_LEVEL 9
)

# 解压
file(ARCHIVE_EXTRACT
    INPUT "${CMAKE_CURRENT_BINARY_DIR}/output.zip"
    DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/extracted"
)

嗯,这里要注意:ARCHIVE_CREATEPATHS 参数可以指定多个路径,但路径必须是绝对路径或相对于当前源码目录的路径。我踩过坑——用了相对路径,结果 CMake 找不到文件。

参数 说明 示例值
FORMAT 压缩格式 zip, tar, 7z, gzip
COMPRESSION_LEVEL 压缩级别(0-9) 9(最高压缩)
PATHS 要打包的文件/目录列表 ${BIN_DIR}, ${LIB_DIR}

4. 路径操作:别自己拼字符串

很多新手喜欢用字符串拼接来构造路径,比如 set(MY_PATH "${BASE_DIR}/sub/folder")。这其实很危险——不同操作系统路径分隔符不一样。CMake 提供了 file(RELATIVE_PATH)file(TO_CMAKE_PATH)file(TO_NATIVE_PATH) 等命令来处理。

# 获取相对路径
file(RELATIVE_PATH REL_PATH
    "/home/user/project"
    "/home/user/project/src/main.cpp"
)
# REL_PATH = "src/main.cpp"

# 转换路径格式
file(TO_CMAKE_PATH "C:\\Users\\me\\file.txt" CMAKE_PATH)
# CMAKE_PATH = "C:/Users/me/file.txt"

file(TO_NATIVE_PATH "/home/user/file.txt" NATIVE_PATH)
# 在 Windows 上 = "C:\home\user\file.txt"(取决于当前盘符)

我个人习惯:所有路径变量都用 CMake 风格(正斜杠),只在最后输出或传给外部工具时才转成本地格式。这样能避免很多跨平台问题。

5. Glob 模式匹配:批量找文件

Glob 模式匹配,说白了就是用通配符批量查找文件。最常用的是 file(GLOB)file(GLOB_RECURSE)

# 查找当前目录下所有 .cpp 文件
file(GLOB SOURCES "src/*.cpp")

# 递归查找所有 .h 文件
file(GLOB_RECURSE HEADERS "include/**/*.h")

# 使用更精确的模式
file(GLOB_RECURSE ALL_FILES
    "src/*.cpp"
    "src/*.h"
    "test/*.cpp"
    "test/*.h"
)

这里有个争议点:很多 CMake 最佳实践文档说「不要用 GLOB 来收集源文件」,因为 CMake 不会自动检测新文件。但我在实际项目中,只要配合 CONFIGURE_DEPENDS 参数(CMake 3.12+),就能让构建系统在源文件变化时重新运行 glob。

# 推荐做法:加上 CONFIGURE_DEPENDS
file(GLOB SOURCES CONFIGURE_DEPENDS "src/*.cpp")
核心要点: Glob 模式中的 * 匹配任意字符(不包括路径分隔符),** 匹配任意目录层级。比如 src/**/*.cpp 会匹配 src 下所有子目录里的 .cpp 文件。

6. 知识体系总览

为了让你更直观地理解 file 命令的各个功能模块,我画了一张图:

file 命令 文件读写 WRITE / READ / APPEND 文件下载 DOWNLOAD / UPLOAD 文件打包 ARCHIVE_CREATE / EXTRACT 路径操作 RELATIVE / TO_CMAKE / TO_NATIVE Glob 匹配 GLOB / GLOB_RECURSE 其他操作 LOCK / RENAME / REMOVE / COPY CMake file 命令功能模块图

7. 综合示例:一个实用的文件处理流程

最后,我分享一个实际项目中的用法。这个例子展示了如何用 file 命令完成「下载依赖 → 解压 → 读取配置 → 生成头文件」的完整流程。

# 1. 下载第三方库
file(DOWNLOAD
    "https://example.com/libfoo-1.0.zip"
    "${CMAKE_CURRENT_BINARY_DIR}/libfoo.zip"
    SHOW_PROGRESS
    STATUS DL_STATUS
)

if(NOT DL_STATUS EQUAL 0)
    message(FATAL_ERROR "下载 libfoo 失败")
endif()

# 2. 解压
file(ARCHIVE_EXTRACT
    INPUT "${CMAKE_CURRENT_BINARY_DIR}/libfoo.zip"
    DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/libfoo"
)

# 3. 读取版本文件
file(READ "${CMAKE_CURRENT_BINARY_DIR}/libfoo/version.txt" LIBFOO_VERSION)
string(STRIP "${LIBFOO_VERSION}" LIBFOO_VERSION)

# 4. 生成配置头文件
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/libfoo_config.h"
    "#ifndef LIBFOO_CONFIG_H\n"
    "#define LIBFOO_CONFIG_H\n"
    "#define LIBFOO_VERSION \"${LIBFOO_VERSION}\"\n"
    "#endif\n"
)

message("libfoo 版本:${LIBFOO_VERSION},配置已生成")

你看,整个流程用 file 命令就串起来了,不需要调用外部脚本。这也是我越来越喜欢 CMake 的原因——它自己就能搞定大部分文件操作,何必再引入 Python 或 Shell 呢?

个人建议: 如果你在写一个跨平台的项目,尽量用 CMake 的 file 命令代替系统命令(比如 cp、rm、wget)。这样你的 CMakeLists.txt 在 Windows、Linux、macOS 上都能跑,不用改一行代码。

好了,关于 file 命令的核心用法就讲到这里。记住:读写用 WRITE/READ,下载用 DOWNLOAD,打包用 ARCHIVE,路径用 RELATIVE/TO_CMAKE,找文件用 GLOB。每个场景都有对应的工具,别自己造轮子。