Last active
August 29, 2015 13:57
-
-
Save wathiede/9856557 to your computer and use it in GitHub Desktop.
Tail in Go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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