Command-line arguments

The shell splits a command line into words and passes them to main as a count and an array of strings. The program name is the first word, the options follow, and the file names come last. This lesson reads them by hand and then with getopt, the POSIX parser.

argc and argv

argc is the number of words, including the program name, and argv points to an array of argc strings followed by a null pointer. The loop stops at argc, not at the null pointer, although both work; the count is the one the standard guarantees.

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

gcc -Wall -Wextra argv.c -o argv
./argv alpha beta

argc: 3
0: ./argv
1: alpha
2: beta

The program name is ./argv because that is what was typed; the shell does not strip the path. A program that prints usage messages uses argv[0] for the name, so a copy under another name reports that name.

getopt and optarg

getopt parses short options in the style of the Unix tools: a letter per option, a colon after a letter that takes an argument. It returns the option character, stores the argument in optarg, and leaves the first non-option index in optind. The loop ends with -1.

#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char **argv) { int verbose = 0; int count = 0; const char *file = "none"; int option; while ((option = getopt(argc, argv, "vn:f:")) != -1) { switch (option) { case 'v': verbose = 1; break; case 'n': count = atoi(optarg); break; case 'f': file = optarg; break; default: return 1; } } printf("verbose %d, count %d, file %s\n", verbose, count, file); for (int i = optind; i < argc; i++) { printf("operand: %s\n", argv[i]); } return 0; }

gcc -Wall -Wextra options.c -o options
./options -v -n 3 -f data.txt input.txt other.txt

verbose 1, count 3, file data.txt
operand: input.txt
operand: other.txt

The option string determines the grammar: v takes no argument, n and f take one each. The three operands after the options are the file names the program works on; they are never parsed by getopt and are reached through optind. The order of options does not matter.

A missing argument

When an option promises an argument and the command line ends, getopt prints a message to standard error and returns ?. The program below does not handle that value, so it prints the question mark and carries on; the exit status stays zero, which is the real bug of an unchecked parser.

#include <stdio.h> #include <unistd.h> int main(int argc, char **argv) { int option; while ((option = getopt(argc, argv, "n:")) != -1) { printf("option %c\n", option); } printf("done\n"); return 0; }

gcc -Wall -Wextra missing.c -o missing
./missing -n; echo "status: $?"

./missing: option requires an argument: n
option ?
done
status: 0

The message comes from getopt itself, with the program name it found in argv[0]. A careful parser checks for ? and returns a failure status, so the shell can stop a script that forgot the value. The initial colon in the option string changes the message and the return value for this case.

Without unistd.h

getopt, optarg and optind are declared in unistd.h. Without the include, C23 rejects the call and the note names the header. The compiler cannot check the option string until the declaration is visible, so the error is the first thing to fix.

#include <stdio.h> int main(int argc, char **argv) { int option; while ((option = getopt(argc, argv, "n:")) != -1) { printf("option %c\n", option); } return 0; }

gcc -Wall -Wextra noheader.c -o noheader

noheader.c: In function 'main':
noheader.c:7:22: error: implicit declaration of function 'getopt' [-Wimplicit-function-declaration]
7 | while ((option = getopt(argc, argv, "n:")) != -1) {
| ^~~~~~
noheader.c:2:1: note: include '<unistd.h>' or provide a declaration of 'getopt'
1 | #include <stdio.h>
+++ |+#include <unistd.h>
2 |

The same rule applies to optarg and optind: they are variables declared in the header, and using them without it is a separate error. The fix is one line at the top, and the parser from the second section then compiles and runs.

Exercises

  1. Add a -h option that prints a usage line built from argv[0] and exits with status 0.
  2. Make an option repeatable, for example -v -v for a higher verbosity, and print the level.
  3. Print every argument in reverse order with a loop that starts at argc - 1.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does argc count?

    Answer

    Every word, including the program name

    The first word is the program name at argv[0], so argc is at least one.

  2. What does a colon after a letter in the getopt string mean?

    Answer

    The option takes an argument

    The colon marks an option with a required value, which getopt stores in optarg.

  3. Where does getopt leave the first non-option argument?

    Answer

    In optind, as an index into argv

    After the option loop, optind is the index of the first operand, and the program reads the rest from there.

  4. Which header declares getopt, optarg and optind?

    Answer

    unistd.h

    The POSIX declarations live in unistd.h, and C23 rejects the call without a visible declaration.

  5. What does ./argv alpha beta print for argc and the arguments?

    Answer

    argc: 3, then ./argv, alpha and beta

    The program name is counted, so three words appear as indexes 0, 1 and 2.

  6. What does this program print? #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void){char *args[]={"options","-v","-n","3","-f","data.txt","input.txt","other.txt",0};int argc=8;char **argv=args;int v=0,n=0;const char*f="none";int o;while((o=getopt(argc,argv,"vn:f:"))!=-1){switch(o){case 'v':v=1;break;case 'n':n=atoi(optarg);break;case 'f':f=optarg;break;default:return 1;}}printf("verbose %d, count %d, file %s\n",v,n,f);for(int i=optind;i<argc;i++)printf("operand: %s\n",argv[i]);return 0;}

    Answer

    verbose 1, count 3, file data.txt / operand: input.txt / operand: other.txt

    The three options are parsed, and the two file names remain as operands.

  7. What does ./missing -n print and return?

    Answer

    The getopt error, option ?, done and status 0

    The missing argument makes getopt return ?, the unchecked program prints it, and the final status is zero.

  8. What does GCC report when getopt is used without unistd.h?

    Answer

    An implicit declaration error naming the header

    C23 rejects the undeclared function, and the note suggests including <unistd.h>.

  9. Why is an unchecked ? from getopt a bug?

    Answer

    The program continues and exits successfully

    The missing-argument case prints a message, but the program keeps going and returns zero, so a script cannot detect the failure.

  10. When a program is started as ./options, what does argv[0] contain?

    Answer

    The string ./options exactly as typed

    The shell passes the typed word unchanged, and the program uses it in usage messages.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev