Guide · Chapter 4
Files and Paths
Walk directories, read text, and write results safely.
1.7.1 controlled concurrency and atomicity
This release fixes resource lifetimes and delivers seven experimental APIs, disabled by default. Review state ownership, cooperative file locks, strict transactions and bounded-task guarantees before enabling them.
Read the complete guide and examplesIncludes the guarantee matrix, failure handling and actual validation evidencePath is not String
Every filesystem API requires Path. path(text) normalizes lexically: it collapses repeated separators and ., and resolves removable .. segments without accessing the filesystem or resolving symlinks. A relative Path is always based on the process startup directory, not the importing file.
let source = path("./src/../src/main.c")
let target = path_join(source.parent, "runtime.c")
print(source)
print(source.name)
print(source.extension)
print(source.parent)
print(target)name, extension, and parent are read-only Path fields—not path_name(), path_extension(), or path_parent() functions. extension includes the leading dot and is empty when absent; path_join(base, child) returns a new combined Path.
files: traversal, globs, and metadata
path("./logs")
|> files("**/*.log")
|> where { file -> file.size > 1mib }
|> flat_map { file -> read_lines(file.path) }
|> where { line -> contains(line, "ERROR") }
|> save_lines(path("errors.txt"))files(root, pattern, options?) returns a lazy Stream<File | Directory> and excludes the traversal root itself. Patterns support *, ?, and **. Directory symlinks are not followed by default; { follow_symlinks: true } opts in with cycle detection.
| Field | Meaning |
|---|---|
| path | Full Path |
| name | File or directory name |
| extension | Extension including its leading dot |
| size | Size as Bytes |
| created | Creation time, or null when unavailable |
| modified | Modification time |
| is_file / is_dir / is_symlink | Object-kind flags |
File and Directory are system objects, not Maps. Map required fields into an ordinary Map before JSON encoding.
Reading text and binary data
Writing, appending, and atomic saves
let input = path("notes.txt")
let backup = path("backup-notes.txt")
write_text(input, "first line\n", { overwrite: true })
append_text(input, "second line\n")
copy(input, backup, { overwrite: false })
read_lines(backup)
|> map { line -> upper(line) }
|> save_lines(path("backup/upper.txt"), { create_parents: true })funcwrite_text#
write_text(Path, String, Map?) -> PathAtomically replace with String, supporting overwrite/create_parents.
funcappend_text#
append_text(Path, String) -> PathAppend String to the end of a file.
funcwrite_bytes#
write_bytes(Path, BytesBuffer, Map?) -> PathAtomically replace a file with BytesBuffer.
funcsave_text#
save_text(String | Stream<String>, Path, Map?) -> PathAtomically save a String or pull a text Stream directly to disk.
funcsave_lines#
save_lines(Stream<String>, Path, Map?) -> PathWrite a String Stream with LF per item and atomically replace the target.
write_text, write_bytes, save_text, and save_lines accept overwrite (default true) and create_parents (default false). They commit through a same-directory temporary file plus rename; overwrite: false uses atomic no-replace to prevent check-then-write races.
Copy, move, remove, and dry-run
hhy run --dry-run backup.hhy
hhy run backup.hhyThe target parent directory for copy and move must already exist; the current 1.7.1 runtime does not process their create_parents option. The example uses overwrite: false; choose a new backup filename before repeating it. Review the dry-run plan before executing a script that copies, moves, or removes files.
