C · lesson 8 of 25
Command-Line Arguments in C
Read arguments with argc and argv, parse flags like -v and -n by hand, validate bad input, and read environment variables with getenv in C.
By SkillByExample · Published
Reading Arguments with argc and argv
main can take two parameters: argc (the argument count, including the program itself) and argv (an array of C strings, one per argument). This is how a C program receives input from the command line rather than from scanf or a file.
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Received %d argument(s)\n", argc - 1);
for (int i = 1; i < argc; i++) {
printf(" argv[%d] = \"%s\"\n", i, argv[i]);
}
return 0;
}
$ gcc -std=c11 -Wall -Wextra -o args_demo args_demo.c
$ ./args_demo report.txt --verbose -n 5
Received 4 argument(s)
argv[1] = "report.txt"
argv[2] = "--verbose"
argv[3] = "-n"
argv[4] = "5"
The loop starts at 1, not 0 — argv[0] conventionally holds the program’s own invocation string, which is rarely useful and not portable across platforms (see the FAQ). argv[argc] is guaranteed by the standard to be a null pointer, which is why some code walks argv with a while (*argv) loop instead of tracking argc separately.
Parsing Flags by Hand
C has no argument-parsing library in the standard library. A hand-rolled parser is just a loop over argv comparing each element against the flags you expect, using strcmp since C strings don’t support == comparison.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
int verbose = 0;
int count = 1;
const char *filename = NULL;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
verbose = 1;
} else if (strcmp(argv[i], "-n") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "error: -n requires a value\n");
return 1;
}
count = atoi(argv[++i]); /* consume the flag's value too */
} else if (argv[i][0] == '-') {
fprintf(stderr, "error: unknown flag '%s'\n", argv[i]);
return 1;
} else {
filename = argv[i]; /* first non-flag argument is positional */
}
}
if (!filename) {
fprintf(stderr, "usage: %s [-v] [-n COUNT] FILENAME\n", "flags_demo");
return 1;
}
for (int i = 0; i < count; i++) {
printf("processing %s%s\n", filename, verbose ? " (verbose)" : "");
}
return 0;
}
$ gcc -std=c11 -Wall -Wextra -o flags_demo flags_demo.c
$ ./flags_demo -v -n 3 report.txt
processing report.txt (verbose)
processing report.txt (verbose)
processing report.txt (verbose)
-n needs a lookahead: when the loop sees -n, it must also consume the next element as -n’s value, which is why i is incremented an extra time inside that branch (argv[++i]). Forgetting this is the most common bug in hand-rolled flag parsing — the parser would otherwise treat -n’s value as a separate, unrecognized argument.
Handling Bad Input
The same parser rejects malformed input instead of silently misbehaving — a missing filename, a flag missing its value, and an unrecognized flag each produce a distinct error and a non-zero exit code:
$ ./flags_demo -v
usage: flags_demo [-v] [-n COUNT] FILENAME
$ echo $?
1
$ ./flags_demo report.txt -n
error: -n requires a value
$ ./flags_demo --bogus report.txt
error: unknown flag '--bogus'
A shell script or CI pipeline calling this program can check $? to detect failure — returning 1 instead of 0 is what makes that possible. A parser that never validates its input (and just crashes or silently uses garbage) breaks that contract.
Reading Environment Variables
Environment variables are a second, separate channel for input — inherited from the parent process rather than typed per-invocation. getenv looks one up by name and returns NULL if it isn’t set.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
const char *log_level = getenv("LOG_LEVEL");
const char *missing = getenv("NOT_A_REAL_VARIABLE");
printf("LOG_LEVEL = %s\n", log_level ? log_level : "(not set, defaulting to INFO)");
printf("NOT_A_REAL_VARIABLE = %s\n", missing ? missing : "(not set)");
return 0;
}
$ gcc -std=c11 -Wall -Wextra -o env_demo env_demo.c
$ ./env_demo
LOG_LEVEL = (not set, defaulting to INFO)
NOT_A_REAL_VARIABLE = (not set)
$ LOG_LEVEL=DEBUG ./env_demo
LOG_LEVEL = DEBUG
NOT_A_REAL_VARIABLE = (not set)
Always check getenv’s return value before using it — an unset variable returns NULL, and passing NULL to printf("%s", ...) or strcpy is undefined behavior on most platforms, not a crash you can rely on catching. Command-line flags and environment variables both feed the same program, but they suit different jobs: flags for per-run choices (-v, --dry-run), environment variables for configuration that stays constant across many runs (API keys, log levels, feature flags).
Practice
- Add a
-h/--helpflag to the flag parser that prints a usage message and exits with status0(not1) as soon as it’s seen, before any other flag is processed.
Solution
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
printf("usage: flags_demo [-v] [-n COUNT] [-h] FILENAME\n");
return 0;
}
}
printf("no --help given, continuing normally\n");
return 0;
}
$ ./practice1 --help
usage: flags_demo [-v] [-n COUNT] [-h] FILENAME
--help is checked in its own pass over argv before the real parsing loop runs, so it short-circuits even if other arguments would otherwise be invalid.
- Modify the parser to collect every non-flag argument as a filename (not just the last one), and print how many were given.
Solution
#include <stdio.h>
int main(int argc, char *argv[]) {
int file_count = 0;
for (int i = 1; i < argc; i++) {
if (argv[i][0] != '-') {
printf("file %d: %s\n", ++file_count, argv[i]);
}
}
printf("total files: %d\n", file_count);
return 0;
}
$ ./practice2 -v report.txt notes.txt -n 5
file 1: report.txt
file 2: notes.txt
file 3: 5
total files: 3
This simplified version doesn’t consume -n’s value the way the original parser does, so 5 is counted as a third “file” — a reminder that once you add value-taking flags to a real parser, every other branch needs to account for the lookahead too.
- Read a
MAX_RETRIESenvironment variable as an integer, defaulting to3if it isn’t set.
Solution
#include <stdio.h>
#include <stdlib.h>
int main(void) {
const char *raw = getenv("MAX_RETRIES");
int max_retries = raw ? atoi(raw) : 3; /* default when unset */
printf("max_retries = %d\n", max_retries);
return 0;
}
$ ./practice3
max_retries = 3
$ MAX_RETRIES=7 ./practice3
max_retries = 7
atoi silently returns 0 for a non-numeric string rather than signalling an error — fine for a quick example, but a production parser would use strtol and check for conversion failures.
- Detect when
-vis passed more than once and print a warning (but don’t treat it as an error).
Solution
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
int verbose_seen = 0;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0) {
if (verbose_seen) {
printf("warning: -v passed more than once\n");
}
verbose_seen = 1;
}
}
printf("verbose = %d\n", verbose_seen);
return 0;
}
$ ./practice4 -v -v
warning: -v passed more than once
verbose = 1
The warning fires on the second occurrence, since verbose_seen is only 1 once the first -v has already been processed.