Capstone: A CLI App

Ruby is a dynamic, beginner-friendly programming language, and in this capstone you'll combine everything you've learned to build a real command-line To-Do application from scratch.

Learn Capstone: A CLI App in our free Ruby course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free Ruby course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

By the end of this lesson you'll have assembled classes, collections, blocks, exceptions, JSON, and file I/O into one working program — proof that the pieces fit together.

What You'll Build in This Capstone

1️⃣ The Task Model

Start with the smallest unit: a single Task . It holds a title and a done flag, toggles its state with a bang method, and defines to_s so it prints as a tidy checkbox line. to_h prepares it for JSON saving later.

2️⃣ The TaskList Collection

Next, the TaskList manages many tasks. It adds with , completes safely with fetch and raise , filters pending items with reject(&:done) , and summarises with count — pure Enumerable.

3️⃣ Persistence with JSON Files

Finally, make tasks survive between runs. save maps tasks to hashes and writes them with File.write and JSON.pretty_generate ; load reads them back, guarding with File.exist? . (Ruby's open classes let us reopen TaskList to add methods.)

Your turn. Extend TaskList with a safe remove and a clear_done . Confirm the TODO s, combine with the classes above, then run it.

Tie it all together with a tiny menu that dispatches commands to your TaskList — the final step toward a real interactive CLI. Run with ruby cli.rb .

📋 Quick Reference — Skills Used

Practice quiz

In the Task class, what does the toggle! method do?

  • Deletes the task
  • Renames the task
  • Flips @done to its opposite with @done = !@done
  • Saves the task to disk

Answer: Flips @done to its opposite with @done = !@done. toggle! sets @done = !@done, switching the completed state; the ! marks that it mutates.

Why does the Task class define a custom to_s method?

  • To control how the task prints with puts and string interpolation
  • To convert the task to JSON
  • Because Ruby requires every class to define to_s
  • To make the task comparable

Answer: To control how the task prints with puts and string interpolation. to_s controls how an object renders when printed, e.g. the checkbox line [x] Learn Ruby.

TaskList#add uses @tasks << Task.new(title). What does << do here?

  • Compares two tasks
  • Removes a task
  • Shifts bits
  • Appends the new Task to the @tasks array

Answer: Appends the new Task to the @tasks array. << (the shovel operator) appends the new Task object to the end of the @tasks array.

complete(index) uses @tasks.fetch(index) { raise IndexError, ... }. Why fetch with a block?

fetch with a block runs the block (raising IndexError) when the index is out of range, guarding against bad input.

pending uses @tasks.reject(&:done). What does it return?

  • The tasks whose done is falsey (the not-yet-done ones)
  • All completed tasks
  • The first task only
  • The number of tasks

Answer: The tasks whose done is falsey (the not-yet-done ones). reject keeps elements for which the block is false, so reject(&:done) returns the pending tasks.

What does the &:done in reject(&:done) and count(&:done) represent?

  • A frozen symbol
  • A reference to the @done variable
  • Symbol-to-proc: it calls the done method on each element
  • A bitwise AND

Answer: Symbol-to-proc: it calls the done method on each element. &:done converts the symbol into a proc that calls .done on each element.

Persistence uses JSON.pretty_generate and File.write. What is their combined job?

  • To print tasks to the screen
  • To serialize the tasks to a JSON string and write it to a file on disk
  • To delete the tasks file
  • To compare two task lists

Answer: To serialize the tasks to a JSON string and write it to a file on disk. pretty_generate turns the data into JSON text and File.write saves it so tasks survive between runs.

load guards with: return ... unless File.exist?(FILE). Why?

  • To avoid loading twice
  • To freeze the file
  • To sort the tasks
  • To avoid an error when the save file doesn't exist yet

Answer: To avoid an error when the save file doesn't exist yet. Checking File.exist? first prevents trying to read a file that isn't there yet.

The capstone reopens class TaskList a second time to add save/load. This is possible because Ruby classes are:

  • Sealed after first definition
  • Open — you can reopen a class to add more methods
  • Compiled to bytecode and locked
  • Single-method only

Answer: Open — you can reopen a class to add more methods. Ruby classes are open, so you can reopen TaskList later and add methods to it.

Why split the app into separate Task and TaskList classes?

  • Ruby requires at least two classes per file
  • To make the program run faster
  • Single responsibility — Task models one item, TaskList manages the collection
  • Because JSON needs two classes

Answer: Single responsibility — Task models one item, TaskList manages the collection. Separating concerns keeps each class small: Task owns one item, TaskList owns the collection.