Safe input with fgets
Working with text12 min readLesson 2 of 3
2.1 One word, no limit
scanf("%s", name) skips leading white space, copies characters until the next white space, and appends a terminator. It has no idea how large the destination is.
A long word writes past the end of the array. The bug may not crash immediately, and the bytes that follow the buffer are whatever happened to be there.
fgets(buffer, size, stdin) takes the size as an argument and reads at most size - 1 characters. That limit is why it is the default choice for line input.
#include <stdio.h>
int main(void)
{
char line[16];
printf("Enter a phrase: "); /* sample input: C is compact */
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
printf("read: %s", line);
return 0;
}Output
Enter a phrase: read: C is compact
2.2 The newline is included
When there is room, fgets stores the newline before the terminator. The string then contains a character that most comparisons do not expect.
The condition len > 0 && line[len - 1] == '\n' removes one trailing newline. strcspn(line, "\r\n") also covers files that use \r\n.
Removing the newline matters before strcmp against a word, before printing with brackets, and before parsing a number, though sscanf skips trailing white space on its own.
#include <stdio.h>
#include <string.h>
int main(void)
{
char line[32];
printf("Name: "); /* sample input: Ada Lovelace */
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n') {
line[len - 1] = '\0';
}
printf("[%s] length %zu\n", line, strlen(line));
return 0;
}Output
Name: [Ada Lovelace] length 12
2.3 Detecting a line that did not fit
When the buffer fills before a newline, fgets stops at size - 1 characters and leaves the rest of the line in the input stream. The next read continues from there.
The missing newline is the signal. If the buffer does not end with '\n' and the read did not hit end of file, the line was longer than the buffer.
A common fix reads the remainder and discards it, or reports an error. Treating the first chunk as the whole line makes later parsing fail on text that looks fine.
#include <stdio.h>
#include <string.h>
int main(void)
{
char line[8];
printf("Enter: "); /* sample input: abcdefghij */
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
if (strchr(line, '\n') == NULL) {
printf("line does not fit\n");
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
}
}
printf("[%s]\n", line);
return 0;
}Output
Enter: line does not fit [abcdefg]
2.4 Parsing with sscanf
sscanf(text, format, ...) reads from a string instead of the input stream. It is the simplest way to turn a line into fields once the line is already in memory.
It returns the number of fields it assigned. A return value smaller than expected means the line did not match the format, and the variables still hold their previous values.
Width limits belong in every %s: %15s into a 16-byte array. Without a width, sscanf has the same overflow problem as scanf.
#include <stdio.h>
int main(void)
{
const char *line = "42 apples 3.5";
int count = 0;
char item[16];
double weight = 0.0;
int fields = sscanf(line, "%d %15s %lf", &count, item, &weight);
printf("fields: %d\n", fields);
printf("%d %s %.1f\n", count, item, weight);
return 0;
}Output
fields: 3 42 apples 3.5
2.5 strtol turns text into numbers with evidence
strtol(text, &end, base) converts a prefix of the text and sets end to the first character it did not use. Comparing end with the start tells you whether anything was converted at all.
The base argument accepts 10, 16, or 0. With 0, the function reads the prefix and chooses: 0x means hexadecimal, a leading 0 means octal, and anything else means decimal.
strtol reports overflow through errno. For a small program, checking end and rejecting trailing characters covers the common mistakes.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const char *inputs[] = {"128", "42abc", "abc", "-7"};
char *end = NULL;
for (int i = 0; i < 4; i++) {
long value = strtol(inputs[i], &end, 10);
printf("%s -> value %ld, rest \"%s\"\n", inputs[i], value, end);
}
return 0;
}Output
128 -> value 128, rest "" 42abc -> value 42, rest "abc" abc -> value 0, rest "abc" -7 -> value -7, rest ""
2.6 A read loop that ends cleanly
fgets returns NULL at end of file and on a read error. A loop that tests the return value handles both without calling feof first.
The common bug is while (!feof(stdin)) fgets(...), which processes the last line twice because feof only becomes true after a read fails.
Input that ends without a trailing newline is still returned once. Testing the return value covers that case too.
#include <stdio.h>
#include <string.h>
int main(void)
{
char line[32];
int count = 0;
/* sample input:
red
green
blue */
while (fgets(line, sizeof line, stdin) != NULL) {
line[strcspn(line, "\r\n")] = '\0';
if (line[0] == '\0') continue;
printf("%d %s\n", ++count, line);
}
printf("lines: %d\n", count);
return 0;
}Output
1 red 2 green 3 blue lines: 3
2.7 Input habits worth keeping
- read whole lines with
fgets, then parse the line - check the return value of every read and every parse
- include a width in every
%sfield - remove the line ending before comparing text
- reject trailing characters when the format expects one number
These rules cover most beginner input bugs. The rest come from mixing scanf with fgets, where scanf leaves a newline in the stream that the next fgets reads as an empty line.
#include <stdio.h>
int main(void)
{
int number = 0;
char line[16];
printf("Number: "); /* sample input: 12
hello */
if (scanf("%d", &number) != 1) {
return 1;
}
getchar();
printf("Line: ");
if (fgets(line, sizeof line, stdin) == NULL) {
return 1;
}
printf("%d and %s", number, line);
return 0;
}Output
Number: Line: 12 and hello
