Topic 89 of 100 File Handling Interactive Tutorial

Using argc and argv[] in C for Program Arguments

11 mins read Last updated: 22 Aug 2026 28,900 views
C Compiler 0.00%

What is Using argc and argv[] in C for Program Arguments? Core Concept & Direct Summary

Core Objective & Problem Statement

How do `argc` and `argv[]` parse command-line parameters passed to `int main(int argc, char *argv[])` from the terminal shell?

Direct Answer Summary

Every C program you've written starts the same way:

But main has a second form, and it's the one that makes your programs tools rather than exercises:

main.c
Run Online
int main(int argc, char *argv[])

With it, your program can be told what to do when it starts — which file to read, which mode to run in, how many iterations to perform — instead of hardcoding those decisions or asking for them interactively. Every command-line utility you've ever used works this way, and so does every build script that runs yours.

(Suggested image: the hero graphic — a console showing fstats -l -s Error report.txt with arrows from each token to argv[0] through argv[4], and argc = 5 labeled)


How Does Using argc and argv[] in C for Program Arguments Work? In-Depth Mechanics & Architecture

What argc and argv Actually Are

main.c
Run Online
int main(int argc, char *argv[])
  • argc = argument count — how many arguments there are, including the program name. It's always at least 1 in any environment that supports this form.
  • argv = argument vector — an array of pointers to strings, one per argument.

Read the type carefully:char *argv[] is an array of char * — an array of strings. Because it's an array parameter, it decays to char **argv. The two declarations are identical, and you'll see both:

main.c
Run Online
int main(int argc, char *argv[]) /* array notation */int main(int argc, char **argv) /* pointer notation — same thing */

There's also a third parameter you'll occasionally encounter for environment variables:

main.c
Run Online
int main(int argc, char *argv[], char *envp[]) /* envp is NOT standard C */

envp is a POSIX/Windows extension rather than standard C — standard C gives you getenv() instead. Prefer getenv("PATH") for portability.

The layout in memory:

Architecture Diagram
argv ──► ┌──────────┐         │ argv[0]  │───► "fstats"        ← program name         ├──────────┤         │ argv[1]  │───► "-l"            ← first real argument         ├──────────┤         │ argv[2]  │───► "report.txt"         ├──────────┤         │ argv[3]  │───► NULL            ← guaranteed terminator         └──────────┘

Two structural facts worth fixing in your mind:

  1. argv[0] exists even though it's not "your" argument. It's the program name, so a program with one real argument has argc == 2.
  2. argv[argc] is a NULL pointer — guaranteed by the C standard. That's not an accident of implementation; it's a specified property, and it's what makes pointer-based iteration safe.

(Suggested image: the argv array drawn as a row of pointers, each pointing to a string elsewhere in memory, ending with an explicit NULL box)


The Smallest Complete Example

main.c
Run Online
#include <stdio.h>
int main(int argc, char *argv[]) {
    printf("argc = %d\n\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = \"%s\"\n", i, argv[i]);
    }
    printf("\nargv[argc] = %p  (guaranteed NULL)\n",(void *)argv[argc]);
    /* The pointer-walking form, using that NULL terminator */ puts("\nIterating with a pointer instead of an index:");
    for (char **p = argv; *p != NULL; p++) {
        printf("  %s\n", *p);
    }
    printf("\nProgram name was: %s\n", argv[0]);
    return 0;
}

Run as:showargs hello "two words" 42

Output (verified):

main.c
Run Online
argc = 4
argv[0] = "showargs"argv[1] = "hello"argv[2] = "two words"argv[3] = "42"
argv[argc] = 0000000000000000(guaranteed NULL)
Iterating with a pointer instead of an index: showargs hello two words 42
Program name was: showargs

Four things this output teaches:

  1. argc is 4, but only three arguments were given — argv[0] is the program name. This is the single most common source of off-by-one bugs for beginners, and it's why loops start at i = 1 when processing your arguments.
  2. "two words" arrived as ONE argument. The shell grouped it because of the quotes, and the quotes were removed before your program saw it. argv[2] is the string two words, with no quote characters in it.
  3. argv[3] is the string "42", not the number 42. Everything from the command line is text. Converting it to a number is your job (covered below).
  4. argv[argc] is NULL — printed here as the all-zero address. So for (char **p = argv; *p; p++) is a valid, standard-guaranteed loop, and you can pass argv + 1, argc - 1 to a helper to shift the view past the program name.

One caution on argv[0]: it's conventionally the program name, but it's not guaranteed to be non-NULL, and it may be a full path (C:\tools\fstats.exe or /usr/bin/fstats) rather than a bare name. Never parse argv[0] to extract the filename — and if you use it in error messages, that's fine, but don't assume it's a plain name. (Extracting a basename portably is itself non-trivial; the simplest cross-platform habit is to keep a string literal like #define PROG "fstats" for diagnostics.)

(Suggested image: the console invocation annotated — quotes highlighted and crossed out with "shell strips these", the spaces inside quotes marked "stays one argument", and argv[3]="42" flagged as text not a number)


How the Shell Decides What's an Argument

Your program never sees the raw command line. The shell (or cmd.exe, or a launcher) splits it into arguments before your main runs, and that splitting follows rules you must know to predict argc:

Command lineargcResult
prog1argv[0] = "prog" only
prog a b c4Three arguments
prog "a b" c3"a b" is one argument
prog 'a b' c3Single quotes — same grouping (POSIX shells)
prog a\ b c3Escaped space — backslash joins (POSIX shells)
prog ""2An empty string argument — legal and distinct from no argument
prog -2A literal -, conventionally meaning stdin
prog *variesExpanded by the shell on POSIX — see below

Three platform gotchas worth knowing before they cost you an afternoon:

1. Wildcards are expanded by the shell on POSIX, but NOT on Windows.

On Linux/macOS, prog *.txt reaches your program with the wildcard already expanded — argc might be 7 because there are 6 matching files. Your program just loops over them.

On Windows, cmd.exe and PowerShell do not expand wildcards for argument passing the same way. Your program receives the literal string *.txt and must expand it itself with FindFirstFile/FindNextFile (Windows API), because standard C has no directory-listing facility at all.

So a portable tool needs to handle both: iterate argv for files that exist, and expand glob patterns itself when it sees them.

2. Quoting rules differ. POSIX shells use backslash escapes and single/double quotes. Windows uses its own backslash-and-quote rules (and CommandLineToArgvW for the real parsing). A command line that quotes correctly in one shell may not in the other — most commonly when paths contain spaces.

3. There's an OS-imposed length limit. POSIX systems cap total arguments and environment around ARG_MAX (often ~2 MB); Windows caps the command line near 32,767 characters. Very long argument lists silently fail at the shell before your program starts.

(Suggested image: three console panels — POSIX wildcard expansion yielding many argv entries, Windows passing the literal pattern, and a quoted path with a space arriving as one argument)


Converting Arguments to Numbers — Safely

Arguments are strings. To use one as a number, you must convert it — and this is where most command-line programs have bugs.

The tempting-but-wrong way: atoi

main.c
Run Online
int n = atoi(argv[1]);
/* ⚠ no way to detect an error */

atoi is a trap for three reasons:

  • atoi("abc") returns 0 — indistinguishable from atoi("0"). Garbage silently becomes a valid-looking number.
  • atoi("42abc") returns 42 — trailing junk is ignored, silently accepting typo'd input.
  • Overflow is undefined behavior — atoi("99999999999999999999") doesn't fail; it's UB.

Use atoi only in throwaway code with input you fully control. For anything a user can type, use strtol/strtod.

The correct way: strtol with endptr

