🔍

ファイル検索ツール fd の使い方メモ

2022/07/10に公開

https://github.com/sharkdp/fd

はじめに

奥が深い症候群を発症しやすい古の find ではなく、人に優しい fd を使いたい。fd は必死にググらなくても自然な使い方ができるし、引数を間違えたときもどう直せばいいのか教えてくれる。

インストール

brew install fd
  • 上のように書いていても実際はその通りに実行してはいけない
    • ~/.Brewfile に brew "fd" を追加して brew bundle --global する
  • M2 Mac だと実行コマンドは /opt/homebrew/bin/fd になる
  • 2023-10-25 での最新バージョンは 8.7.0

もしくは、

cargo install fd-find
  • 実行コマンドは ~/.cargo/bin/fd になる
  • ~/.zshenv に source "$HOME/.cargo/env" を追加すると ~/.cargo/bin にパスが通る
  • 2022-07-10 での最新バージョンは 8.4.0

どちらでもよいがバージョンアップに追随するには brew の方がよい。

オプション

表示形式

表記 意味
--relative-path 相対パス。デフォルト
-a, --absolute-path フルパス
-l, --list-details ls -l 風

指定のパターンをどう扱うか

表記 意味
--regex デフォルト。foo なら /foo/
-s, --case-sensitive デフォルト。foo なら /foo/ か /foo/i で Foo なら /Foo/
-i, --ignore-case デフォルト。foo なら /foo/i ※ -g や -e にも効く
-F, --fixed-strings 正規表現としない
-g, --glob ファイル名専用のマッチング形式を使う 'foo*.rb'

対象に依存する絞り込み

表記 意味
-e, --extension <ext>... 拡張子で絞る。よく使う。複数書ける -e vue -e js など
-t, --type <filetype>... 種類で絞る。ファイルかディレクトリで分ける -tf と -td はよく使う
-o, --owner <user:group> 所有者で絞る。!で反転する
-S, --size <size>... 容量で絞る。+1mなら1MB以上 -1mなら1MB以下
--changed-within <dur> 新しいものだけ。3day なら3日内に更新されたもの
--changed-before <dur> 古いものだけ。3day なら3日前より古いもの

さらに除外する系

表記 意味
-E, --exclude <pattern>... 指定の pattern を除外する。GLOB形式。
--ignore-file <path>... 除外リストファイルを指定する (.gitignore 形式の表記)

毎回除外するものがあるなら .fdignore か .ignore に書いておく

除外しない系

表記 意味
-u, --unrestricted 何も除外しない。--no-ignore --hidden と同じ。よく使う
-I, --no-ignore Git 管理下の除外設定と .ignore .fdignore を無視する
-H, --hidden ドットで始まるものを除外しない。.git/* にマッチするようになる
--no-ignore-parent 先祖ディレクトリにある ignore 系ファイルを無視する
--no-ignore-vcs Git 管理下の除外設定のみ無視する

件数で絞る

表記 意味
--max-results N 最初のN件だけ表示する
-1 --max-results 1 と同じ

ディレクトリの深さで絞る

表記 意味
--min-depth <min> min 以上
-d, --max-depth <max> max 以下
--prune マッチしたらその下のディレクトリには移動しない
--exact-depth <depth> 指定の深さ

--max-depth のかわりに --prune を使った方がいい場合がある

他のコマンドに渡す

表記 意味
-x, --exec <cmd> 1件づつ
-X, --exec-batch <cmd> 1行にまとめて
--batch-size <size> ただし最大 size 件づつ
表記 抽出部分
{} path/to/basename.png
{.} path/to/basename
{/} basename.png
{./} basename
{//} path/to

その他

表記 意味
-q, --quiet 何も表示しない。1件以上マッチしたら0で0件なら1を返す
-L, --follow シンボリックリンク先を追う
-p, --full-path 先祖dir名にもマッチする。"." を書かなくてもよくなる
-0, --print0 null を改行の代わりにする。xargs -0 とペアで使う
--show-errors 異常な権限やデッドリンクのエラーを表示する
--one-file-system ファイルシステムをまたがない (初期値)
-j, --threads <num> スレッド数。デフォルトはCPU数。知らなくていい
--base-directory <path> 指定パスからの相対パス表示になる
--search-path <path> 単に引数で対象のパスを指定するのと同じ
--strip-cwd-prefix 先頭の ./ を取る (リダイレクトやパイプ処理のときだけ)
-c, --color <when> 初期値は auto で never が OFF で always が ON

はまりがちなこと

--no-ignore-parent が効かない?

  • log ディレクトリに降りて特定のログファイルを探したかったとする
  • log ディレクトリや *.log 自体が先祖ディレクトリで除外されているので
  • 先祖の除外設定を無視する --no-ignore-parent を指定する
  • しかし、マッチしない
cd log
fd --no-ignore-parent foo.log
  • この場合、Git のグローバルな除外設定が邪魔をしている
    • git config --get core.excludesfile で出てくるファイル
    • $HOME/.config/git/ignore
  • --no-ignore-vcs を指定するとそれらを無効化できる
cd log
fd --no-ignore-parent --no-ignore-vcs foo.log

--strip-cwd-prefix が効いてない?

  • これは先頭の ./ を消すオプション
  • 非TTYのときだけ効く
    • 非TTYとはリダイレクトやパイプ処理のこと
  • fd -tf | xargs ls とすると ./ が先頭に入る
  • fd --strip-cwd-prefix -tf | xargs ls とすればそれを削除できる

echo {.} が実行できない?

  • eshell で fd -x echo {.} とすると謎のエラーがでる
  • 原因は eshell が {} 内を Emacs Lisp として実行するため
  • fd -x echo '{.}' とすれば通る

マッチしてないのにステイタス0を返してしまう

  • 何も表示しないオプション --quiet を指定するとステイタスが変わる
  • マッチの有無に関わらず指定しないと常に 0 だった
  • このあたり作者が違うのもあって ripgrep と微妙に仕様が異なる
  • できれば合わせてくれんかな(小声)

実用例

XMLファイルはGit管理下にあるけど検索対象からは外したい

echo "*.xml" >> .fdignore

除外ルールは rg (ripgrep) と共有したい

echo "*.xml" >> .ignore

拡張子のないファイルを探す

fd -tf '^[^.]+$'

/etc 以下で所有者が root でないものを調べる

fd -o !root -l . /etc

/etc 以下でグループが wheel でないものを調べる

fd -o :!wheel -l . /etc

実行権限がついてしまっている *.rb の実行権限を外す。ただしbin 以下は除く

fd -tx -e rb -E bin -x chmod -v a-x

拡張子が vue と js のファイルの中でサイズが20KBを超えているものを探す

fd -e vue -e js -S +20k

最近更新された ~/Dropbox 内のファイルを探す

fd --changed-within 12hours . ~/Dropbox

cache 以下の3日以上古いファイルを消す

fd --changed-before 3days -tf . cache -x rm

子ディレクトリだけを対象に何かする

fd -td --exact-depth 1 -x some-command -x {.}

Rakefile が存在するすべてのディレクトリで rake を実行する

fd Rakefile -x rake -C {//}

ただし上のコマンドを親ディレクトリの Rakefile の中に書いていると自分がマッチして無限ループする。その場合は次のように --min-depth 2 をつけてサブディレクトリに限定する。

fd Rakefile --min-depth 2 -x rake -C {//}

すべての jpg を png に変換

fd -e jpg -x convert {} {.}.png

spec ファイルなのに spec が含まれていないファイルを探す

fd -E '*spec*' app_root/spec

正規表現を駆使するのではなく、除外する glob 形式を -E に指定する

foo/**/*.html をまとめて Google Chrome で開く

fd -g "*.html" foo    -X open -a "Google Chrome"
fd -g "*.html" foo -a -X open -a "Google Chrome" --args

--args をつけたときは -a でフルパスにする必要がある

なかなかマッチしないとき

fd -u foo

マッチしすぎてしまうとき

  1. foo/foo_*.* な感じのファイル郡がいくつかのディレクトリ分かれて大量にあったとする
  2. 利用者はそのことを知らずに foo に関連するファイルの場所を探したかった
  3. そこで fd foo としたらマッチしすぎて結局どういう構造かよくわからなかった

そういうとき --prune を指定する

fd --prune foo

そうすると最初にマッチした foo ディレクトリより下に降りなくなるので簡潔な表示になる

ざっくり .DS_Store を探す

fd -u DS_Store
  • .DS_Store はだいたい .gitignore で弾かれているので --no-ignore を指定する
  • さらにドットで始まるものはもともとマッチしないので --hidden も指定する
  • 両方合わせて -u と書ける
  • 適当なプロジェクトの node_modules 配下を探すとかなり出てくるので問題は根深い

安全に .DS_Store を抹殺する

fd -u -tf -g '\.DS_Store'
fd -u -tf -g '\.DS_Store' -x echo rm
fd -u -tf -g '\.DS_Store' -x rm
  • 安全のため -tf で種類を「ファイル」に限定する
  • 部分一致ではなく完全一致するように --glob の -g を使う
  • こういうのはいきなり削除すると経験上取り返しのつかないことになるので段階的に行う

Rubyのコードから空のディレクトリがないのを保証したいみたいなとき

Ruby
if system("fd -te -td -q")
  abort "空のディレクトリがある"
end
  • -te -td は --type empty --type directory で「空のディレクトリ」を表す
  • -q で何も表示せず、空のディレクトリにマッチしたら(fdにとっては正常なので) 0 を返す
  • Ruby 側でそれは想定外なので abort する

Discussion