File Handling · intermediate · ~8 min
## 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.
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.
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.
fgets is and what it guaranteesfgets ("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:
n - 1 characters from f into buf.'\n') — and keeps that newline in the buffer.'\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 doesfgets(buf, sizeof buf, f)copy on the first call, and what is inbufafterward?
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
abcwith no trailing newline. You callfgets(buf, 16, f)twice. What does each call return, and what isbufafter each?
Because fgets keeps the '\n', the buffer after a normal read looks like "name\n\0". That newline is genuinely useful information:
'\n' means you read a complete line.'\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
fgetskeeps the newline instead of discarding it? Give one situation where keeping it changes how your program behaves.
#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:
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.stdin or any FILE * from fopen.NULL explicitly for clarity; while (fgets(...)) is equivalent because NULL is falsy.fgets doeschar *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:
n - 1 characters, or stops early when it reaches a newline (\n).\0) at the end, so buf is always a valid C string.buf on success, or NULL at end of file or on error.NUL terminator: the
\0byte that marks the end of a C string.fgetsalways writes one, which is why it reads onlyn - 1characters — the last slot is reserved for it.
Because fgets keeps the \n, you can tell two cases apart:
\n.\n.If you do not want the newline, strip it after reading.
#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."
Assume notes.txt contains:
alpha\nbeta\ngamma\n
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.char line[64]; reserves 64 bytes on the stack. fgets will use at most 63 for characters and 1 for the '\0'.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.beta and gamma, advancing the stream position each time.fgets returns NULL and the loop ends.ferror(f) is false (we hit EOF, not an error), so we skip the error branch.fclose(f) flushes and releases the file handle.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.
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.
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.
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
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.
Compiler warnings/errors
#include <stdio.h> or #include <string.h>. Add the headers.gets. Replace it with fgets.fgets(...) == 0 mixing types confusingly; prefer != NULL.Runtime / logic errors
NULL return, or are reading from a stream that was never opened (check fopen returned non-NULL).'\n'; strip it with strcspn.while (fgets(...)), not calling it once.Debugging steps
printf("len=%zu [%s]\n", strlen(buf), buf); — brackets reveal trailing whitespace.feof(f) and ferror(f) after the loop to learn why it stopped.-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?
fgets is the safe line reader, but only if you use it correctly. Watch these points for this topic:
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.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.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.'\n'. Guard with len > 0 before touching buf[len - 1] to avoid buf[-1].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.
key=value config lines, /etc/passwd-style records, or CSV rows one line at a time with fgets, then parse each line.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.stdin uses fgets instead of scanf("%s", ...) so a long answer cannot overflow the buffer.Beginner rules
sizeof the array as the size.fopen with fclose, and check both.strcspn when you do not want it.Advanced habits
feof/ferror and report errors via perror.fgets bounds the read, but it does not validate meaning.Read lines from stdin with fgets and print each one prefixed by its 1-based number, newline stripped.
fgets returns NULL, strip the newline with strcspn.red, green → output 1: red then 2: green.sizeof, strcspn.Write a program that counts how many lines read from stdin contain at least one character other than the newline.
"\n" as empty; print the final count.strlen == 0.strlen, newline handling.Read a file (path from argv[1]) and print the length of its longest line, not counting the newline.
fopen, check it, close with fclose; handle a file with no trailing newline on the last line.strlen after stripping; keep a running maximum.argv, tracking state across iterations.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.
buf[strlen(buf)-1] with a len > 0 guard.hello world\n (12 chars) → continued, continued, complete (sizes will vary by your guard).headImplement 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.
n is not a positive integer); handle fopen failure with perror; distinguish EOF from a read error with ferror; close the file.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.gets: because you pass the size, it can never overflow your buffer. Always pass sizeof the array.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.'\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.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.fopen/fclose, validate parsed content after reading, and decide deliberately how to handle lines that are too long — never silently truncate user data.