main.c
Run Online
#include <stdlib.h>
#include <errno.h>
/* Converts a string to a long. Returns 1 on success, 0 on failure. */int parseLong(const char *s, long *out) {
    char *end;
    errno = 0;
    /* strtol only SETS errno, never clears it */
    long v = strtol(s, &end, 10);
    /* base 10 */
    if (end == s) return 0;
    /* no digits at all: "abc", "" */ if (*end != '\0') return 0;
    /* trailing junk: "42abc"        */ if (errno == ERANGE) return 0;
    /* out of long's range           */
    *out = v;
    return 1;
}

Why each check is necessary:

  • end == s — strtol sets end to where parsing stopped. If it equals the start, nothing was converted.
  • *end != '\0' — if parsing stopped before the end of the string, there's trailing garbage. This is the check that rejects "42abc" and also "0x1F" when base is 10.
  • errno == ERANGE — the value didn't fit. strtol returns LONG_MAX or LONG_MIN and sets ERANGE. You must set errno = 0 first, because strtol only ever sets it, never clears it.

Use it at the call site like this:

main.c
Run Online
if (argc > 1) {
    long count;
    if (!parseLong(argv[1], &count)) {
        fprintf(stderr, "%s: '%s' is not a valid number\n", argv[0], argv[1]);
        return 2;
    }
    printf("count = %ld\n", count);
}

The behaviors worth knowing

Inputstrtol(s, &end, 10)Verdict from parseLong
"42"42, end at '\0'✓ accepted
" 42"42, end at '\0'✓ accepted — strtol skips leading whitespace
"+42" / "-42"±42✓ accepted
"42abc"42, end at 'a'✗ rejected (trailing junk)
"abc"0, end == s✗ rejected (no digits)
""0, end == s✗ rejected (empty)
"0x1F" (base 10)0, end at 'x'✗ rejected
"99999999999999999999"LONG_MAX, ERANGE✗ rejected (range)
"12.5"12, end at '.'✗ rejected (use strtod)

Note the third row: strtol skips leading whitespace, so " 42" is accepted. That's usually what you want (users type stray spaces), but it's worth knowing it's happening rather than assuming strictness.

For floating point, the same pattern with strtod:

main.c
Run Online
int parseDouble(const char *s, double *out) {
    char *end;
    errno = 0;
    double v = strtod(s, &end);
    if (end == s || *end != '\0' || errno == ERANGE) return 0;
    *out = v;
    return 1;
}

And if you'd rather not hand-roll it, sscanf works for simple cases — its return value tells you whether the conversion happened:

main.c
Run Online
long n;
if (sscanf(argv[1], "%ld", &n) != 1) {
    fprintf(stderr, "not a number: %s\n", argv[1]);
}

That's more compact than strtol, though it's stricter about trailing junk in some cases and less informative about why it failed. Either is far better than atoi.

(Suggested image: the strtol check sequence as a flow — three rejection gates ("any digits?", "trailing junk?", "in range?") leading to accept)


The Parse Loop: Options, Values, and Files

Most tools have the same shape: some options (flags and options-with-values) plus some operands (usually filenames). The classic structure is one loop over argv, starting at 1:

main.c
Run Online
int wantLines = 0;
/* flags  */const char *searchTerm = NULL;
/* option value */const char *files[MAX_FILES];
/* operands */int fileCount = 0;
for (int i = 1; i < argc; i++) {
    /* ← start at 1: skip argv[0] */ const char *arg = argv[i];
    if (arg[0] == '-' && arg[1] != '\0') {
        /* looks like an option */ if (strcmp(arg, "-l") == 0 || strcmp(arg, "--lines") == 0) {
            wantLines = 1;
        } else if (strcmp(arg, "-s") == 0) {
            if (i + 1 >= argc) {
                /* the value must exist */ fprintf(stderr, "%s: option -s requires a search term\n", argv[0]);
                return 2;
            }
            searchTerm = argv[++i];
            /* consume the NEXT argument */
        } else {
            fprintf(stderr, "%s: unknown option '%s'\n", argv[0], arg);
            return 2;
        }
    } else {
        files[fileCount++] = arg;
        /* an operand */
    }
}

