Module 7.2

Text File Operations in C

Master reading and writing text files in C programming. Learn formatted I/O with fprintf and fscanf, string operations with fgets and fputs, character-by-character processing, and practical techniques for handling configuration files, logs, and data processing.

40 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Formatted output with fprintf()
  • Reading formatted data with fscanf()
  • Line operations: fgets() and fputs()
  • Character I/O: fgetc() and fputc()
  • Practical file processing patterns
Contents
01

Formatted Output with fprintf()

The fprintf() function is your primary tool for writing formatted text to files. If you already know printf(), you essentially know fprintf() - it works exactly the same way, but writes to a file instead of the screen. This makes it incredibly versatile for creating logs, reports, CSV files, and configuration files.

Understanding fprintf()

The fprintf() function is part of the standard I/O library and follows a simple pattern: specify the file stream, provide a format string, and pass the values to be formatted. It returns the number of characters written, or a negative value if an error occurs.

int fprintf(FILE *stream, const char *format, ...);

// Basic usage example
FILE *fp = fopen("output.txt", "w");
if (fp != NULL) {
    fprintf(fp, "Hello, World!\n");
    fclose(fp);
}
Function

fprintf()

fprintf() writes formatted output to a file stream. It accepts a FILE pointer as its first argument, followed by a format string and variables - just like printf(), but directed to a file.

Key difference from printf(): printf() always writes to stdout (the screen), while fprintf() can write to any file stream, including stdout, stderr, or files you open with fopen().

Format Specifiers Review

fprintf() uses the same format specifiers as printf(). Here is a quick reference of the most commonly used specifiers when working with text files:

Specifier Type Example Output
%d Integer 42, -17, 0
%f Float/Double 3.141593
%.2f Float (2 decimals) 3.14
%s String Hello
%c Character A
%x Hexadecimal 2a
%% Literal % %

Practical Examples

Let us look at several practical examples of using fprintf() to create different types of text files:

Example 1: Writing a Simple Log File
#include <stdio.h>
#include <time.h>

int main() {
    FILE *log = fopen("app.log", "a");  // Append mode
    if (log == NULL) {
        perror("Cannot open log file");
        return 1;
    }
    
    // Get current time
    time_t now = time(NULL);
    char *timestamp = ctime(&now);
    timestamp[24] = '\0';  // Remove newline from ctime
    
    // Write log entry
    fprintf(log, "[%s] Application started\n", timestamp);
    fprintf(log, "[%s] User: admin logged in\n", timestamp);
    
    fclose(log);
    return 0;
}
Example 2: Creating a CSV File
#include <stdio.h>

int main() {
    FILE *csv = fopen("students.csv", "w");
    if (csv == NULL) {
        perror("Cannot create CSV file");
        return 1;
    }
    
    // Write header row
    fprintf(csv, "ID,Name,Grade,Average\n");
    
    // Write data rows
    fprintf(csv, "%d,%s,%c,%.2f\n", 101, "Alice Johnson", 'A', 95.5);
    fprintf(csv, "%d,%s,%c,%.2f\n", 102, "Bob Smith", 'B', 85.3);
    fprintf(csv, "%d,%s,%c,%.2f\n", 103, "Carol Davis", 'A', 92.8);
    
    fclose(csv);
    printf("CSV file created successfully!\n");
    return 0;
}
Example 3: Formatted Report Generation
#include <stdio.h>

int main() {
    FILE *report = fopen("sales_report.txt", "w");
    if (report == NULL) {
        perror("Cannot create report");
        return 1;
    }
    
    // Header with fixed-width columns
    fprintf(report, "%-20s %10s %12s\n", "Product", "Quantity", "Revenue");
    fprintf(report, "%-20s %10s %12s\n", "-------", "--------", "-------");
    
    // Data rows with alignment
    fprintf(report, "%-20s %10d %12.2f\n", "Laptop", 45, 67499.55);
    fprintf(report, "%-20s %10d %12.2f\n", "Mouse", 234, 4679.66);
    fprintf(report, "%-20s %10d %12.2f\n", "Keyboard", 156, 7799.44);
    
    // Total line
    fprintf(report, "%-20s %10s %12s\n", "", "", "-------");
    fprintf(report, "%-20s %10s %12.2f\n", "TOTAL", "", 79978.65);
    
    fclose(report);
    return 0;
}
Width Specifiers for Alignment

Use %10d for right-aligned integers in 10 characters, or %-20s for left-aligned strings in 20 characters. This creates neatly formatted tables and reports.

Checking fprintf() Return Value

fprintf() returns the number of characters written. A negative return value indicates an error. While often ignored for simplicity, checking the return value is important for robust programs:

#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) {
        return 1;
    }
    
    int written = fprintf(fp, "Important data: %d\n", 42);
    
    if (written < 0) {
        fprintf(stderr, "Error writing to file!\n");
        fclose(fp);
        return 1;
    }
    
    printf("Successfully wrote %d characters\n", written);
    fclose(fp);
    return 0;
}
Practice Questions

Task: Write a program that saves user information (name, age, email) to a file called "user_info.txt" using fprintf().

Show Solution
#include <stdio.h>

int main() {
    char name[] = "John Doe";
    int age = 28;
    char email[] = "john.doe@email.com";
    
    FILE *fp = fopen("user_info.txt", "w");
    if (fp == NULL) {
        perror("Cannot create file");
        return 1;
    }
    
    fprintf(fp, "User Profile\n");
    fprintf(fp, "============\n");
    fprintf(fp, "Name:  %s\n", name);
    fprintf(fp, "Age:   %d\n", age);
    fprintf(fp, "Email: %s\n", email);
    
    fclose(fp);
    printf("User info saved to file!\n");
    return 0;
}

Task: Write a program that generates a 10x10 multiplication table and saves it to "mult_table.txt" with aligned columns.

Show Solution
#include <stdio.h>

int main() {
    FILE *fp = fopen("mult_table.txt", "w");
    if (fp == NULL) {
        perror("Cannot create file");
        return 1;
    }
    
    // Header row
    fprintf(fp, "    ");
    for (int j = 1; j <= 10; j++) {
        fprintf(fp, "%4d", j);
    }
    fprintf(fp, "\n    ");
    for (int j = 1; j <= 10; j++) {
        fprintf(fp, "----");
    }
    fprintf(fp, "\n");
    
    // Table body
    for (int i = 1; i <= 10; i++) {
        fprintf(fp, "%2d |", i);
        for (int j = 1; j <= 10; j++) {
            fprintf(fp, "%4d", i * j);
        }
        fprintf(fp, "\n");
    }
    
    fclose(fp);
    printf("Multiplication table saved!\n");
    return 0;
}

Task: Write a function that exports an array of temperatures (with dates) to a CSV file. Include proper error handling.

Show Solution
#include <stdio.h>

typedef struct {
    char date[11];  // YYYY-MM-DD
    float high;
    float low;
} DayTemp;

int exportToCSV(const char *filename, DayTemp temps[], int count) {
    FILE *fp = fopen(filename, "w");
    if (fp == NULL) {
        return -1;
    }
    
    // Write header
    fprintf(fp, "Date,High,Low,Average\n");
    
    // Write data
    for (int i = 0; i < count; i++) {
        float avg = (temps[i].high + temps[i].low) / 2.0;
        fprintf(fp, "%s,%.1f,%.1f,%.1f\n", 
                temps[i].date, temps[i].high, 
                temps[i].low, avg);
    }
    
    fclose(fp);
    return count;
}

int main() {
    DayTemp week[] = {
        {"2026-01-25", 45.2, 32.1},
        {"2026-01-26", 48.5, 35.8},
        {"2026-01-27", 52.0, 38.2}
    };
    
    int result = exportToCSV("temps.csv", week, 3);
    if (result < 0) {
        fprintf(stderr, "Export failed!\n");
        return 1;
    }
    
    printf("Exported %d records\n", result);
    return 0;
}
02

Formatted Input with fscanf()

The fscanf() function reads formatted data from a file, acting as the input counterpart to fprintf(). Just as scanf() reads from the keyboard, fscanf() reads from a file stream, parsing text according to format specifiers. It is essential for reading structured text files like CSV data, configuration files, and data exports.

Understanding fscanf()

fscanf() reads input from a file stream, interpreting it according to a format string. It returns the number of items successfully read, or EOF if it reaches the end of the file before reading any items.

int fscanf(FILE *stream, const char *format, ...);

// Basic usage example
int value;
FILE *fp = fopen("numbers.txt", "r");
if (fp != NULL) {
    fscanf(fp, "%d", &value);  // Note the & for address
    printf("Read: %d\n", value);
    fclose(fp);
}
Critical: Check Return Values!

Always check fscanf()'s return value before using the data. It returns the number of items successfully read. If you expect to read 3 items but get 2, something went wrong!

Reading Different Data Types

#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) {
        perror("Cannot open file");
        return 1;
    }
    
    int id;
    char name[50];
    float score;
    
    // Read structured data
    int items = fscanf(fp, "%d %s %f", &id, name, &score);
    
    if (items == 3) {
        printf("ID: %d, Name: %s, Score: %.2f\n", id, name, score);
    } else {
        printf("Error: Expected 3 items, got %d\n", items);
    }
    
    fclose(fp);
    return 0;
}

Reading Multiple Records in a Loop

A common pattern is reading multiple records until reaching the end of the file. Check the return value against the expected number of items to know when to stop:

#include <stdio.h>

