File I/O
Beamtalk's File class provides reading, writing, directory operations, and
lazy file streams. All operations return Result values — check isOk /
isError or call unwrap to extract the value.
Reading and writing files
Write a file with writeAll:contents: and read it back with readAll::
TestCase subclass: Ch18WriteRead
field: tmpDir = "target/bt-doctest-tmp/ch18wr"
setUp =>
File mkdirAll: self.tmpDir
self
tearDown =>
File deleteAll: self.tmpDir
self
testWriteAndRead =>
path := self.tmpDir ++ "/greeting.txt"
File writeAll: path contents: "Hello, Beamtalk!"
content := (File readAll: path) unwrap
self assert: content equals: "Hello, Beamtalk!"
writeAll:contents: creates parent directories automatically and overwrites
any existing file. readAll: returns a Result — if the file doesn't exist,
you get an error:
TestCase subclass: Ch18MissingFile
testMissing =>
result := File readAll: "no/such/file.txt"
self assert: result isError
Checking files
File exists: "docs/learning/README.md" // => true
File isFile: "docs/learning/README.md" // => true
File isDirectory: "docs/learning/README.md" // => false
File isDirectory: "docs/learning" // => true
Directory operations
Create directories, list contents, and clean up:
TestCase subclass: Ch18Directories
field: tmpDir = "target/bt-doctest-tmp/ch18dirs"
setUp =>
File mkdirAll: self.tmpDir
self
tearDown =>
File deleteAll: self.tmpDir
self
testMkdirAndList =>
// Create a nested directory structure
File mkdirAll: self.tmpDir ++ "/a/b"
// Write some files
File writeAll: self.tmpDir ++ "/readme.txt" contents: "hi"
File writeAll: self.tmpDir ++ "/notes.txt" contents: "note"
// List directory contents (returns a list of filenames)
entries := (File listDirectory: self.tmpDir) unwrap
self assert: (entries printString includesSubstring: "readme.txt")
self assert: (entries printString includesSubstring: "notes.txt")
testDeleteAndRename =>
path := self.tmpDir ++ "/temp.txt"
File writeAll: path contents: "temporary"
self assert: (File exists: path)
// Delete a file
File delete: path
self deny: (File exists: path)
// Rename a file
src := self.tmpDir ++ "/old.txt"
dst := self.tmpDir ++ "/new.txt"
File writeAll: src contents: "data"
File rename: src to: dst
self assert: (File readAll: dst) unwrap equals: "data"
Useful path helpers
cwd := File cwd // => _
cwd class // => String
File tempDirectory class // => String
File streams
For large files, use streams instead of reading everything into memory.
File open:do: gives you a handle that auto-closes when the block completes:
TestCase subclass: Ch18Streams
field: tmpDir = "target/bt-doctest-tmp/ch18streams"
field: nl = ""
setUp =>
updated := self withNl: (String fromCodePoint: 10)
File mkdirAll: updated tmpDir
File writeAll: updated tmpDir ++ "/data.txt"
contents: "one" ++ updated nl ++ "two" ++ updated nl ++ "three" ++ updated nl ++ "four" ++ updated nl ++ "five"
updated
tearDown =>
File deleteAll: self.tmpDir
self
testReadAllLines =>
// Read all lines from a file
lines := (File open: self.tmpDir ++ "/data.txt" do: [:h |
h lines asList
]) unwrap
self assert: lines size equals: 5
self assert: (lines at: 1) equals: "one"
self assert: (lines at: 5) equals: "five"
testLazyTake =>
// take: only reads the first N lines — lazy evaluation
count := (File open: self.tmpDir ++ "/data.txt" do: [:h |
(h lines take: 2) inject: 0 into: [:c :_ | c + 1]
]) unwrap
self assert: count equals: 2
testFilterLines =>
// select: filters lazily
result := (File open: self.tmpDir ++ "/data.txt" do: [:h |
(h lines select: [:l | l size <= 3]) asList
]) unwrap
self assert: result size equals: 3
Stream operations are lazy — take: and select: don't read the whole
file. Use asList to materialize results when you need them.
Error handling
All File operations return Result. Use the standard Result methods
(see chapter 12):
TestCase subclass: Ch18Errors
testResultMethods =>
ok := File readAll: "docs/learning/README.md"
self assert: ok isOk
err := File readAll: "does/not/exist.txt"
self assert: err isError
self deny: err isOk
Security
File operations rely on OS-level permissions — there are no path restrictions. Both absolute and relative paths are accepted (ADR 0063). Beamtalk is a trusted developer tool; production deployments should use OS sandboxing as needed.
Summary
Reading & writing:
File readAll: path → Result<String>
File writeAll: path contents: string → Result<ok>
File exists: path → Boolean
Directories:
File mkdir: path → Result (single level)
File mkdirAll: path → Result (creates parents)
File listDirectory: path → Result<List<String>>
File delete: path → Result (file or empty dir)
File deleteAll: path → Result (recursive)
File rename: src to: dst → Result
Queries:
File isFile: path → Boolean
File isDirectory: path → Boolean
File cwd → String
File tempDirectory → String
File absolutePath: path → Result<String>
Streams:
File open: path do: [:handle | ...] → Result<block return value>
File lines: path → Result<Stream>
handle lines → Stream of line Strings
stream asList → List
stream take: n → Stream (lazy)
stream select: [:l | ...] → Stream (lazy)
stream inject: init into: [:a :l | …] → value
Exercises
1. Write and read back. Write "Beamtalk is fun!" to a temporary file, read
it back, and verify the contents match. Don't forget cleanup.
Hint
// In a TestCase with setUp/tearDown for temp dir:
path := tmpDir ++ "/test.txt"
File writeAll: path contents: "Beamtalk is fun!"
content := (File readAll: path) unwrap
self assert: content equals: "Beamtalk is fun!"
2. File vs directory. Use File isFile: and File isDirectory: to check
both a file path and a directory path. How do the two predicates differ from
File exists:?
Hint
File exists: "docs/learning/README.md" // => true
File isFile: "docs/learning/README.md" // => true
File isDirectory: "docs/learning/README.md" // => false
File isDirectory: "docs/learning" // => true
exists: returns true for both files and directories. isFile: and
isDirectory: distinguish between them.
3. Lazy line counting. Use File open:do: and stream operations to count
the number of lines in a file without reading the entire contents into memory.
Hint
count := (File open: path do: [:h |
h lines inject: 0 into: [:c :_ | c + 1]
]) unwrap
The stream is lazy — inject:into: processes one line at a time.
Next: Chapter 19 — Regular Expressions