Embercode
✕
C++ Variables and Data TypesLesson 14 of 15

40 min read

Command-line arguments

Why this matters

Many programs need a value before they start. A file converter needs a file name. A tool that adds numbers needs the numbers. You can type these values on the same line as the program's name, so the program receives them at once, with no prompt and no waiting. Tools such as ls, git and the C++ compiler itself work this way.

In a coding test you will usually read input with cin while the program runs. This lesson covers the other route, where the values arrive on the command line before the program starts. The checks you learn here, which turn text into numbers and reject bad text, also make cin input safer.

What the shell hands to a program

When you type a line in a terminal, a program called the shell reads it and starts the program you named. Here is a line you can type:

./args1 12 abc -x

The shell splits this line at spaces. The first word, ./args1, names the program to run. The other three words, 12, abc and -x, are called arguments. An argument is one word typed after the program's name. The shell passes all of them to the program when it starts, and the program reads them from main.

A word can contain spaces if you wrap it in double quotes. The line ./args2 "1 2" passes one argument, the text 1 2, which contains a space. The program sees it as one piece. This matters in the mistakes section below.

C++ gives the program two values through the parameters of main:

  • argc is the count of words, and it includes the program name. The name stands for "argument count".
  • argv is the list of words. The name stands for "argument vector", and a vector here means a list.

The line int main(int argc, char* argv[]) declares both. Read char* argv[] as "a list of pieces of text". Each piece is one word from the command line, and the list has argc entries. Pointers are covered in a later module, so for now treat each entry as one piece of text. Each piece ends with a hidden zero character, which tells the program where the word stops.

The first entry, argv[0], holds the program name. Usually it is the name as you typed it. The real program can be started in other ways, so do not base any logic on this name. The first real argument is argv[1]. On normal systems argc is never 0, because argv[0] is always there. When the user types nothing after the name, argc is 1.

Example 1: print every argument

This first program prints argc and each entry of argv. It does not use the words for anything yet. Its job is to show you what arrives.

#include <iostream>
#include <span>
#include <string_view>
 
int main(int argc, char* argv[]) {
    std::span<char*> args(argv, static_cast<std::size_t>(argc));
    std::cout << "argc = " << argc << '\n';
    for (std::size_t i = 0; i < args.size(); ++i) {
        std::string_view text = args[i];
        std::cout << "argv[" << i << "] = \"" << text << "\"\n";
    }
    return 0;
}

Two lines need explaining. The line that creates args uses std::span, a standard tool from C++20 that holds a start point and a length. Its length must be an unsigned count, std::size_t, and argc is a signed int. The static_cast says that the conversion is intended. The line std::string_view text = args[i]; wraps one piece of text in a std::string_view. A string view does not copy the text. It records where the text starts and how long it is, so you can compare it, measure it and print it.

Compile and run it with the commands below. Use -std=c++20 to select the C++20 standard, and -Wall to ask the compiler to report common mistakes.

g++ -std=c++20 -Wall -o args1 args1.cpp
./args1
./args1 12 abc -x

The output of the first run is:

argc = 1
argv[0] = "./args1"

The output of the second run is:

argc = 4
argv[0] = "./args1"
argv[1] = "12"
argv[2] = "abc"
argv[3] = "-x"

Each word is text, even 12. The program has not turned 12 into a number. That conversion is the next step.

Turning text into numbers

The text 12 is two characters: the digit 1 and the digit 2. The number 12 is a value that you can add and compare. A program must convert the text, and it must decide what to do when the text is not a number at all, such as abc.

The C++ standard library has several tools for this. Two of them matter here.

ToolOn bad inputNotes
std::stoithrows an exceptionskips spaces at the start and stops at the first letter without an error
std::from_charsreturns an error codedoes not skip spaces, and tells you where it stopped

An exception is an error signal. If the program does not catch it, the program stops. The next program shows what std::stoi does with bad input. Its output is shown after the code.

#include <iostream>
#include <stdexcept>
#include <string>
 
int main() {
    std::cout << "stoi(\"12abc\") = " << std::stoi("12abc") << '\n';
    std::cout << "stoi(\"  7\") = " << std::stoi("  7") << '\n';
    try {
        std::stoi("abc");
    } catch (const std::invalid_argument& e) {
        std::cout << "invalid_argument: " << e.what() << '\n';
    }
    try {
        std::stoi("99999999999");
    } catch (const std::out_of_range& e) {
        std::cout << "out_of_range: " << e.what() << '\n';
    }
    return 0;
}
stoi("12abc") = 12
stoi("  7") = 7
invalid_argument: stoi
out_of_range: stoi