int main() {
    FILE *fp = fopen("students.txt", "r");
    if (fp == NULL) {
        perror("Cannot open file");
        return 1;
    }
    
    char name[50];
    int age;
    float gpa;
    int count = 0;
    
    // Read until we cannot get all 3 items
    while (fscanf(fp, "%s %d %f", name, &age, &gpa) == 3) {
        printf("Student %d: %s, Age: %d, GPA: %.2f\n", 
               ++count, name, age, gpa);
    }
    
    printf("Total students read: %d\n", count);
    fclose(fp);
    return 0;
}

Reading CSV Files

CSV files use commas as delimiters. You can handle this by including the comma in the format string:

#include <stdio.h>

int main() {
    FILE *fp = fopen("products.csv", "r");
    if (fp == NULL) {
        return 1;
    }
    
    // Skip header line
    char header[256];
    fgets(header, sizeof(header), fp);
    
    // Read CSV data (id,name,price format)
    int id;
    char name[50];
    float price;
    
    while (fscanf(fp, "%d,%[^,],%f", &id, name, &price) == 3) {
        printf("Product #%d: %s - $%.2f\n", id, name, price);
    }
    
    fclose(fp);
    return 0;
}
The %[^,] Format Specifier

%[^,] is a scanset that reads all characters UNTIL it hits a comma. This is useful for reading strings that may contain spaces in CSV files. %[^\n] reads until newline.

fscanf() vs fgets() + sscanf()

While fscanf() is convenient, combining fgets() with sscanf() is often safer for real-world files:

fscanf() Limitations
  • Struggles with missing or malformed fields
  • Cannot easily skip bad lines
  • Whitespace handling can be confusing
  • Buffer overflow risk with %s
fgets() + sscanf() Benefits
  • Read full line, then parse
  • Easier to handle errors line-by-line
  • Can log or skip problematic lines
  • More predictable behavior
#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) return 1;
    
    char line[256];
    int lineNum = 0;
    int id;
    float value;
    
    while (fgets(line, sizeof(line), fp)) {
        lineNum++;
        
        // Try to parse the line
        if (sscanf(line, "%d %f", &id, &value) == 2) {
            printf("Line %d: ID=%d, Value=%.2f\n", lineNum, id, value);
        } else {
            printf("Line %d: Invalid format, skipping\n", lineNum);
        }
    }
    
    fclose(fp);
    return 0;
}
Practice Questions

Task: Write a program that reads integers from "numbers.txt" (one per line) and calculates their sum.

Show Solution
#include <stdio.h>

int main() {
    FILE *fp = fopen("numbers.txt", "r");
    if (fp == NULL) {
        perror("Cannot open file");
        return 1;
    }
    
    int num, sum = 0, count = 0;
    
    while (fscanf(fp, "%d", &num) == 1) {
        sum += num;
        count++;
    }
    
    printf("Read %d numbers\n", count);
    printf("Sum: %d\n", sum);
    printf("Average: %.2f\n", (float)sum / count);
    
    fclose(fp);
    return 0;
}

Task: Read a config file with "key=value" pairs and print them. Skip lines starting with # (comments).

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

int main() {
    FILE *fp = fopen("config.txt", "r");
    if (fp == NULL) {
        perror("Cannot open config");
        return 1;
    }
    
    char line[256];
    char key[64], value[128];
    
    while (fgets(line, sizeof(line), fp)) {
        // Skip comments and empty lines
        if (line[0] == '#' || line[0] == '\n') {
            continue;
        }
        
        // Parse key=value
        if (sscanf(line, "%[^=]=%[^\n]", key, value) == 2) {
            printf("Key: '%s' -> Value: '%s'\n", key, value);
        }
    }
    
    fclose(fp);
    return 0;
}

Task: Read a CSV file of products into an array of structs and find the most expensive product.

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

#define MAX_PRODUCTS 100

typedef struct {
    int id;
    char name[50];
    float price;
} Product;

int main() {
    FILE *fp = fopen("products.csv", "r");
    if (fp == NULL) {
        perror("Cannot open CSV");
        return 1;
    }
    
    Product products[MAX_PRODUCTS];
    int count = 0;
    char line[256];
    
    // Skip header
    fgets(line, sizeof(line), fp);
    
    // Read products
    while (fgets(line, sizeof(line), fp) && count < MAX_PRODUCTS) {
        if (sscanf(line, "%d,%[^,],%f", 
                   &products[count].id,
                   products[count].name,
                   &products[count].price) == 3) {
            count++;
        }
    }
    fclose(fp);
    
    // Find most expensive
    int maxIdx = 0;
    for (int i = 1; i < count; i++) {
        if (products[i].price > products[maxIdx].price) {
            maxIdx = i;
        }
    }
    
    printf("Most expensive: %s at $%.2f\n", 
           products[maxIdx].name, products[maxIdx].price);
    return 0;
}
03

Line I/O with fgets() and fputs()

When working with text files, you often want to process data line by line rather than by individual values. The fgets() and fputs() functions are designed exactly for this purpose. They read and write complete lines of text, making them ideal for log files, text processing, and any situation where preserving line structure matters.

Reading Lines with fgets()

