Skip to main content
C · lesson 8 of 25
C beginner 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

  1. Add a -h/--help flag to the flag parser that prints a usage message and exits with status 0 (not 1) 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.

  1. 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.

  1. Read a MAX_RETRIES environment variable as an integer, defaulting to 3 if 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.

  1. Detect when -v is 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.

Frequently Asked Questions

What is argv[0]?
By convention it holds the string used to invoke the program, but the exact format is not portable — it can be a bare name, a relative path, or a full absolute path depending on the platform and shell. Don't rely on its exact content; if you need the program's name for a usage message, hardcode it or use a build-time constant instead.
Does C have a built-in library for parsing flags, like Python's argparse?
The POSIX C library provides getopt() (and getopt_long() for --long-flags) for parsing single-character and long options with less boilerplate than hand-rolled parsing. It is not part of standard C, though — it comes from <unistd.h> on POSIX systems (Linux, macOS), not from the ANSI C standard library, so it is unavailable on some platforms without a compatibility shim.
What is the difference between command-line arguments and environment variables?
Command-line arguments (argv) are explicit, per-invocation values the caller types after the program name — visible in process listings and logged in shell history. Environment variables (getenv) are inherited from the parent process and persist across multiple invocations without being retyped, which makes them better suited for configuration like API keys or log levels that rarely change between runs.
Are the strings in argv mutable?
The C standard permits argv[i] to be modified in place, and it must remain valid for the whole execution. In practice this means you can write into an argv string's existing bytes (up to its original length) but must never reallocate or free it — the memory is owned by the runtime, not your program.