Video Coming Soon...
39: head and tail
The tail command prints out the tail of a file or input stream, which becomes insanely useful when you have to manage your own systems. You'll be "tailing that log" all the time, which means using tail to get the last 20 or so lines of a log file.
The head command rarely shows up in my daily Linux usage, but it's actually the easier of the two to implement which is why I have you start with head.To implement head you count how many lines, then stop when you read the required number. With tail you have to "eat" lines until you reach the end, then print the last 20.
The Challenge
You don't have to be efficient with your implementation, but try to get as many command line options working as possible. If you want to go for extra points you'll try to find a way to make tail not use too much memory. My implementation collects the entire contents of a file into memory, then prints only the tail. See if you can do better.
The documentation is here:
- https://www.gnu.org/software/coreutils/manual/html_node/head-invocation.html
- https://www.gnu.org/software/coreutils/manual/html_node/tail-invocation.html
Requirements
Nothing new here, just the usual file I/O packages.
See the list of requirements
- fmt -- https://pkg.go.dev/fmt
- bufio -- https://pkg.go.dev/bufio
- os -- https://pkg.go.dev/os
- log -- https://pkg.go.dev/log
- flag -- https://pkg.go.dev/flag
- io -- https://pkg.go.dev/io
The head Code
As I mentioned before head is easy, but if you're stuck take a peek:
See my first version of head
View Source file go-coreutils/head/main.go Onlypackage main
import (
"fmt"
"bufio"
"os"
"log"
"flag"
"io"
)
func HeadFile(file io.Reader, count int) {
scan := bufio.NewScanner(file)
for cur_line := 0; scan.Scan(); cur_line++ {
line := scan.Text()
if cur_line < count {
fmt.Println(line)
}
}
if scan.Err() != nil { log.Fatal(scan.Err()) }
}
func main() {
var count int
flag.IntVar(&count, "n", 10, "number of lines")
flag.Parse()
if flag.NArg() > 0 {
files := flag.Args()
for _, fname := range files {
file, err := os.Open(fname)
if err != nil { log.Fatal(err) }
HeadFile(file, count)
}
} else {
HeadFile(os.Stdin, count)
}
}
The tail Code
The tail command is harder, since you have to append to a list and then print the tail from that. However, this is a junk way to implement tail, so maybe you can do better?
See my first version of tail
View Source file go-coreutils/tail/main.go Onlypackage main
import (
"fmt"
"bufio"
"os"
"log"
"flag"
"io"
)
func TailFile(file io.Reader, count int) {
scan := bufio.NewScanner(file)
var lines []string
for scan.Scan() {
lines = append(lines, scan.Text())
}
if scan.Err() != nil { log.Fatal(scan.Err()) }
start := len(lines) - count
if start < 0 { start = 0 }
for line := int(start); line < len(lines); line++ {
fmt.Println(lines[line])
}
}
type Opts struct {
Count int
Files []string
}
func ParseOpts() Opts {
var opts Opts
flag.IntVar(&opts.Count, "n", 10, "number of lines")
flag.Parse()
opts.Files = flag.Args()
return opts
}
func main() {
opts := ParseOpts()
if len(opts.Files) > 0 {
for _, fname := range opts.Files {
file, err := os.Open(fname)
if err != nil { log.Fatal(err) }
TailFile(file, opts.Count)
}
} else {
TailFile(os.Stdin, opts.Count)
}
}
HINT Read about ring buffers.
Testing It
If you want to push your learning further then you can try to implement an automated test for this. I actually need to learn how to test utilities like this with Go, so for now just consider this an extra challenge for later until I learn how to teach it.
Register for Learn Go the Hard Way
Register today for the course and get the all currently available videos and lessons, plus all future modules for no extra charge.