fgets() is the safest way to read a line of text in C. Unlike the dangerous gets() function (which should never be used), fgets() limits how many characters it reads, preventing buffer overflows.

char *fgets(char *str, int n, FILE *stream);

// Parameters:
// str    - buffer to store the line
// n      - maximum characters to read (including null terminator)
// stream - file to read from

// Returns:
// str on success, NULL on error or EOF
Function

fgets()

fgets() reads at most n-1 characters from the stream into the buffer, stopping early if it encounters a newline or EOF. The newline character (if read) is included in the buffer, and a null terminator is always added.

Key behavior: Unlike scanf(), fgets() preserves the newline character at the end of the line. You may need to remove it manually if you do not want it.

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

int main() {
    FILE *fp = fopen("poem.txt", "r");
    if (fp == NULL) {
        perror("Cannot open file");
        return 1;
    }
    
    char line[256];
    int lineNum = 0;
    
    while (fgets(line, sizeof(line), fp) != NULL) {
        // Remove trailing newline if present
        line[strcspn(line, "\n")] = '\0';
        
        printf("Line %d: %s\n", ++lineNum, line);
    }
    
    fclose(fp);
    return 0;
}
Removing the Newline

line[strcspn(line, "\n")] = '\0'; is an elegant one-liner that removes the trailing newline. strcspn() finds the position of the first newline (or returns the string length if none), and we replace it with a null terminator.

Writing Lines with fputs()

fputs() writes a string to a file. Unlike puts(), it does NOT automatically add a newline at the end, giving you complete control over the output format.

int fputs(const char *str, FILE *stream);

// Returns:
// Non-negative on success, EOF on error
#include <stdio.h>

int main() {
    FILE *fp = fopen("output.txt", "w");
    if (fp == NULL) {
        perror("Cannot create file");
        return 1;
    }
    
    fputs("First line\n", fp);
    fputs("Second line\n", fp);
    fputs("Third line", fp);  // No newline after this
    
    fclose(fp);
    printf("File written successfully!\n");
    return 0;
}

Practical Example: File Copy

A classic use case for fgets() and fputs() is copying text files line by line:

#include <stdio.h>

int copyTextFile(const char *src, const char *dest) {
    FILE *srcFile = fopen(src, "r");
    if (srcFile == NULL) {
        perror("Cannot open source");
        return -1;
    }
    
    FILE *destFile = fopen(dest, "w");
    if (destFile == NULL) {
        perror("Cannot create destination");
        fclose(srcFile);
        return -1;
    }
    
    char line[1024];
    int linesCopied = 0;
    
    while (fgets(line, sizeof(line), srcFile) != NULL) {
        fputs(line, destFile);
        linesCopied++;
    }
    
    fclose(srcFile);
    fclose(destFile);
    return linesCopied;
}

int main() {
    int result = copyTextFile("original.txt", "backup.txt");
    
    if (result >= 0) {
        printf("Copied %d lines successfully!\n", result);
    }
    return result < 0 ? 1 : 0;
}

Processing Text Files Line by Line

Here is a more advanced example that processes a file, transforming each line:

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

// Convert string to uppercase
void toUpperCase(char *str) {
    while (*str) {
        *str = toupper(*str);
        str++;
    }
}

int main() {
    FILE *input = fopen("input.txt", "r");
    FILE *output = fopen("output.txt", "w");
    
    if (input == NULL || output == NULL) {
        perror("File error");
        if (input) fclose(input);
        if (output) fclose(output);
        return 1;
    }
    
    char line[512];
    
    while (fgets(line, sizeof(line), input)) {
        toUpperCase(line);
        fputs(line, output);
    }
    
    fclose(input);
    fclose(output);
    printf("Conversion complete!\n");
    return 0;
}
Practice Questions

Task: Write a program that counts the total number of lines in a text file using fgets().

Show Solution
#include <stdio.h>

int countLines(const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        return -1;
    }
    
    char line[1024];
    int count = 0;
    
    while (fgets(line, sizeof(line), fp) != NULL) {
        count++;
    }
    
    fclose(fp);
    return count;
}

int main() {
    int lines = countLines("sample.txt");
    
    if (lines >= 0) {
        printf("File has %d lines\n", lines);
    } else {
        printf("Cannot open file\n");
    }
    return 0;
}

Task: Create a program that displays a file with line numbers prepended (like "cat -n" in Unix).

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

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s filename\n", argv[0]);
        return 1;
    }
    
    FILE *fp = fopen(argv[1], "r");
    if (fp == NULL) {
        perror("Cannot open file");
        return 1;
    }
    
    char line[1024];
    int lineNum = 0;
    
    while (fgets(line, sizeof(line), fp)) {
        // Remove newline for cleaner output
        line[strcspn(line, "\n")] = '\0';
        printf("%6d | %s\n", ++lineNum, line);
    }
    
    fclose(fp);
    return 0;
}

Task: Write a program that searches for a pattern in a file and prints matching lines with their line numbers.

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

int searchFile(const char *filename, const char *pattern) {
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        perror("Cannot open file");
        return -1;
    }
    
    char line[1024];
    int lineNum = 0;
    int matches = 0;
    
    while (fgets(line, sizeof(line), fp)) {
        lineNum++;
        
        if (strstr(line, pattern) != NULL) {
            // Remove newline for output
            line[strcspn(line, "\n")] = '\0';
            printf("%d: %s\n", lineNum, line);
            matches++;
        }
    }
    
    fclose(fp);
    return matches;
}

int main(int argc, char *argv[]) {
    if (argc < 3) {
        printf("Usage: %s pattern filename\n", argv[0]);
        return 1;
    }
    
    int found = searchFile(argv[2], argv[1]);
    
    if (found >= 0) {
        printf("\n%d matches found\n", found);
    }
    return 0;
}
04

Character I/O with fgetc() and fputc()

Sometimes you need the finest level of control over file reading and writing - one character at a time. The fgetc() and fputc() functions provide this granular access, making them perfect for character counting, text transformation, encoding detection, and building your own higher-level reading functions.

Reading Characters with fgetc()

fgetc() reads a single character from a file stream. It returns the character as an int (not char) so it can return EOF (typically -1) to indicate end-of-file or an error.

int fgetc(FILE *stream);

// Returns:
// The character read as unsigned char cast to int
// EOF on end of file or error
Always Use int, Not char!

Store fgetc()'s return value in an int, not a char. Why? Because EOF is typically -1, which might equal a valid char on systems where char is signed. Using int ensures you can distinguish EOF from valid characters.

#include <stdio.h>

int main() {
    FILE *fp = fopen("sample.txt", "r");
    if (fp == NULL) {
        perror("Cannot open file");
        return 1;
    }
    
    int ch;  // Use int, not char!
    
    while ((ch = fgetc(fp)) != EOF) {
        printf("%c", ch);  // Print each character
    }
    
    fclose(fp);
    return 0;
}

Writing Characters with fputc()

fputc() writes a single character to a file stream. It returns the character written on success, or EOF on error.

int fputc(int c, FILE *stream);

// Returns:
// The character written on success
// EOF on error
#include <stdio.h>

int main() {
    FILE *fp = fopen("alphabet.txt", "w");
    if (fp == NULL) {
        perror("Cannot create file");
        return 1;
    }
    
    // Write A-Z to file
    for (char c = 'A'; c <= 'Z'; c++) {
        fputc(c, fp);
    }
    fputc('\n', fp);  // Add newline at end
    
    fclose(fp);
    printf("Alphabet written to file!\n");
    return 0;
}

Practical Examples

Example 1: Character Counter
#include <stdio.h>
#include <ctype.h>

typedef struct {
    int letters;
    int digits;
    int spaces;
    int lines;
    int other;
} FileStats;

FileStats analyzeFile(const char *filename) {
    FileStats stats = {0, 0, 0, 0, 0};
    
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        return stats;
    }
    
    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        if (isalpha(ch)) stats.letters++;
        else if (isdigit(ch)) stats.digits++;
        else if (isspace(ch)) {
            stats.spaces++;
            if (ch == '\n') stats.lines++;
        }
        else stats.other++;
    }
    
    fclose(fp);
    return stats;
}

int main() {
    FileStats s = analyzeFile("sample.txt");
    
    printf("File Statistics:\n");
    printf("  Letters: %d\n", s.letters);
    printf("  Digits:  %d\n", s.digits);
    printf("  Spaces:  %d\n", s.spaces);
    printf("  Lines:   %d\n", s.lines);
    printf("  Other:   %d\n", s.other);
    
    return 0;
}
Example 2: Simple Character Encryption
#include <stdio.h>

// Simple Caesar cipher - shift each letter by key positions
void encryptFile(const char *input, const char *output, int key) {
    FILE *in = fopen(input, "r");
    FILE *out = fopen(output, "w");
    
    if (in == NULL || out == NULL) {
        if (in) fclose(in);
        if (out) fclose(out);
        return;
    }
    
    int ch;
    while ((ch = fgetc(in)) != EOF) {
        if (ch >= 'A' && ch <= 'Z') {
            ch = 'A' + (ch - 'A' + key) % 26;
        } else if (ch >= 'a' && ch <= 'z') {
            ch = 'a' + (ch - 'a' + key) % 26;
        }
        fputc(ch, out);
    }
    
    fclose(in);
    fclose(out);
}

int main() {
    encryptFile("message.txt", "encrypted.txt", 3);
    printf("File encrypted!\n");
    return 0;
}
Example 3: Binary File Copy (Character by Character)
#include <stdio.h>

long copyFile(const char *src, const char *dest) {
    FILE *srcFile = fopen(src, "rb");   // Binary mode
    FILE *destFile = fopen(dest, "wb");
    
    if (srcFile == NULL || destFile == NULL) {
        if (srcFile) fclose(srcFile);
        if (destFile) fclose(destFile);
        return -1;
    }
    
    int ch;
    long bytesCopied = 0;
    
    while ((ch = fgetc(srcFile)) != EOF) {
        fputc(ch, destFile);
        bytesCopied++;
    }
    
    fclose(srcFile);
    fclose(destFile);
    return bytesCopied;
}

int main() {
    long bytes = copyFile("image.png", "image_copy.png");
    
    if (bytes >= 0) {
        printf("Copied %ld bytes\n", bytes);
    } else {
        printf("Copy failed!\n");
    }
    return 0;
}

getc() vs fgetc() and putc() vs fputc()

You might see getc() and putc() in some code. They are nearly identical to fgetc() and fputc():

Function Type Note
fgetc() Always a function Safe to use anywhere
getc() May be a macro Slightly faster, but avoid using expressions with side effects as arguments
fputc() Always a function Safe to use anywhere
putc() May be a macro Slightly faster, same caveat as getc()
Practice Questions

Task: Write a program that counts how many vowels (a, e, i, o, u - case insensitive) are in a text file.

Show Solution
#include <stdio.h>
#include <ctype.h>

int isVowel(int ch) {
    ch = tolower(ch);
    return ch == 'a' || ch == 'e' || ch == 'i' || 
           ch == 'o' || ch == 'u';
}

int main() {
    FILE *fp = fopen("sample.txt", "r");
    if (fp == NULL) {
        perror("Cannot open file");
        return 1;
    }
    
    int ch;
    int vowelCount = 0;
    
    while ((ch = fgetc(fp)) != EOF) {
        if (isVowel(ch)) {
            vowelCount++;
        }
    }
    
    fclose(fp);
    printf("Vowel count: %d\n", vowelCount);
    return 0;
}

Task: Create a program that reads a file and writes a new file with multiple consecutive spaces reduced to single spaces.

Show Solution
#include <stdio.h>
#include <ctype.h>

int main() {
    FILE *in = fopen("input.txt", "r");
    FILE *out = fopen("cleaned.txt", "w");
    
    if (in == NULL || out == NULL) {
        perror("File error");
        if (in) fclose(in);
        if (out) fclose(out);
        return 1;
    }
    
    int ch;
    int lastWasSpace = 0;
    
    while ((ch = fgetc(in)) != EOF) {
        if (ch == ' ') {
            if (!lastWasSpace) {
                fputc(ch, out);
                lastWasSpace = 1;
            }
            // Skip additional spaces
        } else {
            fputc(ch, out);
            lastWasSpace = 0;
        }
    }
    
    fclose(in);
    fclose(out);
    printf("Whitespace cleaned!\n");
    return 0;
}

Task: Using only fgetc(), build words and count the total number of words in a file (separated by whitespace).

Show Solution
#include <stdio.h>
#include <ctype.h>

int main() {
    FILE *fp = fopen("document.txt", "r");
    if (fp == NULL) {
        perror("Cannot open file");
        return 1;
    }
    
    int ch;
    int wordCount = 0;
    int inWord = 0;  // Are we inside a word?
    
    while ((ch = fgetc(fp)) != EOF) {
        if (isspace(ch)) {
            if (inWord) {
                wordCount++;  // Word ended
                inWord = 0;
            }
        } else {
            inWord = 1;  // We're in a word
        }
    }
    
    // Count last word if file doesn't end with whitespace
    if (inWord) {
        wordCount++;
    }
    
    fclose(fp);
    printf("Word count: %d\n", wordCount);
    return 0;
}
05

Practical File Processing Patterns

Now that you know the individual functions, let us combine them into practical patterns you will use repeatedly in real programs. These patterns handle common scenarios like processing log files, managing configuration, filtering data, and creating reports. Mastering these patterns will make you productive with file handling in C.

Pattern 1: Read-Process-Write Pipeline

This pattern reads from one file, transforms the data, and writes to another. It is the foundation of many data processing utilities:

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

// Transform function - customize as needed
void processLine(char *line) {
    // Example: convert to uppercase
    for (int i = 0; line[i]; i++) {
        line[i] = toupper(line[i]);
    }
}

int processFile(const char *input, const char *output) {
    FILE *in = fopen(input, "r");
    FILE *out = fopen(output, "w");
    
    if (in == NULL || out == NULL) {
        if (in) fclose(in);
        if (out) fclose(out);
        return -1;
    }
    
    char line[1024];
    int linesProcessed = 0;
    
    while (fgets(line, sizeof(line), in)) {
        processLine(line);
        fputs(line, out);
        linesProcessed++;
    }
    
    fclose(in);
    fclose(out);
    return linesProcessed;
}

Pattern 2: Filter Lines by Criteria

Extract lines matching specific criteria - like grep but customized for your needs:

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

