Command Line Arguments in C: Mastering char *argv[]

Command Line Arguments in C: Mastering char *argv[]

Command line arguments are one of those features that make C feel immediately "systems-level": no GUI, no ceremony, just a program and a terminal. You type a command, add flags or filenames, hit enter — and your program receives that input through two familiar parameters: argc and argv[].

This post is a practical guide to argv in C: what char *argv[] actually means, how to read arguments safely, how to handle common CLI patterns, and where people go wrong (usually by assuming inputs are nicer than they really are). You'll learn manual parsing, the standard getopt() approach, security considerations, and edge cases that trip up even experienced developers.

c argv argc explained: what are they?

In many C programs, your main function can be defined like this:

#include <stdio.h>

int main(int argc, char *argv[]) {
  printf("argc = %d\n", argc);
  return 0;
}
  • argc = "argument count" (how many arguments)
  • argv = "argument vector" (an array of C strings)

Important detail: argv[0] is typically the program name (or the invoked path). The "real" user arguments start at argv[1].

Quick Reference: argc and argv Basics

Element What it is Example
argc Total number of arguments (including program name) 3 for ./app file.txt
argv[0] Program name or invoked path "./app" or "/usr/bin/app"
argv[1] to argv[argc-1] User-supplied arguments "file.txt"
argv[argc] Always NULL (guaranteed by C standard) NULL

char *argv[] meaning (without the hand-waving)

char *argv[] is an array of pointers to char. Each pointer points to the first character of a null-terminated string.

You'll also see this written as char **argv. In function parameters, these are effectively equivalent:

int main(int argc, char *argv[]);
int main(int argc, char **argv);

Conceptually:

  • argv points to the first element in an array
  • Each element (argv[i]) is a char * pointing at a string
  • Each string ends with '\0'
  • argv[argc] is guaranteed to be NULL

This is similar to how strings work in general C programming (learn more about C's string handling in stdio.h).

How to use command line arguments in C

Example 1: print all arguments

#include <stdio.h>

int main(int argc, char *argv[]) {
  for (int i = 0; i < argc; i++) {
    printf("argv[%d] = %s\n", i, argv[i]);
  }
  return 0;
}

Run it like:

./app hello world --verbose

You might get output like:

argv[0] = ./app
argv[1] = hello
argv[2] = world
argv[3] = --verbose

Example 2: require a filename

#include <stdio.h>

int main(int argc, char *argv[]) {
  if (argc < 2) {
    fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
    return 1;
  }

  printf("File: %s\n", argv[1]);
  return 0;
}

This is a common pattern: validate arguments early, print a usage message, and exit with a non-zero status code when inputs are missing.

c command line parameters tutorial: basic parsing patterns

Flags (e.g., --help, -v)

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

int main(int argc, char *argv[]) {
  int verbose = 0;

  for (int i = 1; i < argc; i++) {
    if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
      verbose = 1;
    }
    if (strcmp(argv[i], "--help") == 0) {
      printf("Usage: %s [-v] [--help]\n", argv[0]);
      return 0;
    }
  }

  if (verbose) {
    printf("Verbose mode enabled.\n");
  }

  return 0;
}

This "manual loop" approach is fine for small tools. For larger CLIs, people often reach for a dedicated parsing library — but it's still useful to understand the underlying mechanics.

Option values (e.g., --port 8080)

When an option takes a value, you usually look ahead one argument and validate it exists:

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

int main(int argc, char *argv[]) {
  int port = 0;

  for (int i = 1; i < argc; i++) {
    if (strcmp(argv[i], "--port") == 0) {
      if (i + 1 >= argc) {
        fprintf(stderr, "Missing value for --port\n");
        return 1;
      }
      port = (int)strtol(argv[i + 1], NULL, 10);
      i++; /* consume the value */
    }
  }

  printf("Port: %d\n", port);
  return 0;
}

Note: strtol (from stdlib.h) is generally safer than atoi because you can detect parsing errors.

Using getopt() for standard option parsing

For production tools, manual parsing gets tedious and error-prone. The POSIX getopt() function provides a standard way to parse single-character options:

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

int main(int argc, char *argv[]) {
  int verbose = 0;
  char *output_file = NULL;
  int opt;

  while ((opt = getopt(argc, argv, "vo:h")) != -1) {
    switch (opt) {
      case 'v':
        verbose = 1;
        break;
      case 'o':
        output_file = optarg;
        break;
      case 'h':
        printf("Usage: %s [-v] [-o output] [-h]\n", argv[0]);
        return 0;
      default:
        fprintf(stderr, "Usage: %s [-v] [-o output] [-h]\n", argv[0]);
        return 1;
    }
  }

  if (verbose) {
    printf("Verbose mode enabled\n");
  }
  if (output_file) {
    printf("Output file: %s\n", output_file);
  }

  // Remaining non-option arguments start at optind
  for (int i = optind; i < argc; i++) {
    printf("File: %s\n", argv[i]);
  }

  return 0;
}

The format string "vo:h" means:

  • v - a flag with no argument
  • o: - an option that requires an argument (available in optarg)
  • h - another flag with no argument

For GNU-style long options (--verbose, --output), use getopt_long() from <getopt.h>.

Comparison: Manual Parsing vs getopt

Approach Best For Pros Cons
Manual loop with strcmp Simple tools, 1-3 options No dependencies, full control, easy to understand Verbose, error-prone for complex CLIs
getopt() Unix-style short options (-v, -o file) POSIX standard, handles option bundling (-vf) Only single-character options
getopt_long() GNU-style long options (--verbose) Supports both short and long, widely used GNU extension (not pure POSIX)
Third-party libraries (argp, popt) Complex CLIs with subcommands Auto-generated help, validation, type conversion External dependency

