|$ curl https://forge-ai.dev/api/markdown?path=docs/c/cli-args
$cat docs/c-—-command-line-arguments.md
updated Recently·35 min read·published

C — Command-line Arguments

CargcargvCommand LineBeginnerBeginner🎯Free Tools
Command-line Arguments

Command-line arguments let your C program receive input from the user at invocation time. Instead of hardcoding values or reading from stdin, you can pass flags, options, and data directly when you run the program. This is the foundation of every useful CLI tool — from ls and grep to custom build scripts and data processors.

hello_args.c
C
1#include <stdio.h>
2
3int main(int argc, char *argv[]) {
4 printf("You passed %d argument(s)\n", argc);
5 for (int i = 0; i < argc; i++) {
6 printf(" argv[%d] = \"%s\"\n", i, argv[i]);
7 }
8 return 0;
9}

Running ./hello_args foo bar produces three entries: the program name, "foo", and "bar". The program name itself is always the first argument.

argc and argv Explained

The main function can accept two parameters: argc (argument count) and argv (argument vector). argc is an int that holds the number of arguments passed, always at least 1 because the program name counts as the first argument. argv is an array of character pointers (strings), where each element points to one argument.

argc_argv.c
C
1#include <stdio.h>
2
3int main(int argc, char *argv[]) {
4 /* argc is always >= 1 */
5 printf("argc = %d\n", argc);
6
7 /* argv[0] is always the program name */
8 printf("Program name: %s\n", argv[0]);
9
10 /* argv[argc] is NULL on most systems (C99/C11 guarantee) */
11 if (argv[argc] == NULL) {
12 printf("argv[argc] is NULL (standard behavior)\n");
13 }
14
15 /* Access each argument */
16 for (int i = 1; i < argc; i++) {
17 printf("Argument %d: %s\n", i, argv[i]);
18 }
19 return 0;
20}
📝

note

The C standard guarantees that argv[argc] is a null pointer. This lets you iterate without checking argc — you can just loop while argv[i] != NULL.
Memory Layout of argv

Understanding how argv is laid out in memory helps you manipulate arguments correctly. The argv array is a contiguous block of pointers, each pointing to a null-terminated string. The OS loads these strings into your process's memory space before main is called.

argv_memory.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(int argc, char *argv[]) {
5 /* Show the address of each argv pointer */
6 printf("argv is at: %p\n", (void *)argv);
7 printf("sizeof(char*): %zu\n", sizeof(char *));
8
9 /* Each string's address and length */
10 for (int i = 0; i < argc; i++) {
11 printf("argv[%d] addr=%p len=%zu str=\"%s\"\n",
12 i, (void *)argv[i], strlen(argv[i]), argv[i]);
13 }
14
15 /* You can also modify individual characters */
16 if (argc > 1) {
17 argv[1][0] = 'X'; /* changes first char of argv[1] */
18 printf("Modified argv[1]: %s\n", argv[1]);
19 }
20 return 0;
21}

warning

Modifying argv strings is technically allowed by the standard, but modifying the argv pointers themselves (e.g., argv[1] = "new") is undefined behavior on many platforms.
main() Function Variations

There are three standard forms of the main function. All are valid C, and the choice depends on whether your program needs command-line arguments.

FormUse Case
int main(void)No arguments needed
int main(int argc, char *argv[])With arguments (array syntax)
int main(int argc, char **argv)With arguments (pointer-to-pointer)

The second and third forms are equivalent — char *argv[] and char **argv decay to the same thing in function parameters. Use whichever you find more readable.

main_forms.c
C
1#include <stdio.h>
2
3/* Form 1: no arguments */
4int main(void) {
5 printf("No arguments accepted.\n");
6 return 0;
7}
8
9/* Form 2: argc + argv (array form) */
10/* int main(int argc, char *argv[]) { ... } */
11
12/* Form 3: argc + argv (pointer form) */
13/* int main(int argc, char **argv) { ... } */
Converting Arguments to Numbers

All command-line arguments arrive as strings. When you need numeric values — like port numbers, file sizes, or coordinates — you must convert them. C provides several functions for this, each with different safety and capability trade-offs.

convert_args.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <errno.h>
4
5int main(int argc, char *argv[]) {
6 if (argc < 3) {
7 fprintf(stderr, "Usage: %s <int> <float>\n", argv[0]);
8 return 1;
9 }
10
11 /* atoi: simple but no error detection */
12 int a = atoi(argv[1]);
13 printf("atoi: %d\n", a);
14
15 /* atof: simple float conversion */
16 double b = atof(argv[2]);
17 printf("atof: %f\n", b);
18
19 /* strtol: proper integer conversion with error checking */
20 char *endptr;
21 errno = 0;
22 long val = strtol(argv[1], &endptr, 10);
23 if (errno != 0 || *endptr != '\0' || endptr == argv[1]) {
24 fprintf(stderr, "Invalid integer: %s\n", argv[1]);
25 return 1;
26 }
27 printf("strtol: %ld\n", val);
28
29 /* strtod: proper float conversion with error checking */
30 errno = 0;
31 double d = strtod(argv[2], &endptr);
32 if (errno != 0 || *endptr != '\0' || endptr == argv[2]) {
33 fprintf(stderr, "Invalid float: %s\n", argv[2]);
34 return 1;
35 }
36 printf("strtod: %f\n", d);
37
38 return 0;
39}
FunctionConverts ToError DetectionRecommended?
atoi()intNone — returns 0 on failureNo
atof()doubleNone — returns 0.0 on failureNo
strtol()longerrno + endptr checkYes
strtod()doubleerrno + endptr checkYes
strtoul()unsigned longerrno + endptr checkYes

best practice

Always use strtol() or strtod() over atoi() and atof(). The latter cannot distinguish between the input "0" and an invalid input — both return 0.
Basic Option Parsing

Most CLI programs accept flags and options. A simple convention is to check for arguments starting with - (single dash for short flags) or -- (double dash for long options). Here's how to build a basic parser by hand.

simple_options.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(int argc, char *argv[]) {
5 int verbose = 0;
6 int count = 1;
7 const char *output = "output.txt";
8
9 for (int i = 1; i < argc; i++) {
10 if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
11 verbose = 1;
12 } else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
13 count = atoi(argv[++i]);
14 } else if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
15 output = argv[++i];
16 } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
17 printf("Usage: %s [-v] [-n count] [-o file]\n", argv[0]);
18 printf(" -v, --verbose Enable verbose output\n");
19 printf(" -n <count> Number of iterations (default: 1)\n");
20 printf(" -o <file> Output file (default: output.txt)\n");
21 return 0;
22 } else {
23 fprintf(stderr, "Unknown option: %s\n", argv[i]);
24 fprintf(stderr, "Try '%s --help' for usage.\n", argv[0]);
25 return 1;
26 }
27 }
28
29 printf("verbose=%d count=%d output=%s\n", verbose, count, output);
30 return 0;
31}

info

Always validate that the next argument exists before accessing argv[i+1]. The check i + 1 < argc prevents buffer overread when an option expects a value but none was provided.
POSIX getopt() — The Standard Way

On POSIX systems (Linux, macOS, BSD), the getopt() function handles short option parsing automatically. It supports combined flags (-abc is equivalent to -a -b -c), options with required arguments, and the special -- separator.

getopt_example.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <unistd.h>
4
5int main(int argc, char *argv[]) {
6 int verbose = 0;
7 int count = 1;
8 char *output = NULL;
9 int opt;
10
11 /* optstring: letters = flags, letter: = requires arg */
12 while ((opt = getopt(argc, argv, "vn:o:h")) != -1) {
13 switch (opt) {
14 case 'v':
15 verbose = 1;
16 break;
17 case 'n':
18 count = atoi(optarg); /* optarg = argument to -n */
19 break;
20 case 'o':
21 output = optarg;
22 break;
23 case 'h':
24 printf("Usage: %s [-v] [-n count] [-o file]\n", argv[0]);
25 exit(0);
26 default:
27 fprintf(stderr, "Unknown option: -%c\n", optopt);
28 exit(1);
29 }
30 }
31
32 /* optind = index of first non-option argument */
33 printf("verbose=%d count=%d output=%s\n", verbose, count, output);
34 printf("Remaining args: ");
35 for (int i = optind; i < argc; i++) {
36 printf("%s ", argv[i]);
37 }
38 printf("\n");
39 return 0;
40}

The optstring controls parsing: a letter alone means a boolean flag, a letter followed by : means it requires an argument. The global optarg holds the argument value, and optind tracks the current index.

Long Options with getopt_long()

GNU systems provide getopt_long() for parsing long options like --verbose and --output=file. You define options in a struct option array.

getopt_long_example.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <getopt.h>
4
5int main(int argc, char *argv[]) {
6 int verbose = 0;
7 int count = 1;
8 char *output = NULL;
9
10 static struct option long_opts[] = {
11 {"verbose", no_argument, 0, 'v'},
12 {"count", required_argument, 0, 'n'},
13 {"output", required_argument, 0, 'o'},
14 {"help", no_argument, 0, 'h'},
15 {0, 0, 0, 0}
16 };
17
18 int opt;
19 while ((opt = getopt_long(argc, argv, "vn:o:h", long_opts, NULL)) != -1) {
20 switch (opt) {
21 case 'v': verbose = 1; break;
22 case 'n': count = atoi(optarg); break;
23 case 'o': output = optarg; break;
24 case 'h':
25 printf("Usage: %s [OPTIONS] [FILES...]\n", argv[0]);
26 printf(" -v, --verbose Verbose output\n");
27 printf(" -n, --count=N Iteration count\n");
28 printf(" -o, --output=FILE Output file\n");
29 exit(0);
30 default:
31 exit(1);
32 }
33 }
34
35 printf("verbose=%d count=%d output=%s\n", verbose, count, output);
36 return 0;
37}
🔥

pro tip

Define long options before the option loop. The {0, 0, 0, 0} sentinel at the end is mandatory. Use no_argument, required_argument, or optional_argument for the third field.
Environment Variables

Beyond command-line arguments, your program can read environment variables using getenv(). This is useful for configuration that persists across runs — like PATH, HOME, or custom variables like MY_APP_CONFIG.

env_vars.c
C
1#include <stdio.h>
2#include <stdlib.h>
3
4int main(int argc, char *argv[]) {
5 /* Read individual environment variables */
6 const char *home = getenv("HOME");
7 const char *path = getenv("PATH");
8 const char *shell = getenv("SHELL");
9
10 printf("HOME = %s\n", home ? home : "(not set)");
11 printf("PATH = %s\n", path ? path : "(not set)");
12 printf("SHELL = %s\n", shell ? shell : "(not set)");
13
14 /* Set a new environment variable (affects child processes) */
15 setenv("MY_APP_VERSION", "1.0.0", 1);
16 printf("MY_APP_VERSION = %s\n", getenv("MY_APP_VERSION"));
17
18 /* Use env var as default, override with argument */
19 const char *config = getenv("MY_APP_CONFIG");
20 if (argc > 1) {
21 config = argv[1]; /* command-line takes precedence */
22 }
23 printf("Config: %s\n", config ? config : "(none)");
24
25 return 0;
26}

warning

getenv() returns a pointer to the environment string. Do not free it or modify it. If you call setenv() or putenv() afterward, the old pointer may become invalid.

On POSIX systems, you can also access the entire environment via the global environ variable (declared in <unistd.h>).

environ_walk.c
C
1#include <stdio.h>
2#include <unistd.h>
3
4extern char **environ;
5
6int main(void) {
7 for (char **env = environ; *env != NULL; env++) {
8 printf("%s\n", *env);
9 }
10 return 0;
11}
Practical: Simple Calculator

Let's build a practical example — a calculator that takes an operator and two numbers from the command line. This demonstrates argument validation, numeric conversion, and error handling.

calc.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <errno.h>
5
6static double parse_number(const char *str, const char *name) {
7 char *end;
8 errno = 0;
9 double val = strtod(str, &end);
10 if (errno != 0 || *end != '\0' || end == str) {
11 fprintf(stderr, "Error: invalid %s \"%s\"\n", name, str);
12 exit(1);
13 }
14 return val;
15}
16
17int main(int argc, char *argv[]) {
18 if (argc != 4) {
19 fprintf(stderr, "Usage: %s <num> <op> <num>\n", argv[0]);
20 fprintf(stderr, " Operations: + - * /\n");
21 return 1;
22 }
23
24 double a = parse_number(argv[1], "first operand");
25 const char *op = argv[2];
26 double b = parse_number(argv[3], "second operand");
27
28 double result;
29 if (strcmp(op, "+") == 0) {
30 result = a + b;
31 } else if (strcmp(op, "-") == 0) {
32 result = a - b;
33 } else if (strcmp(op, "*") == 0) {
34 result = a * b;
35 } else if (strcmp(op, "/") == 0) {
36 if (b == 0.0) {
37 fprintf(stderr, "Error: division by zero\n");
38 return 1;
39 }
40 result = a / b;
41 } else {
42 fprintf(stderr, "Error: unknown operator \"%s\"\n", op);
43 return 1;
44 }
45
46 printf("%g %s %g = %g\n", a, op, b, result);
47 return 0;
48}
Practical: Echo Tool

