Arrays and strings · about 50 min

Command-line arguments

Why this matters

Almost every tool you use in a terminal takes arguments: ls -l /tmp, gcc -o prog main.c, grep -i error log.txt. Arguments are how a program learns what to work on without asking interactively, which is what makes programs scriptable and composable. In C they arrive as the parameters of main, and they are your first real encounter with an array of strings, a structure you will see again and again.

The other form of main

So far main took no parameters. It can instead be written as

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

The char * part means "address of a char"; argv is an array whose elements are each the address of the first character of a string. Pointers are the next module, which explains that notation fully. Until then, all you need is: argv[i] is a string, usable anywhere a string is, with %s, strcmp, strlen and the rest.

#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("argc = %d\n", argc);
    for (int i = 0; i < argc; i++) {
        printf("argv[%d] = \"%s\"\n", i, argv[i]);
    }
    if (argv[argc] == NULL) {
        printf("argv[%d] is NULL\n", argc);
    }
    return 0;
}
$ ls *.txt
notes.txt  todo.txt
$ ./prog hello "two words" 42 *.txt
argc = 6
argv[0] = "./prog"
argv[1] = "hello"
argv[2] = "two words"
argv[3] = "42"
argv[4] = "notes.txt"
argv[5] = "todo.txt"
argv[6] is NULL

Several things to notice:

Where the strings live

The kernel copies the argument strings into the new process's memory, near the top of the stack, and the C startup code builds the argv array pointing at them before calling main:

  argv (array of addresses)              the strings themselves
  +---------+
  | argv[0] | -------------------------> "./prog\0"
  +---------+
  | argv[1] | -------------------------> "hello\0"
  +---------+
  | argv[2] | -------------------------> "two words\0"
  +---------+
  | argv[3] | -------------------------> "42\0"
  +---------+
  |  NULL   |   end marker (argv[argc])
  +---------+

Each string is an ordinary terminated char array. The arrows are pointers; you will draw many diagrams like this in the next module. The strings stay valid for the whole run of the program.

Converting arguments to numbers

To turn "42" into the integer 42, you can use sscanf, which works exactly like scanf but reads from a string instead of from input. It returns how many values it converted. A useful trick detects trailing junk: ask for one more character after the number. If the whole string was a number, there is no character to read and sscanf returns 1; if anything follows, it returns 2.

#include <stdio.h>

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "usage: %s WIDTH HEIGHT\n", argv[0]);
        return 2;
    }

    int w, h;
    char extra;
    if (sscanf(argv[1], "%d%c", &w, &extra) != 1 || sscanf(argv[2], "%d%c", &h, &extra) != 1) {
        fprintf(stderr, "%s: WIDTH and HEIGHT must be whole numbers\n", argv[0]);
        return 1;
    }
    printf("area = %d\n", w * h);
    return 0;
}
$ ./prog 3 4
area = 12
$ ./prog 3
usage: ./prog WIDTH HEIGHT
$ echo $?
2
$ ./prog 3 4x
./prog: WIDTH and HEIGHT must be whole numbers
$ echo $?
1

You will also see atoi(argv[1]) from <stdlib.h> in a lot of code. It is shorter but cannot report errors: atoi("hello") is simply 0, and out-of-range input is undefined behavior. The input/output module introduces strtol, the robust standard choice, which also detects overflow. sscanf with %d does not detect overflow either, so treat it as a stepping stone.

Usage messages, stderr and exit status

Well-behaved command-line programs follow conventions that scripts depend on:

Options

Arguments starting with - are conventionally options: -v, -n 5, --help. A program recognizes them by comparing strings:

if (argc > 1 && strcmp(argv[1], "-v") == 0) {
    verbose = 1;
}

Real programs with many options use the POSIX function getopt (man 3 getopt), which handles combined flags like -lh and option arguments. Hand-parsing one or two options is fine.

Common mistakes

Recap

Going further: run ./prog * in a directory with many files to see how big argc can get. Then read the start of man 3 getopt and its EXAMPLES section.

Exercises

Exercise 1 · quiz

How many arguments?

A program is started from the shell as:

$ ./prog one "two three" four

What is argc inside main?

Answer
  • 4

The shell splits the command line into words before starting the program. The quotes keep "two three" together as one word (and are removed). The words are ./prog, one, two three, four: four strings, so argc is 4, with argv[0] = "./prog" and argv[3] = "four". argv[4] is NULL.

