Repository: bitfield/script
Pull request #19 (2019-07-12 09:07:55)

Added Last filter to mimic tail command of shell
Added the necessary test function
Added example go program

bitfield: Thanks for the PR! Don't forget to update the README, and add the new method to the stress-test list, as described in the [contributor's guide](https://github.com/bitfield/script/blob/master/CONTRIBUTING.md#before-submitting-your-pull-request) checklist.
udithprabhu: Thanks for the review,  will send the updated pr in a day or two 
bitfield: Also, don't forget to mention in the PR description, "Fixes #18" (or something like that). Then GitHub will automatically link it and close the issue when the PR is merged.
bitfield: @udithprabhu how's it going? I hope I haven't overwhelmed you with too many changes. If you like, I could pick up this PR and make the requested changes myself, then we can both assess the state of it and see whether it's ready to merge.
udithprabhu: @bitfield , Sorry for the delay, boggled down by other work, will finish this within 2-3 days if that is fine with you?
bitfield: Not at all! Take all the time you need, your contribution is much appreciated. Just let me know if I can do anything to help.
udithprabhu: @bitfield, I think I am more or less done with all the requested changes. please let me know if further tweaking is necessary.
bitfield: I'll take a look! Thanks very much!

Patch:
diff --git a/README.md b/README.md
index 854a551..ebd1ef1 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,7 @@ script.Args().Concat().Match("Error").First(10).AppendFile("/var/log/errors.txt"
 	- [First](#first)
 	- [Freq](#freq)
 	- [Join](#join)
+	- [Last](#last)
 	- [Match](#match)
 	- [MatchRegexp](#matchregexp)
 	- [Reject](#reject)
@@ -283,6 +284,7 @@ If you're already familiar with shell scripting and the Unix toolset, here is a
 | `head`             | [`First()`](#first)                                           |
 | `ls`               | [`ListFiles()`](#listfiles)                                   |
 | `sed`              | [`Replace()`](#replace) / [`ReplaceRegexp()`](#replaceregexp) |
+| `tail`             | [`Last()`](#last)                                             |
 | `uniq -c`          | [`Freq()`](#freq)                                             |
 | `wc -l`            | [`CountLines()`](#countlines)                                 |
 
@@ -580,6 +582,14 @@ fmt.Println(output)
 // Output: hello world\n
 ```
 
+## Last
+
+`Last()` reads its input and passes on the last N lines of it (like Unix [`tail`](examples/tail/main.go)):
+
+```go
+script.Stdin().Last(10).Stdout()
+```
+
 ## Match
 
 `Match()` returns a pipe containing only the input lines which match the supplied string:
@@ -733,6 +743,8 @@ Since `script` is designed to help you write system administration programs, a f
 * [grep](examples/grep/main.go)
 * [head](examples/head/main.go)
 * [echo](examples/echo/main.go)
+* [tail](examples/tail/main.go)
+* [least_freq](examples/least_freq/main.go)
 * [visitors](examples/visitors/main.go)
 * [ls](examples/ls/main.go)
 
diff --git a/examples/least_freq/main.go b/examples/least_freq/main.go
new file mode 100644
index 0000000..7d2a174
--- /dev/null
+++ b/examples/least_freq/main.go
@@ -0,0 +1,9 @@
+package main
+
+import "github.com/bitfield/script"
+
+// filters the least frequent line from input and copies it to standard output
+
+func main() {
+	script.Stdin().Freq().Last(1).Stdout()
+}
diff --git a/examples/tail/main.go b/examples/tail/main.go
new file mode 100644
index 0000000..50d4645
--- /dev/null
+++ b/examples/tail/main.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+	"log"
+	"os"
+	"strconv"
+
+	"github.com/bitfield/script"
+)
+
+func main() {
+	lines, err := strconv.Atoi(os.Args[1])
+	if err != nil {
+		log.Fatal(err)
+	}
+	script.Stdin().Last(lines).Stdout()
+}
diff --git a/filters.go b/filters.go
index 2535269..4c472b1 100644
--- a/filters.go
+++ b/filters.go
@@ -3,6 +3,7 @@ package script
 import (
 	"bufio"
 	"bytes"
+	"container/ring"
 	"fmt"
 	"io"
 	"os"
@@ -92,6 +93,10 @@ func (p *Pipe) First(lines int) *Pipe {
 	if p == nil || p.Error() != nil {
 		return p
 	}
+	defer p.Close()
+	if lines == 0 {
+		return NewPipe()
+	}
 	scanner := bufio.NewScanner(p.Reader)
 	output := strings.Builder{}
 	for i := 0; i < lines; i++ {
@@ -101,7 +106,6 @@ func (p *Pipe) First(lines int) *Pipe {
 		output.WriteString(scanner.Text())
 		output.WriteRune('\n')
 	}
-	p.Close()
 	err := scanner.Err()
 	if err != nil {
 		p.SetError(err)
@@ -169,6 +173,38 @@ func (p *Pipe) Join() *Pipe {
 	return Echo(output + terminator)
 }
 
+// Last reads from the pipe, and returns a new pipe containing only the last N
+// lines. If there is an error reading the pipe, the pipe's error status is also
+// set.
+func (p *Pipe) Last(lines int) *Pipe {
+	if p == nil || p.Error() != nil {
+		return p
+	}
+	defer p.Close()
+	if lines == 0 {
+		return NewPipe()
+	}
+	scanner := bufio.NewScanner(p.Reader)
+	input := ring.New(lines)
+	for scanner.Scan() {
+		input.Value = scanner.Text()
+		input = input.Next()
+	}
+	output := strings.Builder{}
+	input.Do(func(p interface{}) {
+		value, ok := p.(string)
+		if ok {
+			output.WriteString(value)
+			output.WriteRune('\n')
+		}
+	})
+	err := scanner.Err()
+	if err != nil {
+		p.SetError(err)
+	}
+	return Echo(output.String())
+}
+
 // Match reads from the pipe, and returns a new pipe containing only lines which
 // contain the specified string. If there is an error reading the pipe, the
 // pipe's error status is also set.


Test patch:
diff --git a/filters_test.go b/filters_test.go
index 7bff3c3..05ddbeb 100644
--- a/filters_test.go
+++ b/filters_test.go
@@ -311,6 +311,49 @@ func TestFirst(t *testing.T) {
 	}
 }
 
+func TestLast(t *testing.T) {
+	t.Parallel()
+	want, err := ioutil.ReadFile("testdata/last10.golden.txt")
+	if err != nil {
+		t.Fatal(err)
+	}
+	input := File("testdata/first.input.txt")
+	got, err := input.Last(10).Bytes()
+	if err != nil {
+		t.Error(err)
+	}
+	if !bytes.Equal(got, want) {
+		t.Errorf("want %q, got %q", want, got)
+	}
+	_, err = ioutil.ReadAll(input.Reader)
+	if err == nil {
+		t.Error("input not closed after reading")
+	}
+	input = File("testdata/first.input.txt")
+	gotZero, err := input.Last(0).CountLines()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if gotZero != 0 {
+		t.Errorf("want 0 lines, got %d lines", gotZero)
+	}
+	_, err = ioutil.ReadAll(input.Reader)
+	if err == nil {
+		t.Error("input not closed after reading")
+	}
+	want, err = File("testdata/first.input.txt").Bytes()
+	if err != nil {
+		t.Fatal(err)
+	}
+	got, err = File("testdata/first.input.txt").Last(100).Bytes()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(got, want) {
+		t.Errorf("want %q, got %q", want, got)
+	}
+}
+
 func TestFreq(t *testing.T) {
 	t.Parallel()
 	want, err := ioutil.ReadFile("testdata/freq.golden.txt")
diff --git a/testdata/last10.golden.txt b/testdata/last10.golden.txt
new file mode 100644
index 0000000..198a52f
--- /dev/null
+++ b/testdata/last10.golden.txt
@@ -0,0 +1,10 @@
+
+Free from desire, you realize the mystery.
+Caught in desire, you see only the manifestations.
+
+Yet mystery and manifestations
+arise from the same source.
+This source is called darkness.
+
+Darkness within darkness.
+The gateway to all understanding.