exercise 7.1

2026-10-14

#include <stdio.h>
#include <string.h>     //strrchr, strcmp
#include <ctype.h>      //tolower, toupper
//compile with: gcc -Wall -Wextra cc-c.c -o cc-c
//make two names for the same program (like gunzip/zcat, or busybox):
//              ln -s cc-c lower
//              ln -s cc-c upper
//run with:     echo 'Hello, World 123' | ./lower     # hello, world 123
//              echo 'Hello, World 123' | ./upper     # HELLO, WORLD 123
//              echo 'Hello' | ./cc-c                 # error: invoked as cc-c
//              echo 'Hello' | /home/win/webdev-projects/upper   # HELLO
//no extra files instead -- bash's exec -a sets argv[0] to any name:
//              echo 'Hello' | (exec -a lower ./cc-c) # hello
//clean up:     rm lower upper

//K&R2 exercise 7-1: convert upper case to lower or lower case to upper,
//depending on the name the program is invoked with, as found in argv[0].
//
//argv[0] is whatever the shell typed to start the program, path and all:
//"./lower", "/home/win/webdev-projects/lower", or plain "lower" if found
//on $PATH. A symlink keeps ITS name in argv[0], not the target's, so one
//compiled file behaves differently under each name. To compare just the
//name, skip past the last '/'.
//
//The choice is made once, up front, by pointing a function pointer at
//tolower or toupper (section 5.11); the copy loop then just calls it.

int main(int argc, char *argv[])
{
    char *name;
    int (*conv)(int);
    int c;

    (void) argc;                        //unused; silences -Wextra

    //IDIOM: basename -- strrchr finds the LAST '/'; if there is none the
    //name is already bare, otherwise start one char past the slash
    name = strrchr(argv[0], '/');
    name = (name == NULL) ? argv[0] : name + 1;

    //IDIOM: strcmp(a, b) == 0 -- "equal"; strcmp returns 0 on a match,
    //so a bare if (strcmp(a, b)) would mean "DIFFERENT"
    if (strcmp(name, "lower") == 0)
        conv = tolower;
    else if (strcmp(name, "upper") == 0)
        conv = toupper;
    else {
        //IDIOM: errors go to stderr, exit status nonzero -- so a pipe's
        //output isn't polluted and the shell can see it failed (section 7.6)
        fprintf(stderr, "error: invoked as %s; run me as lower or upper\n",
                name);
        return 1;
    }

    //IDIOM: the getchar copy loop -- c is int, not char, so EOF (-1)
    //fits; and getchar's values (unsigned char or EOF) are exactly what
    //tolower/toupper accept, so no cast is needed here
    while ((c = getchar()) != EOF)
        //IDIOM: call through a function pointer -- conv(c) is the same as
        //(*conv)(c); non-letters come back unchanged
        putchar(conv(c));
    return 0;
}

win@DESKTOP-MEIH88T:~/webdev-projects$ gcc -Wall -Wextra cc-c.c -o cc-c
win@DESKTOP-MEIH88T:~/webdev-projects$ echo 'Hello, World 123' | ./lower
-bash: ./lower: No such file or directory
win@DESKTOP-MEIH88T:~/webdev-projects$ ln -s cc-c lower
ln -s cc-c upper
echo 'Hello, World 123' | ./lower
echo 'Hello, World 123' | ./upper
hello, world 123
HELLO, WORLD 123
win@DESKTOP-MEIH88T:~/webdev-projects$ echo 'Hello' | (exec -a lower ./cc-c)
hello
win@DESKTOP-MEIH88T:~/webdev-projects$ echo 'hello' | (exec -a upper ./cc-c)
HELLO

Here's the 7-1 block, top to bottom. I'll use one concrete run as the example throughout: echo 'Hi!' | ./lower.

Lines 1–3: the headers

#include <stdio.h>
#include <string.h>     //strrchr, strcmp
#include <ctype.h>      //tolower, toupper
stdio.h provides getchar, putchar, fprintf, stderr and EOF.
string.h provides strrchr and strcmp (Appendix B3).
ctype.h provides tolower and toupper (Appendix B2).
Lines 4–26 are just comments: the run lines and the description.

Line 28: int main(int argc, char *argv[])
When you type ./lower, the shell asks the kernel to start the file lower. The kernel follows the symlink to cc-c, but it still passes the string you typed as argv[0]. So inside the program:


argc    = 1
argv[0] → "./lower\0"
argv[1] = NULL
Lines 30–32: the three variables

    char *name;
    int (*conv)(int);
    int c;
char *name: this will point at the program's name inside argv[0]. It's a pointer, not a copy, so no array is needed.

int (*conv)(int): a pointer to a function that takes an int and returns an int. Read it from the inside out:

*conv means conv is a pointer,
(*conv)(int) means it points to something you call with an int,
the leading int is what that call returns.
The parentheses around *conv are essential. Without them, int *conv(int) declares a function called conv that returns int *. That's the same trap section 5.12 covers. You can check it with cdecl explain 'int (*conv)(int)'.

int c: holds each character as it's read. It has to be int rather than char; line 58 explains why.

Line 34: (void) argc;
argc is never used. Since -Wextra turns on -Wunused-parameter, gcc would warn about it. Casting an expression to (void) means "evaluate this and throw the result away on purpose," which silences the warning.

One honest gap: a program can be started with argc == 0 (via execve with an empty argument list). Then argv[0] is NULL, and line 38 would crash. The shell never does this, but a stricter version would replace line 34 with if (argc < 1) return 1;, which also gives argc a real use. I can add that if you'd like.

Lines 38–39: strip the path

    name = strrchr(argv[0], '/');
    name = (name == NULL) ? argv[0] : name + 1;
strrchr returns a pointer to the last occurrence of '/' in the string, or NULL if there isn't one. (strchr finds the first one; the extra r means "reverse".)


argv[0] → . / l o w e r \0
            ↑ ↑
            │ └ name + 1
            └ strrchr returns this
Line 38: name points at the /.

Line 39 uses the conditional expression a ? b : c from section 2.11:

If there was no slash (e.g. you ran plain lower found via $PATH), the whole of argv[0] is already the name.
Otherwise, step one character past the slash.
name + 1 is pointer arithmetic: the next char in memory.

Taking the last slash matters. With /home/win/webdev-projects/lower there are four slashes, and only the last one comes right before the name. Nothing is copied here: name simply points into the middle of the existing string.

Lines 43–53: pick the conversion

    if (strcmp(name, "lower") == 0)
        conv = tolower;
    else if (strcmp(name, "upper") == 0)
        conv = toupper;
    else {
        fprintf(stderr, "...invoked as %s...\n", name);
        return 1;
    }
strcmp returns a negative number, 0, or a positive number, like a sort comparison. 0 means equal, so you test == 0. Writing if (strcmp(name, "lower")) would mean "if different". This is a classic bug.
name == "lower" would compile but is wrong. It compares two addresses, and they are never the same, so the test is always false. Comparing the contents of two strings always needs strcmp.
conv = tolower;: a function name used without () turns into a pointer to that function, just as an array name turns into a pointer to its first element. That's why no & is needed. The types match exactly: the standard declares int tolower(int), which is what conv points to.
The else branch needs braces because it holds two statements:
fprintf(stderr, ...) is printf with a chosen output stream. stderr is a separate output (file descriptor 2), so the message still reaches your screen even if stdout is piped or redirected. For example, ./cc-c > out.txt puts the error on the terminal and leaves out.txt empty.
return 1; from main becomes the exit status, which is why you saw status 1 earlier. By convention, 0 means success and anything else means failure (section 7.6).
Lines 58–61: the copy loop

    while ((c = getchar()) != EOF)
        putchar(conv(c));
This is the chapter 1 copy loop with one change: each character goes through conv before it's printed.

The inner parentheses are required. != binds tighter than =. Without them, c = getchar() != EOF means c = (getchar() != EOF), so c would always be 1 or 0.
Why int c:
getchar returns either a character value (0–255) or EOF, which is −1. That's 257 possible values, and a char holds only 256.
If char is unsigned, which it is on some machines, −1 turns into 255 and the loop never ends.
If char is signed, the byte 255 (ÿ in Latin-1) looks like EOF and the loop stops early.
conv(c) calls whichever function conv points to. K&R writes it as (*conv)(c) (see (*comp)(v[i], v[j]) in 5.11). Both forms mean the same thing, and modern code usually uses the shorter one.
What tolower does to each character of Hi!\n:
'H' becomes 'h'.
'i', '!' and '\n' come back unchanged: tolower only changes uppercase letters and returns everything else as is.
Why no cast is needed: tolower is only defined for values that fit in an unsigned char, or EOF. Pass it anything else (for example a negative char from a byte above 127) and the behaviour is undefined. getchar always returns a value in that safe range, so conv(c) is fine. If you were converting characters from a char array, you'd write tolower((unsigned char) s[i]).
Line 62: return 0;
Exit status 0 means success. Output to a pipe is held in a buffer, and returning from main flushes whatever is left in that buffer. So the final hi!\n actually gets written out at this point.

The run, end to end
Step    Value
argv[0] "./lower"
name after line 39  "lower"
conv    tolower
loop    H→h i→i !→! \n→\n
output  hi!
exit status 0