A custom echo tool that supports flags for newlines, escaping, and separators. This pattern mirrors how real Unix utilities handle options.

my_echo.c
C
1#include <stdio.h>
2#include <string.h>
3
4int main(int argc, char *argv[]) {
5 int newline = 1;
6 int escape = 0;
7 const char *sep = " ";
8 int start = 1;
9
10 /* Parse options */
11 for (int i = 1; i < argc; i++) {
12 if (strcmp(argv[i], "-n") == 0) {
13 newline = 0;
14 } else if (strcmp(argv[i], "-e") == 0) {
15 escape = 1;
16 } else if (strcmp(argv[i], "-s") == 0 && i + 1 < argc) {
17 sep = argv[++i];
18 } else if (strcmp(argv[i], "--") == 0) {
19 start = i + 1;
20 break;
21 } else {
22 break; /* first non-option argument */
23 }
24 }
25
26 /* Print remaining arguments with separator */
27 for (int i = start; i < argc; i++) {
28 if (i > start) {
29 fputs(sep, stdout);
30 }
31 if (escape) {
32 /* Simple escape: \n -> newline, \t -> tab */
33 for (const char *p = argv[i]; *p; p++) {
34 if (*p == '\\' && *(p + 1)) {
35 p++;
36 switch (*p) {
37 case 'n': putchar('\n'); break;
38 case 't': putchar('\t'); break;
39 case '\\': putchar('\\'); break;
40 default: putchar('\\'); putchar(*p); break;
41 }
42 } else {
43 putchar(*p);
44 }
45 }
46 } else {
47 fputs(argv[i], stdout);
48 }
49 }
50
51 if (newline) {
52 putchar('\n');
53 }
54 return 0;
55}
Practical: Line Filter

A grep-like tool that reads stdin and filters lines matching a pattern. This shows how arguments and stdin work together — a very common Unix pattern.

line_filter.c
C
1#include <stdio.h>
2#include <string.h>
3#include <stdlib.h>
4
5#define MAX_LINE 4096
6
7int main(int argc, char *argv[]) {
8 int invert = 0;
9 int ignore_case = 0;
10 const char *pattern = NULL;
11
12 for (int i = 1; i < argc; i++) {
13 if (strcmp(argv[i], "-v") == 0) {
14 invert = 1;
15 } else if (strcmp(argv[i], "-i") == 0) {
16 ignore_case = 1;
17 } else if (argv[i][0] == '-') {
18 fprintf(stderr, "Unknown option: %s\n", argv[i]);
19 return 1;
20 } else {
21 pattern = argv[i];
22 }
23 }
24
25 if (!pattern) {
26 fprintf(stderr, "Usage: %s [-v] [-i] PATTERN\n", argv[0]);
27 return 1;
28 }
29
30 char line[MAX_LINE];
31 while (fgets(line, sizeof(line), stdin)) {
32 /* Remove trailing newline */
33 line[strcspn(line, "\n")] = '\0';
34
35 int match;
36 if (ignore_case) {
37 match = (strstr(line, pattern) != NULL);
38 } else {
39 /* Simple case-insensitive: use strcasestr if available */
40 match = (strstr(line, pattern) != NULL);
41 }
42
43 if (match != invert) {
44 printf("%s\n", line);
45 }
46 }
47
48 return 0;
49}
Program Name from argv[0]

The value of argv[0] depends on how the program was invoked. If you create a symlink or alias, argv[0] reflects that name. This is how tools like busybox and vim (which symlinks to vi, nvim, etc.) detect their own name and change behavior accordingly.

prog_name.c
C
1#include <stdio.h>
2#include <string.h>
3#include <libgen.h> /* basename() */
4
5int main(int argc, char *argv[]) {
6 printf("argv[0] = %s\n", argv[0]);
7
8 /* Extract just the filename (POSIX) */
9 char *prog = basename(argv[0]);
10 printf("basename = %s\n", prog);
11
12 /* Behavior changes based on program name */
13 if (strcmp(prog, "list") == 0) {
14 printf("Running in LIST mode\n");
15 } else if (strcmp(prog, "count") == 0) {
16 printf("Running in COUNT mode\n");
17 } else {
18 printf("Running in default mode\n");
19 }
20 return 0;
21}
Cross-platform Considerations

Command-line handling works similarly across Unix, Linux, and Windows, but there are differences. On Windows, you may need to handle argument quoting differently, and getopt() is not natively available — you must use a third-party implementation or handle parsing manually.

FeatureUnix/LinuxWindows (MSVC)
Argument splittingShell splits before calling main()GetCommandLine + CommandLineToArgvW
getopt()Native (POSIX)Not available — use a library
Wildcard expansionShell expands globsProgram receives literal * (must expand)
argv[0]Full path or invoked nameFull path to executable
📝

note

On Windows with MSVC, use wmain(int argc, wchar_t *argv[]) for Unicode argument support. The standard main function receives arguments encoded in the system's active code page.
Practical: File Info Tool

A more complete example that combines numeric arguments, flags, file operations, and comprehensive error handling. This mirrors the structure of real-world CLI tools.

fileinfo.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <errno.h>
5#include <sys/stat.h>
6
7static void usage(const char *prog) {
8 fprintf(stderr, "Usage: %s [OPTIONS] FILE...\n", prog);
9 fprintf(stderr, "Options:\n");
10 fprintf(stderr, " -s Show file size in bytes\n");
11 fprintf(stderr, " -l Show long format (size + permissions)\n");
12 fprintf(stderr, " -h Show this help message\n");
13}
14
15int main(int argc, char *argv[]) {
16 int show_size = 0;
17 int long_format = 0;
18 int file_start = 1;
19
20 /* Parse options */
21 for (int i = 1; i < argc; i++) {
22 if (strcmp(argv[i], "-s") == 0) {
23 show_size = 1;
24 } else if (strcmp(argv[i], "-l") == 0) {
25 long_format = 1;
26 } else if (strcmp(argv[i], "-h") == 0) {
27 usage(argv[0]);
28 return 0;
29 } else if (argv[i][0] == '-') {
30 fprintf(stderr, "Unknown option: %s\n", argv[i]);
31 usage(argv[0]);
32 return 1;
33 } else {
34 file_start = i;
35 break;
36 }
37 }
38
39 if (file_start >= argc) {
40 fprintf(stderr, "Error: no files specified\n");
41 usage(argv[0]);
42 return 1;
43 }
44
45 int exit_code = 0;
46 for (int i = file_start; i < argc; i++) {
47 struct stat st;
48 if (stat(argv[i], &st) != 0) {
49 fprintf(stderr, "%s: %s\n", argv[i], strerror(errno));
50 exit_code = 1;
51 continue;
52 }
53
54 if (long_format) {
55 printf("%s: size=%ld bytes\n", argv[i], (long)st.st_size);
56 } else if (show_size) {
57 printf("%ld\n", (long)st.st_size);
58 } else {
59 printf("%s\n", argv[i]);
60 }
61 }
62
63 return exit_code;
64}
Error Handling Patterns

Good error handling in argument parsing is critical. Users will pass invalid input, missing arguments, and unknown flags. Your program should provide clear, helpful error messages and exit with appropriate codes.

error_patterns.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <errno.h>
5#include <limits.h>
6
7/* Safe integer parsing with range checking */
8int parse_int(const char *str, const char *name, int min, int max) {
9 char *end;
10 errno = 0;
11 long val = strtol(str, &end, 10);
12
13 if (errno != 0) {
14 fprintf(stderr, "Error: %s overflow/underflow\n", name);
15 exit(1);
16 }
17 if (*end != '\0' || end == str) {
18 fprintf(stderr, "Error: %s is not a valid integer (\"%s\")\n",
19 name, str);
20 exit(1);
21 }
22 if (val < min || val > max) {
23 fprintf(stderr, "Error: %s must be between %d and %d\n",
24 name, min, max);
25 exit(1);
26 }
27 return (int)val;
28}
29
30int main(int argc, char *argv[]) {
31 if (argc < 3) {
32 fprintf(stderr, "Usage: %s <port> <threads>\n", argv[0]);
33 fprintf(stderr, " port: 1-65535\n");
34 fprintf(stderr, " threads: 1-256\n");
35 return 1;
36 }
37
38 int port = parse_int(argv[1], "port", 1, 65535);
39 int threads = parse_int(argv[2], "threads", 1, 256);
40
41 printf("port=%d threads=%d\n", port, threads);
42 return 0;
43}

best practice

Always print the program name in error messages using argv[0]. When users pipe output or run programs from scripts, seeing mytool: error is much more helpful than just error.
$Blueprint — Engineering Documentation·Section ID: C-CLI-ARGS·Revision: 1.0