🔍
ファイル検索ツール fd の使い方メモ
はじめに
奥が深い症候群を発症しやすい古の find ではなく、人に優しい fd を使いたい。fd は必死にググらなくても自然な使い方ができるし、引数を間違えたときもどう直せばいいのか教えてくれる。
インストール
brew install fd
- 上のように書いていても実際はその通りに実行してはいけない
- ~/.Brewfile に
brew "fd"
を追加してbrew bundle --global
する
- ~/.Brewfile に
- 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
マッチしすぎてしまうとき
-
foo/foo_*.*
な感じのファイル郡がいくつかのディレクトリ分かれて大量にあったとする - 利用者はそのことを知らずに foo に関連するファイルの場所を探したかった
- そこで
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