[[main]] receives command-line arguments through its two parameters: [[argc]], the number of arguments, and [[argv]], the array of strings. The first element of [[argv]] is always the program name, and the rest are what the user typed. Everything beyond that — interpreting [[--verbose]] or [[--count 5]] — is your code's job, and doing it well means failing clearly when the arguments do not make sense.
The two parameters
argc is an int counting the arguments and argv is an array of null-terminated strings. The standard also allows a third parameter, envp, holding the environment.
#include
int main(int argc, char* argv[]) {
std::cout << "argc: " << argc << '\n';
for (int i = 0; i < argc; ++i) {
std::cout << "argv[" << i << "]: " << argv[i] << '\n';
}
return 0;
}Running the program with no arguments gives:
argc: 1
argv[0]: ./69-a.binargv[0] is the program name exactly as the shell used to launch it, which may be an absolute path or nothing useful at all. Run the same program with three extra arguments and argc becomes 4, with argv[1] to argv[3] holding them.
Parsing options by hand
The usual approach is a loop over the arguments, recognising the ones you know and collecting the rest.
#include
#include
int main(int argc, char* argv[]) {
int count = 10;
bool verbose = false;
std::string path = "members.txt";
for (int i = 1; i < argc; ++i) {
std::string argument = argv[i];
if (argument == "--verbose" || argument == "-v") {
verbose = true;
} else if (argument == "--count") {
if (i + 1 >= argc) {
std::cerr << "--count needs a value\n";
return 1;
}
count = std::stoi(argv[++i]);
} else if (argument == "--path") {
if (i + 1 >= argc) {
std::cerr << "--path needs a value\n";
return 1;
}
path = argv[++i];
} else {
std::cerr << "unknown option: " << argument << '\n';
return 1;
}
}
std::cout << "count: " << count << '\n';
std::cout << "path: " << path << '\n';
std::cout << "verbose: " << std::boolalpha << verbose << '\n';
return 0;
}Running it with no arguments gives the defaults:
count: 10
path: members.txt
verbose: falseRunning it with --count 5 --verbose instead gives count 5, path members.txt and verbose true.
Two things make this robust. Every option that takes a value checks that a value is actually there before reading it, so --count at the end of the line does not read past the end of the array. And an unknown option is an error rather than being silently ignored.
A cleaner argument parser
Once there are more than two or three options, a small parser with a helper function keeps the main loop readable.
#include
#include
#include
struct Options {
int count = 10;
bool verbose = false;
std::string path = "members.txt";
};
Options parse(int argc, char* argv[]) {
Options options;
for (int i = 1; i < argc; ++i) {
std::string argument = argv[i];
if (argument == "-v" || argument == "--verbose") {
options.verbose = true;
} else if (argument == "--count" || argument == "-c") {
if (i + 1 >= argc) {
throw std::invalid_argument("--count needs a value");
}
options.count = std::stoi(argv[++i]);
} else if (argument == "--path" || argument == "-p") {
if (i + 1 >= argc) {
throw std::invalid_argument("--path needs a value");
}
options.path = argv[++i];
} else {
throw std::invalid_argument("unknown option: " + argument);
}
}
return options;
}
int main(int argc, char* argv[]) {
try {
Options options = parse(argc, argv);
std::cout << "count: " << options.count << '\n';
std::cout << "path: " << options.path << '\n';
std::cout << "verbose: " << std::boolalpha << options.verbose << '\n';
} catch (const std::exception& error) {
std::cerr << "error: " << error.what() << '\n';
return 1;
}
return 0;
}The output with valid arguments is the same as before. With an unknown option the program prints the error and exits with status one, which is what a shell script expects.
Usage text and exit codes
A program that takes arguments should say what they are. The convention is to print the usage to standard error and exit with a non-zero status when the arguments are wrong.
#include
#include
void print_usage(const char* program) {
std::cout << "usage: " << program << " [--count N] [--path FILE] [--verbose]\n";
std::cout << " --count N number of records to process\n";
std::cout << " --path FILE file to read\n";
std::cout << " --verbose print more detail\n";
}
int main(int argc, char* argv[]) {
if (argc > 1 && std::string(argv[1]) == "--help") {
print_usage(argv[0]);
return 0;
}
if (argc < 2) {
print_usage(argv[0]);
return 0;
}
std::cout << "processing " << argv[1] << '\n';
return 0;
}The output when the program is run with no arguments is:
usage: ./69-d.bin [--count N] [--path FILE] [--verbose]
--count N number of records to process
--path FILE file to read
--verbose print more detailReturning zero for --help is the convention every shell script expects. A program that treats missing arguments as a mistake would return one from the second branch instead of zero.
Converting arguments safely
std::stoi throws when the text is not a number, which is fine if you catch it, and unhelpful if you do not. The safer conversion checks the whole string was consumed.
#include
#include
#include
#include
std::optional to_int(const std::string& text) {
int value = 0;
const char* first = text.data();
const char* last = text.data() + text.size();
const auto result = std::from_chars(first, last, value);
if (result.ec != std::errc{} || result.ptr != last) {
return std::nullopt;
}
return value;
}
std::string describe(const std::optional& value) {
if (!value.has_value()) {
return "rejected";
}
return std::to_string(*value);
}
int main(int argc, char* argv[]) {
const std::string text = (argc >= 2) ? argv[1] : "12";
std::cout << "text '" << text << "' -> " << describe(to_int(text)) << '\n';
std::cout << "text '12x' -> " << describe(to_int("12x")) << '\n';
std::cout << "text '' -> " << describe(to_int("")) << '\n';
return 0;
}Running it with 12 gives "text '12' -> 12", and running it with 12x gives "text '12x' -> rejected". In a real program the rejected branch would print an error and exit with status one.
Quick reference
argcis the number of arguments andargvthe strings;argv[0] is the program name.- Options with values need a check that the value exists before you read it.
- An unknown option should be an error, not something to ignore.
- Print usage text to standard error and exit non-zero when the arguments are wrong.
- Use
std::from_charsor catch the exception when converting text to a number. - A third parameter,
envp, optionally gives you the environment.
Try it yourself: write a program that takes a file path and a search term, and prints every line of the file that contains the term. Handle the case where the file cannot be opened and the case where the term is missing, printing usage text in both. Then add a [[--count]] option that limits how many matches are printed.