Program Arguments and Input
A program that only talks to a human at a prompt cannot be scripted, tested, or combined with anything else. This week connects your programs to the command line and to standard input — and takes validation seriously, because every one of those inputs comes from outside your control.
- Read command-line arguments and explain the structure of
argv. - Convert text to numbers with full error detection, and say why
atoicannot. - Reject malformed, out-of-range, and missing input with useful messages.
- Read an arbitrary number of lines from standard input safely.
- Write a program that behaves correctly inside a shell pipeline.
1argc and argv
int main(int argc, char *argv[])
{
for (int i = 0; i < argc; i++) {
printf("argv[%d] = \"%s\"\n", i, argv[i]);
}
return 0;
}$ ./prog hello 42 "two words"
argv[0] = "./prog"
argv[1] = "hello"
argv[2] = "42"
argv[3] = "two words"argv is an array of pointers to strings — exactly the shape week 15 introduced. Four facts about it:
argv[0]is the program name as invoked. Use it in error messages so they identify themselves.- Real arguments start at
argv[1], soargcis always at least 1. argv[argc]is guaranteed to beNULL, which allows a pointer-walk loop as an alternative to counting.- Everything is a string. The shell has already split the command line on whitespace and removed quotes;
42arrives as two characters and a terminator, not as an integer.
/* two equivalent traversals */
for (int i = 1; i < argc; i++) { use(argv[i]); }
for (char **p = argv + 1; *p; p++) { use(*p); }Always check argc before indexing. Reading argv[1] when the user supplied no arguments reads the guaranteed NULL and then dereferences it.
2Text to number, done properly
Week 16 introduced strtol; here is the full argument for it.
atoi | strtol | |
|---|---|---|
| Reports "not a number" | No — returns 0 | Yes, via endptr |
| Reports trailing rubbish | No — "12abc" gives 12 | Yes, via endptr |
| Reports overflow | No — undefined behavior | Yes, via errno |
| Handles other bases | No | Yes |
atoi("abc") returns 0, indistinguishable from atoi("0"). There is no combination of checks that recovers the difference. Treat atoi as unusable for input you did not generate yourself.
The complete strtol pattern
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
bool parse_int(const char *text, int *out)
{
if (text == NULL || *text == '\0') {
return false; /* empty */
}
errno = 0;
char *end;
long value = strtol(text, &end, 10);
if (end == text) return false; /* no digits */
if (*end != '\0') return false; /* trailing */
if (errno == ERANGE) return false; /* long range */
if (value < INT_MIN || value > INT_MAX) return false; /* int range */
*out = (int)value;
return true;
}Every line earns its place. errno = 0 before the call is necessary because strtol only ever sets errno; a stale ERANGE from an earlier call would otherwise be misread. And the final range check is separate because strtol returns long, which on Linux is wider than int — without it, "3000000000" parses successfully and then truncates.
strtod follows the identical pattern for floating point, and strtoul for unsigned values — with one trap: strtoul("-1", …) succeeds and returns ULONG_MAX, because negation is applied after conversion. Reject a leading minus yourself.
3Reading standard input
Week 8 established that scanf is difficult to use safely. The professional pattern is to read a whole line and then parse it, which separates two concerns that scanf conflates.
char line[256];
while (fgets(line, sizeof line, stdin) != NULL) {
line[strcspn(line, "\n")] = '\0'; /* strip the newline */
process(line);
}fgets reads at most size - 1 characters and always terminates, so it cannot overflow. It returns NULL at end of input, which makes the loop condition natural. And because it keeps the trailing newline when the line fits, the strcspn idiom removes it: strcspn(line, "\n") returns the index of the first newline, or the length if there is none, so the assignment is correct either way.
Detecting an over-long line
If no newline is present, the line was longer than the buffer and the rest is still waiting:
if (strchr(line, '\n') == NULL && !feof(stdin)) {
/* line was truncated: consume the remainder */
int c;
while ((c = getchar()) != '\n' && c != EOF) { }
}Silently processing a truncated line is a real defect — in a configuration parser it can turn a long entry into a different, valid one. Week 26 covers stream state in full.
4Behaving well in a pipeline
A Unix tool is expected to follow four conventions. Following them costs nothing and makes your program composable:
| Convention | Why |
|---|---|
Results to stdout, diagnostics to stderr | prog > out.txt keeps errors visible |
| Exit 0 on success, non-zero on failure | prog && next works |
Read stdin when given no file | cat data | prog works |
| Say nothing extra on success | Output can be piped into another tool |
The last is the one people find surprising. A program that prints "Processing complete!" to stdout has corrupted its own output for any consumer. If the message matters, send it to stderr.
fprintf(stderr, "%s: cannot open '%s'\n", argv[0], path);
return EXIT_FAILURE;Prefixing with argv[0] is the standard form: in a long pipeline, an unattributed error message is nearly useless.
5Worked example: a numeric filter
A tool that reads numbers from standard input, applies a scale and an offset given as arguments, and prints the results — validating everything and composing properly.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <limits.h>
static const char *program_name = "scale";
static void usage(void)
{
fprintf(stderr,
"usage: %s SCALE OFFSET\n"
" reads one number per line from standard input,\n"
" prints value * SCALE + OFFSET to standard output\n",
program_name);
}
/* Full validation: empty, non-numeric, trailing text, out of range. */
static bool parse_double(const char *text, double *out)
{
if (text == NULL || *text == '\0') {
return false;
}
errno = 0;
char *end;
double value = strtod(text, &end);
if (end == text) return false; /* no conversion */
while (*end == ' ' || *end == '\t') end++; /* allow trailing spaces */
if (*end != '\0') return false; /* trailing rubbish */
if (errno == ERANGE) return false; /* overflow/underflow */
*out = value;
return true;
}
static bool parse_int(const char *text, int *out)
{
if (text == NULL || *text == '\0') {
return false;
}
errno = 0;
char *end;
long value = strtol(text, &end, 10);
if (end == text) return false;
if (*end != '\0') return false;
if (errno == ERANGE) return false;
if (value < INT_MIN || value > INT_MAX) return false;
*out = (int)value;
return true;
}
int main(int argc, char *argv[])
{
if (argc > 0 && argv[0] != NULL) {
program_name = argv[0];
}
if (argc != 3) {
fprintf(stderr, "%s: expected 2 arguments, got %d\n",
program_name, argc - 1);
usage();
return EXIT_FAILURE;
}
double scale, offset;
if (!parse_double(argv[1], &scale)) {
fprintf(stderr, "%s: SCALE is not a number: \"%s\"\n",
program_name, argv[1]);
return EXIT_FAILURE;
}
if (!parse_double(argv[2], &offset)) {
fprintf(stderr, "%s: OFFSET is not a number: \"%s\"\n",
program_name, argv[2]);
return EXIT_FAILURE;
}
char line[256];
long line_number = 0;
long accepted = 0, rejected = 0;
while (fgets(line, sizeof line, stdin) != NULL) {
line_number++;
/* Detect a line too long for the buffer. */
if (strchr(line, '\n') == NULL && !feof(stdin)) {
fprintf(stderr, "%s: line %ld too long, skipping\n",
program_name, line_number);
int c;
while ((c = getchar()) != '\n' && c != EOF) { }
rejected++;
continue;
}
line[strcspn(line, "\n")] = '\0';
if (line[0] == '\0' || line[0] == '#') {
continue; /* blank line or comment */
}
double value;
if (!parse_double(line, &value)) {
fprintf(stderr, "%s: line %ld: not a number: \"%s\"\n",
program_name, line_number, line);
rejected++;
continue;
}
printf("%g\n", value * scale + offset); /* result to stdout */
accepted++;
}
if (ferror(stdin)) {
fprintf(stderr, "%s: error reading input\n", program_name);
return EXIT_FAILURE;
}
fprintf(stderr, "%s: %ld accepted, %ld rejected\n",
program_name, accepted, rejected);
return rejected == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}gcc -std=c17 -Wall -Wextra -g -fsanitize=address -o scale scale.cExercise it from the shell
printf '1\n2\n3\n' | ./scale 2 10
12
14
16
scale: 3 accepted, 0 rejectedNote that the summary went to stderr, so it appeared on the terminal but is not part of the output. Prove it:
printf '1\n2\n3\n' | ./scale 2 10 > results.txt
cat results.txt12
14
16The results file contains only numbers — which is what lets the tool be chained:
printf '1\n2\n3\n' | ./scale 2 0 | ./scale 10 0 | paste -sd+ | bcEvery failure mode, checked
./scale ; echo "status $?" # too few arguments
./scale 2 ; echo "status $?" # still too few
./scale two 10 ; echo "status $?" # bad SCALE
printf 'x\n' | ./scale 2 10 ; echo "status $?" # bad data line
printf '' | ./scale 2 10 ; echo "status $?" # empty input
printf '1e400\n' | ./scale 2 0 ; echo "status $?" # overflow
yes 1 | head -1000 | ./scale 1 0 > /dev/null ; echo "status $?"| Input | Expected |
|---|---|
| No arguments | Usage on stderr, status 1 |
two as SCALE | Named error, status 1 |
| A non-numeric data line | Line number reported, other lines still processed |
| Empty input | No output, status 0 — zero lines is not an error |
1e400 | Rejected via ERANGE, not silently turned into infinity |
| A 500-character line | Reported as too long; the remainder is discarded, not misparsed |
Why the over-long line matters
Remove the length check and feed it a long line:
python3 -c "print('1' * 300)" | ./scale 1 0Without the check, fgets returns the first 255 characters — a perfectly valid number — and the remaining 45 characters become a second line, also a valid number. One input line silently becomes two outputs. In a billing system or a sensor log that is a data-corruption bug, not a formatting one.
The atoi comparison
Replace parse_double with atof and try the failure cases again. atof("two") returns 0.0 and atof("1e400") returns infinity, both silently. Every rejection in the table above becomes an accepted wrong answer.
6Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
Reading argv[1] without checking argc | Dereferences NULL | Check argc first, always. |
Using atoi on user input | Failure indistinguishable from 0 | strtol with endptr and errno. |
Forgetting errno = 0 before strtol | A stale ERANGE misreported | Clear it immediately before the call. |
Not range-checking the long result | Silent truncation into int | Compare against INT_MIN/INT_MAX. |
Accepting "12abc" | Trailing rubbish ignored | Require *end == '\0'. |
Diagnostics on stdout | Corrupts piped output | Everything but results goes to stderr. |
| Always returning 0 | Scripts cannot detect failure | Non-zero on any error. |
Ignoring a truncated fgets line | One line silently becomes two records | Check for the newline and drain the rest. |
7Check yourself
What is argv[argc], and why is it useful?
It is guaranteed to be NULL. That lets you walk the arguments with a pointer loop terminating on the null entry instead of counting, and it means an out-of-range read of argv[argc] yields NULL rather than garbage — though dereferencing it is still a crash, so check argc before indexing.
Why can't atoi be made safe with extra checks?
Because it discards the information you would need. It returns 0 both for the string "0" and for anything unparseable, stops silently at the first non-digit, and has undefined behavior on overflow. No check applied afterwards can recover which case occurred. strtol reports all three through endptr and errno.
Why must errno be set to 0 before calling strtol?
Because library functions only ever set errno; nothing clears it. A value left over from an unrelated earlier failure would still be there, so a successful conversion could appear to have overflowed. Clearing it immediately before the call makes the subsequent test meaningful.
Why should a successful program print nothing beyond its results?
Because stdout is the channel another program reads. A "done!" line becomes a record in the next tool's input and corrupts it. Progress and summary messages belong on stderr, which stays on the terminal when output is redirected and is ignored by a pipeline.
What goes wrong if you ignore that fgets may have truncated a line?
The remainder of the line is still in the stream and is returned by the next call, so one input line becomes two apparently valid records. If both halves happen to parse — as they do for a long run of digits — the program produces wrong output with no error at all. Check for the newline and discard the remainder explicitly.
8Where this leads
Week 24 surveys the rest of the standard library — the mathematics, time, and random-number facilities you have not needed until now — and teaches you to read its documentation rather than memorize it. Week 31 then returns to the command line with getopt, environment variables, and layered configuration, turning the filter you just wrote into a properly optioned tool.