std:fs — ファイルシステム
標準ライブラリモジュール (index.md)。本書中の裸の §N は本書の節を指す。言語の意味論は ../language-spec.md を参照。
import fs := "std:fs" は read / read_bytes / write / write_bytes / mkdir / mkdtemp / readdir / walk / remove / remove_all / rename / exists / stat / open / open_rw の 15 slot と、型メンバー File を持つ namespace object を fs に束縛する。名前 destructure (language-spec.md §6.3) で個別に取り出すか、namespace のまま slot アクセスする:
# 個別に引く import { read, write } := "std:fs" content := read("foo.hika")!.unwrap! # namespace のまま使う import fs := "std:fs" content := fs.read("foo.hika")!.unwrap!
| 名前 | 形 | 効果 | 意味 |
|---|---|---|---|
read |
read(path) |
Fs |
path のファイルを UTF-8 テキストとして読み、Future (Result(String)) を返す |
read_bytes |
read_bytes(path) |
Fs |
path のファイルを Bytes として読み (デコードなし)、Future (Result(Bytes)) を返す |
write |
write(path, content) |
Fs |
path に content を UTF-8 テキストとして書き出し (上書き)、Future (Result(Unit)) を返す |
write_bytes |
write_bytes(path, content) |
Fs |
path に Bytes content をそのまま書き出し (上書き)、Future (Result(Unit)) を返す |
mkdir |
mkdir(path) |
Fs |
path を親ごと再帰作成 (冪等)、Future (Result(Unit)) を返す |
mkdtemp |
mkdtemp(prefix) |
Fs |
ホストの一時ディレクトリ直下に prefix で始まる一意な dir を作り、その完全パスを Future (Result(String)) で返す |
readdir |
readdir(path) |
Fs |
path 直下のエントリ ({name, is_dir} の List) を読み、Future (Result(List)) を返す |
walk |
walk(path) |
Fs |
path 配下の通常ファイルのパス (String の List) を再帰的に集め、Future (Result(List)) を返す |
remove |
remove(path) |
Fs |
path (ファイル or 空ディレクトリ) を 1 つ削除、Future (Result(Unit)) を返す |
remove_all |
remove_all(path) |
Fs |
path を中身ごと再帰削除 (冪等)、Future (Result(Unit)) を返す |
rename |
rename(old, new) |
Fs |
old を new へ改名/移動、Future (Result(Unit)) を返す |
exists |
exists(path) |
Fs |
path の存在を確かめ、Future (Result(Bool)) を返す |
stat |
stat(path) |
Fs |
path のメタ情報 ({size, mtime, is_dir}) を読み、Future (Result) を返す |
以下の例では import fs := "std:fs" で束縛したものとして fs.read 等で記す。全関数は Future を返し、! で resolve 値 (Result) を待つ。非 String 引数は呼び出し位置で Error (panic 型, language-spec.md §16)。
1. fs.read
- 引数:
path(String) 1 個。非 String を渡すと呼び出し位置でError - 戻り値:
Future。!(=()) で resolve 値を待つ (prelude.md §9.4 のブロッキング I/O 規則に乗る) - resolve 値は
Result(String): - 成功時:
Ok(content)— content はファイル内容の String (UTF-8 として読む) - 失敗時:
Err(e)— e は回復可能エラーオブジェクト{kind, message}(§14)。kindは機械可読な種別 String、messageは人間可読 String
match fs.read("foo.hika")! { Ok(content) => use(content) Err(e) => match e.kind { "not_found" => print("no such file") _ => print(e.message) # default = 未知 kind } }
ファイル内容に不正な UTF-8 バイト列が含まれる場合の挙動は実装定義 (バイト列のまま String として渡す可能性があり、後続の文字列処理が壊れることがある)。バイナリやエンコーディングの不確かなデータを扱うなら fs.read_bytes を使うこと。
2. fs.read_bytes
- 引数:
path(String) 1 個。非 String を渡すと呼び出し位置でError - 戻り値:
Future。!(=()) で resolve 値を待つ - resolve 値は
Result(Bytes): - 成功時:
Ok(content)— content はファイルの生バイト列 (Bytes、デコードなし) - 失敗時:
Err(e)— e は回復可能エラーオブジェクト{kind, message}(§14)。kindは機械可読な種別 String、messageは人間可読 String
match fs.read_bytes("image.png")! { Ok(content) => print(content.length!) # content.0 は先頭バイト (Int 0〜255) Err(e) => print(e.message) }
UTF-8 として解釈したい場合は content.to_string! を呼ぶ。不正 UTF-8 なら None が返る。生バイト列をそのまま別ファイルへ書くなら fs.write_bytes (§4) と対で使う。
3. fs.write
- 引数:
(path, content)2-tuple。両方 String。非 String を渡すと呼び出し位置でError - 既存ファイルは 上書き (truncate 後に書き込み)
- 親ディレクトリが存在しない場合は失敗 (mkdir はしない。先に
fs.mkdirで作る) - 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result(Unit): - 成功時:
Ok(())— success に値は無い (Result(Unit)) - 失敗時:
Err(e)— e は回復可能エラーオブジェクト{kind, message}(§14)。kindは機械可読な種別 String、messageは人間可読 String
match fs.write("out.txt", "hello")! { Ok(_) => () Err(e) => print(e.message) }
バイト列をそのまま書くなら fs.write_bytes (§4) を使う。
4. fs.write_bytes
- 引数:
(path, content)2-tuple。pathは String、contentは Bytes。型違いは呼び出し位置でError contentのバイト列を デコード/変換せずそのまま 書く。fs.read_bytes(§2) で読んだ生バイト列をそのまま書き戻せる- 既存ファイルは 上書き (truncate 後に書き込み)
- 親ディレクトリが存在しない場合は失敗 (mkdir はしない)
- 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result(Unit): - 成功時:
Ok(()) - 失敗時:
Err(e)—{kind, message}(§14)
match fs.read_bytes("a.png")! { Ok(bytes) => fs.write_bytes("b.png", bytes)!.unwrap! # 生バイト列をそのままコピー Err(e) => print(e.message) }
テキストを書くなら fs.write (§3)。
5. fs.mkdir
- 引数:
path(String) 1 個。非 String を渡すと呼び出し位置でError - 親ディレクトリごと 再帰的に作成 し、冪等 (既に同じ dir が存在しても成功する。
mkdir -p相当) - 経路上の構成要素に 同名の非ディレクトリ (既存ファイル等) があると失敗
- 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result(Unit): - 成功時:
Ok(()) - 失敗時:
Err(e)—{kind, message}(§14)。経路上に非ディレクトリがあるときはio_error
match fs.mkdir("data/2026-06-19")! { Ok(_) => () Err(e) => print(e.message) }
5.1 fs.mkdtemp
- 引数:
prefix(String) 1 個。非 String を渡すと呼び出し位置でError - ホストの一時ディレクトリ (
TMPDIR等。どこかは実装定義) の直下に、prefixで始まる一意な名前のディレクトリを作り、その完全パスを返す - 名前の残りは実装が選ぶ。同じ
prefixで何度呼んでも衝突しない — 作成は排他的で、既にあるディレクトリのパスを返すことはない - unix では
0700で作る。一時ディレクトリは他の利用者と共有する場所なので、umask 任せにすると中身を差し替えられる - 自動では消えない。 プロセスが終わっても残る。後始末は呼び手の責任で、相手が §7.1
fs.remove_allである prefixにパス区切りを含めてはならない (含むとio_error)。置き場所を選ぶ口ではない — 場所を選びたいならmkdirで自分の名前を作る- 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result(String): - 成功時:
Ok(path)— 作ったディレクトリの完全パス - 失敗時:
Err(e)—{kind, message}(§14)。一時ディレクトリへ書けないときはpermission_denied/io_error
dir := fs.mkdtemp("hikari-build-")!.unwrap! fs.write(path.join([dir, "out.txt"]), body)!? fs.remove_all(dir)!?
6. fs.readdir
- 引数:
path(String) 1 個。非 String を渡すと呼び出し位置でError - 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result(List): - 成功時:
Ok(entries)—entriesはpath直下のエントリの List。各要素はスロット{name, is_dir}を持つ closed・immutable なレコード: name— エントリの basename (String。path自体は含まない)is_dir— ディレクトリならtrue、それ以外はfalse(Bool)- 失敗時:
Err(e)—{kind, message}(§14)。path不在はnot_found、pathがディレクトリでないときはio_error - 列挙順は
nameの昇順で決定的 ./..は含めない。ドットファイル (.trash等) は含むis_dirはリンクを辿らずに決める (§12.1)。ディレクトリへのリンクはis_dir=falseである
match fs.readdir(".")! { Ok(entries) => entries.each { e | print(e.name) } Err(e) => print(e.message) }
6.1 fs.walk
- 引数:
path(String) 1 個。非 String を渡すと呼び出し位置でError readdirの再帰版。path配下の通常ファイルのパスを深さ優先で集める。ディレクトリ自身は結果に含めない- 集めるのは通常ファイルだけである。ディレクトリ・シンボリックリンク・名前付きパイプ・ソケット・デバイスは結果に載せない — 通常ファイルでないものを混ぜると、受け取った側が
readして止まる - ツリーの中のリンクは辿らない (§12.1)。ディレクトリへのリンクの下へは降りず、壊れたリンクがあっても歩き続ける。
path自身がリンクならそれは辿る pathが通常ファイルなら[path]を返す (単一要素)- 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result(List): - 成功時:
Ok(paths)—pathsはStringの List。各要素はpathを接頭にした完全パス (readdirの basename とは異なる) - 失敗時:
Err(e)—{kind, message}(§14)。path不在はnot_found - 列挙順は パスの昇順で決定的。ドットファイルも含む
match fs.walk("docs")! { Ok(files) => files.each { p | print(p) } # docs/specs/std/fs.md ... Err(e) => print(e.message) }
7. fs.remove
- 引数:
path(String) 1 個。非 String を渡すと呼び出し位置でError - ファイル または空のディレクトリ を 1 つ削除する。再帰削除はしない
- 非空のディレクトリを渡すと失敗 (
io_error)。ディレクトリツリーを消すなら §7.1fs.remove_allを使う - 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result(Unit): - 成功時:
Ok(()) - 失敗時:
Err(e)—{kind, message}(§14)。path不在はnot_found、非空ディレクトリはio_error
match fs.remove("out.txt")! { Ok(_) => () Err(e) => print(e.message) }
7.1 fs.remove_all
- 引数:
path(String) 1 個。非 String を渡すと呼び出し位置でError removeの再帰版。pathがファイルならそれを、ディレクトリなら中身ごと消す (rm -rf相当)- 冪等 —
pathが不在でもOk(())を返す。remove(不在はnot_found) とわざと違える。後始末が求めているのは「無くなっていること」であって、既に無いのは失敗ではない。mkdirが「既にあっても成功」なのと同じ向きである - シンボリックリンクを辿らない。
path自身がリンクならリンクを消し、リンク先には触れない。ツリーの中のリンクも同じ — 辿ると消す対象の外まで消えてしまう。path自身も辿らない点だけ §12.1 の既定と違う (消す操作なので、名指されたリンクの先を消すわけにはいかない) - 途中で失敗したときにどこまで消えたかは実装定義。部分的に消えたツリーが残りうるので、失敗を握り潰して先へ進んではならない
- 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result(Unit): - 成功時:
Ok(())—pathが無くなった (元から無かった場合を含む) - 失敗時:
Err(e)—{kind, message}(§14)。権限不足はpermission_denied、それ以外はio_error
match fs.remove_all("build/stage")! { Ok(_) => () Err(e) => print(e.message) }
8. fs.rename
- 引数:
(old, new)2-tuple。両方 String。非 String を渡すと呼び出し位置でError oldをnewへ改名・移動する- 既存の 宛先ファイルは上書き する。宛先が非空ディレクトリのときは失敗 (
io_error) - 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result(Unit): - 成功時:
Ok(()) - 失敗時:
Err(e)—{kind, message}(§14)。old不在・newの親ディレクトリ不在はnot_found、ファイルシステムを跨ぐ移動 (EXDEV) はio_error(自動コピーはしない)
match fs.rename("a.md", ".trash/a.md")! { Ok(_) => () Err(e) => print(e.message) }
9. fs.exists
- 引数:
path(String) 1 個。非 String を渡すと呼び出し位置でError - 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result(Bool): - 存在する:
Ok(true) - 確実に存在しない:
Ok(false) - 判定自体が失敗 (権限不足など):
Err(e)—{kind, message}(§14) - 不在 (
not_found相当) は エラーにせずOk(false)へ倒す。Errは「存在を確かめられなかった」場合に限る
match fs.exists("data")! { Ok(true) => () Ok(false) => fs.mkdir("data")!.unwrap! Err(e) => print(e.message) }
10. fs.stat
- 引数:
path(String) 1 個。非 String を渡すと呼び出し位置でError - 戻り値:
Future。!で resolve 値を待つ - resolve 値は
Result: - 成功時:
Ok(info)—infoはスロット{size, mtime, is_dir}を持つ closed・immutable なレコード: size— バイト数 (Int)。ディレクトリに対する値は OS 定義で、意味を持たないmtime— 最終更新時刻を epoch ミリ秒 で表す Int (now()と同じ単位・基点)is_dir— ディレクトリならtrue、それ以外はfalse(Bool)- 失敗時:
Err(e)—{kind, message}(§14)。path不在はnot_found
match fs.stat("note.md")! { Ok(info) => print(info.size) # info.mtime はミリ秒 Int、info.is_dir は Bool Err(e) => print(e.message) }
11. ファイルハンドル File — 位置指定の部分読み書き
全体読み書き (read / write 等) がファイル 1 個 = 値 1 個の写像であるのに対し、File ハンドルはファイルの一部分をオフセット指定で読み書きする (SQLite のようなページ構造のバイナリファイルを直接扱う用途)。カーソル (seek) は持たず、位置は毎回明示する (POSIX の pread/pwrite に対応)。
import fs := "std:fs" f := fs.open_rw("data.db")!.unwrap_or_else { e | panic(e.message) } page := f.read_at(4096, 4096)! # 2 ページ目を読む (Result(Bytes)) f.write_at(0, header)! # 先頭に書く f.sync()! # fsync (耐久性の確定) f.close()!
コンストラクター
| 名前 | 形 | 効果 | 意味 |
|---|---|---|---|
open |
fs.open(path) |
Fs |
読み取り専用で開く。不在は Err(not_found)。→ Future(Result(File)) |
open_rw |
fs.open_rw(path) |
Fs |
読み書きで開く (不在なら作成)。→ Future(Result(File)) |
非 String 引数は呼び出し位置で Error (panic 型, language-spec.md §16) になる。
「作成して空にする」は open_rw + truncate(0) の合成で表す。
File のメソッド
I/O メソッドは他の fs slot と同じく Future(Result(...)) を返す (prelude.md §9.4 のブロッキング I/O 契約)。
| メソッド | 形 | 効果 | 意味 |
|---|---|---|---|
read_at |
f.read_at(offset, n) |
Fs |
オフセットから最大 n bytes 読む → Ok(Bytes)。EOF 越えは短い Bytes (長さで判定)。offset は非負 Int (負は Error)。n は 0..2^30 (1 GiB) の Int (0 は空 Bytes・範囲外は Error) |
write_at |
f.write_at(offset, bytes) |
Fs |
オフセットに全量書く → Ok(())。末尾を越える位置への書き込みはファイルを拡張する |
size |
f.size() |
Fs |
現在のサイズ (bytes) → Ok(Int) |
truncate |
f.truncate(n) |
Fs |
サイズを n に切り詰め / 拡張 → Ok(()) |
sync |
f.sync() |
Fs |
OS バッファーをディスクへ確定 (fsync) → Ok(()) |
close |
f.close() |
Fs |
閉じる → Ok(())。呼んだ時点でハンドルは封印され、以降どのメソッドを呼んでも Error (use after close。array.md §2.1 の frozen 封印と同じ動的規律) |
ハンドル規律
File は array.md の Array・random.md のジェネレーターと同じ可変 scratch ハンドルである。束縛自体は immutable (f = other は再代入エラー)、内部状態 (OS ファイル記述子) だけがメソッドで前進する。値型ではないため == / compare を持たず、std:map / std:set のキー / 要素にできない。
File は std:fs の型メンバーとして export され、型注釈に書ける (fs.File、または import { File } := "std:fs")。不透明型であり内部表現は露出しない。
File は非 Copyable である (../language-spec.md の複製の節)。ホストのファイル記述子を持つ可変ハンドルで、作り直しても同じものにならないためである。copy は panic し、spawn / std:parallel の捕獲検査も弾く。
File はExactlyOnce の多重度型である (../language-spec.md の多重度型の節)。close が Consuming メソッドで、read_at / write_at は借用である。閉じ忘れは記述子の漏れなので消費義務を課す。
将来枠
- ファイルロック (flock / byte-range lock) — 複数プロセスから同一 DB ファイルへ安全に書くには必要になる
- ストリーム読み (行単位イテレーション等)
12. パス解決
- 起点は プロセスの cwd。
fs.read "foo.hika"は cwd 配下のfoo.hikaを見る - 絶対パスはそのまま使う
- ファイル import (
import m := "x.hika"、caller file 相対、language-spec.md §13.4) とは規則が異なる。std:fsの各関数はデータファイルの取り回し (cwd 起点)、ファイル import はソース解決 (caller 相対) という用途の違いに対応した使い分け - 渡したパスは正規化しない。
./..を含んでいてもその綴りのままホストへ渡し、パス区切りの解釈もホストに委ねる。まとめてから渡したいなら path.md のnormalizeを通す
12.1 シンボリックリンク
リンクを辿るかどうかは渡したパス自身と、ツリーの中で見つけたもので分ける。
- 渡したパス自身は辿る。 呼び手がその綴りを名指しているので、リンクであることは呼び手が承知している。
read/write/stat/exists/readdir/walk/openはいずれもリンク先を見る (stat("link-to-dir")のis_dirはtrue、壊れたリンクのexistsはfalse) - ツリーの中で見つけたものは辿らない。
readdirのis_dir(§6)・walkの再帰 (§6.1)・remove_allの再帰 (§7.1) は、リンクをリンクとして扱う。辿ると輪で止まらなくなり・同じファイルを 2 度数え・ツリーの外へ出る(remove_allなら消す対象の外まで消える) - 壊れたリンクはツリーを歩く妨げにならない。 リンク先が無いことは、そのリンクを含むツリーが読めないことではない。
walkは壊れたリンクを結果に載せずに歩き続ける
この分け方は「呼び手が名指したものには従い、こちらが見つけたものは疑う」という 1 つの規律である。リンクそのものを作る口・読む口 (symlink / readlink) と、リンクを辿らずにメタ情報を読む口 (lstat 相当) は持たない。リンクかどうかを知る必要があるときは、readdir の is_dir と stat の is_dir が食い違うことで間接的に分かる。
13. embed バイナリでの挙動
hikari build (build.md) が生成する embed バイナリ内でも std:fs の各関数は ホスト FS のみ を見る。embed FS (バイナリに同梱されたソースツリー) はファイル import 専用で、std:fs からはアクセスできない。std:fs モジュール自体 (ホストコード) は embed バイナリにも常に含まれ、std:fs の import は常に解決可能 (language-spec.md §13.4)。
14. エラーモデル
失敗時の err は回復可能エラーオブジェクト {kind, message} (language-spec.md §16)。std:fs が返す正規 kind (閉じた一覧。全 15 関数で共通):
| kind | 意味 |
|---|---|
not_found |
パス (またはその親) が存在しない |
permission_denied |
権限不足でアクセスできない |
io_error |
その他の I/O 失敗 |
新しい kind は設けず、上記 3 種で全関数を表す。種別が紛れやすい代表的な対応:
removeで 非空ディレクトリ →io_error(再帰削除は §7.1remove_all)remove_allで 不在のパス →kindではなくOk(())で表す (§7.1。removeとは違う)mkdtempでprefixにパス区切り →io_error(§5.1)readdirでpathが ディレクトリでない →io_errorrenameで ファイルシステムを跨ぐ移動 (EXDEV) →io_error(自動コピーはしない)mkdirで 経路上に同名の非ディレクトリ →io_errorexistsの不在はkindではなくOk(false)で表す (§9)
message は fs.mkdir: ... のように関数名を含む人間可読文字列。分岐は match res { Ok(v) => … Err(e) => match e.kind {...} } で行い、message の文字列マッチに依存しないこと。
Future / scheduler との関係は prelude.md §9.4 と同じ。root リテラル本体の評価が完了した時点で未解決の fs Future は language-spec.md §13.3 のとおり放棄される。別フローで進み続ける I/O は無い — fs はホストのスレッドへ仕事を渡さないので、放棄した時点で止めるべき走行中の仕事は残らない。