// Filter function - return 1 to keep line, 0 to skip
int shouldKeepLine(const char *line) {
    // Example: keep lines containing "ERROR" or "WARNING"
    return strstr(line, "ERROR") != NULL || 
           strstr(line, "WARNING") != NULL;
}

int filterFile(const char *input, const char *output) {
    FILE *in = fopen(input, "r");
    FILE *out = fopen(output, "w");
    
    if (in == NULL || out == NULL) {
        if (in) fclose(in);
        if (out) fclose(out);
        return -1;
    }
    
    char line[1024];
    int kept = 0, total = 0;
    
    while (fgets(line, sizeof(line), in)) {
        total++;
        if (shouldKeepLine(line)) {
            fputs(line, out);
            kept++;
        }
    }
    
    fclose(in);
    fclose(out);
    
    printf("Kept %d of %d lines\n", kept, total);
    return kept;
}

Pattern 3: Configuration File Reader

Parse key-value configuration files - a pattern used in countless applications:

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

#define MAX_KEY 64
#define MAX_VALUE 256
#define MAX_CONFIG 50

typedef struct {
    char key[MAX_KEY];
    char value[MAX_VALUE];
} ConfigEntry;

typedef struct {
    ConfigEntry entries[MAX_CONFIG];
    int count;
} Config;

int loadConfig(const char *filename, Config *config) {
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) return -1;
    
    config->count = 0;
    char line[512];
    
    while (fgets(line, sizeof(line), fp) && config->count < MAX_CONFIG) {
        // Skip comments and empty lines
        if (line[0] == '#' || line[0] == '\n' || line[0] == ';') {
            continue;
        }
        
        char *eq = strchr(line, '=');
        if (eq == NULL) continue;
        
        // Split at '='
        *eq = '\0';
        char *key = line;
        char *value = eq + 1;
        
        // Remove newline from value
        value[strcspn(value, "\n")] = '\0';
        
        // Trim spaces (simplified)
        while (*key == ' ') key++;
        while (*value == ' ') value++;
        
        strncpy(config->entries[config->count].key, key, MAX_KEY - 1);
        strncpy(config->entries[config->count].value, value, MAX_VALUE - 1);
        config->count++;
    }
    
    fclose(fp);
    return config->count;
}

const char* getConfigValue(Config *config, const char *key) {
    for (int i = 0; i < config->count; i++) {
        if (strcmp(config->entries[i].key, key) == 0) {
            return config->entries[i].value;
        }
    }
    return NULL;
}

int main() {
    Config config;
    
    if (loadConfig("app.conf", &config) < 0) {
        printf("Cannot load config\n");
        return 1;
    }
    
    printf("Loaded %d settings\n", config.count);
    
    const char *host = getConfigValue(&config, "host");
    const char *port = getConfigValue(&config, "port");
    
    printf("Host: %s\n", host ? host : "(not set)");
    printf("Port: %s\n", port ? port : "(not set)");
    
    return 0;
}

Pattern 4: Log File Analyzer

Analyze log files to extract statistics - useful for monitoring and debugging:

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

typedef struct {
    int errors;
    int warnings;
    int info;
    int total;
} LogStats;

LogStats analyzeLog(const char *filename) {
    LogStats stats = {0, 0, 0, 0};
    
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) return stats;
    
    char line[1024];
    
    while (fgets(line, sizeof(line), fp)) {
        stats.total++;
        
        if (strstr(line, "ERROR") || strstr(line, "[E]")) {
            stats.errors++;
        } else if (strstr(line, "WARNING") || strstr(line, "[W]")) {
            stats.warnings++;
        } else if (strstr(line, "INFO") || strstr(line, "[I]")) {
            stats.info++;
        }
    }
    
    fclose(fp);
    return stats;
}

int main() {
    LogStats s = analyzeLog("application.log");
    
    printf("=== Log Analysis ===\n");
    printf("Total lines: %d\n", s.total);
    printf("Errors:      %d (%.1f%%)\n", s.errors, 
           100.0 * s.errors / s.total);
    printf("Warnings:    %d (%.1f%%)\n", s.warnings, 
           100.0 * s.warnings / s.total);
    printf("Info:        %d (%.1f%%)\n", s.info, 
           100.0 * s.info / s.total);
    
    return 0;
}

Pattern 5: Append-Only Log Writer

A simple but effective logging function that safely appends timestamped entries:

#include <stdio.h>
#include <time.h>
#include <stdarg.h>

typedef enum { LOG_INFO, LOG_WARNING, LOG_ERROR } LogLevel;

