Capstone 2: the command line

The command layer is the only part of the tracker a user sees. It reads argv, decides which operation was asked for, calls the model or the storage and prints the result, and it owns the exit code.

The layer is thin on purpose: no business rule lives here, only the mapping from words to functions and from exceptions to messages. That is what makes the model and the storage testable without a terminal, which the next lesson's tests rely on.

The command layer is where the program meets the shell, and the shell's rules are part of the interface: quoting, globbing and the exit status. A program that respects them composes with other tools.

Arguments into a command

argv[0] is the program name, and the rest are the words the user typed. Copying them into a std::vector<std::string> makes the size and the indexing ordinary, and the first element is the command.

The program prints the command and the number of parameters for two invocations: add with one parameter and list with none. The quotes around the title keep it one argument, which is the shell's job.

An empty command list is the case with no arguments at all, and the program answers with a usage line and a non-zero status. A user who types the program name alone gets the explanation rather than a crash.

#include <iostream> #include <string> #include <vector> int main(int argc, char** argv) { std::vector<std::string> args(argv + 1, argv + argc); if (args.empty()) { std::cout << "usage: tasks <command>\n"; return 1; } std::cout << "command " << args[0] << ' ' << args.size() - 1 << '\n'; }

g++ -std=c++23 -Wall -Wextra argv.cpp -o tasks_argv
./tasks_argv add \"write tests\"
./tasks_argv list

command add 2
command list 0

Dispatching to handlers

The command is a chain of comparisons, and each branch calls a handler with the parameters it needs. add pushes a task, list prints the titles, done marks one finished by id.

The program runs two commands in one process to keep the transcript short: it adds a task, lists the list, and marks the task done. The output shows the list before and after, so the handlers really change the model.

A real tracker builds the list from the storage of the previous lesson and saves it at the end. The dispatch does not change: the handlers take the model, and the model is wherever it came from.

#include <iostream> #include <string> #include <vector> struct task { int id; std::string title; bool done = false; }; void add(std::vector<task>& tasks, const std::string& title) { tasks.push_back({static_cast<int>(tasks.size()) + 1, title}); } void show(const std::vector<task>& tasks) { for (const task& item : tasks) { std::cout << item.id << ' ' << (item.done ? "[x] " : "[ ] ") << item.title << '\n'; } } void done(std::vector<task>& tasks, int id) { for (task& item : tasks) { if (item.id == id) { item.done = true; } } } int main() { std::vector<task> tasks; add(tasks, "write the parser"); show(tasks); done(tasks, 1); show(tasks); }

g++ -std=c++23 -Wall -Wextra dispatch.cpp -o tasks_dispatch
./tasks_dispatch

1 [ ] write the parser
1 [x] write the parser

The help text

The help text is part of the interface, and it is where the commands are documented. A user who types help or makes a mistake reads it, so it lists the commands and their parameters in the order of the dispatch.

The program prints the help for the word help and for an unknown word: both end in the same text, the unknown one also names what it did not understand, and its status is 2.

The text lives in one function so that the two paths cannot drift apart. A help text copied into two branches is a help text that is right in one of them.

#include <iostream> #include <string> void print_help() { std::cout << "usage: tasks <command>\n"; std::cout << " add <title> add a task\n"; std::cout << " list show the tasks\n"; std::cout << " done <id> mark a task done\n"; std::cout << " help show this text\n"; } int main(int argc, char** argv) { std::string command = argc > 1 ? argv[1] : "help"; if (command == "help") { print_help(); return 0; } std::cout << "unknown command: " << command << '\n'; print_help(); return 2; }

g++ -std=c++23 -Wall -Wextra help.cpp -o tasks_help
./tasks_help help
./tasks_help puzzle; echo "exit=$?"

usage: tasks <command>
add <title> add a task
list show the tasks
done <id> mark a task done
help show this text
unknown command: puzzle
usage: tasks <command>
add <title> add a task
list show the tasks
done <id> mark a task done
help show this text
exit=2

The exit code of a wrong command

A command that fails returns a status the shell and the scripts can see. The unknown command returns 2, which is the conventional code for a usage error, and a successful command returns 0.

The command runs the program with no arguments and prints the status after it. The output is the usage line and exit=1: the empty invocation was treated as an error, not as a request to list.

The code is the contract with the caller. A script that checks if tasks add ...; then depends on it, and a program that always returns 0 makes every check succeed, which is the same mistake as a test runner with no failing status.

#include <iostream> #include <string> #include <vector> int main(int argc, char** argv) { std::vector<std::string> args(argv + 1, argv + argc); if (args.empty()) { std::cout << "usage: tasks <command>\n"; return 1; } std::cout << "unknown command: " << args[0] << '\n'; return 2; }

g++ -std=c++23 -Wall -Wextra status.cpp -o tasks_status
./tasks_status; echo \"exit=$?\"
./tasks_status puzzle; echo \"exit=$?\"

usage: tasks <command>
"exit=1"
unknown command: puzzle
"exit=2"

Exercises

  1. Add a `remove ` command to the dispatch and print the list after it.
  2. Make the help text list the commands from one table and print the table in the dispatch.
  3. Return 64 for a wrong parameter count and print the status from the shell.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is argv[0]?

    Answer

    The program name

    The first entry is how the program was invoked, and the user's words start at argv[1].

  2. Why copy argv into a vector of strings?

    Answer

    Because the size and the indexing become ordinary operations

    The vector gives size(), range-for and the same string type the rest of the program uses.

  3. What does the unknown command return?

    Answer

    2

    Two is the conventional status for a usage error, and it is not zero.

  4. What does this program print? #include <iostream> #include <string> #include <vector> int main() { std::vector<std::string> args{"add", "write tests"}; if (args.empty()) { std::cout << "usage: tasks <command>"; return 1; } std::cout << "command " << args[0] << ' ' << args.size() - 1; }

    Answer

    command add 1

    The quoted title is one argument, so the command is followed by one parameter.

  5. What does this program print? #include <iostream> #include <string> #include <vector> struct task { int id; std::string title; bool done = false; }; void show(const std::vector<task>& tasks) { for (const task& item : tasks) std::cout << item.id << (item.done ? " [x] " : " [ ] ") << item.title << '\n'; } int main() { std::vector<task> tasks; tasks.push_back({1, "write the parser"}); show(tasks); tasks[0].done = true; show(tasks); }

    Answer

    1 [ ] write the parser / 1 [x] write the parser

    The task is listed open, then marked done and listed again.

  6. What does ./tasks_help puzzle print?

    Answer

    unknown command: puzzle and the help text

    The unknown branch names the word and reuses the help function.

  7. What does the status command print for no arguments?

    Answer

    The usage line and exit=1

    The empty argument list is an error, and main returns 1.

  8. Why does the help text live in one function?

    Answer

    So the two paths cannot print different texts

    A text duplicated in two branches is correct in one of them after the next change.

  9. Why does the exit code matter to a script?

    Answer

    The script branches on the status of the command

    The shell's if and the build server both look at the status before the text.

  10. What belongs in the command layer?

    Answer

    Parsing words and turning results into output

    The layer maps words to calls; the rules live in the model and the storage.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev