fd:从可解释文件发现到安全仓库候选集
fd 只负责发现文件,不负责替你执行动作
fd 的输出看起来只是一组路径,真正需要验收的却是候选集合:从哪个根目录开始,隐藏文件和 ignore 规则怎样参与,符号链接是否跟随,特殊文件名怎样编码,退出状态又怎样交给下一段脚本。候选不完整会漏改,候选过宽会把生成物、凭证或缓存送进后续命令。
因此 fd 的权威入口先把“发现什么”讲透,再讨论怎样接入 fzf、xargs 或项目脚本。交互选择属于下游能力;fd 页面只保留两者之间的记录协议和责任边界,不再把 fzf 的预览、按键与取消语义混成 fd 的内置功能。
架构层面应把 fd 当作候选生产者:输入是根目录与过滤策略,输出是带明确分隔符的路径流。后续是否预览、选择或执行写操作,必须由独立步骤授权。
安装后先确认命令名与路径
以下行为以 fd 10.4.2 与 fzf 0.74.0 为基线。fd 采用 MIT 或 Apache-2.0 双许可,fzf 采用 MIT 许可;两者都是继承当前进程文件与终端权限的本地 CLI,没有需要部署的服务端,也没有托管版与自托管版的功能差异。
Windows 可用 winget、Scoop 或 Chocolatey,macOS 可用 Homebrew,Linux 使用发行版仓库。Debian/Ubuntu 的 fd 包可能把可执行文件命名为 fdfind,因为 fd 名称与已有包冲突;团队脚本必须检测实际命令,不能只在个人 shell 里悄悄建 alias。
winget install sharkdp.fd
winget install junegunn.fzf
fd --version
fzf --version
Get-Command fd,fzf | Select-Object Name,Sourcebrew install fd fzf # macOS
sudo apt-get install fd-find fzf # Debian / Ubuntu
sudo dnf install fd-find fzf # Fedora
fdfind --version || fd --version
fzf --version
command -v fdfind || command -v fd
command -v fzf发布制品和更多平台入口见 fd 官方仓库 与 fzf 官方仓库。发行版仓库可能滞后于上游发布线;直接下载发布制品时要校验发布页摘要,并固定 CPU 架构。项目若依赖 --print0/--read0、预览占位符或特定 shell 集成,应固定经验证版本线,并在升级时执行含空格、换行、隐藏路径、.git、取消选择和预览中断的回归样例。
两个进程之间只传“候选”,不传意图
fd PATTERN ROOT 默认用正则匹配路径的文件名部分,并从 ROOT 并行遍历;--glob 才把模式改为 glob,--type f 只保留普通文件,--extension ts 按扩展名过滤。fzf 从标准输入读取记录,在内存中按查询评分、过滤和排序,确认后把所选记录写到标准输出。
两者都不建立持久索引,也不解析 AST。fd 每次调用都从根目录重新读取目录项和元数据,ignore、隐藏、类型、大小与名称字段共同决定候选;fzf 只处理生产者送来的记录,不会回到文件系统补查遗漏。于是 fd 漏掉的路径无法靠 fzf 查询“搜回来”,fzf 高分候选也不代表语义相关,更不代表路径在确认后仍指向同一个对象。
模糊匹配只负责人机收敛,不会验证文件属于哪个模块、是否为生成代码、是否有权限修改。后续动作必须重新检查路径仍在仓库内、对象类型符合预期,并把选择作为一个完整参数传递。
ignore 优先级与 Git 工作树必须显式验证
fd 默认跳过隐藏条目,并在 Git 仓库内采用 .gitignore;它也读取通用 .ignore、专用 .fdignore 和全局 fd ignore。所有规则使用 gitignore 风格解释,但不能把文件名想当然地排成一条通用的 .fdignore > .ignore > .gitignore 优先级口诀:目录层级、规则出现位置和是否仍会遍历父目录都会影响重新纳入。--ignore-file 追加规则,命令行 --exclude/-E 即使与 --no-ignore 同时出现仍会排除对象。最终候选必须用正反命令验证。
printf 'packages/legacy/\n' > .fdignore
fd --no-ignore-vcs --type f --glob 'config.yaml' .
fd --no-ignore --type f --glob 'config.yaml' .
fd --no-ignore --type f --glob 'config.yaml' \
--exclude 'packages/payment v2/**' .第一条只关闭版本控制 ignore,.fdignore 仍会排除 legacy;第二条关闭全部 ignore 后,legacy 才出现;第三条证明 --exclude 即使与 --no-ignore 同时使用,仍会排除 payment 路径。若团队希望 rg 和 fd 共享排除,使用 .ignore;只服务 fd 的规则放 .fdignore。规则进入仓库评审,并用固定 fixture 验证,避免个人全局 ignore 让候选集漂移。
Git 元数据同样影响发现结果。把仓库内容复制到不含 .git 的导出目录后,.gitignore 未必仍按工作树规则生效;容器构建上下文、源码压缩包和 CI artifact 都可能出现这种差异。当前 fd 版本的完整帮助是判定入口:
fd --help | rg 'ignore|require-git'当工具提供 --no-require-git 时,它表示即使未识别到 Git 仓库也采用 Git ignore;团队应在目标版本线上验证后再写进脚本。没有这个能力或需要完全确定时,使用受控 --ignore-file/专用 .fdignore,并把导出目录作为测试场景。--no-ignore-vcs 只关闭版本控制 ignore,--no-ignore 会放宽更多 ignore 来源,两者不能混成同一个开关。
fd 10.0.0 还改变了一个容易沿用旧经验的边界:使用 --hidden 且仍启用 VCS ignore 时,不再自动把 .git 目录排除。只要规则没有另行忽略它,.git 就可能进入候选。仓库脚本启用 --hidden 时应显式加 --exclude .git,并用“应出现的隐藏配置”和“不得出现的 .git/objects”组成正反断言。
--hidden、--no-ignore 与 --unrestricted
fd --hidden --type f --glob 'config.yaml' .
fd --no-ignore --type f --glob 'config.yaml' .
fd --unrestricted --type f --glob 'config.yaml' .--hidden/-H 只纳入隐藏路径,仍尊重 ignore;--no-ignore/-I 关闭 ignore 过滤,仍不自动纳入隐藏路径;--unrestricted/-u 相当于同时放宽两者。它们不会自动变成“只看 Git 跟踪文件”。若候选必须严格等于索引内容,用 git ls-files -z 更准确;若需要工作树中未跟踪但未忽略的文件,使用 git ls-files -z --cached --others --exclude-standard。
这也是 fd 与 rg -u/-uu/-uuu 的重要差异:两个工具都叫 -u,层级语义并不相同。不要在共享脚本里凭简称迁移参数。
反向实验:换行分隔怎样悄悄破坏路径
空格路径在良好引用下还能幸存,文件名换行会直接破坏“每行一条”的假设:
printf 'mode: newline\n' > $'packages/payment v2/line\nbreak.yaml'
fd --type f --extension yaml . | while IFS= read -r path; do
printf '<%s>\n' "$path"
done预期这个文件被打印成两条伪路径,证明换行协议已经丢失记录边界。改成 NUL 后,每个路径仍是一个参数:
fd --type f --extension yaml --print0 . \
| while IFS= read -r -d '' path; do
printf '<%q>\n' "$path"
done在 GNU 工具链中也可以用 xargs -0,但修改类命令还应关闭并行或证明并行安全:
fd --type f --extension yaml --print0 . \
| xargs -0 -r -n 1 printf 'candidate=%s\n'-r 是 GNU xargs 的行为,BSD/macOS 兼容性不同。跨平台项目更稳妥的是在 Bash 循环或 Node/Python 小程序里按 NUL 读取,并把每条路径作为参数数组元素执行,绝不拼成 shell 字符串再 eval。
配置字段怎样改变运行边界
FZF_DEFAULT_COMMAND 只在 fzf 没有标准输入时提供候选生产命令;当 fd ... | fzf 已有管道输入,它不接管候选。FZF_DEFAULT_OPTS 会注入每次 fzf 调用,适合颜色、布局等纯界面参数,不适合预览、--multi、--bind execute(...) 或会改变退出语义的选项。
export FZF_DEFAULT_COMMAND='fd --type f --strip-cwd-prefix'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
export FZF_DEFAULT_OPTS='--height=60% --layout=reverse --border'--multi 允许多选,会把输出从一个路径变成多个记录,下游必须逐条处理并定义部分失败语义;--select-1 在仅有一个候选时自动选择,减少交互却绕过人工确认;--exit-0 改变空结果退出行为,容易让包装脚本误判成功;--bind 'enter:execute(...)' 把执行权限放进选择器内部,绕过外层状态检查和审计。工程脚本优先让 fzf 只输出选择,把执行留在可测试函数中。
在 fzf 0.74.0 与 tmux 3.7+ 的组合里,--popup 默认使用可切换到其他 pane/window 的浮动 pane,而不是严格模态 popup;显式边框样式时才回到 popup。包装器不能把“fzf 仍显示在屏幕上”当作调用方被阻塞或用户仍在当前上下文的证据。真正的同步边界仍是 fzf 进程退出、状态码被检查、NUL 输出被完整读取。
fd 侧的 --max-depth 控制遍历深度,--type 控制对象类型,--extension 和 --glob 控制名称,--exclude 排除高成本或敏感路径,--follow 跟随符号链接。启用 --follow 前要处理环路、越界目录和重复文件;多数源码仓库保持默认不跟随更安全。
项目接入:让候选源成为仓库契约
团队可以把下面脚本保存为受评审的工具入口,由任务运行器调用。它优先使用 fd,在 Debian 风格环境回退到 fdfind,固定仓库根目录,并从发现阶段排除凭据和依赖缓存:
#!/usr/bin/env bash
set -euo pipefail
root=$(git rev-parse --show-toplevel)
if command -v fd >/dev/null 2>&1; then
FD=fd
elif command -v fdfind >/dev/null 2>&1; then
FD=fdfind
else
echo '缺少 fd/fdfind' >&2
exit 127
fi
command -v fzf >/dev/null 2>&1 || { echo '缺少 fzf' >&2; exit 127; }
cd "$root"
"$FD" --type f --print0 \
--exclude .git \
--exclude node_modules \
--exclude .env \
--exclude '*.pem' . \
| fzf --read0 --print0 --no-multi \
--preview 'sed -n "1,160p;160q" < {}'这个入口仍只负责选择。调用者若要格式化、删除、上传或修改文件,必须验证规范化路径仍位于 root 下,拒绝符号链接越界,记录工具版本和所选相对路径,并在执行前显示不可变动作摘要。批量动作使用 --multi 时,先冻结完整候选清单,再逐项产生 diff;任何一项失败都要定义继续、停止和回滚策略。
清理、回滚与失败证据
实验结束后离开目录并删除 fd-fzf-lab。若设置过环境变量,执行 unset FZF_DEFAULT_COMMAND FZF_CTRL_T_COMMAND FZF_DEFAULT_OPTS;PowerShell 使用 Remove-Item Env:FZF_DEFAULT_COMMAND,Env:FZF_CTRL_T_COMMAND,Env:FZF_DEFAULT_OPTS -ErrorAction SilentlyContinue。卸载使用原安装器,例如 winget uninstall sharkdp.fd、winget uninstall junegunn.fzf 或 brew uninstall fd fzf。
排障时按链路找第一处差异。fd 没输出,检查根目录、pattern 是正则还是 glob、对象类型、隐藏与 ignore;fd -HI 才出现时,分别用 -H 和 -I 定位是哪层过滤。fd 有候选而 fzf 无结果,清空查询并检查 --read0 是否与生产者 --print0 配对。预览报错而选择正常,检查 $SHELL、占位符引用、输入重定向和预览工具是否安装。按 Esc 后仍执行下游,检查包装脚本是否吞掉 130、是否把空字符串替换为 .。路径被拆分,检查整条链是否从生产到消费都保持 NUL,而不是中途进入 shell 变量或逐行文本。
回滚不只是删除配置。若曾把危险 execute 绑定写入 FZF_DEFAULT_OPTS,先从 shell 启动文件移除并开启新 shell;若预览访问了网络或凭据,撤销相关 Token、检查服务端审计记录和终端历史;若下游命令已经修改仓库,先审查 git diff,只回退这次选择产生的文件,不能用破坏性命令覆盖其他人的工作树。
权限、敏感数据、容量与成本
两个工具都以当前进程权限运行。fd --hidden --no-ignore --follow 可以把搜索面扩到密钥、依赖缓存、挂载目录和仓库外符号链接;fzf 预览又会把内容显示在屏幕、录屏、终端日志或远程会话中。企业脚本应使用最小仓库根目录,默认排除密钥扩展名与配置目录,禁用预览中的网络和写操作,并为共享终端、CI artifact 与录屏设置数据分级规则。
容量瓶颈有三处:fd 的目录遍历和元数据读取、fzf 保存与排序候选的内存、预览进程随焦点变化的重复启动。候选达到大型 monorepo 规模时,先用 --type、--extension、--max-depth 和 --exclude 收敛,不要把整个磁盘送进 fzf;预览限制行数和文件大小。性能测量要同时保存候选数、冷/热缓存条件、首屏延迟、峰值内存和预览启动次数,否则“更快”可能只是少遍历了本应命中的路径。性能门槛由代表性仓库基线、交互延迟预算和开发机配置决定,不能用一个固定文件数承诺所有团队。
软件本身没有集中式服务成本,治理成本主要是跨平台版本、shell 差异、脚本维护和误操作恢复。团队应保留包含空格、换行、隐藏、ignore 冲突、连字符开头和取消选择的 fixture;升级 fd、fzf 或 shell 后自动回归。配置 owner 评审 .ignore/.fdignore 与全局环境变量,任何 execute 绑定、符号链接跟随或多选写操作都需要单独安全审查。
选型时看候选从哪里来、最后要做什么
按路径名、扩展名、类型和深度发现本地文件时选 fd;已有任意文本候选、需要人快速收敛时选 fzf;两者组合适合交互导航、挑选测试、打开配置和生成只读候选清单。候选必须严格等于 Git 索引时,用 git ls-files -z 取代 fd;需要搜索文件正文时用 rg;需要符号身份和调用关系时用 LSP;无人值守批处理则通常不应依赖交互式 fzf,而应使用可审计的确定性清单。
成熟的组合不是一条炫目的管道,而是一份状态契约:生产者决定哪些路径有资格进入,NUL 保证路径身份不被文本分隔破坏,预览只帮助判断,fzf 的退出状态表达确认或取消,外层程序最后才获得执行权。只要其中一层含糊,模糊查找就会把效率问题升级成权限和数据问题。
出现版本差异时,先以目标二进制的 fd --help、fzf --man 为字段证据,再查 fd Releases、fzf Releases 与 fzf 官方使用说明。帮助文本决定当前机器究竟支持哪些参数,发布说明用于发现行为变化;团队脚本不能因上游文档已经更新,就假定发行版仓库里的旧二进制也有同样语义。
