c-foundations Lesson 15 18 min read

How Do I Use Command-Line Arguments in C?

argc, argv, and the connection between your C programs and every Unix command

Reading: C Text: Ch. 12 §4 (pp. 715–720)

After this lesson, you will be able to:

  • Write main with int argc, char *argv[] to accept command-line arguments
  • Explain that argc includes the program name, so user arguments start at argv[1]
  • Validate argc and print a usage message when wrong number of arguments is provided
  • Convert string arguments to numeric values using atoi and atof
  • Explain why argv entries are always strings

Every Unix Command Does This

Think about every command you’ve used this quarter:

ls -la /home
grep -rn "printf" *.c
chmod 755 myscript.sh
gcc -Wall -o hello hello.c

Each one takes arguments — flags, filenames, patterns. How does the program access them? Through argc and argv — the same mechanism you’ll use in your own programs.


argc and argv

The Full main Signature

Until now, we’ve written:

int main(void) { ... }

The full version accepts command-line arguments:

int main(int argc, char *argv[]) { ... }
  • argcargument count (number of arguments, including the program name)
  • argvargument vector (array of strings, each argument as a char*)

How It Works

If you run:

./calculator 10 + 5

Then:

  • argc = 4
  • argv[0] = "./calculator" (program name)
  • argv[1] = "10"
  • argv[2] = "+"
  • argv[3] = "5"
  • argv[4] = NULL (always NULL-terminated)

Key Insight: Every Unix command you’ve used — ls -l, grep pattern file, chmod 755 file — is a C program that reads its flags and arguments from argc and argv. When you write programs that accept command-line arguments, you’re building tools that work exactly like the Unix commands you’ve been using.

Printing All Arguments

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("You passed %d arguments:\n", argc);
    for (int i = 0; i < argc; i++)
    {
        printf("  argv[%d] = \"%s\"\n", i, argv[i]);
    }
    return 0;
}
./args hello world 42
You passed 4 arguments:
  argv[0] = "./args"
  argv[1] = "hello"
  argv[2] = "world"
  argv[3] = "42"

Validating Arguments

Always check that the user provided the right number of arguments:

if (argc != 3)
{
    printf("Usage: %s <filename> <count>\n", argv[0]);
    return 1;
}

Using argv[0] in the usage message is a nice touch — it shows the actual program name the user typed.

Common Pitfall: argc includes the program name, so ./program file1 file2 has argc = 3, not 2. The actual arguments start at argv[1]. This off-by-one trips up everyone at least once.

Check Your Understanding
You run ./greet Alice Bob. What is the value of argc?
A 2 — there are two arguments (Alice and Bob)
B 3 — the program name counts as the first argument
C 4 — including the NULL terminator at the end of argv
D 1 — argc only counts the program name
Answer: B. argc counts all arguments including the program name (argv[0]). So ./greet is argument 0, Alice is argument 1, Bob is argument 2 — three total. The trailing NULL in argv is not counted by argc. This off-by-one is the most common argc/argv mistake.

Converting String Arguments to Numbers

Command-line arguments are always strings. To use them as numbers:

#include <stdlib.h>

int count = atoi(argv[1]);        // String to int
double rate = atof(argv[2]);      // String to double

Common Pitfall: atoi has no error checking — atoi("hello") returns 0 without any indication that the conversion failed. For production code, use strtol or strtod which report errors. For labs, atoi/atof are fine.

Check Your Understanding
You run ./calc 42 and use argv[1] directly in arithmetic: int result = argv[1] + 10;. What happens?
A result is 52 — C auto-converts the string to an integer
B The program crashes with a type mismatch error
C It does pointer arithmetic — adding 10 to the memory address of the string, not to the number 42
D The compiler refuses to compile this code
Answer: C. argv[1] is a char* (a pointer to a string), not a number. Adding 10 to it performs pointer arithmetic — it moves the pointer 10 bytes forward in memory, which is not what you want. C doesn't auto-convert strings to numbers. You must explicitly call atoi(argv[1]) or atof(argv[1]) to get a numeric value.
Why does this matter?

