Video course contents16
All video lessons

LESSON 06 · HHY 1.7.0

Read and Write Files

Read a directory of text files and save an uppercase copy.

4:44 · English audio & captions

Your progress stays in this browser. No account needed.

Read the companion manual chapter

Chapters & transcript

Select a timestamp to jump in the video. Expand a title to read the transcript and code.

A useful file task

In this lesson, we will turn a folder of text files into a small inventory, then save an uppercase copy of one file. The original files stay unchanged. Open the downloaded examples folder before running the command. All reads use the fixtures directory, and all generated files go into out. This separation makes it easy to inspect the result and repeat the demonstration without touching your own documents.

Terminal
hhy run files.hhy

Run the complete example from its examples directory.

Paths describe locations

A Path represents a filesystem location. A String represents text, so wrap a filename in path when calling these file APIs. The name and extension fields are read only, and the extension includes its leading dot. A relative data path starts at your current working directory. That is why our run instructions first move into examples. Later, module imports will have a different relative path rule.

files.hhy
let source = path("fixtures/notes.txt")
print(source.name)
print(source.extension)
notes.txt
.txt

Run the complete example from its examples directory.

Discover matching entries

The files function produces a lazy stream of filesystem entries. Our pattern selects text files, including matches below nested directories. We explicitly retain regular files, then project each entry to its name. Sorting makes our teaching output deterministic instead of depending on directory traversal order. Collect consumes the stream and creates a List. Notice that a filesystem entry contains metadata; it is not simply a filename string.

files.hhy
let names = path("fixtures")
    |> files("**/*.txt")
    |> where { file -> file.is_file }
    |> map { file -> file.name }
    |> sort_by({ order: "asc" }) { name -> name }
    |> collect
print(names)

Run the complete example from its examples directory.

Read only what you need

Use read_text when you need one complete text value. Use read_lines when your processing works one line at a time. Both are text APIs and expect valid UTF eight input. Images and archives belong with the byte APIs instead. Here, read_lines supplies a stream directly, so we do not add stream after it. Each callback returns a transformed line without changing the original file on disk.

files.hhy
read_lines(source)
    |> map { line -> upper(line) }

Run the complete example from its examples directory.

Save the transformed stream

Now add a terminal operation. Save_lines consumes the line stream and adds a line ending for each value. Create_parents allows the out directory to be created on the first run. The documented save operation writes a temporary sibling file and commits the replacement atomically. That prevents readers from seeing a half written target. It does not mean several different output files become one transaction together.

files.hhy
read_lines(source)
    |> map { line -> upper(line) }
    |> save_lines(path("out/upper.txt"),
        { create_parents: true })

Run the complete example from its examples directory.

Inspect the actual output

Read the saved file back and print it. This checks the complete path from discovery and transformation to disk output. The uppercase text should contain the same two lines as notes, with their letters changed. Trim is only used for clean terminal presentation here; it is not part of the saved transformation. Open both files side by side and verify that the input still contains its original lowercase text.

files.hhy
read_text(path("out/upper.txt"))
    |> trim
    |> print
FIRST LINE
SECOND LINE

Run the complete example from its examples directory.

Write versus append

Write_text replaces the target by default, while append_text adds content to an existing file. We intentionally reset our demonstration journal at the start of every run, then append the second line. That makes repeated runs predictable. Pay attention to the explicit newline characters: a text write receives exactly the text you supply. Choose save_lines when your input already consists of separate line values and needs line endings.

files.hhy
write_text(path("out/journal.txt"), "first\n")
append_text(path("out/journal.txt"), "second\n")

Run the complete example from its examples directory.

Protect an existing target

A common mistake is assuming a write will refuse to replace an existing file automatically. Set overwrite to false when replacement is not acceptable. This deliberate attempt fails because our journal already exists. The catch block lets the demonstration continue, and the journal still contains first and second. We will study structured errors in lesson nine. For now, compare the protected failure with the successful explicit reset above.

files.hhy
try {
    write_text(path("out/journal.txt"), "replace",
        { overwrite: false })
} catch err {
    print("overwrite prevented")
}
overwrite prevented

Run the complete example from its examples directory.

Practice: inventory another fixture

For practice, add a third text file inside fixtures and rerun the script. The sorted inventory should grow, while the uppercase copy should still come from notes. Then change the selected source to your new fixture and predict its output before running again. Keep writes inside out. You have now connected Path values, lazy directory traversal, line processing, and controlled saving into one useful automation script.

Terminal
hhy run files.hhy
# Inspect out/upper.txt and out/journal.txt

Run the complete example from its examples directory.