Rust製コマンドラインツール「exa」のコードリーディング
この記事はRust Advent Calendar 2023 シリーズ1の9日目の記事です。
exaとは
exaは、lsコマンドの代替として開発されたRust製のコマンドラインツールです。色付き表示で視認性が高く、使い勝手が良いことが特徴です。
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,
},
),
..
}
- file_type:
- この項目は、対象のファイルがどのようなタイプであるかを示す
FileType
オブジェクトを含んでいます。 -
mode: 16877
という値は、ファイルの種類とパーミッションを示すUNIXのモードを表します。この具体的な値16877
は、ディレクトリを表す特別な値で、正確にはビットマスクとして解釈されます。
- この項目は、対象のファイルがどのようなタイプであるかを示す
- is_dir:
- この値はブール型で、対象がディレクトリの場合に
true
を示します。
- この値はブール型で、対象がディレクトリの場合に
- is_file:
- この値はブール型で、対象が通常のファイルの場合に
true
を示します。
- この値はブール型で、対象が通常のファイルの場合に
- permissions:
- この項目は、ファイルのパーミッションを示す
Permissions
オブジェクトを含んでいます。 -
FilePermissions
のmode: 16877
も、上記のfile_type
のmode
と同様に、UNIXのモードを示しています。
- この項目は、ファイルのパーミッションを示す
- modified:
- この項目は、ファイルの最終変更日時を示す
SystemTime
オブジェクトを含んでいます。 -
tv_sec
は、1970年1月1日からの秒数を示しており、tv_nsec
はその追加のナノ秒数を示しています。
- この項目は、ファイルの最終変更日時を示す
- accessed:
- この項目は、ファイルの最後のアクセス日時を示す
SystemTime
オブジェクトを含んでいます。
- この項目は、ファイルの最後のアクセス日時を示す
- 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にコードを投げることで有用な情報を得ることができ、それを足がかりにして検索する流れが効果的でした。
参考記事
Discussion