Rustで書いた小さな `ls` クローン
私のシリーズ「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のmanページや、macOSの基盤であるDarwinのmanページを読むことをおすすめします。
ふう、こんな小さなツールにしては情報量が多いですね。とはいえ、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ユーティリティを書き上げた成功を祝いましょう。ワンポイント: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変数の型を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のmanページから引用しました。
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をご覧ください。
記事をランダムに読む