1. Why does the question of two modes arise?
When you start writing utilities, you quickly notice a funny thing: some users want the program to work like a coffee machine — press a button, get coffee, and go. Others want it to work like a barista — you can talk, clarify, change your mind, ask for another portion without restarting everything. Both requests are valid; they are simply different scenarios.
Let us call them plainly:
“One command = one run” (one-shot).
The user runs the program with arguments, the program performs exactly one action and exits.
Interactive mode (interactive).
The user runs the program once and then interacts with it in a loop: enters commands as lines, receives responses, and repeats.
Important: this is not “two different programs.” Ideally it is a single application that can operate in both models.
When each scenario is appropriate
Sometimes students try to pick a mode based on “aesthetics” or “habit.” It is better to choose based on how the program will be used. To avoid keeping everything in your head, it helps to summarize it in a table:
| Criterion | One-shot (“one command”) | Interactive |
|---|---|---|
| User model | “Run and get the result” | “I converse with the program” |
| Automation (scripts/CI) | Excellent | Poor (waits for input indefinitely) |
| Input error | Usually fatal: return != 0 | Usually nonfatal: report and continue |
| stdout | Should be as clean as possible | Can include hints/prompt (but carefully) |
| Speed of repeated operations | You need to restart the process | You can run commands consecutively |
| UX | Simple contract: parameters and --help | Need help, quit, clear messages |
The main idea from the table is simple: there is no single “correct” mode. The correct one matches the scenario.
2. One-shot mode: “do the job and exit”
One-shot mode seems boring until you try to automate something in real life. Scripts, CI, pipelines, batch file processing — all these rely on the assumption that the program is run, performs a predictable action, prints the result, and exits. No “now enter 1 to continue.” Machines do not like dialogues — they get upset and start hanging.
What the structure of a one-shot program looks like
In simplified form one-shot mode is a “pipeline”:
- parsed arguments,
- validated,
- performed the action,
- printed the result,
- returned an exit code.
For visualization it helps to keep a simple flowchart in mind:
flowchart TD
A[Process start] --> B[Parse argv]
B --> C{Are arguments valid?}
C -- no --> D[Error message to cerr]
D --> E[return != 0]
C -- yes --> F[Execute command]
F --> G[Result to cout]
G --> H[return 0]
Tiny example: “one command — one run”
Suppose we have a very toy command --echo TEXT that prints text and exits. The example is short but shows the “rhythm” of a one-shot application:
#include <iostream>
#include <string_view>
int main(int argc, char* argv[]) {
if (argc != 3 || std::string_view{argv[1]} != "--echo") {
std::cerr << "Error: usage: app --echo TEXT\n";
return 2;
}
std::cout << argv[2] << '\n'; // prints TEXT
return 0;
}
From the user's perspective this is convenient:
- ran it;
- got the output;
- it finished.
From a script's perspective — even more convenient.
What matters specifically in one-shot mode
In one-shot mode argument errors are usually considered fatal: if the user provided wrong parameters, there is nothing to continue with. Therefore the behavior “print an error and continue as if nothing happened” is harmful here: it is better to exit immediately with a clear message and a nonzero return code.
Also one-shot mode prefers a “clean stdout”. That means if the result of your program is a number or JSON or a string, std::cout should contain only the result, without “Hello, user!” or “Now I will compute…”. All “human” and diagnostic output is better sent to std::cerr.
3. Interactive mode: a mini-REPL
Interactive mode appears where the user needs to “feel” the system: try commands, see what happened, clarify, repeat. This is typical for educational programs, small admin utilities, internal tools, and sometimes serious tools (think of the python/node REPL or database consoles).
Key idea: interactive mode is a loop, and in that loop a command error should usually not “kill” the whole program.
Basic interactive loop template
We are saved here by the old pattern: print a prompt, read a line via std::getline, parse it, execute.
#include <iostream>
#include <string>
int main() {
std::string line;
while (true) {
std::cout << "> ";
if (!std::getline(std::cin, line)) {
break; // EOF: input closed (Ctrl+D / Ctrl+Z)
}
std::cout << "You typed: " << line << '\n';
}
return 0;
}
This code does an important thing: it exits cleanly if input ends. In real life this happens not only when the user pressed “weird keys”, but also when you feed stdin from a file or a pipe.
Why interactive usually does getline first, then parse
Beginners like std::cin >> token because “it is simpler”. But an interactive interface usually lives in lines. The user may enter a command with spaces (for example, a note or task text), and if you read tokens with >> you lose everything after the first space.
So the “right” approach is:
first take the whole line (getline),
then split it into command and arguments (for example, via std::stringstream, which you've seen earlier in the course).
4. How to support both modes in one application
It is very easy to do: “if there are arguments — one-shot, if not — interactive”. This is a reasonable start. But as soon as flags, defaults and --help appear, you want the code to remain readable and not turn into nested ifs.
Our goal is a structure where main stays “thin”: it determines the mode and delegates work to functions.
Simple rule for choosing a mode
Let's adopt a simple but useful rule:
- if --interactive is present, we always go interactive;
- otherwise, if there are no arguments besides the program name (argc == 1), we also go interactive;
- otherwise — one-shot.
This is not “the only correct” rule, but it's a good start: an explicit flag has priority, and the behavior “no arguments” becomes convenient for manual runs.
Mode selection code
#include <string_view>
enum class Mode { one_shot, interactive };
Mode choose_mode(int argc, char* argv[]) {
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "--interactive") {
return Mode::interactive;
}
}
return (argc == 1) ? Mode::interactive : Mode::one_shot;
}
Note: here we do not implement a “full argument parser”. We only choose the mode. This is important: each function should solve one task, otherwise you will introduce bugs (and free depression).
5. Practical example application: mini task manager TaskPad
Now we need not only to “talk about modes” but to feel them in code. So let's take a simple educational app — a task manager. We'll store tasks in a std::vector<std::string> (in memory). Commands will be:
- add TEXT — add a task (text may contain spaces),
- list — show all tasks,
- done N — mark a task done (for simplicity: remove it),
- help — show help,
- quit — exit (interactive only).
Frankly: this is not an industrial todo list. But it is perfect to see the difference between modes.
Why we need a “command model”
If you handle commands directly in main with five ifs, it will work. But once you want to support both one-shot and interactive, you will notice duplication: the commands are the same, but input sources differ.
Therefore it is convenient to have a small Command structure that can be obtained either from argv or from an input line.
#include <string>
enum class CommandType { add, list, done, help, quit, unknown };
struct Command {
CommandType type = CommandType::unknown;
std::string arg; // for add this is the text, for done this is the index as a string
};
Yes, storing arg as a string is not ideal. But for an educational example it is convenient: we will parse the index later just before execution.
6. Interactive mode in TaskPad
Now we will assemble the “heart” of the interactive mode: a loop that does not crash from the user's first typo. In interactive mode the user learns, experiments, and makes mistakes — and that's fine.
Parsing a line into a command
We need to turn a line like:
- add Buy milk
- done 2
- list
into a Command.
#include <sstream>
#include <string>
Command parse_line(const std::string& line) {
std::stringstream ss(line);
std::string verb;
ss >> verb;
if (verb == "list") return {CommandType::list, ""};
if (verb == "help") return {CommandType::help, ""};
if (verb == "quit") return {CommandType::quit, ""};
if (verb == "done") {
std::string idx;
ss >> idx;
return {CommandType::done, idx};
}
if (verb == "add") {
std::string rest;
std::getline(ss, rest); // rest starts with a space
if (!rest.empty() && rest[0] == ' ') rest.erase(0, 1);
return {CommandType::add, rest};
}
return {CommandType::unknown, verb};
}
Note the small trick: after add we take the “remainder of the line in full”. This makes add convenient for a human: you can write task text freely without escaping spaces.
Executing a command
Now make a function that executes a command on our vector of tasks. Let it return bool: true — continue, false — exit the interactive loop.
#include <iostream>
#include <vector>
bool execute(std::vector<std::string>& tasks, const Command& cmd) {
if (cmd.type == CommandType::list) {
for (std::size_t i = 0; i < tasks.size(); ++i) {
std::cout << (i + 1) << ") " << tasks[i] << '\n';
}
return true;
}
if (cmd.type == CommandType::add) {
if (cmd.arg.empty()) {
std::cerr << "Error: add requires text\n";
return true;
}
tasks.push_back(cmd.arg);
std::cout << "Added.\n";
return true;
}
if (cmd.type == CommandType::quit) {
return false;
}
std::cerr << "Error: unknown command\n";
return true;
}
We have not implemented done and help yet to avoid overloading a single code example. The main point is the approach: one function “understands the command,” and it can be called from any mode.
TaskPad interactive loop
Now assemble the loop:
#include <iostream>
#include <string>
#include <vector>
int run_interactive() {
std::vector<std::string> tasks;
std::string line;
std::cout << "TaskPad interactive. Type 'help' or 'quit'.\n";
while (true) {
std::cout << "> ";
if (!std::getline(std::cin, line)) break;
Command cmd = parse_line(line);
if (!execute(tasks, cmd)) break;
}
return 0;
}
There are three important UX points here.
First, a greeting in interactive mode is fine: the user has “entered” the program.
Second, the prompt > is printed only in interactive mode. In one-shot it would be noise in stdout.
Third, exiting on EOF works correctly: this is an important courtesy to pipes/files.
7. One-shot mode in TaskPad
Now let's make the version where the command is provided via argv. Note: if we do not persist tasks to a file, a one-shot task manager looks “weird” because the list is empty on each run. But as an educational example for structure (modes, parsing, execution) it is excellent.
Parsing argv into a command
We will define a very simple contract:
- app list
- app add TEXT... (problem: TEXT... will be split across multiple argv; we will join it back)
- app done N
#include <string>
Command parse_argv(int argc, char* argv[]) {
if (argc < 2) return {CommandType::help, ""};
std::string verb = argv[1];
if (verb == "list") return {CommandType::list, ""};
if (verb == "help") return {CommandType::help, ""};
if (verb == "done" && argc >= 3) {
return {CommandType::done, argv[2]};
}
if (verb == "add" && argc >= 3) {
std::string text = argv[2];
for (int i = 3; i < argc; ++i) {
text += ' ';
text += argv[i];
}
return {CommandType::add, text};
}
return {CommandType::unknown, verb};
}
Yes, this is manual joining. In one-shot the user could write app add "Buy milk" (quotes glue the argument at the shell level), but we prefer not to depend on shell quirks — so we show an approach that works “as is”.
Running one-shot
#include <iostream>
#include <vector>
int run_one_shot(int argc, char* argv[]) {
std::vector<std::string> tasks;
Command cmd = parse_argv(argc, argv);
bool ok = execute(tasks, cmd);
if (!ok) {
std::cerr << "Error: quit is not meaningful in one-shot mode\n";
return 2;
}
return 0;
}
Note the detail: the quit command only makes sense in interactive mode; in one-shot it is meaningless (the process will exit anyway). Such “semantic differences” are normal. The main thing is that the program explains them with a clear message.
8. Thin main: choose mode, delegate logic
Final piece: main determines the mode and calls the appropriate function. It looks boring — and that means it's right.
#include <iostream>
int main(int argc, char* argv[]) {
Mode mode = choose_mode(argc, argv);
if (mode == Mode::interactive) {
return run_interactive();
}
return run_one_shot(argc, argv);
}
If your main looks roughly like this — congratulations: you already write code the way “grown-up” utilities are written. Not because it is trendy, but because it is maintainable.
9. Error policy: interactive vs one-shot
Now the important idea that justified having two modes in the first place.
In one-shot mode an error is almost always “nothing was done”, and the program should report it via std::cerr and a nonzero exit code.
In interactive mode an error is more often “the user mistyped”, and the program should:
- say what went wrong,
- suggest help,
- continue the loop.
This leads to different “error policies” even when commands are identical. And that's fine. More importantly, it shows you are thinking about UX, not just compilation.
10. Common mistakes
Mistake #1: the interactive prompt is printed always, even in one-shot mode.
This breaks automation: your stdout gets polluted with > lines, and scripts expecting “clean result” start to fail. Get used to the idea that the prompt is part of the interactive interface and must appear only there.
Mistake #2: in interactive mode commands are read via std::cin >> token.
Initial tests pass while commands have no spaces. Then add Buy milk appears, and suddenly the task becomes Buy, while milk stays in the input stream as the “next command”. Good habit: std::getline first, then parse the line.
Mistake #3: in interactive mode any error terminates the program.
The user mistyped — and that's it, “goodbye”. This is more annoying than it seems: interactive mode is designed for safe mistakes. Better print the error to std::cerr and continue the loop, keeping “hard exit” for quit or EOF.
Mistake #4: one-shot and interactive are implemented as two almost identical copies of logic.
First you write “quick and simple”, then add the second version, then fix a bug in one and forget to fix it in the other. The result: two diverging behaviors. The cure is architecture: parse commands to a common model (Command), and have a single executor function execute(...).
Mistake #5: missing a clear quit command and clear help.
Interactive without help and quit becomes “a room without a door”: the user pokes around, doesn't understand syntax or how to exit, and starts killing the program by closing the terminal (which works, but looks like a cry for help). Even a minimal help greatly improves friendliness.
GO TO FULL VERSION