int writeLog(const char *filename, LogLevel level, const char *fmt, ...) {
    FILE *fp = fopen(filename, "a");  // Append mode
    if (fp == NULL) return -1;
    
    // Get timestamp
    time_t now = time(NULL);
    struct tm *t = localtime(&now);
    
    // Level string
    const char *levelStr[] = {"INFO", "WARNING", "ERROR"};
    
    // Write timestamp and level
    fprintf(fp, "[%04d-%02d-%02d %02d:%02d:%02d] [%s] ",
            t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
            t->tm_hour, t->tm_min, t->tm_sec,
            levelStr[level]);
    
    // Write message using variable arguments
    va_list args;
    va_start(args, fmt);
    vfprintf(fp, fmt, args);
    va_end(args);
    
    fprintf(fp, "\n");
    fclose(fp);
    return 0;
}

int main() {
    writeLog("app.log", LOG_INFO, "Application started");
    writeLog("app.log", LOG_INFO, "User %s logged in", "admin");
    writeLog("app.log", LOG_WARNING, "Memory usage at %d%%", 85);
    writeLog("app.log", LOG_ERROR, "Connection failed: %s", "timeout");
    
    printf("Log entries written!\n");
    return 0;
}
Best Practice: Close Files Promptly

In logging functions, open and close the file for each write. This ensures data is flushed to disk immediately and prevents data loss if the program crashes. For high-performance logging, consider keeping the file open but calling fflush() regularly.

Practice Questions

Task: Create a utility that reports file statistics: total lines, non-empty lines, longest line length, and average line length.

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

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s filename\n", argv[0]);
        return 1;
    }
    
    FILE *fp = fopen(argv[1], "r");
    if (fp == NULL) {
        perror("Cannot open file");
        return 1;
    }
    
    char line[4096];
    int totalLines = 0;
    int nonEmptyLines = 0;
    int longestLine = 0;
    long totalChars = 0;
    
    while (fgets(line, sizeof(line), fp)) {
        totalLines++;
        int len = strlen(line);
        
        // Remove newline for accurate length
        if (len > 0 && line[len-1] == '\n') {
            len--;
        }
        
        totalChars += len;
        
        if (len > 0) {
            nonEmptyLines++;
        }
        
        if (len > longestLine) {
            longestLine = len;
        }
    }
    
    fclose(fp);
    
    printf("=== File Statistics: %s ===\n", argv[1]);
    printf("Total lines:     %d\n", totalLines);
    printf("Non-empty lines: %d\n", nonEmptyLines);
    printf("Longest line:    %d chars\n", longestLine);
    printf("Average length:  %.1f chars\n", 
           totalLines > 0 ? (float)totalChars / totalLines : 0);
    
    return 0;
}

Task: Write a program that merges two sorted text files (one number per line) into a single sorted output file.

Show Solution
#include <stdio.h>

int main() {
    FILE *f1 = fopen("sorted1.txt", "r");
    FILE *f2 = fopen("sorted2.txt", "r");
    FILE *out = fopen("merged.txt", "w");
    
    if (!f1 || !f2 || !out) {
        perror("File error");
        if (f1) fclose(f1);
        if (f2) fclose(f2);
        if (out) fclose(out);
        return 1;
    }
    
    int n1, n2;
    int have1 = (fscanf(f1, "%d", &n1) == 1);
    int have2 = (fscanf(f2, "%d", &n2) == 1);
    
    while (have1 && have2) {
        if (n1 <= n2) {
            fprintf(out, "%d\n", n1);
            have1 = (fscanf(f1, "%d", &n1) == 1);
        } else {
            fprintf(out, "%d\n", n2);
            have2 = (fscanf(f2, "%d", &n2) == 1);
        }
    }
    
    // Write remaining from file 1
    while (have1) {
        fprintf(out, "%d\n", n1);
        have1 = (fscanf(f1, "%d", &n1) == 1);
    }
    
    // Write remaining from file 2
    while (have2) {
        fprintf(out, "%d\n", n2);
        have2 = (fscanf(f2, "%d", &n2) == 1);
    }
    
    fclose(f1);
    fclose(f2);
    fclose(out);
    
    printf("Files merged successfully!\n");
    return 0;
}

Key Takeaways

fprintf() for Formatted Output

Write formatted text to files using the same format specifiers as printf() - essential for logs, reports, and CSV files

fscanf() with Caution

Always check return values and consider using fgets() + sscanf() for more robust parsing of real-world data

fgets() is Your Friend

Safely read lines with buffer overflow protection - remember it keeps the newline character in the string

Character I/O for Precision

Use fgetc() and fputc() when you need byte-level control - store return in int to detect EOF properly

Always Check Returns

Every file function can fail - check fopen() for NULL, fscanf() for item count, and close files promptly

Master Common Patterns

Read-process-write pipelines, line filtering, config parsing, and log analysis are patterns you will use repeatedly

Knowledge Check

Quick Quiz

Test what you have learned about C text file operations

1 What is the main difference between fprintf() and printf()?
2 What does fgets() do when it encounters a newline character?
3 Why should you store fgetc()'s return value in an int instead of a char?
4 What does fscanf() return?
5 Which approach is generally safer for parsing structured text files?
6 What does the format specifier %[^,] do in fscanf() or sscanf()?
Answer all questions to check your score