Tutorial
Read saved tasks
Load a text file and handle failures with typed errors.
Start with chapter 4's command-aware program. We'll keep the commands and display functions, but replace the embedded sample list with tasks read from a file.
Give the data a home
Create tasks.txt beside tasks.tea with this content:
tasks.txt
[ ] Learn Tea
[ ] Build a CLI
[x] Take a breakEach line starts with [ ] for an open task or [x] for a completed task, followed by its title. Numbers are positions in the file, starting at 1; we don't need to store them separately.
Run commands from your project directory. tasks.txt is relative to the directory where you run the command, not the location of the Tea source or executable. This lets different directories have separate task lists.
Import file and string helpers
Add use fs from "std.fs" and use string from "std.string" beside the existing import. In the list branch of run, replace the embedded task list with:
const tasks = load_tasks("tasks.txt")We'll add the functions it needs before run. Start with a domain error:
error TaskError {
InvalidData(line: Int)
}An error can have named variants and carry useful information. Here the line number lets us explain where a saved file is malformed.
Turn text into tasks
Add valid_title and decode_tasks from the complete checkpoint. valid_title rejects titles containing only spaces or tabs, and titles containing a newline or carriage return. It preserves other text, including punctuation and Unicode.
decode_tasks builds an initially empty List[Task]. An empty list doesn't provide an element type to infer, so we annotate it explicitly. @append(tasks, value) adds a task to the list.
The decoder normalizes Windows line endings, splits the content into lines, and checks each line's prefix. line[4..@len(line)] takes the title after the four-character marker. The .. range excludes its endpoint. A malformed line triggers throw TaskError.InvalidData(number) rather than silently disappearing from the list.
Make failures part of the signature
Add this loading function:
def load_tasks(file: String) -> List[Task] ! { fs.FsError, TaskError }
if ! fs.exists(file)
return[]
end
decode_tasks(fs.read_file(file))
endThe ! after the return type declares errors the function can raise. The braces list two possible error types: file operations can fail with fs.FsError, and decoding can fail with TaskError.
Tea propagates those errors through the calls in this function. The same error set must be declared on run, because it calls load_tasks. A missing file is treated as an empty list; other filesystem failures propagate.
Replace the final run() call with the try run() catch err block in the complete checkpoint. Its case is arms handle each error variant. A filesystem error carries an operation, path, and message; our data error carries a line number. Both go to standard error and end the program with @exit(1).
Read the saved list
tea tasks.tea list --all1. [ ] Learn Tea
2. [ ] Build a CLI
3. [x] Take a breakChange Learn Tea to another title in tasks.txt and run again. You should see the changed title without editing tasks.tea. Restore the sample title before continuing so your output matches the next chapters.
Complete checkpoint
Keep the tasks.txt file you just created. Replace tasks.tea with this version if you need a fresh starting point:
Complete tasks.tea for this chapter
tasks.tea
use args from "std.args"
use fs from "std.fs"
use string from "std.string"
struct Task {
title: String
done: Bool
}
def format_task(task: Task, number: Int) -> String
const marker = if(task.done) "[x]" else "[ ]"
`${number}. ${marker} ${task.title}`
end
def show_tasks(tasks: List[Task], show_all: Bool) -> Void
if @len(tasks) == 0
@println("No tasks yet.")
return
end
var number = 1
var shown = 0
for task in tasks
if show_all ||!task.done
@println(format_task(task, number))
shown = shown + 1
end
number = number + 1
end
if shown == 0
@println("No open tasks.")
end
end
error TaskError {
InvalidData(line: Int)
}
def valid_title(title: String) -> Bool
const without_spaces = string.replace(title, " ", "")
const without_tabs = string.replace(without_spaces, "\t", "")
@len(without_tabs) > 0 &&!string.contains(title, "\n") &&!string.contains(title, "\r")
end
def decode_tasks(content: String) -> List[Task] ! TaskError
var tasks: List[Task] = []
var number = 1
for line in string.lines(string.replace(content, "\r\n", "\n"))
if @len(line) < 5 ||!(string.starts_with(line, "[ ] ") || string.starts_with(line, "[x] "))
throw TaskError.InvalidData(number)
end
const title = line[4..@len(line)]
if ! valid_title(title)
throw TaskError.InvalidData(number)
end
@append(tasks, Task(title: title, done: string.starts_with(line, "[x] ")))
number = number + 1
end
tasks
end
def load_tasks(file: String) -> List[Task] ! { fs.FsError, TaskError }
if ! fs.exists(file)
return[]
end
decode_tasks(fs.read_file(file))
end
def help() -> Void
@println("Usage: tasks [list [--all] | --help]")
@println("Tasks are numbered from 1. Listing shows open tasks; --all includes completed tasks.")
end
def usage() -> Void
@eprintln("tasks: invalid arguments; run tasks --help for usage")
@exit(2)
end
def run() -> Void ! { fs.FsError, TaskError }
var argv = args.all()
if @len(argv) == 0
argv = ["list"]
end
if argv[0] == "--help" && @len(argv) == 1
help()
return
end
if argv[0] == "list"
if @len(argv) > 2 ||(@len(argv) == 2 && argv[1] != "--all")
usage()
return
end
const tasks = load_tasks("tasks.txt")
show_tasks(tasks, @len(argv) == 2)
return
end
usage()
end
try run() catch err
case is fs.FsError.OperationFailed
@eprintln(`tasks: cannot ${err.operation} '${err.path}': ${err.message}`)
@exit(1)
case is TaskError.InvalidData
@eprintln(`tasks: invalid data on line ${err.line}; expected [ ] title or [x] title`)
@exit(1)
case _
@eprintln("tasks: unexpected error")
@exit(1)
endTry it yourself
Temporarily remove the marker from the second line of tasks.txt. Run the program and inspect the error, then restore the file.
Expected result
The program reports tasks: invalid data on line 2; expected [ ] title or [x] title. It doesn't print a partial list or change the file. Tea's launcher may add its own unsuccessful-exit message.
You can now turn external data into typed values and explain a failure to your user. Next, we'll write updated values back to disk.
Read more: typed errors, filesystem operations, string helpers.