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.