The first line shows the trap. The text 12abc becomes 12, and the letters are dropped without any message. If a user types 12abc as a count, the program quietly uses 12. The std::stoi tool is fine when you control the text, but it is weak for checking user input.

std::from_chars does the checking for you. It reads a range of characters and returns two things. The first is an error code, which says whether the conversion worked. The second is a pointer, which says where the reading stopped. If the stop point is not the end of the text, then some characters were left over and the whole text was not a number. You will see this in the next example.

std::from_chars needs the start and end of the text. A std::string_view gives both: text.data() is the start, and text.data() + text.size() is the end. Because from_chars lives in the <charconv> header, you must include it.

Example 2: add the whole numbers you were given

This program adds every argument after the program name. It checks each one, and if one is bad it prints a message and stops. The function parse_int does the checking, and it returns a std::optional<int>. An optional is a value that either holds an int or holds nothing. The nothing case means the text was not a whole number.

#include <charconv>
#include <iostream>
#include <optional>
#include <span>
#include <string_view>
 
// Returns the whole text as an int, or nothing if the text is not a whole number.
std::optional<int> parse_int(std::string_view text) {
    int value = 0;
    const char* first = text.data();
    const char* last = text.data() + text.size();
    auto [ptr, ec] = std::from_chars(first, last, value);
    if (ec != std::errc{} || ptr != last) {
        return std::nullopt;
    }
    return value;
}
 
int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "usage: " << argv[0] << " NUMBER [NUMBER ...]\n";
        return 1;
    }
    std::span<char*> args(argv, static_cast<std::size_t>(argc));
    long long total = 0;
    for (std::size_t i = 1; i < args.size(); ++i) {
        std::optional<int> value = parse_int(args[i]);
        if (!value) {
            std::cerr << "error: argument " << i << " (\"" << args[i] << "\") is not a whole number\n";
            return 2;
        }
        total += *value;
    }
    std::cout << "sum = " << total << '\n';
    return 0;
}

The line auto [ptr, ec] = ... is a structured binding. It unpacks the two results of from_chars into two names, ptr and ec. The condition ec != std::errc{} is true when there was an error. std::errc{} is the code for "no error", written with empty braces so that it has the default value. The condition ptr != last is true when characters remain after the number. For the text 12abc, from_chars reads 12 and stops at the a, so ptr points at a, and the check rejects the text. That is the difference from std::stoi.

The loop starts at index 1, not 0, because index 0 is the program name. The variable total is a long long, which can hold larger sums than an int.

Three runs show the three outcomes:

./args2 12 30 -4
./args2
./args2 12 abc
sum = 38
usage: ./args2 NUMBER [NUMBER ...]
error: argument 2 ("abc") is not a whole number

The usage message uses argv[0] so that it shows the name the user typed. The error message gives the position of the bad argument, which helps the user find it.

Example 3: a count with a range check

Many tools take a count and then some text. The next program repeats the text the given number of times. It must check two things: the count is a whole number, and the count is in a sensible range. Here the range is 1 to 5.

#include <charconv>
#include <cstddef>
#include <iostream>
#include <optional>
#include <span>
#include <string_view>
 
std::optional<int> parse_int(std::string_view text) {
    int value = 0;
    const char* first = text.data();
    const char* last = text.data() + text.size();
    auto [ptr, ec] = std::from_chars(first, last, value);
    if (ec != std::errc{} || ptr != last) {
        return std::nullopt;
    }
    return value;
}
 
int main(int argc, char* argv[]) {
    if (argc < 3) {
        std::cerr << "usage: " << argv[0] << " COUNT TEXT [TEXT ...]\n";
        return 1;
    }
    std::span<char*> args(argv, static_cast<std::size_t>(argc));
 
    std::optional<int> count = parse_int(args[1]);
    if (!count) {
        std::cerr << "error: COUNT must be a whole number, got \"" << args[1] << "\"\n";
        return 2;
    }
    if (*count < 1 || *count > 5) {
        std::cerr << "error: COUNT must be from 1 to 5, got " << *count << '\n';
        return 2;
    }
 
    for (int round = 0; round < *count; ++round) {
        for (std::size_t i = 2; i < args.size(); ++i) {
            std::cout << args[i] << (i + 1 < args.size() ? " " : "\n");
        }
    }
    return 0;
}

The first check needs argc to be at least 3, because the program needs a count and at least one word. The count is then checked twice: once to see that it is a number, and once to see that it is in range. Keeping the two checks separate gives two different messages, so the user knows which part of the input is wrong.

The inner loop prints each word and puts a space after it, except for the last word, where it prints a line break. The conditional (i + 1 < args.size() ? " " : "\n") picks one of two values. This is the same pattern as choosing between two prints in an if, written in one line.

./repeat 2 hi there
./repeat 9 x
./repeat abc x
hi there
hi there
error: COUNT must be from 1 to 5, got 9
error: COUNT must be a whole number, got "abc"

Example 4: decimal numbers

Whole numbers are not always enough. An average, a price or a measurement can have a decimal point. The standard library has std::from_chars for double too, and it works the same way. The text must match a number format, and the whole text must be used.

#include <charconv>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <optional>
#include <span>
#include <string_view>
 
std::optional<double> parse_double(std::string_view text) {
    double value = 0.0;
    const char* first = text.data();
    const char* last = text.data() + text.size();
    auto [ptr, ec] = std::from_chars(first, last, value);
    if (ec != std::errc{} || ptr != last) {
        return std::nullopt;
    }
    return value;
}
 
int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "usage: " << argv[0] << " NUMBER [NUMBER ...]\n";
        return 1;
    }
    std::span<char*> args(argv, static_cast<std::size_t>(argc));
 
    double sum = 0.0;
    std::size_t count = 0;
    for (std::size_t i = 1; i < args.size(); ++i) {
        std::optional<double> value = parse_double(args[i]);
        if (!value) {
            std::cerr << "error: \"" << args[i] << "\" is not a number\n";
            return 2;
        }
        sum += *value;
        ++count;
    }
    std::cout << std::fixed << std::setprecision(2);
    std::cout << "average of " << count << " numbers = " << sum / static_cast<double>(count) << '\n';
    return 0;
}

std::fixed and std::setprecision(2) come from <iomanip>. They make the output show exactly two digits after the decimal point. Without them, the average would print as 2.66667, with six significant digits.

./average 1.5 2.5 4
./average -2e1 10
./average 1.5 x
average of 3 numbers = 2.67
average of 2 numbers = -5.00
error: "x" is not a number

The e in -2e1 is the exponent form, meaning minus 2 times 10 to the power 1, which is minus 20. The from_chars function accepts this form. Like the integer version, it does not accept a leading plus sign, so +5 is rejected. Running ./average +5 1 prints error: "+5" is not a number.

Exit codes

When main returns a number, that number goes to the shell as the exit code. By convention 0 means success, and any other value means the program failed. The shell can read the last exit code through $?. The programs above use 1 for a usage error and 2 for bad input. The numbers are a convention chosen by the program's author, so write them down and keep them the same across a program.

./args2 12 abc
echo "exit code $?"
error: argument 2 ("abc") is not a whole number
exit code 2

Scripts and other programs use this number to decide what to do next, so a message on the screen is not enough. Return a non-zero code whenever the program could not do its work.

Common mistakes

Reading argv[1] without checking argc. When the user types nothing after the program name, argc is 1, and argv[1] is past the last word. In C++ this is undefined behaviour, which means the program may do anything. The next program reads argv[1] without a check:

#include <iostream>
#include <string_view>
 
int main(int argc, char* argv[]) {
    std::cout << "argc = " << argc << '\n';
    std::string_view first = argv[1];
    std::cout << "first argument: " << first << '\n';
    return 0;
}

Run it with no arguments:

./argv_crash
echo "exit code $?"
exit code 139

On Linux and macOS, the exit code 139 means the program was stopped by a segmentation fault. A segmentation fault is the operating system stopping a program that touched memory it is not allowed to use. The line argc = 1 may not reach the screen when the output goes to a pipe or a file, because the program stopped while the text was still in a holding area that had not been sent out yet. The fix is the check at the start of each example: if (argc < 2), with a usage message.

Comparing an argument with == to a string literal. The text "add" in the code is a piece of memory, and argv[1] is another piece of memory. The == operator compares addresses here (the memory locations where each text starts), not letters. The next program makes the mistake:

#include <iostream>
 
int main(int argc, char* argv[]) {
    if (argv[1] == "add") {
        std::cout << "pointer comparison said: add\n";
    } else {
        std::cout << "pointer comparison said: not add\n";
    }
    return 0;
}

The compiler warns about it:

warning: comparison with string literal results in unspecified behavior [-Waddress]

