<ContentPage eyebrow="Tutorial" title="Complete tasks" description="Find a task by number and update its status safely.">

<TutorialNavigation slug="tutorial-complete-tasks" />

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:

```tea
const task = tasks[number - 1]
if task.done
  return false
end
task.done = true
true
```

Subtracting 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

```bash
tea tasks.tea done 2
```

```text
Completed task 2.
```

```bash
tea tasks.tea list
```

```text
1. [ ] Learn Tea
4. [ ] Write a test
```

```bash
tea tasks.tea list --all
```

```text
1. [ ] Learn Tea
2. [x] Build a CLI
3. [x] Take a break
4. [ ] Write a test
```

The 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:

<details>
<summary>Complete tasks.tea for this chapter</summary>

**`tasks.tea`**

```tea checkpoint="examples/tutorial/tasks/07-complete-tasks/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)
end
```

</details>

## Try it yourself

Try `done 0`, `done many`, and `done 999`. Check that all three fail without changing your tasks.

<details>
<summary>Expected result</summary>

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.

</details>

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](/reference/parse), [error handling](/docs/error-handling).

<TutorialNavigation slug="tutorial-complete-tasks" position="bottom" />

</ContentPage>
