C / CONSOLE INPUT AND OUTPUT
Safer input with fgets and parsing with sscanf
Read one bounded line with fgets, parse it with sscanf, check the match count, and reject bad input without leaving anything stuck in stdin.
What you will learn
- Read a full line with fgets(buf, sizeof buf, stdin) and test the result for NULL
- Remove the newline fgets keeps with buf[strcspn(buf, "\n")] = '\0'
- Compare sscanf's return count against the number of conversions you asked for
- Spot a truncated line by the missing newline and handle the queued remainder
Understanding Safer input with fgets and parsing with sscanf
fgets(buf, size, stdin) copies bytes into buf until it has taken size-1 of them or until it has copied a newline, whichever comes first, then writes a terminating '\0'. The limit comes from the number you pass, not from how much the user typed, which is why the call cannot overrun buf as long as that number really is the array's size. It returns buf on success and NULL when nothing could be read because of end-of-file or an error, so the NULL test belongs in the loop condition rather than in a check on the contents.
sscanf understands the same conversion directives as scanf, but it reads from a string you already own, which splits reading from parsing into two independent steps. Nothing about the stream changes when a conversion fails: the offending line is still sitting in your buffer, so you can echo it back, try a second format on it, or simply read the next line. Its return value is the number of conversions that actually stored a value, so compare it with how many you requested instead of assuming your variables were touched.
Two details of fgets leak into everything built on top of it. First, the newline is part of the data it hands you, so write line[strcspn(line, "\n")] = '\0' before comparing or printing, otherwise strcmp(line, "quit") never matches what the user typed. Second, if the line did not fit, there is no newline at the end and the rest of it is still queued in the stream, arriving later as a bogus line, so check the last character and decide whether to discard the remainder or use a bigger buffer. sscanf is equally quiet about text it never reached, so add %n or a trailing conversion when "the whole line must parse" is part of your rule.
concept placeholder
<stdio.h>
<string.h>
/* run as: printf '12 5\n7\n3 4 cm\n' | ./rect */
int main(void)
{
char line[64];
while (fgets(line, sizeof line, stdin) != NULL) {
int w, h, n;
char junk;
line[strcspn(line, "\n")] = '\0'; /* fgets keeps the newline */
n = sscanf(line, "%d %d %c", &w, &h, &junk);
if (n == 2)
printf("[%s] -> area %d\n", line, w * h);
else if (n == 3)
printf("[%s] -> extra text after the two numbers\n", line);
else
printf("[%s] -> wanted two integers, matched %d\n", line, n);
}
return 0;
}Reading and parsing are separate jobs: fgets takes a whole line under a size you control, and sscanf parses a private copy, so a malformed line can never desynchronize stdin.
Worked examples
Parsing strings, not streams
sscanf works on plain strings, and %n tells you whether the whole string was consumed.
<stdio.h>
int main(void)
{
const char *rows[] = { "ada 36", "grace", "linus 24 extra" };
int i;
for (i = 0; i < 3; i++) {
char name[20];
int age, n, pos = 0;
n = sscanf(rows[i], "%19s %d%n", name, &age, &pos);
printf("%-16s n=%d", rows[i], n);
if (n == 2 && rows[i][pos] == '\0')
printf(" ok: %s is %d\n", name, age);
else
printf(" rejected\n");
}
return 0;
}Example explained
Line 1%19s stops at the first whitespace and stores at most 19 characters plus the terminator, so name[20] is safe no matter how long the row is.
Line 2%n records how many characters were consumed and is not counted in the return value, which is why a complete match still reports 2.
Line 3"grace" fails at the second conversion, so the return value is 1 and age keeps whatever it held before; that is why the count is tested before either value is used.
Line 4"linus 24 extra" matches two numbers but leaves characters behind, and rows[i][pos] != '\0' is what catches it.
Retrying a bad line
A failed sscanf costs nothing, so the loop can just ask for another line.
<stdio.h>
/* run as: printf 'twelve\n-4\n12\n' | ./count */
int main(void)
{
char line[32];
long n;
while (1) {
printf("count? ");
if (fgets(line, sizeof line, stdin) == NULL) {
printf("\nno input\n");
return 1;
}
if (sscanf(line, "%ld", &n) == 1 && n > 0)
break;
printf("not a positive number, try again\n");
}
printf("counting to %ld\n", n);
return 0;
}Example explained
Line 1fgets returning NULL is the only case where line holds nothing usable, so end of input is handled separately from a parse failure.
Line 2sscanf(line, "%ld", &n) == 1 proves a value was stored; the extra n > 0 test is a range check that no conversion specifier can express.
Line 3"-4" parses fine and is rejected by the range test, which shows why parse success and validity are two different questions.
Line 4The failed lines were already removed from stdin by fgets, so the loop reads again without discarding anything.
When the line does not fit
A short buffer splits one line into two reads, and the missing newline is the only warning.
<stdio.h>
<string.h>
/* run as: printf 'hi\nabcdefghij\n' | ./trunc */
int main(void)
{
char buf[8];
while (fgets(buf, sizeof buf, stdin) != NULL) {
size_t len = strlen(buf);
if (len > 0 && buf[len - 1] == '\n') {
buf[len - 1] = '\0';
printf("full line: \"%s\"\n", buf);
} else {
printf("partial: \"%s\" (line longer than %zu chars)\n",
buf, sizeof buf - 1);
}
}
return 0;
}Example explained
Line 1char buf[8] leaves room for 7 characters plus the terminator, so the 10-character line comes back in two pieces.
Line 2The absence of '\n' at the end of the buffer is the only signal that the line was cut; strlen locates the last character to test.
Line 3The remainder "hij\n" arrives as the next fgets result, which is how an unnoticed truncation turns one record into two.
Important notes
sscanf returns EOF, not 0, when the string ends before its first conversion can start, so an empty line slips through a != 0 test; compare against the exact count you expect.
%s and %[...] in sscanf still need an explicit width (%31s for a char[32]): parsing your own buffer removes the stream problem, not the overflow one.
Common mistakes
Writing fgets(p, sizeof p, stdin) where p is a char * rather than an array: sizeof gives the pointer size, so every read stops after 7 characters and long lines split silently.
Forgetting that the newline is still in the buffer, so strcmp(line, "yes") never matches what was typed and printf("%s\n", line) leaves a blank line behind.
Using a variable after sscanf without checking the return value: on input like "abc" nothing is assigned, so the int keeps its old or indeterminate value and the arithmetic that follows is garbage.
Try it yourself
Change, predict, then run
Read lines with fgets into a char[64] and use sscanf with the dashes written literally in the format to split each line into year, month and day, printing the three numbers or "bad date" when fewer than three match. Try it on 2026-09-03, 2026-9 and 2026-09-03 extra.
Open the C workspaceCheck your understanding
A program calls fgets(line, sizeof line, stdin) and then sscanf(line, "%d", &n). The user types abc and presses Enter. What is the state of stdin, and what must happen before the next line is read?
- Nothing is left over: fgets already removed abc and its newline from the stream, and sscanf only inspected the copy, so fgets can be called again immediately.
- The characters abc are still in stdin because sscanf could not convert them, so they must be drained with a getchar loop.
- Only the newline is left in stdin, so the next fgets returns an empty line unless that newline is consumed first.
- The stream is left in an error state and needs clearerr(stdin) before the next read succeeds.
Show answer
fgets takes an entire line, newline included, out of the stream before any parsing happens, and sscanf reads a private copy in memory, so a failed conversion cannot leave characters in stdin. The leftover-newline answer describes scanf("%d", &n), where the %d conversion stops at the newline and never consumes it, and that is precisely the problem fgets plus sscanf removes.