Exercise 2 · write code

List the arguments

Print every command-line argument except the program name, one per line, numbered from 1, in this format:

1: first
2: second

If there are no arguments, print exactly no arguments instead.

File: main.c

Starting point

#include <stdio.h>

int main(int argc, char *argv[])
{
    /* your code here */
    return 0;
}

Example with arguments first second

expected output

1: first
2: second

Example with arguments hello world x -v

expected output

1: hello world
2: x
3: -v
Hint 1

argv[0] is the program name, so the real arguments are argv[1] up to argv[argc - 1].

Hint 2

With no arguments, argc is 1 (just the program name).

Reference solution
#include <stdio.h>

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

Exercise 3 · write code

Add up the arguments

Write a program that adds up its integer arguments and prints sum = <total>.

$ ./prog 10 20 -5
sum = 25

File: main.c

Starting point

#include <stdio.h>

int main(int argc, char *argv[])
{
    int total = 0;
    /* convert each argument and add it */
    printf("sum = %d\n", total);
    return 0;
}

Example with arguments 10 20 -5

expected output

sum = 25

Example with arguments 7

expected output

sum = 7
Hint 1

sscanf(argv[i], "%d", &n) works like scanf but reads from a string. It returns how many values it converted.

Hint 2

To reject 12abc, also try to read one more character: sscanf(argv[i], "%d%c", &n, &extra) == 1 is true only if nothing follows the number.

Hint 3

Check argc < 2 first, print the usage line with fprintf(stderr, ...) and return 2;.

Reference solution
#include <stdio.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "usage: sum NUMBER...\n");
        return 2;
    }

    int total = 0;
    for (int i = 1; i < argc; i++) {
        int n;
        char extra;
        if (sscanf(argv[i], "%d%c", &n, &extra) != 1) {
            fprintf(stderr, "sum: not a number: %s\n", argv[i]);
            return 1;
        }
        total += n;
    }
    printf("sum = %d\n", total);
    return 0;
}

Exercise 4 · write code

An option flag

Write a program used as

repeat [-u] COUNT WORD

that prints WORD COUNT times on one line, separated by single spaces. If the optional first argument -u is given, the word is printed in uppercase. COUNT is a whole number from 1 to 100.

$ ./prog 3 hi
hi hi hi
$ ./prog -u 2 Bye
BYE BYE

On any misuse (wrong number of arguments, an unknown option such as -x, a COUNT that is not a whole number or is outside 1 to 100), print usage: repeat [-u] COUNT WORD to standard error and exit with status 2, printing nothing on standard output.

File: main.c

Starting point

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

int main(int argc, char *argv[])
{
    /* work out whether -u was given, then where COUNT and WORD are */
    return 0;
}

Example with arguments 3 hi

expected output

hi hi hi

Example with arguments -u 2 Bye

expected output

BYE BYE
Hint 1

argc tells you which form you have: 3 means COUNT WORD, 4 means -u COUNT WORD (and argv[1] must then be exactly "-u", checked with strcmp). Anything else is misuse.

Hint 2

Keep the index of COUNT in a variable (1 or 2) so the rest of the code is the same for both forms.

Hint 3

To uppercase, copy the word into your own array first (snprintf), then change that copy with toupper.

Reference solution
#include <stdio.h>
#include <ctype.h>
#include <string.h>

static int usage(void)
{
    fprintf(stderr, "usage: repeat [-u] COUNT WORD\n");
    return 2;
}

int main(int argc, char *argv[])
{
    int upper = 0;
    int first = 1;   /* index of COUNT */

    if (argc == 4) {
        if (strcmp(argv[1], "-u") != 0) {
            return usage();
        }
        upper = 1;
        first = 2;
    } else if (argc != 3) {
        return usage();
    }

    int count;
    char extra;
    if (sscanf(argv[first], "%d%c", &count, &extra) != 1 || count < 1 || count > 100) {
        return usage();
    }

    char word[256];
    snprintf(word, sizeof word, "%s", argv[first + 1]);
    if (upper) {
        for (int i = 0; word[i] != '\0'; i++) {
            word[i] = toupper((unsigned char)word[i]);
        }
    }

    for (int i = 0; i < count; i++) {
        printf("%s%s", i > 0 ? " " : "", word);
    }
    printf("\n");
    return 0;
}