Four decisions embedded in that loop:

1. arg[0] == '-' && arg[1] != '\0' is the option test. The second condition matters: a bare - is not treated as an option, which is the universal convention for "read from stdin." A lone dash should fall through to the operand branch.

2. Options-with-values consume the next argument.searchTerm = argv[++i] — the ++i both reads the value and advances the loop, so the value isn't re-examined as an option. Forgetting the increment is a classic bug: the value gets treated as an operand, or as an unknown option.

3. Missing values must be caught before the increment.if (i + 1 >= argc) guards against argv[i + 1] being out of bounds — that's argv[argc], which is the guaranteed NULL, and reading a string from it would crash.

4. Unknown options are an error, not a silent skip. Reporting and exiting non-zero is what makes a tool scriptable: a typo'd flag fails loudly instead of quietly doing the wrong thing.

Adding the -- convention

Standard practice: -- means "everything after this is an operand, even if it starts with a dash." That's how you pass a file literally named -weird-name.txt:

main.c
Run Online
int afterDashDash = 0;
for (int i = 1; i < argc; i++) {
    const char *arg = argv[i];
    if (!afterDashDash && strcmp(arg, "--") == 0) {
        afterDashDash = 1;
        continue;
        /* the -- itself is consumed */
    }
    if (!afterDashDash && arg[0] == '-' && arg[1] != '\0') {
        /* ... option handling ... */
    } else {
        files[fileCount++] = arg;
    }
}

Verified behavior:fstats -- -weird-name.txt treats -weird-name.txt as a filename and reports -weird-name.txt: No such file or directory — rather than "unknown option."

Clustered short flags (-lw)

POSIX tools commonly accept -lw as shorthand for -l -w. Hand-rolling it looks like this:

main.c
Run Online
if (arg[0] == '-' && arg[1] != '-' && arg[1] != '\0') {
    for (const char *c = arg + 1; *c != '\0';
    c++) {
        /* walk each letter */ switch (*c) {
            case 'l': wantLines = 1;
            break;
            case 'w': wantWords = 1;
            break;
            case 'c': wantChars = 1;
            break;
            default: fprintf(stderr, "%s: unknown option '-%c'\n", argv[0], *c);
            return 2;
        }
    }
}

The honest alternative: on Linux and macOS there's getopt/getopt_long in <unistd.h> / <getopt.h>, which handles clustering, --long options, and option arguments for you. It is not standard C and isn't available on MSVC by default — so using it makes your tool POSIX-only. For a portable tool, the hand-rolled loop is the price of portability. (There are small third-party getopt implementations you can bundle if you want both.)

(Suggested image: the parse loop as a decision diagram — "is it --? → everything after is an operand", "does it start with -? → option", "else → operand", with the "-s consumes next arg" branch marked)


Subcommands: The git-Style Pattern

Larger tools dispatch on the first argument:

main.c
Run Online
mytool count report.txtmytool search Error report.txtmytool help

The clean way to implement it: shift the view once, then hand each handler the remainder.

main.c
Run Online
int cmdCount(int argc, char *argv[]);
/* argv[0] is now "count" */int cmdSearch(int argc, char *argv[]);
int main(int argc, char *argv[]) {
    if (argc < 2) {
        usage(stderr, argv[0]);
        return 2;
    }
    const char *cmd = argv[1];
    if (strcmp(cmd, "count") == 0) {
        return cmdCount(argc - 1, argv + 1);
        /* ← shift by one */
    }
    if (strcmp(cmd, "search") == 0) {
        return cmdSearch(argc - 1, argv + 1);
    }
    if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0) {
        usage(stdout, argv[0]);
        return 0;
    }
    fprintf(stderr, "%s: unknown command '%s'\n", argv[0], cmd);
    usage(stderr, argv[0]);
    return 2;
}