This is a common mistake when you’re used to languages like Python or JavaScript that auto-convert types. C never converts a string to a number implicitly. Every command-line argument is a string — you must convert it yourself. Getting comfortable with atoi/atof (and eventually strtol/strtod) is essential for any program that takes numeric input from the command line.

Building a Calculator

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    if (argc != 4)
    {
        printf("Usage: %s <num1> <op> <num2>\n", argv[0]);
        printf("  ops: + - x /\n");
        return 1;
    }

    double num1 = atof(argv[1]);
    char op = argv[2][0];
    double num2 = atof(argv[3]);
    double result;

    switch (op)
    {
        case '+':
            result = num1 + num2;
            break;
        case '-':
            result = num1 - num2;
            break;
        case 'x':
            result = num1 * num2;
            break;
        case '/':
            if (num2 == 0)
            {
                printf("Error: division by zero\n");
                return 1;
            }
            result = num1 / num2;
            break;
        default:
            printf("Unknown operator: %c\n", op);
            return 1;
    }

    printf("%.2f %c %.2f = %.2f\n", num1, op, num2, result);
    return 0;
}
./calc 10 + 5
# 10.00 + 5.00 = 15.00

./calc 100 / 3
# 100.00 / 3.00 = 33.33

The Trick: We use 'x' for multiplication instead of '*' because the shell interprets * as a wildcard (glob). Running ./calc 10 * 5 would expand * to filenames in the current directory. Using 'x' avoids the issue.

Connecting to Unix Philosophy

Think about it: you’ve now built a command-line tool. It takes arguments, validates them, does a computation, and prints results. That’s exactly what grep, chmod, and wc do. Your programs are Unix commands.

From Java: In Java, public static void main(String[] args) serves the same purpose. args[0] is the first argument (not the class name — Java strips that). In C, argv[0] is the program name and argv[1] is the first user argument. The shift by one is important.

Check Your Understanding
Why does the calculator example use 'x' for multiplication instead of '*'?
A C doesn't support the * character in switch statements
B * is reserved for pointer operations and can't be used as a value
C It's a style convention — either character would work identically
D The shell expands * as a glob wildcard before the program sees it, replacing it with filenames
Answer: D. This is a shell issue, not a C issue. When you type ./calc 10 * 5, the shell sees * and expands it to all filenames in the current directory before your program runs. Your program would receive argv full of filenames instead of the multiplication operator. Using x sidesteps the problem. (You could also quote it: ./calc 10 '*' 5.)
Quick Check: If you run ./myprog file.txt 42, what are argc, argv[0], and argv[2]?

argc = 3, argv[0] = "./myprog", argv[2] = "42" (as a string, not an integer — use atoi to convert).

Quick Check: Why is argv[2] a string even when the user typed a number?

The shell passes all arguments as strings. argv is an array of char* (string pointers). To use an argument as a number, you must convert it explicitly with atoi() (for int) or atof() (for double).

Quick Check: Why use argv[0] in usage messages?

It shows the actual command name the user typed. If they rename the executable or run it from a different path, the usage message still shows the correct name. This is standard Unix convention.


Your Programs Are Unix Commands Now

With argc and argv, your C programs fit right into the Unix ecosystem. They can take file names, flags, and parameters just like ls, grep, and gcc. Combined with stdin/stdout (from Lesson 1.5), your programs can participate in pipelines too.

Series 2 is complete. You’ve gone from your first #include <stdio.h> through arrays, strings, sorting, and command-line tools. You can write real C programs — the kind that compile, run, and do useful work.

But there’s a fundamental limitation you’ve been bumping into: functions can’t modify their caller’s variables. swap(x, y) doesn’t work. You can’t return two values from a function. You can’t dynamically allocate memory.

In Series 3: Pointers & Memory, all of that changes. Pointers are C’s most powerful feature — and the key to understanding how computers actually work.

The CTF Challenges for Weeks 3–5 cover C foundations — programming basics, loops, functions, arrays, and strings. Good way to stress-test what you just learned.

Big Picture: You now understand C’s approach to data (raw arrays with no safety net), text (null-terminated character arrays), and code organization (header files and Makefiles). Next week, you tackle the most powerful and dangerous feature of C: pointers. Everything from here builds on what you’ve learned in Series 2 — especially arrays, since pointer arithmetic is just array indexing in disguise.