C Part I · Module 12 · Standard Library, Files and Program Interfaces
Command-Line Arguments
A program that can only work on a path written into its own source is not much use to anybody. C's mechanism for being told what to do from outside is a parameter list on main that this curriculum has mentioned twice and never examined — a count and an array of strings, which sounds unremarkable until you notice that the count and the array deliberately disagree about how many elements there are. The array has one more than the count says, and the extra element is a null pointer, guaranteed by the standard. That is not an accident or an implementation detail: it means there are two correct ways to walk the arguments, they are useful to different callers, and a program that knows only one of them is missing the reason the other exists.
Intermediate18 min readCPart IFundamentalsStandard Library
Part I · Module 12 · Chapter 12.6 · Command-Line Arguments
1. The Two Forms of main
The standard sanctions exactly two:
int main(void) { ... }
int main(int argc, char *argv[]) { ... }The second may also be written char **argv, which is the same type — Chapter 7.7 established that a parameter declared as an array is a pointer, so char *argv[] and char **argv declare the same parameter. The bracket form is conventional because it reads as what it is: an array of pointers to char (Chapter 7.9), each one a string.
main(void) is not a restricted form of the other. It is a different declaration that has no way to see the arguments at all:
$ ./a3 these are ignored
main(void) ran; it has no way to see argumentsThe arguments were supplied; this main simply has no parameters to receive them in. Use main(void) when the program takes none, and the two-parameter form when it does.
Implementations may offer more — a third parameter for the environment is common — but it is an extension, not one of the two forms the standard defines, and a program using it is relying on something outside C.
2. What Is Guaranteed
argcis non-negative, and when it is greater than zero,argv[0]throughargv[argc-1]are pointers to strings.argv[0]is the program name, or an empty string if the name is not available.argv[argc]is a null pointer. Always.- The strings are modifiable, and their values persist for the whole run of the program.
Measured, with no arguments:
argc = 1
argv[0] = "./a2" (len 4)
argv[1] (== argv[argc]) is NULLand with three:
argc = 4
argv[0] = "./a2" (len 4)
argv[1] = "alpha" (len 5)
argv[2] = "beta" (len 4)
argv[3] = "gamma" (len 5)
argv[4] (== argv[argc]) is NULLNote that argc counts the program name. A program invoked with three arguments sees argc == 4, and the arguments you care about start at index 1. An argc of 1 means "no arguments were given", which is the common case worth handling first.
3. Two Ways to Walk It
Because argv has argc + 1 elements with a null at the end, there are two correct traversals:
/* by count */
for (int i = 1; i < argc; i++) {
printf("%s\n", argv[i]);
}
/* by terminator */
for (char **p = argv + 1; *p != NULL; p++) {
printf("%s\n", *p);
}counted by walking to NULL: 4 matches argc ? 1The two agree, measured, with no arguments and with three.
The terminator is not redundant with the count — it is what lets
argvbe passed on withoutargc. A function that receives only achar **can still find the end, which is how the operating-system calls that launch a program take an argument vector: one pointer, self-delimiting.
In your own code, use the index form. It is clearer, it gives you i for error messages, and argc is right there. The terminator form is worth recognising because you will read it, and worth knowing about because it tells you something true about the shape of the data.
This is Chapter 8.1's null-terminated idea applied one level up: a null-terminated array of pointers, exactly as a string is a null-terminated array of characters, with the same trade — self-delimiting, at the cost of a scan to find the length.
4. Arguments Can Be Empty
$ ./a2 "" second
argc = 3
argv[0] = "./a2" (len 4)
argv[1] = "" (len 0)
argv[2] = "second" (len 6)
argv[3] (== argv[argc]) is NULLAn argument that exists, is counted, and has length zero. It is not a null pointer and not a missing argument — argv[1] is a perfectly valid pointer to a perfectly valid string that happens to be empty.
This is the case argument parsers forget. Code like this reads a real byte and draws a conclusion from it:
if (argv[i][0] == '-') { /* it is an option */ }For an empty argument, argv[i][0] is the terminator, so the test is false and the argument is treated as a filename — which is defensible. But any code that assumes there is a character to inspect, or that does argv[i][1] after finding a -, needs to have checked the length first.
Validate the length before inspecting the contents, exactly as Chapter 8.1 requires for any string whose origin you do not control. An argument's length is whatever whoever ran the program decided.
5. The Strings Are Modifiable, Within Their Length
argv[1][0] = 'X';after argv[1][0] = 'X': "Xlpha" (len unchanged: 5)The standard explicitly permits modifying the argument strings, which distinguishes them from the string literals of Chapter 8.2 — those are not modifiable, and writing to one is undefined. Argument strings are real, writable arrays.
The limit is the one that matters:
6. Validating Arguments
Everything the rest of this module taught about checking applies, with one addition specific to argv: check argc before indexing.
/* wrong: reads argv[1] whether or not it exists */
int n = atoi(argv[1]);
/* right */
if (argc < 2) {
fprintf(stderr, "usage: %s COUNT\n", argv[0]);
return 1;
}Indexing argv[1] when argc is 1 reads argv[argc], which is the null pointer — so the usual symptom is a crash on dereferencing it, and Chapter 7.4 §3 established that it is undefined rather than guaranteed to crash.
Four checks, in order, for any argument a program accepts:
1 — Is it there? argc tells you, and the message when it is not should name the program (argv[0]) and say what was expected.
2 — Is it the right shape? Length, prefix, presence of a separator. §4's empty argument is the case to remember.
3 — Does it convert? For a number, this is Chapter 12.3 §6's job and all four of strtol's failure modes apply unchanged — no digits, trailing characters, out of long's range, and out of int's range. The only difference is that the text comes from argv[i] instead of a line buffer.
4 — Is the value acceptable? Converting successfully is not the same as being a sensible count, size or index. That check is yours and belongs to the program, not the library.
7. A Complete Program
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/* 12.3 §7's parse, with the line replaced by an argument */
static int parse_int(const char *text, int *out)
{
errno = 0;
char *end;
long v = strtol(text, &end, 10);
if (end == text) { return 0; } /* no digits */
while (*end == ' ' || *end == '\t') { end++; }
if (*end != '\0') { return 0; } /* trailing junk */
if (errno == ERANGE) { return 0; } /* beyond long */
if (v < -2147483647L || v > 2147483647L) { return 0; } /* beyond int */
*out = (int)v;
return 1;
}
static void usage(const char *program)
{
fprintf(stderr, "usage: %s COUNT PATH\n", program);
fprintf(stderr, " COUNT must be between 1 and 1000\n");
}
int main(int argc, char *argv[])
{
const char *program = (argc > 0 && argv[0][0] != '\0') ? argv[0] : "program";
if (argc != 3) { /* 1: is it there */
usage(program);
return 1;
}
int count;
if (!parse_int(argv[1], &count)) { /* 3: does it convert */
fprintf(stderr, "%s: '%s' is not a number\n", program, argv[1]);
return 1;
}
if (count < 1 || count > 1000) { /* 4: is it acceptable */
fprintf(stderr, "%s: count %d is out of range\n", program, count);
return 1;
}
if (argv[2][0] == '\0') { /* 2: is it the right shape */
fprintf(stderr, "%s: the path must not be empty\n", program);
return 1;
}
FILE *f = fopen(argv[2], "r"); /* 12.4: check every open */
if (f == NULL) {
fprintf(stderr, "%s: cannot open %s\n", program, argv[2]);
return 1;
}
printf("would process %d records from %s\n", count, argv[2]);
fclose(f);
return 0;
}Six habits in that program, each from a measurement in this module:
argv[0] is defended. The standard permits it to be an empty string, so the program has a fallback name rather than printing usage: with nothing after it.
argc is checked before any indexing, and checked for the exact count rather than a minimum, so a stray extra argument is reported rather than ignored.
Errors go to stderr — Chapter 12.2 §8 — so a user redirecting output still sees them.
The usage message names the program and the constraint, which is what makes it useful rather than decorative.
Every argument is validated before use, and the numeric one goes through all four of strtol's failure modes.
The empty-string case is tested explicitly, because §4 measured that it can happen.
8. What the Shell Already Did
One point of orientation, because it explains why argv looks the way it does.
By the time main runs, the splitting is finished. Whatever launched your program decided where one argument ended and the next began, expanded any wildcards, removed any quotes, and handed over a finished array. None of that is C's doing, and your program cannot see the original command line as one string.
The practical consequences:
An argument may contain spaces. ./prog "two words" gives one argument of nine characters. A program that tries to re-split its arguments on spaces is undoing work that was done deliberately.
A wildcard is already expanded. ./prog *.txt arrives as however many filenames matched — possibly hundreds, possibly one, possibly the literal *.txt if nothing matched. argc is the only thing that knows.
Quotes are gone. There is no way to tell ./prog abc from ./prog "abc".
And the count is not under your control. A program must behave sensibly for argc == 1, for far more arguments than it expects, and for arguments of any length or none.
Treat
argvas untrusted input of unbounded size, which is what it is. Everything Chapter 12.3 established about reading input applies, minus the reading.
A note on what is not here: POSIX provides getopt for parsing option flags, and most Unix programs use it. It is not ISO C, so it is outside this curriculum's baseline — worth knowing it exists the moment you need -v, --output=FILE and the rest, rather than writing that parser yourself.
9. What You Should Be Able to Say Now
- The standard sanctions two forms of
main—(void)and(int, char *[]). A third parameter is an extension. char *argv[]andchar **argvare the same parameter — Chapter 7.7's decay.- Measured:
main(void)runs normally and cannot see arguments that were supplied. argccounts the program name, so real arguments start at index 1 andargc == 1means none were given.argv[0]is the program name or an empty string — defend against the second.argv[argc]is a null pointer, guaranteed — measured with 0 and 3 arguments.- So
argvhasargc + 1elements and can be walked two ways; measured, both agree. - The terminator is what lets
argvbe passed on withoutargc— the reason it exists. - Measured: an argument can be an empty string — counted, valid, length zero.
- The strings are modifiable, within their existing length. There is no room past the terminator; shortening in place is safe, extending is not.
- Check
argcbefore indexing —argv[1]withargc == 1reads the null pointer. - Four checks: is it there · is it the right shape · does it convert (all four
strtolmodes) · is the value acceptable. Treatargvas untrusted input.
10. Exercises
Exercise 1 — give argc and argv. For each invocation of a program called tool, state argc and every element of argv including argv[argc].
./tool
./tool a b
./tool "" x
./tool "two words"Exercise 2 — both traversals. Write a loop that prints every argument after the program name, twice: once using argc and once using the terminator. Say which you would ship and what the other one is for.
Exercise 3 — find the crash. This crashes when run with no arguments. Say exactly which expression is undefined, what it reads, and give the fix.
int main(int argc, char *argv[]) {
FILE *f = fopen(argv[1], "r");
...
}Exercise 4 — the empty argument. For each, say what happens when argv[1] is "".
if (argv[1][0] == '-') { ... }
if (argv[1][0] == '-' && argv[1][1] == 'v') { ... }
size_t n = strlen(argv[1]);
char c = argv[1][0];Exercise 5 — modify safely. Write the code that splits argv[i] at the first = into a name and a value, in place. State why it is safe, and give one modification of argv[i] that would not be.
Exercise 6 — validate a number. Write the complete validation for an argument that must be an integer between 1 and 65535, listing every rejection reason and which chapter established it.
Exercise 7 — write the usage message. For a program taking a required path and an optional count, write the usage output and the code that prints it. Say why it goes to stderr and why it names argv[0] rather than a literal.
Exercise 8 — the shell's share. For each, say whether your program can tell the difference and why.
./prog abcversus./prog "abc"./prog a bversus./prog "a b"./prog *.txtin a directory with three.txtfiles versus with none./progwith no arguments versus./prog ""
11. Summary
- Two sanctioned forms of
main;char *argv[]andchar **argvare the same. argcincludes the program name; real arguments start at 1.argv[0]may be empty;argv[argc]is a null pointer, guaranteed — measured.- So there are two correct traversals, and the terminator exists so
argvcan travel withoutargc. - Measured: an argument may be an empty string — counted, valid, zero length.
- Modifiable within its length only — no room past the terminator; shortening is safe, extending is not.
- Check
argcbefore indexing. - Four checks: present · right shape · converts · acceptable value.
- The shell already split, expanded and unquoted. Treat
argvas untrusted input of unbounded size.
Every validation failure in §7 ended the same way — a message to stderr and return 1 — and that number has been appearing since Chapter 1.4 without ever being explained. It is not an arbitrary convention: it is the program's report to whatever invoked it, the only thing a shell script or a build system can see of everything your program decided, and the value 1 is not even the portable way to say "something went wrong". There is also more than one way to end a program, the two ways differ in what they do to the buffered data Chapter 12.4 measured sitting in memory, and one of them is a function you can call from anywhere that skips the ordinary return path entirely. The next chapter is about ending a program on purpose and saying why.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
