Rustで書いた小さな`ls`クローン
原文は Matthias Endler により に公開されました。 このブログを購読する
「Rustで書き直す無用のUnixツール」シリーズの今回は、私の大のお気に入りのひとつであるlsを取り上げます。
まず最初にお断りしておきますが、このコードを手元のマシンでlsの代わりに使うことは、たぶんおすすめしません(もちろん使おうと思えば使えますが!)。これから見ていくように、lsは実のところ内部ではかなり高機能なツールなのです。ここでは完全な再実装を目指すのではなく、コマンドラインでls -lを実行したときに期待される、ごく基本的な出力だけを扱います。その出力とはどんなものでしょうか? 喜んでお答えしましょう。
期待される出力
> ls -l
drwxr-xr-x 2 mendler staff 13468 Feb 4 11:19 Top Secret
-rwxr--r-- 1 mendler staff 6323935 Mar 8 21:56 Never Gonna Give You Up - Rick Astley.mp3
-rw-r--r-- 1 mendler staff 0 Feb 18 23:55 Thoughts on Chess Boxing.doc
-rw-r--r-- 1 mendler staff 380434 Dec 24 16:00 nobel-prize-speech.txt出力は環境によって多少異なりますが、一般的には次のような注目すべき項目が並んでいます。左から順に見ていくと、次のようなフィールドがあります。
- 冒頭の
drwxのような部分はファイルパーミッション(ファイルモードとも呼ばれます)です。dが立っていればディレクトリを表します。rは読み取り、wは書き込み、xは実行を意味します。このrwxのパターンが、順に現在のユーザー、グループ、その他のユーザー向けに3回繰り返されます。 - 次は、ファイルであればハードリンク数、ディレクトリであれば含まれるディレクトリエントリの数です。(参考)
- 所有者の名前
- グループ名
- ファイルのバイト数
- ファイルが最後に変更された日時
- 最後にパス名
より詳しい情報については、ほとんどのLinuxディストリビューションで使われているGNU coreutilsのlsのマニュアルページや、macOSの基盤であるDarwinのマニュアルページを読むことをおすすめします。
ふう、こんな小さなツールにしては情報量が多いですね。とはいえ、これをRustに移植するのがそう難しいわけでもなさそうですよね? さっそく始めましょう!
Rustで作るごくシンプルなls
こちらがlsの最もミニマルなバージョンで、カレントディレクトリ内のすべてのファイルをただ表示するだけのものです。
use std::fs;
use std::path::Path;
use std::error::Error;
use std::process;
fn main() {
if let Err(ref e) = run(Path::new(".")) {
println!("{}", e);
process::exit(1);
}
}
fn run(dir: &Path) -> Result<(), Box<Error>> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let file_name = entry
.file_name()
.into_string()
.or_else(|f| Err(format!("Invalid entry: {:?}", f)))?;
println!("{}", file_name);
}
}
Ok(())
}これはドキュメントからそのままコピーしてきたものです。実行すると、期待通りの出力が得られます。
> cargo run
Cargo.lock
Cargo.toml
src
targetファイルを表示して終了します。シンプルですね。
ここで少し立ち止まって、ゼロから小さなUnixユーティリティを書き上げた成功を祝いましょう。Pro Tip: cargo installでバイナリをインストールすれば、今後は他のバイナリと同じように呼び出せます。
とはいえ、私たちはもっと上を目指すので、先に進みましょう。
ディレクトリを指定するパラメータを追加する
通常、ls mydirと入力すれば、mydir以外のどのディレクトリでもない、そのディレクトリのファイル一覧が表示されることを期待します。私たちのバージョンにも同じ機能を追加しましょう。
そのためにはコマンドライン引数を受け取る必要があります。こういう場合に私が気に入って使っているRustのクレートがstructoptです。引数のパースがとても簡単になります。
Cargo.tomlに追加してください。(以下のコマンドにはcargo-editが必要です)。
cargo add structoptあとはプロジェクトにインポートして使えます。
#[macro_use]
extern crate structopt;
// use std::...
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
struct Opt {
/// Output file
#[structopt(default_value = ".", parse(from_os_str))]
path: PathBuf,
}
fn main() {
let opt = Opt::from_args();
if let Err(ref e) = run(&opt.path) {
println!("{}", e);
process::exit(1);
}
}
fn run(dir: &PathBuf) -> Result<(), Box<Error>> {
// Same as before
}Opt構造体を追加することで、コマンドラインフラグや入力パラメータ、helpの出力を非常に簡単に定義できます。設定オプションは豊富にあるので、プロジェクトのホームページをチェックする価値があります。
また、パス変数の型をPathからPathBufに変更したことにも注目してください。違いは、PathBufは内部のパス文字列を所有するのに対し、Pathはそれへの参照を提供するだけという点です。この関係はStringと&strの関係に似ています。
更新時刻を読み取る
次はメタデータを扱いましょう。まずはファイルの更新時刻を取得してみます。ドキュメントをさっと見ると、やり方がわかります。
use std::fs;
let metadata = fs::metadata("foo.txt")?;
if let Ok(time) = metadata.modified() {
println!("{:?}", time);
}出力は期待と違うかもしれません。得られるのはSystemTimeオブジェクトで、これはシステムクロックの計測値を表します。たとえばこちらのコードは
println!("{:?}", SystemTime::now());
// Prints: SystemTime { tv_sec: 1520554933, tv_nsec: 610406401 }しかし私たちが欲しいフォーマットは、たとえば次のようなものです。
Mar 9 01:24ありがたいことに、chronoというライブラリがあり、このフォーマットを読み取って好きな人間が読める形式に変換してくれます。
let current: DateTime<Local> = DateTime::from(SystemTime::now());
println!("{}", current.format("%_d %b %H:%M").to_string());これは次のように表示されます。
9 Mar 01:29(ええ、もう夜遅いのはわかっています。)
この知識を武器に、ファイルの更新時刻を読み取ってみましょう。
cargo add chronouse chrono::{DateTime, Local};
fn run(dir: &PathBuf) -> Result<(), Box<Error>> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let file_name = ...
let metadata = entry.metadata()?;
let size = metadata.len();
let modified: DateTime<Local> = DateTime::from(metadata.modified()?);
println!(
"{:>5} {} {}",
size,
modified.format("%_d %b %H:%M").to_string(),
file_name
);
}
}
Ok(())
}この{:>5}は奇妙に見えるかもしれません。これはstd::fmtが提供するフォーマット指示子です。「このフィールドを幅5で右寄せし、余白をスペースで埋める」という意味で、まさに本家ls -lがやっていることと同じです。
同様に、ファイルサイズはmetadata.len()で取得しました。
Unixのファイルパーミッションは千差万別
ファイルパーミッションの読み取りはもう少し厄介です。rwxという表記は*BSDやGNU/LinuxといったUnix系では非常に一般的ですが、他の多くのOSは独自のパーミッション管理を採用しています。Unix系の間でも違いがあります。
Wikipediaでは、遭遇するかもしれないファイルパーミッションの拡張として、次のようなものが挙げられています。
- +(プラス)の接尾辞は、追加のパーミッションを制御できるアクセス制御リストが存在することを示します。
- .(ドット)の接尾辞は、SELinuxコンテキストが存在することを示します。詳細は
ls -Zコマンドで確認できます。 - @の接尾辞は、拡張ファイル属性が存在することを示します。
これだけ見ても、実際に実装する際には考慮すべき重要な詳細がたくさんあることがわかります。
ごく基本的なファイルモードの実装
今は基本に絞り、rwxファイルモードをサポートするプラットフォームにいると仮定しましょう。
r、w、xの裏側では、実際には8進数が使われています。その方がコンピュータは扱いやすく、熱心なユーザーの多くは記号よりも数字での入力を好むほどです。それらの8進数の背後にあるルールは次の通りです。chmodのマニュアルページから引用しました。
Modes may be absolute or symbolic.
An absolute mode is an octal number constructed
from the sum of one or more of the following values
0400 Allow read by owner.
0200 Allow write by owner.
0100 For files, allow execution by owner.
0040 Allow read by group members.
0020 Allow write by group members.
0010 For files, allow execution by group members.
0004 Allow read by others.
0002 Allow write by others.
0001 For files, allow execution by others.たとえば、所有者は読み取り・書き込み・実行ができ、他の誰も何もできないようにファイルのパーミッションを設定するなら、700(400 + 200 + 100)となります。
確かに、これらの数字は70年代から同じで、近いうちに変わることもありませんが、それでもファイルパーミッションを直接その値と比較するのは得策ではありません。互換性の理由でなくとも、可読性のため、そしてコード中のマジックナンバーを避けるためにもです。
そこで、これらのマジックナンバーに対する定数を提供してくれるlibcクレートを使います。前述の通り、これらのファイルパーミッションはUnix固有のものなので、そのためにstd::os::unix::fs::PermissionsExt;というUnix専用のライブラリをインポートする必要があります。
extern crate libc;
// Examples:
// * `S_IRGRP` stands for "read permission for group",
// * `S_IXUSR` stands for "execution permission for user"
use libc::{S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR};
use std::os::unix::fs::PermissionsExt;これでファイルパーミッションを次のように取得できます。
let metadata = entry.metadata()?;
let mode = metadata.permissions().mode();
parse_permissions(mode as u16);parse_permissions()は次のように定義された小さなヘルパー関数です。
fn parse_permissions(mode: u16) -> String {
let user = triplet(mode, S_IRUSR, S_IWUSR, S_IXUSR);
let group = triplet(mode, S_IRGRP, S_IWGRP, S_IXGRP);
let other = triplet(mode, S_IROTH, S_IWOTH, S_IXOTH);
[user, group, other].join("")
}これはファイルモードをu16として受け取り(単にlibcの定数がu16だからです)、それに対してtripletを呼び出します。各フラグread、write、executeについて、modeに対してビット単位の&演算を実行します。その出力は、考えられるすべてのパーミッションパターンに対して網羅的にマッチされます。
fn triplet(mode: u16, read: u16, write: u16, execute: u16) -> String {
match (mode & read, mode & write, mode & execute) {
(0, 0, 0) => "---",
(_, 0, 0) => "r--",
(0, _, 0) => "-w-",
(0, 0, _) => "--x",
(_, 0, _) => "r-x",
(_, _, 0) => "rw-",
(0, _, _) => "-wx",
(_, _, _) => "rwx",
}.to_string()
}まとめ
最終的な出力はこんな感じです。十分近いでしょう。
> cargo run
rw-r--r-- 7 6 Mar 23:10 .gitignore
rw-r--r-- 15618 8 Mar 00:41 Cargo.lock
rw-r--r-- 185 8 Mar 00:41 Cargo.toml
rwxr-xr-x 102 5 Mar 21:31 src
rwxr-xr-x 136 6 Mar 23:07 targetこれで完了です! おもちゃのlsの最終版はGithubで見つけられます。まだ本格的なlsの代替にはほど遠いですが、少なくとも内部の仕組みについて少しは学べました。
Rustで書かれたまともなlsの代替を探しているなら、lsdをぜひチェックしてみてください。代わりに同じシリーズの別のブログ記事を読みたいなら、A Little Story About the yes Unix Commandをご覧ください。
記事をランダムに読む
コメント
ログインしてコメントする