Register

C for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

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.

c
#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.

c
#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.

c
#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.

c
#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.

c
#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.

c
#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 %s field
  • 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.

c
#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

Interview questions

3 questions from this note. Open one to see the answer; no account needed.

  • 01Why does `fgets` take a size argument?

    It reads at most `size - 1` characters, so it cannot write past the buffer. `scanf` with `%s` has no such limit.

  • 02What does a `NULL` return from `fgets` mean?

    End of file or a read error. Check `feof` or `ferror` afterwards if the difference matters.

  • 03What does `sscanf` return?

    The number of input items it assigned. A smaller number than the format expects means the text did not match.