File Handling · intermediate · ~8 min

fgets for safe line reading

## What you will learn - How to read one line of text at a time from a file or `stdin` using `fgets`, while controlling exactly how many bytes are written into your buffer. - Why `fgets` reads at most `n - 1` characters and always adds a NUL terminator, so the result is a valid C string. - How to detect and correctly handle a *short read* (a line longer than your buffer) versus a complete line. - How to strip the trailing newline cleanly with `strcspn`, and when you should keep it instead. - How to check the return value to tell end-of-file and read errors apart, and how to loop over an entire file safely. - Why `fgets` is the safe replacement for the removed `gets` function, and how that prevents buffer overflows.

Overview

Reading text line by line is one of the most common things a C program does: parsing a config file, processing a log, reading a name the user typed, or walking through a CSV. The standard library gives you fgets for exactly this. It is part of the stdio (standard input/output) library you met in fopen and the stdio model, and it works on any FILE * stream — a file you opened with fopen, or one of the always-open streams stdin, stdout, stderr.

The whole point of fgets is bounded reading. You hand it a buffer and tell it the buffer's size, and it promises never to write past that size. That single guarantee is what makes it safe, and it is why the old gets function — which had no size argument and could write forever past the end of your buffer — was banned from the language.

In plain terms: fgets copies characters from the stream into your array until it hits a newline, fills the array, or reaches the end of the file — whichever comes first. It then puts a '\0' at the end so you have a proper string. Your job is to give it a big enough buffer, check whether you got a whole line, and decide what to do with the newline it leaves behind.

This builds directly on the stdio model: a FILE * is a buffered stream with a current position, and fgets advances that position as it reads. Once you understand fopen and fclose, fgets is the natural next step for pulling text out of a file.

Why it matters

Unbounded input is one of the oldest and most dangerous bugs in C. The notorious 1988 Morris worm and countless later exploits relied on programs that read input into a fixed array with no length limit, letting an attacker overrun the buffer and corrupt memory. gets was the textbook example, and it was eventually removed from the C standard entirely.

fgets matters because it makes the safe choice the easy choice. By forcing you to pass the buffer size, it turns "how much can I read?" from a guess into a hard limit the library enforces. In real software — shells, parsers, network daemons, embedded firmware — line-oriented input from untrusted sources is everywhere, and a single missing bounds check can become a remote crash or worse. Learning to use fgets correctly, including handling short reads and checking return values, is a foundational defensive-coding habit even in non-security code: it produces programs that fail gracefully instead of corrupting memory.

Core concepts

1. What fgets is and what it guarantees

fgets ("file get string") reads a line of text from a stream into a buffer you provide. Its prototype, from <stdio.h>, is:

char *fgets(char *buf, int n, FILE *f);

On each call it does four things:

  • Reads at most n - 1 characters from f into buf.
  • Stops early if it reads a newline ('\n') — and keeps that newline in the buffer.
  • Stops at end of file.
  • Writes a terminating NUL byte ('\0') after the last character, so buf is always a valid C string.

The reason it reads only n - 1 characters is that the final slot is reserved for the '\0'. This is the core safety property: no matter what the input looks like, fgets never writes more than n bytes total into buf.

buf with size n = 8, input line "Hi\n":

 index:   0   1   2   3   4   5   6   7
         +---+---+---+---+---+---+---+---+
         | H | i |\n |\0 | ? | ? | ? | ? |
         +---+---+---+---+---+---+---+---+
          \______ written _____/  \untouched/

When to use it: any time you read text a line at a time from a file or stdin. When NOT to: for binary data with embedded NUL bytes (use fread instead — fgets stops at newlines and treats '\0' as just another byte, so you cannot tell where the data really ended).

Pitfall: assuming n is "the number of characters I will get." You get at most n - 1. Size your buffer for the longest line you expect plus one for the NUL — and ideally plus one more for the '\n'.

Knowledge check: If char buf[5]; and the input line is "hello\n", how many characters of the word does fgets(buf, sizeof buf, f) copy on the first call, and what is in buf afterward?

2. The return value: success, EOF, and error

fgets returns buf (the same pointer you passed in) on success, and NULL when it reads no characters because of end of file, or when a read error occurs. You must check it:

 result = fgets(buf, n, f)
          |
   +------+------+
   |             |
 non-NULL       NULL
 got a line     EOF or error
 process it     stop the loop

A subtle point: NULL lumps together "end of file" and "hard error." To tell them apart after the loop ends, use feof(f) (true if you hit EOF) and ferror(f) (true if a read error occurred). For most simple programs, stopping the loop on NULL is enough; robust programs distinguish the two.

Pitfall: ignoring the return value and using buf anyway. If fgets returned NULL, the contents of buf are unspecified — reading them is a bug.

Knowledge check (predict the output): A file contains exactly abc with no trailing newline. You call fgets(buf, 16, f) twice. What does each call return, and what is buf after each?

3. The trailing newline: keep it or strip it

Because fgets keeps the '\n', the buffer after a normal read looks like "name\n\0". That newline is genuinely useful information:

  • A buffer that ends with '\n' means you read a complete line.
  • A buffer with no '\n' (and a non-NULL return) means the line was longer than your buffer — a short read — and the rest of the line is still waiting in the stream.

If you do not want the newline (for comparisons, printing, or storing), strip it. The cleanest idiom uses strcspn, which returns the index of the first matching character:

buf[strcspn(buf, "\n")] = '\0';   // replace first '\n' (if any) with NUL

If there is no newline, strcspn returns the string length, and you simply overwrite the existing '\0' with another '\0' — harmless.

Pitfall: the older trick buf[strlen(buf) - 1] = '\0'; assumes a newline is always present. On a short read, or on an empty buffer, it deletes a real character or indexes out of bounds. Prefer strcspn.

Detecting a short read:

  size_t len = strlen(buf);
  if (len > 0 && buf[len-1] == '\n')  ->  complete line
  else                                ->  short read (line too long) OR EOF without newline

Knowledge check (explain in your own words): Why is it useful that fgets keeps the newline instead of discarding it? Give one situation where keeping it changes how your program behaves.

Syntax notes

Syntax and structure

#include <stdio.h>
#include <string.h>

char line[256];                       // your buffer

// Read one line; the loop ends when fgets returns NULL (EOF or error)
while (fgets(line, sizeof line, stdin) != NULL) {
    line[strcspn(line, "\n")] = '\0'; // optional: drop the trailing newline
    // ... use `line` as a NUL-terminated string ...
}

Key points:

  • Pass sizeof line (not a hard-coded number) when line is a real array, so the size always matches the declaration. Do not use sizeof on a pointer parameter — there it gives the pointer size, not the buffer size.
  • The stream can be stdin or any FILE * from fopen.
  • Compare the result against NULL explicitly for clarity; while (fgets(...)) is equivalent because NULL is falsy.

Lesson

What fgets does

char *fgets(char *buf, int n, FILE *f);

fgets reads one line of text into the buffer buf. It is the safe way to read user-supplied text, because you tell it exactly how much room you have.

On each call, fgets:

  • Reads at most n - 1 characters, or stops early when it reaches a newline (\n).
  • Keeps the newline in the buffer if one was read.
  • Adds a terminating NUL byte (\0) at the end, so buf is always a valid C string.
  • Returns buf on success, or NULL at end of file or on error.

NUL terminator: the \0 byte that marks the end of a C string. fgets always writes one, which is why it reads only n - 1 characters — the last slot is reserved for it.

Why the newline is useful

Because fgets keeps the \n, you can tell two cases apart:

  • A complete line ends with \n.
  • A short read (the line was longer than your buffer) has no \n.

If you do not want the newline, strip it after reading.

Code examples

#include <stdio.h>
#include <string.h>

/* Read a text file line by line, number each line, and report whether the
   last line was complete. Demonstrates bounded reading, return-value checks,
   newline handling, and resource cleanup. */
int main(void) {
    const char *path = "notes.txt";

    FILE *f = fopen(path, "r");
    if (f == NULL) {                 // fopen failed (missing file, no permission)
        perror("fopen");            // prints "fopen: No such file or directory", etc.
        return 1;
    }

    char line[64];                   // room for 63 chars + the NUL terminator
    int line_no = 0;
    int last_was_complete = 1;       // assume complete until proven otherwise

    while (fgets(line, sizeof line, f) != NULL) {
        size_t len = strlen(line);
        // A complete line ends in '\n'; otherwise this was a short read.
        last_was_complete = (len > 0 && line[len - 1] == '\n');

        line[strcspn(line, "\n")] = '\0';   // strip newline for clean printing
        line_no++;
        printf("%d: %s\n", line_no, line);
    }

    if (ferror(f)) {                 // distinguish a real error from plain EOF
        perror("fgets");
        fclose(f);
        return 1;
    }

    printf("Read %d line(s). Last line %s.\n",
           line_no,
           last_was_complete ? "ended with a newline" : "had no trailing newline");

    if (fclose(f) != 0) {            // always close what you opened
        perror("fclose");
        return 1;
    }
    return 0;
}

What it does: it opens notes.txt for reading, then loops with fgets, copying at most 63 characters per call into line. For each line it records whether the read ended in a newline (so it can tell whether the file's last line was terminated), strips the newline, and prints the line with a 1-based number. When fgets returns NULL the loop ends; the program then checks ferror to separate a read error from a normal end of file, prints a summary, and closes the file.

Expected output (for a notes.txt containing alpha, beta, gamma on three newline-terminated lines):

1: alpha
2: beta
3: gamma
Read 3 line(s). Last line ended with a newline.

Edge cases to note: a line longer than 63 characters is split across multiple fgets calls (each call after the first picks up where the last stopped), so it would be counted as more than one "line" here — that is the short-read behavior, not a bug in fgets. An empty file produces Read 0 line(s).. A file whose final line lacks a newline reports "had no trailing newline."

Line by line

Walkthrough of the key example

Assume notes.txt contains:

alpha\nbeta\ngamma\n
  1. fopen(path, "r") opens the file for reading and returns a FILE *. If it returns NULL, perror prints a human-readable reason and we exit with status 1 — never proceed with a failed open.
  2. char line[64]; reserves 64 bytes on the stack. fgets will use at most 63 for characters and 1 for the '\0'.
  3. First iteration: fgets(line, 64, f) reads alpha\n (6 bytes), stopping at the newline, then appends '\0'. strlen(line) is 6; line[5] is '\n', so last_was_complete is true. strcspn(line, "\n") returns 5, so line[5] = '\0' turns the buffer into "alpha". We print 1: alpha.
  4. Second and third iterations: the same happens for beta and gamma, advancing the stream position each time.
  5. Fourth call: the stream is at end of file, so fgets returns NULL and the loop ends.
  6. ferror(f) is false (we hit EOF, not an error), so we skip the error branch.
  7. We print the summary: 3 lines, last line ended with a newline.
  8. fclose(f) flushes and releases the file handle.

Trace of line and the stream position

Call Bytes consumed from stream line after read last_was_complete line after strip
1 alpha\n alpha\n\0 true alpha\0
2 beta\n beta\n\0 true beta\0
3 gamma\n gamma\n\0 true gamma\0
4 (none — EOF) unchanged n/a (returns NULL) n/a

If instead a line were 100 characters long, call 1 would consume the first 63 characters with no trailing '\n', last_was_complete would be false, and the next call would continue from character 64 — that is a short read in action.

Common mistakes

Mistake 1: Using gets (or rolling your own unbounded read)

char buf[64];
gets(buf);          // WRONG: no size limit — overruns buf on long input

gets has no way to know how big buf is, so any input longer than 63 characters writes past the end of the array, corrupting the stack. This is a classic buffer overflow and gets was removed from C11. Fix: use fgets, which takes the size:

char buf[64];
if (fgets(buf, sizeof buf, stdin) != NULL) { /* ... */ }

How to recognize it: modern compilers warn loudly (the 'gets' function is dangerous). Treat any such warning as a must-fix.

Mistake 2: Passing the wrong size

char buf[64];
fgets(buf, 128, stdin);   // WRONG: claims 128 bytes but buf is only 64

You told fgets it may write up to 128 bytes into a 64-byte array — the same overflow you were trying to avoid. Fix: always pass sizeof buf for a true array, so the size cannot drift out of sync with the declaration.

Mistake 3: Blindly chopping the last character

char buf[16];
fgets(buf, sizeof buf, stdin);
buf[strlen(buf) - 1] = '\0';   // WRONG when there is no newline, or buf is empty

If the line was longer than the buffer (no '\n'), this deletes a real character. If fgets returned NULL and buf is empty, strlen is 0 and buf[-1] is out of bounds. Fix:

buf[strcspn(buf, "\n")] = '\0';   // safe whether or not a newline is present

Mistake 4: Ignoring the return value

fgets(buf, sizeof buf, f);
printf("%s", buf);   // WRONG: if fgets returned NULL, buf is unspecified

Fix: branch on the return value; only use buf when fgets returned non-NULL. How to prevent: make if (fgets(...) != NULL) or a while loop your default pattern, never a bare call.

Debugging tips

When it does not work

Compiler warnings/errors

  • implicit declaration of function 'fgets' or 'strcspn' — you forgot #include <stdio.h> or #include <string.h>. Add the headers.
  • the 'gets' function is dangerous and should not be used — you (or example code) called gets. Replace it with fgets.
  • comparison between pointer and integer — you wrote fgets(...) == 0 mixing types confusingly; prefer != NULL.

Runtime / logic errors

  • The loop never ends or reads garbage. You probably ignored the NULL return, or are reading from a stream that was never opened (check fopen returned non-NULL).
  • Lines look truncated. Your buffer is too small for the data; a long line is being split across calls. Increase the buffer or handle short reads on purpose.
  • An extra blank line or stray character at the end of each value. You kept the '\n'; strip it with strcspn.
  • Reading stops after one line when you expected more. Make sure you are looping with while (fgets(...)), not calling it once.

Debugging steps

  1. Right after the read, print the length and a visible form of the buffer: printf("len=%zu [%s]\n", strlen(buf), buf); — brackets reveal trailing whitespace.
  2. Check feof(f) and ferror(f) after the loop to learn why it stopped.
  3. Run under a sanitizer: compile with -fsanitize=address -g and rerun; it pinpoints any out-of-bounds access from a size mismatch.

Questions to ask: Did fopen succeed? Is my buffer size really sizeof the array? Am I checking the return value before using the buffer? Could this line be longer than my buffer?

Memory safety

Memory safety and robustness

fgets is the safe line reader, but only if you use it correctly. Watch these points for this topic:

  • Buffer bounds. The whole guarantee rests on the second argument matching the real buffer. Always pass sizeof arr for a stack array. Inside a function that receives a pointer, you must pass the size as a separate parameter — sizeof ptr is the size of the pointer, not the buffer, and using it is a classic overflow bug.
  • Initialization. After a NULL return, the contents of buf are unspecified. Do not read buf until you have confirmed a non-NULL return. If you want a guaranteed-valid empty string even on failure, initialize with char buf[64] = ""; before the call.
  • NUL termination is automatic — but only within bounds. fgets always terminates within the n bytes you allowed, so the result is a valid string. Do not, however, assume there are no embedded NUL bytes: if the input file contains a '\0', strlen will report a length shorter than what fgets actually read. For text this is fine; for arbitrary bytes use fread.
  • Off-by-one with the newline. When stripping or indexing, remember the line may or may not contain '\n'. Guard with len > 0 before touching buf[len - 1] to avoid buf[-1].
  • No integer overflow on the count, since fgets takes an int size and reads at most n - 1; just keep n positive and no larger than your buffer.

Using fgets plus strcspn and a return-value check eliminates the most common text-input memory errors in C. Combined with always checking fopen and always calling fclose, this is the standard safe pattern for line-oriented file reading.

Real-world uses

Where this shows up

  • Configuration and data files. Tools read key=value config lines, /etc/passwd-style records, or CSV rows one line at a time with fgets, then parse each line.
  • Log processing. Filters like a simplified grep read a log line by line, test each against a pattern, and print matches — bounded reading keeps a single enormous line from blowing up memory.
  • Interactive prompts. A command-line program reading a username, a menu choice, or a path from stdin uses fgets instead of scanf("%s", ...) so a long answer cannot overflow the buffer.
  • Embedded and systems code. Firmware and daemons that parse text protocols rely on bounded reads because they often run with no memory protection and cannot afford an overflow.

Professional best practices

Beginner rules

  • Always pass sizeof the array as the size.
  • Always check the return value before using the buffer.
  • Always pair fopen with fclose, and check both.
  • Strip the newline with strcspn when you do not want it.

Advanced habits

  • After the loop, distinguish EOF from error with feof/ferror and report errors via perror.
  • Decide and document how you handle lines longer than the buffer: reject them, grow a dynamic buffer, or read the line in chunks. Do not silently truncate user data.
  • Size buffers from a defined maximum (a named constant), not a magic number, so the limit is visible and tunable.
  • For untrusted input, validate the parsed content (length, allowed characters, numeric range) after reading — fgets bounds the read, but it does not validate meaning.

Practice tasks

Practice

Beginner 1 — Echo with line numbers

Read lines from stdin with fgets and print each one prefixed by its 1-based number, newline stripped.

  • Requirements: use a 128-byte buffer, loop until fgets returns NULL, strip the newline with strcspn.
  • Example: input red, green → output 1: red then 2: green.
  • Concepts: the read loop, sizeof, strcspn.

Beginner 2 — Count non-empty lines

Write a program that counts how many lines read from stdin contain at least one character other than the newline.

  • Requirements: treat a line that is just "\n" as empty; print the final count.
  • Hint: after stripping the newline, an empty line has strlen == 0.
  • Concepts: return-value check, strlen, newline handling.

Intermediate 1 — Find the longest line

Read a file (path from argv[1]) and print the length of its longest line, not counting the newline.

  • Requirements: open with fopen, check it, close with fclose; handle a file with no trailing newline on the last line.
  • Hint: compute strlen after stripping; keep a running maximum.
  • Constraints: assume no single line exceeds 1023 characters.
  • Concepts: file reading, argv, tracking state across iterations.

Intermediate 2 — Detect short reads

Read from stdin with a deliberately small 8-byte buffer. For each fgets call, print whether the chunk ended in a newline ("complete") or not ("continued"), so you can watch a long line span multiple reads.

  • Requirements: do not strip the newline before testing; inspect buf[strlen(buf)-1] with a len > 0 guard.
  • Example: input hello world\n (12 chars) → continued, continued, complete (sizes will vary by your guard).
  • Concepts: short-read detection, careful indexing.

Challenge — Safe head

Implement a mini head: given a path in argv[1] and a count n in argv[2], print the first n lines of the file, exactly as written (keep original newlines). If the file has fewer than n lines, print them all.

  • Requirements: validate both arguments (print usage and exit non-zero if missing or if n is not a positive integer); handle fopen failure with perror; distinguish EOF from a read error with ferror; close the file.
  • Hint: since you keep the original formatting, do not strip newlines — but remember a final short read may have none.
  • Concepts: argument validation, the full safe read loop, error reporting, resource cleanup.

Summary

Summary

  • fgets(buf, n, f) reads at most n - 1 characters into buf, stops at a newline (which it keeps) or end of file, and always writes a '\0' so buf is a valid string.
  • It is the safe replacement for gets: because you pass the size, it can never overflow your buffer. Always pass sizeof the array.
  • It returns buf on success and NULL at EOF or on error — check the return value before using the buffer, and use feof/ferror to tell EOF and error apart.
  • The trailing '\n' distinguishes a complete line from a short read (line longer than the buffer). Strip it safely with buf[strcspn(buf, "\n")] = '\0';, and guard any buf[len-1] indexing with len > 0.
  • Common mistakes: treating n as the count of characters returned, passing a size larger than the buffer, chopping buf[strlen(buf)-1] blindly, and ignoring the return value.
  • Remember: pair fopen/fclose, validate parsed content after reading, and decide deliberately how to handle lines that are too long — never silently truncate user data.

Practice with these exercises