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
-
Add a `remove
` command to the dispatch and print the list after it. - Make the help text list the commands from one table and print the table in the dispatch.
- 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.
-
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].
-
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.
-
What does the unknown command return?
Answer
2
Two is the conventional status for a usage error, and it is not zero.
-
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.
-
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.
-
What does
./tasks_help puzzleprint?Answer
unknown command: puzzle and the help text
The unknown branch names the word and reuses the help function.
-
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.
-
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.
-
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.
-
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