Skip to content

Instantly share code, notes, and snippets.

@wathiede
Last active August 29, 2015 13:57
Show Gist options
  • Save wathiede/9856557 to your computer and use it in GitHub Desktop.
Save wathiede/9856557 to your computer and use it in GitHub Desktop.
Tail in Go
package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
"syscall"
)
var nLines = flag.Int("n", 10, "number of lines to show.")
func tailFile(n int, f *os.File) error {
fi, err := f.Stat()
if err != nil {
return err
}
data, err := syscall.Mmap(int(f.Fd()), 0, int(fi.Size()), syscall.PROT_READ, syscall.MAP_PRIVATE)
if err != nil {
return err
}
var (
start int64
crCnt int
)
for i := len(data) - 1; i >= 0; i-- {
if data[i] == '\n' {
crCnt++
// n+1 because we're counting end of lines, and we need to print
// the nth line before the EOF.
if crCnt == (n + 1) {
start = int64(i) + 1 // +1 to advance past \n
break
}
}
}
if _, err := f.Seek(start, os.SEEK_SET); err != nil {
return err
}
if _, err := io.Copy(os.Stdout, f); err != nil {
return err
}
return nil
}
func tail(n int, r io.Reader) error {
// No error if n=0, just return.
if n <= 0 {
return nil
}
if f, ok := r.(*os.File); ok {
return tailFile(n, f)
}
// Keep a buffer of strings big enough for our -n value.
buf := make([]string, n)
scanner := bufio.NewScanner(r)
i := 0
for scanner.Scan() {
// Scan over the reader, treating buf as a circular buffer storing the
// last n lines. We use .Text() which causes an allocation when
// scanner's internals convert from a mutable byte slice to an
// immutable string. .Bytes() documentation states:
// The underlying array may point to data that will be overwritten
// by a subsequent call to Scan. It does no allocation.
buf[i%n] = scanner.Text()
i++
}
if err := scanner.Err(); err != nil {
return err
}
var start, size int
if i < n {
// r had less than the requested number of lines. The size is the
// number of lines we read, and we start at 0 (int's default value).
size = i
} else {
// r has the requested number of lines or more. Size is n, and we
// start where n number of elements before i, which modulo math lets
// us figure out easily.
start = i % n
size = n
}
// Print size number of items, starting at start, wrapping around the
// circular buffer is necessary.
for i := 0; i < size; i++ {
fmt.Println(buf[(start+i)%n])
}
return nil
}
func main() {
flag.Parse()
// No files specified defaults to reading stdin.
if flag.NArg() == 0 {
if err := tail(*nLines, os.Stdin); err != nil {
fmt.Fprintln(os.Stderr, err)
}
return
}
for i, fn := range flag.Args() {
r, err := os.Open(fn)
if err != nil {
// On error, print to stderr and continue on to next file. More
// idomatic go might be to log.Fatal() from main().
fmt.Fprintln(os.Stderr, err)
continue
}
if i > 0 {
// Insert newline before printing filename when tailing multiple
// files.
fmt.Println()
}
if flag.NArg() > 1 {
// Only print filename if multiple files specified.
fmt.Printf("==> %s <==\n", fn)
}
if err := tail(*nLines, r); err != nil {
fmt.Fprintln(os.Stderr, err)
}
r.Close()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment