👋

ファイルが多すぎてlsが打てなくなったディレクトリでファイル名のリストを出すllsを作りました

2021/08/29に公開

2021/09/18追記: v0.0.2を出しています https://zenn.dev/catatsuy/articles/7e3292778396b4

この記事を読みました。
http://be-n.com/spw/you-can-list-a-million-files-in-a-directory-but-not-with-ls.html

私はファイルが増えすぎてlsが打てなくなってしまったディレクトリを見たことをこれまで何度かあります。その度に無力感を味わうことになりました。それに対して挑む方法をずっと求めてきました。そんな中、記事を読みました。記事内ではC言語での実装方法が紹介されていますが、実際に使われたC言語の実装は公開されていなそうでした。
しかしやるべきことはgetdents64というシステムコールを実行することだけで、別に実装することは難しくなさそうです。

Goの場合はsyscallパッケージを使うことでシステムコールを実行できます。今回のgetdents64はLinuxにしか実装されていないので実装すればMacなど他のOSで実行できなくなります。なのでGoのポータビリティは失われてしまいますが、OS固有の機能も触ろうと思えば触れるのがGoの魅力です。ということで今回はGoで実装してみました。

https://github.com/catatsuy/lls

使い方はlsと同じで、ディレクトリ名を渡せばそのディレクトリ、何も渡さなければカレントディレクトリのファイル名のリストを標準出力に表示します。

動きとしては以下のようになります。

  1. ディレクトリのサイズ分のバッファを確保
  2. getdents64を呼び出す
  3. ファイル名を1つずつ標準出力に表示

ディレクトリのサイズはls -dlした結果と同じ値です。実際に必要なサイズよりも余裕を持った数値になるはずなので、十分全ファイルのリストを取得できるはずです。実際に必要なサイズよりも小さなバッファを渡した場合、値が途中で切れてしまうので全ファイルのリストは表示できません。panicなどはしないので一部が出せればよいということであれば小さくしても問題ありません。-buf-sizeを指定することでバッファのサイズを調整できるので小さくしたい場合は使ってください。-debugオプションを付与すれば実際に使ったバッファのサイズを表示することができます。-buf-sizeに指定した値よりも少し小さいだけならすべて出力できていない可能性があります。

どの程度小さければすべて出力できていない可能性があるのでしょうか。バッファに保存される値はC言語の構造体で、Goでは構造体syscall.Direntとして以下のように定義されています。

type Dirent struct {
        Ino       uint64
        Off       int64
        Reclen    uint16
        Type      uint8
        Name      [256]int8
        Pad_cgo_0 [5]byte
}

なので大体1つ辺り (20+ファイル名) byte を消費します。ファイル名がどの程度の長さかによって左右されますが、Linuxの場合ファイル名は255文字まで(Name [256]int8でこれは元々C言語のchar[]型なので最後はNull文字で255文字までになる)なので、ザックリ300byte以上余裕がなければ全部出ていない可能性を疑うべきです。

実装を読みたい人は特にこの辺りを読むと良いと思います。

https://github.com/catatsuy/lls/blob/04a1893ee8cd3e9ba5f67bc0b0ec8d4b450db519/cli/cli.go#L115-L144

基本的にシステムコールを実行した後はGoの構造体にキャストして必要な情報を取得していきます。Reclenに使用しているメモリ量が入っているので、その分バッファを先に進めたり、入っているファイル名はC言語の文字列で最後がNull文字になっているので、Null文字になっている箇所を探したりしているコードがGoだとあまり見ないコードだと思いますが、そこ以外は普通にGoなので慣れ親しんだコードになっていると思います。

ReleasesにLinux用のバイナリも置いておいたので、手軽に試せると思います。使ってもらえたらうれしいです。

Discussion