010 Project 10: Build a tail -f Clone

010 Build a tail -f Clone

Read last N lines and follow appended content.

open file -> ring buffer last N -> print -> poll size growth -> print new data

Full main.go

package main

import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "os"
    "time"
)

func lastNLines(path string, n int) ([]string, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    buf := make([]string, 0, n)
    s := bufio.NewScanner(f)
    for s.Scan() {
        if len(buf) == n {
            copy(buf, buf[1:])
            buf[n-1] = s.Text()
        } else {
            buf = append(buf, s.Text())
        }
    }
    return buf, s.Err()
}

func follow(path string, offset int64) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()

    if _, err := f.Seek(offset, io.SeekStart); err != nil {
        return err
    }

    for {
        _, _ = io.Copy(os.Stdout, f)
        time.Sleep(500 * time.Millisecond)
    }
}

func main() {
    n := flag.Int("n", 10, "show last n lines")
    followMode := flag.Bool("f", false, "follow file")
    flag.Parse()

    if flag.NArg() != 1 {
        fmt.Fprintln(os.Stderr, "usage: gtail [-n N] [-f] <file>")
        os.Exit(2)
    }

    path := flag.Arg(0)
    lines, err := lastNLines(path, *n)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    for _, line := range lines {
        fmt.Println(line)
    }

    if *followMode {
        st, err := os.Stat(path)
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
        if err := follow(path, st.Size()); err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }
    }
}

Run

go run . -n 50 app.log
go run . -n 20 -f app.log

Step-by-Step Explanation

  1. Parse command-line flags and validate inputs early.
  2. Keep the core operation in a small, testable function.
  3. Process data as a stream when possible to reduce memory use.
  4. Print stable output and meaningful exit codes.
  5. Add one extension feature and test edge cases.

Code Anatomy

  • main handles flags, orchestration, and errors.
  • Worker/helper functions hold business logic.
  • Output section should be deterministic for scripting and CI usage.

Learning Goals

  • Write composable Unix-style Go tools.
  • Improve error messages and operator experience.
  • Practice iterative improvement over one clear baseline.