💪

Rust製コマンドラインツール「exa」のコードリーディング

2023/12/09に公開

この記事はRust Advent Calendar 2023 シリーズ1の9日目の記事です。

exaとは

exaは、lsコマンドの代替として開発されたRust製のコマンドラインツールです。色付き表示で視認性が高く、使い勝手が良いことが特徴です。
exa公式サイト
exa

現行のバージョンでは機能が多岐にわたるため、初期のコミットを基にChatGPTの助けを借りて、現代のRust文法に適合させた上でコードリーディングを行いました。特に興味深い部分を中心に紹介します。

引数の受け取り

exaはenv::args()を使用してコマンドライン引数を受け取ります。この関数はString型のベクターを返し、第一引数には通常、ファイル名が格納されます。以降の引数が実際のコマンドライン引数として機能します。

fn main() {
    let args: Vec<String> = env::args().collect();
    match args.as_slice() {
        [] => unreachable!(),
        [_] => list(Path::new(".")),
        [_, p] => list(Path::new(p)),
        _ => panic!("args?"),
    }
}

match構文を見てみます。

[] => unreachable!()は到達不能を意味し、この部分は通常考慮する必要がありません。
[_] => list(Path::new("."))は引数がない場合、カレントディレクトリの内容を表示します。
[_, p] => list(Path::new(p))では第二引数が与えられたディレクトリの内容を表示します。
_ => panic!("args?")は第三引数以降が存在する場合、エラーを発生させます。

ファイルのメタデータ取得・表示

fn list(path: &Path) {
    let files = fs::read_dir(path).expect("Failed to read directory");
    let mut files: Vec<_> = files.map(|f| f.unwrap().path()).collect();

    files.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
    for file in files.iter() {
        let filename = file.file_name().unwrap().to_str().unwrap();
        let metadata = fs::symlink_metadata(file).expect("Failed to get metadata");
        let colour = file_colour(&metadata, filename);

        println!(
            "{} {}",
            perm_str(&metadata),
            colour.paint(filename.to_string())
        );
    }
}

fs::read_dir(path)はディレクトリ内のファイルに対するイテレータを返し、各ファイルのパスを取得します。Vec<_>の部分はコンパイラが型を自動で推論することを意味しています。

fs::symlink_metadata(file)でファイルのメタデータを取得でき、以下のような内容が含まれます。

Metadata {
    file_type: FileType(
        FileType {
            mode: 16877,
        },
    ),
    is_dir: true,
    is_file: false,
    permissions: Permissions(
        FilePermissions {
            mode: 16877,
        },
    ),
    modified: Ok(
        SystemTime {
            tv_sec: 1695962138,
            tv_nsec: 858978057,
        },
    ),
    accessed: Ok(
        SystemTime {
            tv_sec: 1695962253,
            tv_nsec: 564211575,
        },
    ),
    created: Ok(
        SystemTime {
            tv_sec: 1695962138,
            tv_nsec: 726325412,
        },
    ),
    ..
}
  1. file_type:
    • この項目は、対象のファイルがどのようなタイプであるかを示すFileTypeオブジェクトを含んでいます。
    • mode: 16877という値は、ファイルの種類とパーミッションを示すUNIXのモードを表します。この具体的な値16877は、ディレクトリを表す特別な値で、正確にはビットマスクとして解釈されます。
  2. is_dir:
    • この値はブール型で、対象がディレクトリの場合にtrueを示します。
  3. is_file:
    • この値はブール型で、対象が通常のファイルの場合にtrueを示します。
  4. permissions:
    • この項目は、ファイルのパーミッションを示すPermissionsオブジェクトを含んでいます。
    • FilePermissionsmode: 16877も、上記のfile_typemodeと同様に、UNIXのモードを示しています。
  5. modified:
    • この項目は、ファイルの最終変更日時を示すSystemTimeオブジェクトを含んでいます。
    • tv_secは、1970年1月1日からの秒数を示しており、tv_nsecはその追加のナノ秒数を示しています。
  6. accessed:
    • この項目は、ファイルの最後のアクセス日時を示すSystemTimeオブジェクトを含んでいます。
  7. created:
    • この項目は、ファイルの作成日時を示すSystemTimeオブジェクトを含んでいます。

特に注目したいのはmode: 16877の部分です。

modeについて

modeはUNIXシステムにおいてファイルの種類とパーミッションを示します。Rustのfs::Metadataでは10進数で返され、これを8進数に変換すると40755になります。UNIXではファイルのmodeやパーミッションを8進数で表すのが一般的です。

8進数が使われるのはファイルのパーミッションが3ビットで表されるためで、読み取り(r)、書き込み(w)、実行(x)の各ビットを組み合わせて表現します。例えば、読み取りと実行のパーミッションがある場合は101となり、8進数では5となります。UNIXではこの表現方法が古くから使われています。

4はディレクトリを、0755はパーミッションを表します。表示例は以下の通りです。

色の表示方法

最後に、ディレクトリとパーミッションに色をつける方法を見ていきます。

UNIXでは通常、ANSIエスケープコードを使ってテキストの色やスタイルを変更します。基本的なカラーコードは以下の通りです。

  • Foreground (文字色)
    • 30: 黒 (Black)
    • 31: 赤 (Red)
    • 32: 緑 (Green)
    • 33: 黄 (Yellow)
    • 34: 青 (Blue)
    • 35: 紫 (Magenta)
    • 36: シアン (Cyan)
    • 37: 白 (White)
  • Background (背景色)
    • 40: 黒 (Black)
    • 41: 赤 (Red)
    • 42: 緑 (Green)
    • 43: 黄 (Yellow)
    • 44: 青 (Blue)
    • 45: 紫 (Magenta)
    • 46: シアン (Cyan)
    • 47: 白 (White)

カラーコードは\x1B[で始まりmで終わるエスケープシーケンスに含めます。例えば、\x1B[31mは赤い文字を表します。

echo -e "\x1B[31mThis is red text\x1B[0m"

これを実行すると赤色のテキストが表示されます。
\x1B[0mはスタイルをリセットするためのコードです。

exaみたいに出力しようとすると次のような文字列になります。

echo "\x1B[1;33mr\x1B[0m\x1B[1;31mw\x1B[0m\x1B[4;32mx\x1B[0m\x1B[33mr\x1B[0m\x1B[1;30m-\x1B[0m\x1B[32mx\x1B[0m \x1B[34msrc\x1B[0m"

実行すると以下のように表示されます。

おわりに

exaの最初のコミットは2014年5月で、当時のRust文法は現在では使われていないものが多かったです。コードの書き換えはChatGPTにやってもらい、UNIXシステムに関する質問も全てChatGPTに頼りながらソースを探しにいったりしました。

何も知らない状態での検索は難しいですが、ChatGPTにコードを投げることで有用な情報を得ることができ、それを足がかりにして検索する流れが効果的でした。

参考記事

https://www.gnu.org/software/libc/manual/html_node/Permission-Bits.html
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/stat.2.html
https://qiita.com/PruneMazui/items/8a023347772620025ad6

Discussion