Tutorial
Complete tasks
Find a task by number and update its status safely.
Start with chapter 6's program and its four saved tasks. We'll add done NUMBER, keeping task numbers stable as tasks move out of the open view.
Convert a number into a task
Add use parse from "std.parse" to your imports, and add InvalidNumber(input: String) to TaskError.
Add complete_task below add_task using the complete checkpoint. Its input is a string because command-line arguments are text. parse.try_int converts it into an Int, or throws parse.ParseError.InvalidInt.
We first require between 1 and 19 characters, all ASCII digits. After parsing, we check that the value is between 1 and the list length, and that converting it back to text exactly reproduces the input. This accepts the numbers displayed by list, while rejecting signs, leading zeros, and excessively large values. Validation happens before any list access or mutation.
The catch inside complete_task translates a standard-library parse error into our program's TaskError.InvalidNumber. That keeps the command's error message focused on what the user needs: an existing task number.
Update the task
The last part of complete_task is:
const task = tasks[number - 1]
if task.done
return false
end
task.done = true
trueSubtracting one converts the displayed number into a zero-based index. Structs, like lists, have shared reference semantics: changing task.done also changes that task in tasks. const task prevents assigning a different struct to the binding, but permits changing a field.
The boolean return value tells the command whether anything changed. A task that's already complete returns false, so repeating the command doesn't rewrite the file.
Connect the command
Inside run, add a done branch before the final usage() call. Require exactly two arguments, load the tasks, and pass the second argument to complete_task. Save only when it returns true.
Update the usage text to include done NUMBER. Add a TaskError.InvalidNumber arm to the outer catch block; it prints the rejected input, suggests list --all, and exits with status 2. The complete checkpoint contains both changes.
Complete a task
tea tasks.tea done 2Completed task 2.tea tasks.tea list1. [ ] Learn Tea
4. [ ] Write a testtea tasks.tea list --all1. [ ] Learn Tea
2. [x] Build a CLI
3. [x] Take a break
4. [ ] Write a testThe open view still labels the fourth task 4, rather than renumbering it 2. Run done 2 again and you'll get Task 2 is already complete.
Complete checkpoint
Keep tasks.txt as it is and replace tasks.tea with this source if needed:
Complete tasks.tea for this chapter
tasks.tea
use args from "std.args"
use fs from "std.fs"
use string from "std.string"
use parse from "std.parse"
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)
InvalidTitle
InvalidNumber(input: String)
}
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 encode_tasks(tasks: List[Task]) -> String
var content = ""
for task in tasks
const marker = if(task.done) "[x]" else "[ ]"
content = content + `${marker} ${task.title}\n`
end
content
end
def save_tasks(file: String, tasks: List[Task]) -> Void ! fs.FsError
fs.write_file_atomic(file, encode_tasks(tasks))
end
def add_task(tasks: List[Task], title: String) -> Void ! TaskError
if ! valid_title(title)
throw TaskError.InvalidTitle
end
@append(tasks, Task(title: title, done: false))
end
def complete_task(tasks: List[Task], input: String) -> Bool ! TaskError
if @len(input) == 0 || @len(input) > 19
throw TaskError.InvalidNumber(input)
end
var i = 0
while i < @len(input)
if ! string.contains("0123456789", input[i])
throw TaskError.InvalidNumber(input)
end
i = i + 1
end
const number = try parse.try_int(input) catch err
case is parse.ParseError.InvalidInt
throw TaskError.InvalidNumber(err.input)
case _
throw TaskError.InvalidNumber(input)
end
if number < 1 || number > @len(tasks) || @to_string(number) != input
throw TaskError.InvalidNumber(input)
end
const task = tasks[number - 1]
if task.done
return false
end
task.done = true
true
end
def help() -> Void
@println("Usage: tasks [list [--all] | add \"title\" | done NUMBER | --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
if argv[0] == "add" && @len(argv) == 2
const tasks = load_tasks("tasks.txt")
add_task(tasks, argv[1])
save_tasks("tasks.txt", tasks)
@println(`Added task ${@len(tasks)}.`)
return
end
if argv[0] == "done" && @len(argv) == 2
const tasks = load_tasks("tasks.txt")
if complete_task(tasks, argv[1])
save_tasks("tasks.txt", tasks)
@println(`Completed task ${argv[1]}.`)
else
@println(`Task ${argv[1]} is already complete.`)
end
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 is TaskError.InvalidTitle
@eprintln("tasks: title must be nonempty and on one line")
@exit(2)
case is TaskError.InvalidNumber
@eprintln(`tasks: '${err.input}' is not an existing task number; use list --all`)
@exit(2)
case _
@eprintln("tasks: unexpected error")
@exit(1)
endTry it yourself
Try done 0, done many, and done 999. Check that all three fail without changing your tasks.
Expected result
Each command reports that the supplied value is not an existing task number. list --all still shows the same four tasks and completion states. The current script launcher adds its own failure message; the compiled program will return the usage status directly.
Your tool now has all three core operations: list, add, and complete. Next, we'll make its logic easier to test and reuse.
Read more: text parsing, error handling.