C / STRINGS AND BUFFER SAFETY
Command-line arguments through argc and argv
Read, validate and convert command-line arguments in C: index argv safely against argc, use the NULL sentinel, and reject bad input before parsing.
What you will learn
- Read operands from argv[1] onward and bound every index with a check against argc
- Walk argv using the guaranteed NULL at argv[argc] instead of carrying your own count
- Check i + 1 < argc before consuming an option's value at argv[i + 1]
- Convert numeric arguments with strtol and endptr checks rather than atoi
Understanding Command-line arguments through argc and argv
When a process starts, the runtime hands main a count and an array: argc is the number of argument strings, argv is an array of argc pointers to NUL-terminated byte strings, and the standard guarantees one extra element, argv[argc], which is a null pointer. By convention argv[0] holds the name the program was invoked under, so the first thing the user actually asked for is argv[1], and "at least one operand" means argc >= 2. Both the array and the strings it points at live for the whole run of the program, so you can keep pointers into argv rather than copying anything out of it.
The mental model that prevents most argument bugs is that the shell finished its work before your program existed. It split the line on whitespace, expanded *.c and $HOME, and removed quotes, so ./p 'a b' arrives as exactly one argument containing a space while ./p *.c can arrive as fifty separate arguments. That means you never re-split an argument on spaces, and you never assume the count or the lengths: both are chosen by whoever runs the program. An argument of 200,000 bytes is an ordinary thing for a caller to pass, so any buffer that receives one must be bounded by its own size, not by a guess about the input.
Every read of argv[i] needs a reason to believe i < argc, and the case that catches people is an option that takes a value: seeing -n at argv[i] says nothing about whether argv[i + 1] exists. Fetching argv[argc] is defined and gives you the null sentinel, but using that pointer as a string is not, and stepping past it walks into whatever the platform stored next, typically the environment pointers. Two further properties are worth internalising: the argument bytes are modifiable, so you can drop a NUL in the middle of one to split it in place with no allocation, but you cannot make an argument longer, and every argument is text, so a number is only a number after strtol and an explicit check.
<stdio.h>
int main(int argc, char *argv[])
{
/* built as ./args, then run as: ./args -n 5 report.txt */
printf("argc = %d\n", argc);
for (int i = 0; i < argc; i++)
printf("argv[%d] = \"%s\"\n", i, argv[i]);
printf("argv[%d] = %s\n", argc, argv[argc] == NULL ? "NULL" : "a string");
if (argc < 2) {
fprintf(stderr, "usage: %s -n COUNT FILE\n", argv[0]);
return 2;
}
printf("first operand: \"%s\"\n", argv[1]);
return 0;
}
argv is a NULL-terminated array of argc modifiable strings whose count and lengths are chosen by the caller, so every index you touch must be justified by argc first.
Worked examples
An option whose value is missing
Shows the i + 1 < argc guard that turns a trailing -n into an error instead of a dereference of the NULL sentinel.
<stdio.h>
<string.h>
/* Same signature as main, so the parser can be driven without a shell. */
static int run(int argc, char **argv)
{
const char *count = "10";
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-n") == 0) {
if (i + 1 == argc) {
printf("%s: -n requires a value\n", argv[0]);
return 2;
}
count = argv[++i];
} else {
printf("%s: operand %s\n", argv[0], argv[i]);
}
}
printf("%s: count = %s\n", argv[0], count);
return 0;
}
int main(void)
{
char *good[] = { "show", "-n", "3", "data.txt", NULL };
char *bad[] = { "show", "data.txt", "-n", NULL };
printf("exit %d\n", run(4, good));
printf("exit %d\n", run(3, bad));
return 0;
}
Example explained
Line 1run takes (argc, argv) exactly like main, which is how you exercise argument handling from a test harness instead of a command line.
Line 2The NULL at the end of good[] and bad[] is not counted in argc; it mirrors the sentinel the runtime places at argv[argc].
Line 3if (i + 1 == argc) is the whole safety mechanism: without it the second call would read argv[3], which is that null pointer, and hand it to strcmp.
Line 4count = argv[++i] consumes the value and advances past it, so "3" is never re-examined as though it were another option.
Splitting KEY=VALUE inside argv
Walks argv using the NULL sentinel instead of argc and splits each argument in place, relying on the guarantee that argument strings are modifiable.
<stdio.h>
int main(int argc, char **argv)
{
/* run as: ./setvars PATH=/bin USER=amy noequals */
int pairs = 0;
if (argc < 1)
return 1; /* no argv[0]: argv + 1 is already past the end */
for (char **p = argv + 1; *p != NULL; p++) {
char *s = *p;
char *eq = NULL;
for (char *c = s; *c != '\0'; c++) {
if (*c == '=') {
eq = c;
break;
}
}
if (eq == NULL) {
printf("%s: not a KEY=VALUE pair\n", s);
continue;
}
*eq = '\0'; /* split the argument in place */
printf("key=%s value=%s\n", s, eq + 1);
pairs++;
}
printf("%d pairs, argc was %d\n", pairs, argc);
return 0;
}
Example explained
Line 1for (char **p = argv + 1; *p != NULL; p++) needs no counter because argv[argc] is guaranteed to be a null pointer.
Line 2*eq = '\0' overwrites the '=' inside the argument itself, which is permitted: the strings main receives are modifiable.
Line 3eq + 1 points just past that overwritten byte and still inside the same argument, so the value needs no buffer of its own.
Line 4The argc < 1 check matters because with an empty argument list argv[0] is the sentinel and argv + 1 points past the array.
Turning an argument into a number
Validates numeric arguments with strtol so malformed and out-of-range text is rejected instead of silently becoming a value.
<errno.h>
<limits.h>
<stdio.h>
<stdlib.h>
static int to_int(const char *s, long *out)
{
char *end;
long v;
errno = 0;
v = strtol(s, &end, 10);
if (end == s || *end != '\0')
return 0; /* empty, or not entirely a number */
if (errno == ERANGE || v < INT_MIN || v > INT_MAX)
return 0; /* does not fit an int */
*out = v;
return 1;
}
int main(void)
{
const char *tests[] = { "42", "42x", "", "99999999999999999999", "-7" };
long v;
for (size_t i = 0; i < sizeof tests / sizeof tests[0]; i++) {
if (to_int(tests[i], &v))
printf("\"%s\" -> %ld\n", tests[i], v);
else
printf("\"%s\" -> rejected\n", tests[i]);
}
return 0;
}
Example explained
Line 1errno = 0 comes first because strtol only sets errno on overflow and leaves any earlier value untouched.
Line 2end == s means no digits were consumed at all, which is how the empty string is caught; atoi("") would just answer 0.
Line 3*end != '\0' rejects trailing junk: strtol stops happily at the 'x' in "42x" and reports a successful 42.
Line 4The INT_MIN/INT_MAX test is separate from ERANGE because strtol only range-checks against long, not against your target type.
Important notes
argv[0] is only a convention. It can be an empty string, and argc can be 0 with argv[0] == NULL when a program is exec'd with an empty argument list, so never print it or use it as a path without a fallback.
The argument strings are writable, but you may not write past an argument's existing NUL: the bytes after it belong to the next argument or to the environment on typical layouts.
Common mistakes
Looping from i = 0 and treating argv[0] as an operand, so the program tries to open its own name as the input file.
Reading argv[i + 1] for an option's value without checking i + 1 < argc: run as ./p file.txt -n, that read yields the null sentinel and the next strcmp or atoi crashes.
Using atoi(argv[1]): "abc" and "" both come back as 0, so invalid input is indistinguishable from a legitimate 0 and the program keeps running with a wrong value.
Try it yourself
Change, predict, then run
Write int run(int argc, char **argv) that requires exactly two operands, a word and a repeat count, prints the word that many times, and otherwise returns 2 after a usage message. Call it from main with the hand-built arrays { "rep", "hi", "3", NULL }, { "rep", "hi", NULL } and { "rep", "hi", "x", NULL } so all three paths run without a shell.
Open the C workspaceCheck your understanding
A parser sees "-n" at argv[i] and immediately reads argv[i + 1] without comparing i + 1 to argc. The program is run as ./copy file.txt -n. What actually happens at that read?
- argv[i + 1] is argv[argc], which the standard guarantees to be a null pointer; fetching it is fine, but passing that NULL to a string function is undefined and usually crashes
- argv[i + 1] is an empty string, so the option's value parses as 0 and the program quietly does the wrong thing
- argc is adjusted by the runtime to cover the extra index, so argv[i + 1] holds whatever word followed on the command line
- It is a compile-time error, because the compiler knows argv's length from argc and can see the index is out of range
Show answer
argc is 3 here, so the flag sits at argv[2] and argv[3] is the guaranteed null sentinel: reading the pointer is legal, dereferencing it is not, which is why the failure surfaces inside strcmp, atoi or printf rather than at the indexing itself. Option 1 is the usual misreading, but the sentinel is a null pointer, not a pointer to an empty string, so nothing parses. Nothing is resized either, and argv is just a char **, so the compiler has no idea how many elements it points to.