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:
-
argvpoints to the first element in an array - Each element (
argv[i]) is achar *pointing at a string - Each string ends with
'\0' -
argv[argc]is guaranteed to beNULL
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 inoptarg) -
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
--helpand-h - Print to
stdout(notstderr) 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
- main() function reference (cppreference) - Complete specification
- POSIX getopt() - Standard option parsing
- getopt(3) man page - Linux manual
- GNU getopt documentation - Including getopt_long
Security
- CERT C Secure Coding - String handling
- CWE-78: OS Command Injection - Understanding the risks
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