The key technique: argc - 1, argv + 1. This passes a shifted slice of the arguments — so inside cmdCount, argc excludes the subcommand and argv[0] is "count". Every handler can be written exactly like a standalone main, and it never needs to know it was dispatched. (argv + 1 is plain pointer arithmetic — argv is a char **, so advancing it moves one pointer forward. That's the array-pointer equivalence from the pointers cluster, applied to argv itself.)

Each subcommand gets its own usage text and its own options — which is how git, docker, and dotnet keep their interfaces manageable.

(Suggested image: the dispatch diagram — argv split into a command token and a remaining slice, with arrows showing (argc-1, argv+1) passed to each handler)


usage(): A Function You Should Always Write

Every tool with arguments needs one, and it should take a stream so help and errors go to the right places:

main.c
Run Online
static void usage(FILE *out, const char *prog) {
    fprintf(out, "Usage: %s [options] FILE [FILE...]\n" "\n" "Options:\n" "  -l, --lines       count lines\n" "  -w, --words       count words\n" "  -c, --chars       count characters\n" "  -s TERM           print lines containing TERM\n" "  -h, --help        show this help\n", prog);
}

Why the FILE *out parameter matters:

SituationCallDestinationExit code
User asked for help (-h)usage(stdout, argv[0])stdout0 (success)
User made a mistakeusage(stderr, argv[0])stderr2 (failure)

Help requested is not an error, so it belongs on stdout with a success exit code — that way mytool --help | less works, and scripts that check exit codes don't see a false failure. Usage shown because of a mistake belongs on stderr — so it appears even when stdout is redirected to a file, and so mytool -x > out.txt still prints the error to the console instead of burying it in out.txt.

Using argv[0] for prog in the message means the usage text always names the program the way the user invoked it.

(Suggested image: the two usage paths — help to stdout (pipeable) and errors to stderr (always visible), with exit codes 0 and 2 labeled)


Exit Codes: How Your Program Reports Success

main's return value becomes the process exit status, and that's how shells and scripts decide what to do next:

main.c
Run Online
int main(int argc, char *argv[]) {
    if (argc < 2) return 2;
    /* wrong usage            */ /* ... */ if (failure) return 1;
    /* something went wrong   */ return 0;
    /* success                */
}

The convention that's worth following:

CodeMeaning
0Success
1General runtime failure (file missing, operation failed)
2Usage error (bad arguments) — a common convention, not a standard
126 / 127Couldn't execute / command not found — set by the shell, not you

Why it matters practically:

main.c
Run Online
fstats report.txt && echo "OK"
# echo runs only if exit status is 0fstats missing.txt || echo "FAILED"
# echo runs only on non-zero
main.c
Run Online
fstats report.txt;
echo "exit code: $LASTEXITCODE"
main.c
Run Online
fstats report.txtif errorlevel 1 echo FAILED

Two details:EXIT_SUCCESS and EXIT_FAILURE (from <stdlib.h>) are the portable macros — they're usually 0 and 1, but using the names documents intent. And a program that reaches the end of main without a return implicitly returns 0. Write the return explicitly anyway — a function that falls off the end is a habit that causes real bugs elsewhere, and it's better not to train it.

And a design rule: if one of several files fails, still process the rest, and return non-zero at the end. Reporting partial failure is what lets a script react correctly. (In the verified run below, fstats report.txt missing.txt prints the stats for the good file, the error for the bad one, and exits 1.)

(Suggested image: the exit-code decision chart, plus the shell operators && and || shown responding to 0 vs non-zero)


📋 Complete Program: fstats — A File Statistics Tool

Everything on this page in one working tool.

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define LINE_MAX 256
#define MAX_FILES 64
/* usage() takes a stream so help can go to stdout while   errors go to stderr — the correct convention. */static void usage(FILE *out, const char *prog) {
    fprintf(out, "Usage: %s [options] FILE [FILE...]\n" "\n" "Options:\n" "  -l, --lines       count lines\n" "  -w, --words       count words\n" "  -c, --chars       count characters\n" "  -s TERM           print lines containing TERM\n" "  -h, --help        show this help\n" "\n" "With no counting option given, lines/words/chars are all shown.\n" "\n" "Use -- to end options: %s -- -weird-name.txt\n", prog, prog);
}
static int processFile(const char *path, int wantLines, int wantWords, int wantChars, int showAll, const char *searchTerm) {
    FILE *fp = fopen(path, "r");
    if (fp == NULL) {
        perror(path);
        /* e.g.  report.txt: No such file or directory */ return 0;
    }
    char line[LINE_MAX];
    long lines = 0, words = 0, chars = 0;
    int lineno = 0, matches = 0;
    while (fgets(line, sizeof line, fp) != NULL) {
        lineno++;
        lines++;
        chars +=(long)strlen(line);
        /* includes the newline fgets kept */
        int inWord = 0;
        for (const char *p = line; *p != '\0'; p++) {
            if (isspace((unsigned char)*p)) {
                inWord = 0;
            } else if (!inWord) {
                inWord = 1;
                words++;
            }
        }
        if (searchTerm != NULL && strstr(line, searchTerm) != NULL) {
            if (matches == 0) printf("%s:\n", path);
            printf("  line %2d: %s", lineno, line);
            matches++;
        }
    }
    if (ferror(fp)) {
        perror(path);
        fclose(fp);
        return 0;
    }
    fclose(fp);
    if (searchTerm != NULL && matches == 0) {
        printf("%s: no lines contain \"%s\"\n", path, searchTerm);
    }
    if (showAll || wantLines || wantWords || wantChars) {
        printf("%s:", path);
        if (showAll || wantLines) printf(" lines=%ld", lines);
        if (showAll || wantWords) printf(" words=%ld", words);
        if (showAll || wantChars) printf(" chars=%ld", chars);
        putchar('\n');
    }
    return 1;
}
int main(int argc, char *argv[]) {
    int wantLines = 0, wantWords = 0, wantChars = 0;
    const char *searchTerm = NULL;
    const char *files[MAX_FILES];
    int fileCount = 0;
    int afterDashDash = 0;
    /* ---- argv[0] is the program name; the real arguments start at 1 ---- */ for (int i = 1; i < argc; i++) {
        const char *arg = argv[i];
        if (!afterDashDash && strcmp(arg, "--") == 0) {
            afterDashDash = 1;
            /* everything after is a filename */ continue;
        }
        if (!afterDashDash && arg[0] == '-' && arg[1] != '\0') {
            if (strcmp(arg, "-h") == 0 || strcmp(arg, "--help") == 0) {
                usage(stdout, argv[0]);
                /* requested help -> stdout */ return 0;
            } else if (strcmp(arg, "-l") == 0 || strcmp(arg, "--lines") == 0) {
                wantLines = 1;
            } else if (strcmp(arg, "-w") == 0 || strcmp(arg, "--words") == 0) {
                wantWords = 1;
            } else if (strcmp(arg, "-c") == 0 || strcmp(arg, "--chars") == 0) {
                wantChars = 1;
            } else if (strcmp(arg, "-s") == 0) {
                if (i + 1 >= argc) {
                    /* the option needs a value */ fprintf(stderr, "%s: option -s requires a search term\n", argv[0]);
                    return 2;
                }
                searchTerm = argv[++i];
                /* consume the NEXT argument */
            } else {
                fprintf(stderr, "%s: unknown option '%s'\n", argv[0], arg);
                usage(stderr, argv[0]);
                return 2;
            }
        } else {
            if (fileCount >= MAX_FILES) {
                fprintf(stderr, "%s: too many files (max %d)\n", argv[0], MAX_FILES);
                return 2;
            }
            files[fileCount++] = arg;
            /* a filename */
        }
    }
    if (fileCount == 0) {
        fprintf(stderr, "%s: no input files given\n\n", argv[0]);
        usage(stderr, argv[0]);
        return 2;
    }
    int showAll = !(wantLines || wantWords || wantChars);
    int failures = 0;
    for (int i = 0; i < fileCount; i++) {
        if (!processFile(files[i], wantLines, wantWords, wantChars, showAll, searchTerm)) {
            failures++;
        }
    }
    return failures > 0 ? 1 : 0;
    /* exit code reflects any failure */
}

Verified Behavior

fstats report.txt — no options, so all counts are shown:

main.c
Run Online
report.txt: lines=5 words=16 chars=106

(exit 0)

fstats -l -w -s Error report.txt — two flags plus a search:

main.c
Run Online
report.txt: line 1: Error: disk full line 3: Error: file not found line 5: Error: permission deniedreport.txt: lines=5 words=16

(exit 0 — note only lines and words are printed; -c wasn't requested)

fstats — no operands at all:

main.c
Run Online
fstats: no input files given
usage -> stderr

(exit 2)

fstats -x report.txt — unknown option:

main.c
Run Online
fstats: unknown option '-x'usage -> stderr

(exit 2)

fstats -s report.txt — the trap case:

main.c
Run Online
fstats: no input files given
usage -> stderr

(exit 2)

This one is genuinely instructive. -sconsumedreport.txt as its search term, so no files remained — and the tool reported exactly that. It's the correct behavior for the design, and it's why many real tools accept -s=TERM or require the value to be attached: with a separate-argument option, a forgotten value silently eats your filename.

fstats -- -weird-name.txt — the -- convention:

main.c
Run Online
-weird-name.txt: No such file or directory

(exit 1)

Without --, that filename would have been rejected as an unknown option. With it, the leading dash is just a filename character.

fstats report.txt missing.txt — partial failure:

main.c
Run Online
report.txt: lines=5 words=16 chars=106missing.txt: No such file or directory

(exit 1)

The readable file was still processed, the unreadable one produced a perror message, and the exit code signalled that something failed — exactly the behavior a script needs.

(Suggested image: the five invocations laid out in a console panel with their exit codes annotated on the right, highlighting the "-s report.txt" trap)


Patterns Cheat Sheet

TaskCode
Declare main with argumentsint main(int argc, char *argv[])
Skip the program nameloop for (int i = 1; i < argc; i++)
Number of your argumentsargc - 1
Program nameargv[0] (may be a path; may be NULL)
Iterate all argumentsfor (char **p = argv; *p; p++)
Required arguments checkif (argc < 3) { usage(stderr, argv[0]); return 2; }
Is this an option?arg[0] == '-' && arg[1] != '\0'
A bare -treat as an operand (convention: stdin)
End of optionsif (strcmp(arg, "--") == 0) { afterDashDash = 1; continue; }
Option with a valueif (i + 1 >= argc) { /* error */ } value = argv[++i];
Unknown optionreport to stderr, usage(stderr, argv[0]), return 2
String → int, safelystrtol(s, &end, 10) + end == s + *end + ERANGE
String → double, safelystrtod(s, &end) + the same three checks
Quick conversion checksscanf(argv[1], "%ld", &n) == 1
Subcommand dispatchreturn cmd(argc - 1, argv + 1);
Help outputusage(stdout, argv[0]); return 0;
Environment variablegetenv("HOME") (returns NULL if unset)
Success exitreturn 0; / EXIT_SUCCESS
Failure exitreturn 1; / EXIT_FAILURE

(Suggested image: the cheat sheet as a shareable two-column reference card)


Common Mistakes

  1. Starting the parse loop at i = 0 — argv[0] is the program name, so you'd try to parse the executable as an option or filename.
  2. Assuming argc counts only your arguments — it includes the program name; a one-argument program has argc == 2.
  3. Using atoi on user input — errors are undetectable; "abc" becomes 0 and overflow is undefined. Use strtol with endptr.
  4. Forgetting errno = 0 before strtol — the function only sets errno, never clears it, so stale values cause false ERANGE rejections.
  5. Checking only end == s (no digits) and not *end != '\0' (trailing junk) — "42abc" slips through as 42.
  6. Reading argv[i + 1] without bounds-checking — for the last argument that's argv[argc], which is NULL; using it crashes.
  7. Forgetting the ++ when consuming an option's value — the value gets reprocessed as an option, or treated as an operand.
  8. Treating a bare - as an option — the arg[1] != '\0' test prevents this; - conventionally means stdin.
  9. Accessing argv[argc] as a real argument — it exists only as the NULL terminator. It's the loop bound, not an element.
  10. Dereferencing argv[0] assuming it's non-NULL — legal to assume a name, not legal to assume a pointer. Guard it if you use it.
  11. Parsing argv[0] to get the program's filename — it may be a full path or an alias; keep a literal for messages instead.
  12. Not validating the number of arguments before use — dereferencing argv[1] when argc == 1 is out of bounds.
  13. Returning 0 on failure — scripts check exit codes; reporting success when a file couldn't be read breaks automation.
  14. Sending help text to stderr — --help should go to stdout so it can be piped, with exit code 0.
  15. Sending error text to stdout — it vanishes into a redirect; errors belong on stderr.
  16. Assuming wildcards are expanded — POSIX shells expand *.txt; Windows passes the literal pattern and you must expand it yourself.
  17. Expecting one argv entry per word — quoting and escaping change the split before your program ever sees it.
  18. Modifying argv strings while also using them as const char * keys — legal in C (they're modifiable), but confusing if the same string is stored in several places.
  19. Using getopt and expecting portability — it's POSIX, not standard C, and absent from MSVC by default.
  20. Forgetting the OS argument-length limit — huge argument lists fail at the shell, before main runs.

(Suggested image: "20 argc/argv Mistakes" checklist — with #1, #3, and #6 highlighted as the most common, and #6 flagged as a crash)


Final Thoughts

argc and argv are the smallest possible interface between your program and everything that runs it — and they're deceptively simple. The whole mechanism is:

  • argc counts the arguments, including the program name
  • argv is an array of strings, argv[0] is the program name, and argv[argc] == NULL is guaranteed
  • Every argument is text, so converting and validating them is your job
  • The shell splits the line first, so quoting and wildcard rules (and their platform differences) determine what you actually receive

The habits that turn a script-friendly tool from an exercise:

  1. Loop from i = 1, and use arg[0] == '-' && arg[1] != '\0' to spot options
  2. Validate argc before indexing — and bounds-check before consuming an option's value
  3. Convert with strtol/strtod and all three checks, never atoi
  4. Report errors on stderr with a non-zero exit code; send --help to stdout with 0
  5. Support -- so filenames can't be mistaken for options
  6. Keep processing what you can, and let the exit code report partial failure

Follow those and your C programs stop being things you run and start being things you use — composable with pipes, usable in build scripts, and consistent with the conventions every developer already expects from a command-line tool.

Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
    printf("Total arguments passed (argc): %d\
", argc);
    printf("Program binary name (argv[0]): %s\
", argv[0]);
    for (int i = 1; i < argc; i++) {
        printf("  Argument [%d]: %s\
", i, argv[i]);
    }
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Always validate argc >= expected_count before accessing argv[1], argv[2] to prevent segmentation faults from missing arguments.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: What is guaranteed to be stored in `argv[argc]` in standard C?

A: `NULL` (a null pointer constant).
Q

Q: Why should `strtol()` be preferred over `atoi()` for parsing numbers from `argv`?

A: `atoi()` provides no error detection on invalid input and returns 0 for both error and literal "0". `strtol()` detects overflows, underflows, and points to the first invalid character.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations