Rustについて基本からしっかり学んでいく本連載。第16回は、ファイルやディレクトリのパスの操作、そしてファイルシステムの操作について。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
連載第15回では、ファイルの入出力を紹介しました。その際、ファイル名をopen関数などに与えて対象のファイルを指定しましたが、実際にはパスを表すPath構造体のインスタンスが与えられていることに触れました。ここでは、Path構造体とその可変版であるPathBuf構造体を掘り下げて、パス文字列の自由自在の操作を紹介します。
Path構造体とPathBuf構造体は、std::pathモジュールにあります。std::pathモジュールには7個の構造体がありますが、代表的なのはこの2つです。それぞれのおおまかな役割は以下の通りです。
Path構造体ではパスは不変となり、参照系の操作しか受け付けませんが、PathBuf構造体ではパスは可変となり、更新系の操作を受け付けます。では、常にPathBuf構造体を使えばいいかというとそうではなく、PathBuf構造体は判定や取得などの関数は持たないので、それらの機能が必要ならPath構造体を使う必要があります。つまり、以下のような使い分けになります。
Path構造体の判定関数も、PathBuf構造体の加工関数も、OSのパスの区切り文字などを極力意識しないで操作できるようになっています。文字列操作関数をパスの判定や加工に使うと、どうしても区切り文字などを意識しなければなりませんが、そういった配慮が不要なので極めて容易にパスが操作できるようになっています。
以降、Path構造体を中心にさまざまな判定の例を紹介し、PathBuf構造体を用いたパスの加工についても例を紹介します。
Path構造体には、主に以下の関数が実装されています。
それぞれ、代表的なものを例とともに紹介していきます。最初はインスタンスの生成です。Path構造体のインスタンスは、new関数で生成します。引数に、文字列リテラル(&str)、String型の参照、そしてPath型のいずれかを受け取ってインスタンスを生成できます。以下は、3つの方法でPath構造体のインスタンスを生成し、文字列として表示しています。
- use std::path::Path; (1)
- fn main() {
- let path_str = Path::new("str.txt"); (2)
- let string = String::from("string.txt"); (3)
- let path_string = Path::new(&string);
- let path_path = Path::new(path_string); (4)
- println!("&str: {}", path_str.to_str().unwrap()); (5)
- // &str: str.txt
- println!("String: {}", path_string.to_str().unwrap());
- // String: string.txt
- println!("Path: {}", path_path.to_str().unwrap());
- // Path: string.txt
- }
(1)は、Path構造体を使うために必要なuse文です。(2)では文字列リテラルから、(3)ではString型から、(4)ではPath型から、それぞれインスタンスを生成しています。(5)以降では、Path構造体のto_str関数を用いて、Path構造体を文字列として表示しています。to_str関数はOption型の列挙体を返すので、unwrap関数で値を取り出しています。
Path構造体は、さまざまな判定関数を持ちます。これにより、指定されるパスがファイルなのかディレクトリなのか、絶対パスなのか相対パスなのかといった判定や、パスの先頭部分や末尾部分の一致、不一致の判定が可能です。このように、パスの操作にはファイルシステムに絡んだ操作も含まれています。以下は、ファイルとディレクトリのパスに対して、判定関数を使った結果を表示しています。
- let file_str = "src/bin/path_is.rs";
- let dir_str = "target";
- let path_file = Path::new(file_str);
- let path_dir = Path::new(dir_str);
- println!("{} is file: {}", file_str, path_file.is_file()); (1)
- println!("{} is directory: {}", dir_str, path_dir.is_dir());
- println!("{} is absolute: {}", dir_str, path_dir.is_absolute());
- println!("{} is relative: {}", dir_str, path_dir.is_relative());
- println!("{} has root: {}", dir_str, path_dir.has_root()); (2)
- println!("{} exists: {}", file_str, path_file.exists());
- println!("{} starts with 'src': {}", file_str, path_file.starts_with("src")); (3)
- println!("{} ends with 'path_is.rs': {}", file_str,
- path_file.ends_with("path_is.rs"));
実行結果は、下記です。
src/bin/path_is.rs is file: true target is directory: true target is absolute: false target is relative: true target has root: false src/bin/path_is.rs exists: true src/bin/path_is.rs starts with 'src': true src/bin/path_is.rs ends with 'path_is.rs': true
(1)からの4行は、ファイルであるかどうか、ディレクトリであるかどうか、絶対パスであるかどうか、相対パスであるかどうかの判定です。(2)からの2行は、パスがルート部分を持っているかどうか、パスが存在するかどうかの判定です。(3)からの2行は、パスが"src"で始まるかどうか、"path_is.rs"で終わるかどうかの判定です。ここで使用しているstarts_with関数、ends_with関数は、単なる文字列のマッチングではなく、パスの構成要素でなければならないことに注意してください。例えばends_with関数の引数を".rs"にすると結果はfalseになります。
Path構造体は、さまざまな取得関数も持ちます。これにより、パスからファイル名だけ、あるいは拡張子だけを切り出す、パスの構成要素を順番に取得するといった操作が可能になります。以下は、パスから正規形(Canonical)、ファイル名(File)、ファイル名から拡張子を除いた部分(Stem)、拡張子(Extension)を取得しています。
- let file_str = "src/bin/path_is.rs";
- let path_file = Path::new(file_str);
- println!("Of {} ...", file_str);
- println!("Canonical: {}", path_file.canonicalize().unwrap().to_str().unwrap());
- println!("File: {}", path_file.file_name().unwrap().to_str().unwrap());
- println!("Stem: {}", path_file.file_stem().unwrap().to_str().unwrap());
- println!("Extenstion: {}", path_file.extension().unwrap().to_str().unwrap());
実行結果は、下記です。
Of src/bin/path_is.rs ... Canonical: /Users/nao/Documents/atmarkit_rust/filesystems/src/bin/path_is.rs File: path_is.rs Stem: path_is Extenstion: rs
そして以下は、iter関数を用いてパスを要素に分解して表示しています。
- let file_str = "src/bin/path_iter.rs";
- let path_file = Path::new(file_str);
- println!("Part of {}", file_str);
- for elem in path_file.iter() {
- println!("{}", elem.to_str().unwrap());
- }
実行結果は、下記です。
Part of src/bin/path_iter.rs src bin path_iter.rs
Path構造体はイミュータブル(変更不可)なので、パスを加工したいなどという場合にはPathBuf構造体を利用します。PathBuf構造体はPath構造体と異なり、まず中身が空のインスタンスを生成します。また、Path構造体のjoin関数、to_path_buf関数、with_file_name関数、with_extension関数などを使ってもインスタンスを生成できます。
- use std::path::{Path, PathBuf}; (1)
- fn main() {
- let path_file = Path::new("src/bin/path_is.rs");
- let path_buf = path_file.to_path_buf(); (2)
- let path_buf_file = path_file.with_file_name("source.c"); (3)
- let path_buf_ext = path_file.with_extension("bak"); (4)
- println!("PathBuf: {}", path_buf.as_path().to_str().unwrap());
- // PathBuf: src/bin/path_is.rs
- println!("PathBuf with filename: {}", path_buf_file.as_path().to_str().unwrap());
- // PathBuf with filename: src/bin/source.c
- println!("PathBuf with extension: {}", path_buf_ext.as_path().to_str().unwrap());
- // PathBuf with extension: src/bin/path_is.bak
- }
(1)は、Path構造体とPathBuf構造体を使うというuse文です。ここでは、3種類の方法でPathBuf構造体のインスタンスを生成しています。(2)to_path_buf関数、(3)with_file_name関数、(4)with_extension関数です。それぞれ、生成後のインスタンスの内容を表示していますが、as_path関数を使ってPath構造体の参照を取得し、それに対してto_str関数を使用しています。PathBuf構造体には、直接表示可能な文字列を取得する関数がないので、このようにPath構造体から操作をすることになります。
パスへの追加は、push関数を使用します。push関数は、Path構造体をはじめとするAsRef<Path>トレイトを実装した型を引数に受け取り、現在のパスの末尾に追加します。
- let mut path_buf = PathBuf::new(); (1)
- let path_file = Path::new("src/bin/path_push.rs");
- path_buf.push(path_file); (2)
- println!("PathBuf: {}", path_buf.as_path().to_str().unwrap());
- // PathBuf: src/bin/path_push.rs
- path_buf.clear(); (3)
- path_buf.push("src"); (4)
- path_buf.push("bin");
- path_buf.push("path_push.rs");
- println!("PathBuf: {}", path_buf.as_path().to_str().unwrap());
- // PathBuf: src/bin/path_push.rs
ここでは、(1)で生成した空のPathBuf構造体のインスタンスに、push関数で別のPath構造体のインスタンスを追加しています(2)。また、clear関数でパスを消去した後(3)、個別にパスの構成要素を追加しています(4)。このとき、パスの区切り文字であるスラッシュ(/)を含まないことに注意してください。仮にスラッシュを含む"/bin"などをpushすると、それはルートからのパスと見なされ、すでにあるパスはそれに置き換わってしまいます。
なお、pop関数を使うと、パスの末尾方向から要素を削除します。これ以上削除できなくなると、最後の削除でtrueが返されます。
パスの区切り文字は、Unix系のOSではスラッシュ(/)、Windowsでは円記号およびバックスラッシュ(\)です。パスをハードコードするときに、パスの区切りにスラッシュを用いれば、macOS環境でもWindows環境でも正しく動作します。ただし円記号等を用いた場合、Windows環境での動作には問題ありませんが、macOS環境では正しく動作しません。ですので、パスの区切り文字にはスラッシュを使うようにしましょう。
std::fsモジュールには、連載第15回で紹介したopenやcreateといった関数の他に、表1に示すようにファイルシステムの操作のための便利な関数が多数あります。
分類 | 関数 |
---|---|
作成系 | copy(属性を含めたファイルのコピー)、create_dir(ディレクトリの作成)、create_dir_all(ディレクトリの再帰的な作成)、hard_link(ハードリンクの作成) |
読み出し系 | read(ファイルの読み込み)read_to_string(ファイルの内容を全て文字列として読み込む) |
書き込み系 | write(ファイルに書き込む) |
削除系 | remove_dir(ディレクトリの削除)、remove_dir_all(ディレクトリの再帰的な削除)、remove_file(ファイルの削除) |
変更系 | rename(ファイルおよびディレクトリ名の変更)、set_permissions(パーミッションの変更) |
取得系 | canonicalize(正規のパスを取得)、metadata(パスのメタデータを取得)、read_dir(ディレクトリのイテレータを取得)、read_link(シンボリックリンクのリンク先を取得)、symlink_metadata(シンボリックリンクのメタデータを取得) |
表1 std::fsモジュールの関数 |
ここでは、代表的な操作(作成、名称変更、削除など)に絞って関数を紹介します。
ファイルの内容が単純なものならば、ファイルの作成と書き込み、あるいは読み込みを1個の関数で実行できます。write関数は、引数の内容(文字列など)を指定するファイルに書き込みます。read関数は、指定するファイルから読み込んでbyte型のベクターを返します。既出のread_to_string関数は、指定するファイルから読み込んで文字列を返します。以下は、write関数でファイルの作成と簡単なメッセージを書き込み、それをread関数およびread_to_string関数で読み込んで表示する例です。関数名は同じですが、readトレイト、writeトレイト、File構造体の関数でないことに注意してください。
- use std::fs;
- fn main() {
- let s = "ningen_shikkaku.txt";
- fs::write(s, "by O. Dazai").unwrap();
- println!("read: {}", String::from_utf8(fs::read(s).unwrap()).unwrap());
- println!("read_to_string: {}", fs::read_to_string(s).unwrap());
- }
実行結果は、下記です。
read: by O. Dazai read_to_string: by O. Dazai
write関数では、ファイルがすでに存在する場合、上書きされます。read関数で返されるのはあくまでもベクターなので、結果を文字列で欲しい場合には、例えばString::from_utf8関数でベクターから文字列に変換してあげる必要があります。read関数、from_utf8関数ともにResult<>型を返すので、unwrap関数で値を取り出しています。read_to_string関数を使うと、文字列に限定して読み込むことができます。
ディレクトリを作成するのは、create_dir関数とcreate_dir_all関数です。両者の違いは、指定するパスの途中のディレクトリが存在している必要があるかどうか(create_dir)、存在しない場合には再帰的に作成するかどうか(create_dir_all)、です。create_dir_all関数は途中のディレクトリも作成してくれるので便利ですが、意図しないパスを指定しても作成されてしまうので、使用時には注意が必要です。以下は、ディレクトリtempをcreate_dir関数で作成し、その後にcreate_dir_all関数で階層の深いディレクトリを作成する例です。
- fs::create_dir("temp").unwrap(); (1)
- fs::create_dir_all("temp/one/two/three").unwrap(); (2)
tempディレクトリがすでに存在していれば、(1)はpanicとなります(panicにしたくない場合、実行前にtempディレクトリを削除してください)。(2)は、tempディレクトリの存在にかかわらず実行されます。最も下位のthreeディレクトリが存在していてもエラーとはなりません。それは、create_dir_all関数が、パス途中のディレクトリを必要な場合なときのみ作成するからです。
削除する関数には、remove_file、remove_dir、remove_dir_allがあります。それぞれ、ファイルの削除、ディレクトリの削除、下位のファイルとディレクトリを含んだディレクトリの削除に対応します。以下は、ディレクトリtemp以下をまず削除し、ディレクトリを作成、ディレクトリ内にファイルを作成、ファイルを削除、ディレクトリを削除する例です。
- let d = "temp"; // ディレクトリ名
- let f = "temp/hello.txt"; // ファイル名
- fs::remove_dir_all(d).unwrap(); // ディレクトリ以下を全部削除
- fs::create_dir(d).unwrap(); // ディレクトリを作成
- fs::write(f, "Hello").unwrap(); // ファイルを作成
- fs::remove_file(f).unwrap(); // ファイルを削除
- fs::remove_dir(d).unwrap(); // ディレクトリを削除
remove_dir_all関数は、指定したディレクトリ以下のファイルとディレクトリも全て削除します。remove_dir関数は、指定したディレクトリが空でないと削除できません。
rename関数は、ファイルとディレクトリの名称を変更します(リネーム)。同一のディレクトリ内での名称変更と、異なる階層への名称変更(ムーブ)の双方が可能です。以下は、rename関数の例です。サンプルcopy.rsで作成されたneko.txtをcat.txtにリネームし、さらにtemp/cat.txtにムーブします。
- // neko.txtをcat.txtにリネーム
- fs::rename("neko.txt", "cat.txt").unwrap();
- // cat.txtをtempディレクトリに移動
- fs::rename("cat.txt", "temp/cat.txt").unwrap();
このとき、リネームまたはムーブ先のファイルやディレクトリが存在する場合、それは上書きされますので注意が必要です。エラーはResult<()>型で返します。
copy関数は、ファイルをコピーします。ファイルのオープンとは無関係に、ファイルの内容をそのままコピーしたいときに有用です。以下は、copy関数の例です。
- fs::copy("wagahaiwa_nekodearu.txt", "neko.txt").unwrap();
実行すると、ファイルwagahaiwa_nekodearu.txtがファイルneko.txtにコピーされます。このとき、neko.txtが存在している場合には上書きされますので、上書きを避けたい場合には事前に存在のチェックが必要です。また、ファイルのパーミッションや属性などもそのままコピーされます。
copy関数の戻り値はResult<i64>型で、正常終了時にはコピーしたバイト数が返されます。
今回は、ファイルやディレクトリのパスの操作、ファイルシステムの操作について紹介しました。
次回は、スマートポインタを紹介します。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.
Coding Edge 髫ェ蛟�スコ荵斟帷ケ晢スウ郢ァ�ュ郢晢スウ郢ァ�ー