Handling edge cases and special characters

Arguments with spaces

The shell handles quoting, so spaces in arguments are already split for you:

./app "hello world" test

Results in:

  • argv[0] = "./app"
  • argv[1] = "hello world" (as a single argument)
  • argv[2] = "test"

Arguments with special characters

Characters like *, ?, ~ may be expanded by the shell before your program sees them. If you need literal special characters, users must quote or escape them:

./app "*.txt"        # Literal *.txt (not expanded)
./app \*.txt         # Also literal
./app *.txt          # Shell expands to matching files

Empty arguments

An empty string is a valid argument:

./app "" test

Results in argv[1] = "" (not NULL). Always check string length if you need non-empty values.

Security considerations

1) Validate and sanitize all inputs

Never trust that arguments are well-formed. Check lengths, validate formats, and handle unexpected characters:

#include <string.h>
#include <limits.h>

if (strlen(argv[1]) > PATH_MAX) {
  fprintf(stderr, "Path too long\n");
  return 1;
}

2) Avoid shell injection

If you pass arguments to system() or popen(), you're vulnerable to shell injection. Instead, use execv() family functions that don't invoke a shell:

// DANGEROUS - don't do this
char cmd[256];
snprintf(cmd, sizeof(cmd), "cat %s", argv[1]);
system(cmd);  // argv[1] could be "; rm -rf /"

// SAFER - use exec family
char *args[] = {"cat", argv[1], NULL};
execvp("cat", args);

3) Path traversal attacks

If arguments are file paths, validate they don't escape intended directories:

if (strstr(argv[1], "..") != NULL) {
  fprintf(stderr, "Path traversal attempt detected\n");
  return 1;
}

For more robust validation, use realpath() to resolve the canonical path and verify it's within allowed directories.

4) Buffer overflows

Never assume argument lengths. Use size-bounded functions like strncpy or snprintf:

char buffer[64];
strncpy(buffer, argv[1], sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';  // Ensure null termination

See CERT C Secure Coding guidelines for comprehensive string safety practices.

Common mistakes and pitfalls to avoid

1) Assuming argv[1] exists

If you access argv[1] without checking argc, you're reading beyond the provided arguments. Always validate counts first:

if (argc < 2) {
  fprintf(stderr, "Missing required argument\n");
  return 1;
}

2) Modifying argv strings

Many environments treat argument strings as modifiable, but you shouldn't rely on it. Treat argv[i] as read-only unless you have a very specific reason (and you know what your platform guarantees). If you need to modify an argument, copy it first:

char *arg_copy = strdup(argv[1]);
if (!arg_copy) {
  fprintf(stderr, "Memory allocation failed\n");
  return 1;
}
// ... work with arg_copy ...
free(arg_copy);

3) Confusing "program name" with "real path"

argv[0] is what the caller used to invoke the program — it may be a relative path, absolute path, or just a name that was resolved via PATH. Don't assume it's canonical. Examples:

  • ./myapp - relative path
  • /usr/bin/myapp - absolute path
  • myapp - resolved from PATH

If you need the actual executable path, use platform-specific APIs like /proc/self/exe on Linux or _NSGetExecutablePath() on macOS.

4) Not handling --help cleanly

A tiny, consistent usage message makes your CLI feel professional. The "help path" should be frictionless. Consider these best practices:

  • Always respond to --help and -h
  • Print to stdout (not stderr) for help messages
  • Exit with status 0 for help (it's not an error)
  • Include examples in your help text

5) Ignoring the third parameter: envp

The main function can actually take a third parameter for environment variables:

int main(int argc, char *argv[], char *envp[]) {
  // envp is an array of environment variables
  // Same NULL-terminated structure as argv
}

While not part of the C standard, it's widely supported on Unix systems. Alternatively, use the standard extern char **environ or getenv().

Platform differences: Windows vs Unix

On Windows, if your program needs to handle Unicode arguments, use wmain instead of main:

int wmain(int argc, wchar_t *argv[]) {
  // Wide character arguments for Unicode support
}

Alternatively, use GetCommandLineW() and CommandLineToArgvW() from the Windows API for full Unicode support.

Best practices summary

Practice Why
Always check argc before accessing argv[i] Prevents reading uninitialized memory
Use getopt() for 3+ options Reduces bugs, handles standard patterns
Print usage to stdout for --help Standard convention, allows piping
Exit with 0 for success, non-zero for errors Allows shell scripting and error detection
Validate all inputs (length, format, characters) Security and robustness
Use strtol instead of atoi Error detection and better control
Never pass argv to system() without sanitization Prevents shell injection attacks

Related concepts and further reading

Official Documentation

Security

Argument Parsing Libraries

  • GNU argp - Advanced argument parsing with automatic help generation
  • popt - Popular option parsing library

Conclusion

Understanding argv[] is a rite of passage for writing real command-line tools in C. Once you're comfortable with argc validation, iterating arguments, parsing values safely, and handling edge cases, you've unlocked a whole category of practical programs: importers, linters, converters, scripts-that-got-serious, and all the little utilities that make a developer's day smoother.

Start simple with manual parsing for small tools, graduate to getopt() when things get complex, and always validate your inputs. The command line is unforgiving, but that's exactly what makes it powerful.

The elegance of command line arguments is also exactly the kind of tiny detail that C developers appreciate. If argv[] is a familiar part of your C programming basics, you can wear the code with our argv[] developer t-shirt (dark mode) or argv[] developer t-shirt (light mode).

Explore more: C developer t-shirts

0 comments

Leave a comment

Please note, comments need to be approved before they are published.