🦀

Ruby の Pathname みたいなライブラリが Rust にもあった

2022/03/26に公開
2

引数にファイル名を渡すのではなくファイル名をレシーバとして各種メソッドを呼ぶ Ruby の Pathname のようなライブラリが Rust にもあった。

単に文字列として構造を確認する系

Path::new("/aa/bb/cc.tar.gz").is_absolute()  // => true
Path::new("cc.tar.gz").is_relative()         // => true
Path::new("/aa/bb/cc.tar.gz").has_root()     // => true
Path::new("/aa/bb/cc.tar.gz").starts_with("/aa/bb")   // => true
Path::new("/aa/bb/cc.tar.gz").ends_with("cc.tar.gz")  // => true

文字列とはいえパーツの境界を無視してはいけない。starts_with("/a")ends_with("gz") などは false になる。

各パーツを取得する

Path::new("/aa/bb/cc.tar.gz").extension()    // => Some("gz")
Path::new("/aa/bb/cc.tar.gz").file_name()    // => Some("cc.tar.gz")
Path::new("/aa/bb/cc.tar.gz").file_stem()    // => Some("cc.tar")
Path::new("/aa/bb/cc.tar.gz").file_prefix()  // => Some("cc")
Path::new("/aa/bb/cc.tar.gz").parent()       // => Some("/aa/bb")

stem は「幹」という意味らしい。拡張子を省いたファイル名の取得方法が明確なのがありがたい。Ruby ではよく使うのに basename(".*") のように書かないといけなくて美しくなかった。

全パーツを順に取得する

Path::new("/aa/bb/cc.tar.gz").ancestors().collect::<Vec<_>>()   // => ["/aa/bb/cc.tar.gz", "/aa/bb", "/aa", "/"]
Path::new("/aa/bb/cc.tar.gz").components().collect::<Vec<_>>()  // => [RootDir, Normal("aa"), Normal("bb"), Normal("cc.tar.gz")]
Path::new("/aa/bb/cc.tar.gz").iter().collect::<Vec<_>>()        // => ["/", "aa", "bb", "cc.tar.gz"]

components はなんかおおごとになっている感があるけど Rust では enum で列挙した定義でそのままラップできるので気軽にこういうことをやってしまう傾向がある。

パス作成・調整

// パスを追加していく(よく使う)
Path::new("/aa").join("bb").join("cc.tar.gz")         // => "/aa/bb/cc.tar.gz"

// ファイル名・拡張子の置き換え
Path::new("/aa/bb/cc.tar.gz").with_file_name("xxx")   // => "/aa/bb/xxx"
Path::new("/aa/bb/cc.tar.gz").with_extension("xxx")   // => "/aa/bb/cc.tar.xxx"

// 祖先のディレクトリ除去
Path::new("/aa/bb/cc.tar.gz").strip_prefix("/aa/bb")  // => Ok("cc.tar.gz")

新しい Path 型インスタンスを返してくれるので使いやすい。

ファイルシステムに少しアクセスする

Path::new("/bin").exists()       // => true
Path::new("/bin").try_exists()   // => Ok(true)
Path::new("/bin/cat").is_file()  // => true
Path::new("/bin").is_dir()       // => true
Path::new("/etc").is_symlink()   // => true

メソッド名がわかりやすい。

ファイルシステムにがっつりアクセスする

Path::new("/etc").metadata()                                       // => Ok(Metadata { file_type: FileType(FileType { mode: 16877 }), is_dir: true, is_file: false, permissions: Permissions(FilePermissions { mode: 16877 }), modified: Ok(SystemTime { tv_sec: 1646948709, tv_nsec: 53641797 }), accessed: Ok(SystemTime { tv_sec: 1646964920, tv_nsec: 777256864 }), created: Ok(SystemTime { tv_sec: 1577865600, tv_nsec: 0 }), .. })
Path::new("/etc").symlink_metadata()                               // => Ok(Metadata { file_type: FileType(FileType { mode: 41453 }), is_dir: false, is_file: false, permissions: Permissions(FilePermissions { mode: 41453 }), modified: Ok(SystemTime { tv_sec: 1577865600, tv_nsec: 0 }), accessed: Ok(SystemTime { tv_sec: 1577865600, tv_nsec: 0 }), created: Ok(SystemTime { tv_sec: 1577865600, tv_nsec: 0 }), .. })
Path::new("/etc").read_link()                                      // => Ok("private/etc")
Path::new("/etc").read_dir().unwrap().take(2).collect::<Vec<_>>()  // => [Ok(DirEntry("/etc/kcpassword")), Ok(DirEntry("/etc/hosts~"))]

肝心の glob はないようだ。

続き↓

https://zenn.dev/megeton/articles/5e16c6a0b395a4

正規

// . を含むパスを正規化する
Path::new("/usr/bin/..").canonicalize()     // => Ok("/usr")
Path::new("../../../../..").canonicalize()  // => Ok("/Users")

文字列操作だけでなくファイルシステムの構造も実際に見ている。~/ は展開してくれなかった。

型変

// 表示する用の文字列 (メソッド名にかなり違和感はある)
Path::new("/aa/bb/cc.tar.gz").display()          // => "/aa/bb/cc.tar.gz"

// OsStr 型にする (するとどうなる?)
let s = Path::new("/aa/bb/cc.tar.gz").as_os_str()
s                                                // => "/aa/bb/cc.tar.gz"
std::any::type_name_of_val(s)                    // => "std::ffi::os_str::OsStr"

// 不正な文字があると失敗する版
Path::new("/aa/bb/cc.tar.gz").to_str()           // => Some("/aa/bb/cc.tar.gz")

// 不正な文字は無視して強行する版
Path::new("/aa/bb/cc.tar.gz").to_string_lossy()  // => "/aa/bb/cc.tar.gz"

// PathBuf 型にする
let path = Path::new("/aa/bb/cc.tar.gz").to_path_buf();
std::any::type_name_of_val(&path)                // => "std::path::PathBuf"

PathBuf 型にすると自分を更新できる

let mut path = PathBuf::new();
path.push("/");
path.push("aa");
path.push("xx");
path  // => "/aa/xx"
path.pop();
path  // => "/aa"
path.push("bb");
path  // => "/aa/bb"
path.push("xxx.txt");
path  // => "/aa/bb/xxx.txt"

// set_file_name はいったん pop して push するのと同等っぽい
path.set_file_name("cc.tar.xx");
path  // => "/aa/bb/cc.tar.xx"

path.set_extension("gz");

// 完成
path  // => "/aa/bb/cc.tar.gz"
// 更新が終わったら Path 型に戻しておこう
let path = path.as_path();
std::any::type_name_of_val(path)  // => "std::path::Path"

Path 型だけで柔軟な操作ができるので PathBuf はあまり使わなそう。

~/ を展開してくれるやつはない?

Ruby
Pathname("~/").expand_path  # => "/Users/alice"

これをやりたいけど対応するメソッドが見つからなかった。プラットフォームに依存するので標準では用意してないのかもしれない。

続き↓

https://zenn.dev/megeton/articles/f9ef31fbbbada3

Discussion

kyoheiukyoheiu

ホームディレクトリ展開についてはdirs - Rustとパターンマッチングなどの組み合わせで読み取るのが現状一番お手軽だと思います。Linux, Windows, Macなど主要OSに対応。
ホームディレクトリ以外に.configなども読んでくれるなど、わりとよく使うクレートですね。

megetonmegeton

助言ありがとうございます!
いろんなディレクトリを取得したいときは dirs がいいみたいですね。
単に Path の文字列に含まれる ~/ を展開したいときは home_dir がぴったりでした。