# 06 · Read and Write Files

English narration · HHY 1.7.0

File inventory; original fixture unchanged; uppercase copy and append demonstration.

## 00:00:00.000 — 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.

```
hhy run files.hhy
```

## 00:00:31.270 — 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.

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

Expected output:
```text
notes.txt
.txt
```

## 00:01:01.390 — 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.

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

## 00:01:35.290 — 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.

```
read_lines(source)
    |> map { line -> upper(line) }
```

## 00:02:06.200 — 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.

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

## 00:02:37.770 — 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.

```
read_text(path("out/upper.txt"))
    |> trim
    |> print
```

Expected output:
```text
FIRST LINE
SECOND LINE
```

## 00:03:08.510 — 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.

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

## 00:03:40.980 — 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.

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

Expected output:
```text
overwrite prevented
```

## 00:04:12.830 — 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.

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