When you run it with ./argv_pitfalls add, the output is pointer comparison said: not add, even though the argument is add. Compare with a std::string_view instead, as in std::string_view(argv[1]) == "add". This compares the letters.

Trusting std::stoi with user text. As shown earlier, std::stoi turns 12abc into 12 with no error. Use std::from_chars and check the stop point, as in parse_int.

Forgetting the <charconv> include. Without #include <charconv>, the compiler stops with an error. The real message is:

error: ‘from_chars’ is not a member of ‘std’

The fix is the include line at the top of the file.

Expecting a quoted argument to be split. The shell keeps "5 6" as one argument. The program receives the text 5 6, and from_chars stops at the space, so the check rejects it:

./args2 "5 6"
echo "exit code $?"
error: argument 1 ("5 6") is not a whole number
exit code 2

If you want two numbers, the user must type them as two words, with no quotes: ./args2 5 6.

Practice

Exercise 1. Write a program that prints how many arguments the user typed, not counting the program name. Running ./count 4 9 x should print typed 3 arguments.

Show the answer

#include <iostream>
 
int main(int argc, char* argv[]) {
    std::cout << "typed " << argc - 1 << " arguments\n";
    return 0;
}

The count is argc minus 1, because argv[0] is the program name. Running ./count 4 9 x prints typed 3 arguments.

Exercise 2. The program calls std::stoi(argv[1]) and prints the result. What does ./predict 7x print?

Show the answer

It prints 7. The function std::stoi reads the digit 7, stops at the x, and returns 7 without an error. The same behaviour was shown with 12abc earlier.

Exercise 3. Write a program that prints the largest whole number among the arguments. It must print a usage message and exit with code 1 when no argument is given. It must print an error and exit with code 2 when an argument is not a whole number.

Show the answer

#include <charconv>
#include <cstddef>
#include <iostream>
#include <optional>
#include <span>
#include <string_view>
 
std::optional<int> parse_int(std::string_view text) {
    int value = 0;
    const char* first = text.data();
    const char* last = text.data() + text.size();
    auto [ptr, ec] = std::from_chars(first, last, value);
    if (ec != std::errc{} || ptr != last) {
        return std::nullopt;
    }
    return value;
}
 
int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "usage: " << argv[0] << " NUMBER [NUMBER ...]\n";
        return 1;
    }
    std::span<char*> args(argv, static_cast<std::size_t>(argc));
    std::optional<int> largest = parse_int(args[1]);
    if (!largest) {
        std::cerr << "error: argument 1 is not a whole number\n";
        return 2;
    }
    for (std::size_t i = 2; i < args.size(); ++i) {
        std::optional<int> value = parse_int(args[i]);
        if (!value) {
            std::cerr << "error: argument " << i << " is not a whole number\n";
            return 2;
        }
        if (*value > *largest) {
            largest = value;
        }
    }
    std::cout << "largest = " << *largest << '\n';
    return 0;
}

Running ./ex_largest 4 19 -3 7 prints largest = 19. Running ./ex_largest 4 x prints error: argument 2 is not a whole number and exits with 2.

Exercise 4. A program compares argv[1] with the text start using if (argv[1] == "start"). The compiler warns, and the comparison is wrong. Fix it.

Show the answer

Use a std::string_view for the comparison, and check argc first:

#include <iostream>
#include <string_view>
 
int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cout << "no command given\n";
        return 1;
    }
    if (std::string_view(argv[1]) == "start") {
        std::cout << "starting\n";
    } else {
        std::cout << "unknown command: " << argv[1] << '\n';
    }
    return 0;
}

Running ./start_check start prints starting. Running ./start_check stop prints unknown command: stop. Running it with no argument prints no command given and exits with 1.

Exercise 5. Predict the output of ./args2 "5 6" and explain why.

Show the answer

It prints error: argument 1 ("5 6") is not a whole number and exits with 2. The quotes make the shell pass one argument, 5 6, with a space in it. The from_chars function reads 5, stops at the space, and the check sees characters left over, so the text is rejected.

Summary

  • The shell splits the command line at spaces, and the program receives the words as argc and argv.
  • argv[0] is the program name, so the first real argument is argv[1], and argc is 1 when nothing was typed.
  • Check argc before reading argv[k], and compare text with std::string_view, not == on argv entries.
  • Use std::from_chars to convert text to a number, because std::stoi accepts 12abc as 12.
  • Check that the whole text was used, and return a non-zero exit code when the input is wrong.
Practice this topic: coding problems for C++ Variables and Data Types →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Division by zero