Repository: bitfield/script
Pull request #47 (2020-04-06 15:08:42)

Add Slice Sink
It resolve this issue: #45

thomaspoignant: @bitfield I have made the requested changes.
I hope this is what you have in mind?

Patch:
diff --git a/.gitignore b/.gitignore
index 6f80c42..0e70a4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,6 @@ examples/cat2/cat2
 examples/echo/echo
 examples/head/head
 examples/visitors/visitors
+examples/*/go.sum
 .vscode/settings.json
 examples/ls/ls
diff --git a/README.md b/README.md
index 764f72c..08aaf67 100644
--- a/README.md
+++ b/README.md
@@ -121,6 +121,7 @@ John is also a [Kubernetes and cloud infrastructure consultant](https://bitfield
 	- [Read](#read)
 	- [SHA256Sum](#sha256sum)
 		- [Why not MD5?](#why-not-md5)
+	- [Slice](#slice-1)
 	- [Stdout](#stdout)
 	- [String](#string)
 	- [WriteFile](#writefile)
@@ -452,13 +453,13 @@ fmt.Println(files)
 `Slice()` creates a pipe from a slice of strings, one per line.
 
 ```go
-p := Slice([]string{"1", "2", "3"})
+p := script.Slice([]string{"1", "2", "3"})
 output, err := p.String()
 fmt.Println(output)
 // Output:
-1
-2
-3
+// 1
+// 2
+// 3
 ```
 
 ## Stdin
@@ -614,7 +615,7 @@ The first command which results in an error will set the pipe's error status acc
 
 ```go
 // Execute all PHP files in current directory and print output
-script.ListFiles("*.php").Exec("php {{.}}").Stdout()
+script.ListFiles("*.php").ExecForEach("php {{.}}").Stdout()
 ```
 
 ## First
@@ -805,6 +806,17 @@ sha256Sum, err := script.File("test.txt").SHA256Sum()
 
 [MD5 is insecure](https://en.wikipedia.org/wiki/MD5#Security).
 
+## Slice
+
+`Slice()` returns the contents of the pipe as a slice of strings, one element per line, plus an error. An empty pipe will produce an empty slice. A pipe containing a single empty line (that is, a single `\n` character) will produce a slice of one element which is the empty string.
+
+```go
+args, err := script.Args().Slice()
+for _, a := range args {
+	fmt.Println(a)
+}
+```
+
 ## Stdout
 
 `Stdout()` writes the contents of the pipe to the program's standard output. It returns the number of bytes written, or an error:
@@ -871,6 +883,7 @@ Since `script` is designed to help you write system administration programs, a f
 * [least_freq](examples/least_freq/main.go)
 * [ls](examples/ls/main.go)
 * [sha256sum](examples/sha256sum/main.go)
+* [slice](examples/slice/main.go)
 * [tail](examples/tail/main.go)
 * [visitors](examples/visitors/main.go)
 
diff --git a/examples/slice/errors.txt b/examples/slice/errors.txt
new file mode 100644
index 0000000..f64525e
--- /dev/null
+++ b/examples/slice/errors.txt
@@ -0,0 +1,13 @@
+Line containing the word 'error'
+Another line containing an error message
+Oh no, an error occurred!
+Here's another error
+More errors
+To err is human, but forgiveness is surprisingly difficult.
+something something error happened
+error: a suffusion of yellow
+An UNKNOWN ERROR occurred
+Drive Z: does not exist, please try again.
+error deactivating tyrannosaur fence: you didn't say the magic word!
+error: the operation completed successfully
+Fatal error: keyboard not present. Press F1 to continue.
\ No newline at end of file
diff --git a/examples/slice/main.go b/examples/slice/main.go
new file mode 100644
index 0000000..d6868da
--- /dev/null
+++ b/examples/slice/main.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+	"fmt"
+	"log"
+
+	"github.com/bitfield/script"
+)
+
+// This program reads all the files supplied to it on the command line, and
+// prints out the first ten lines matching the string 'error', with an 'ERROR:'
+// prefix.
+//
+// For example:
+// go run main.go errors.txt
+// ERROR: Line containing the word 'error'
+// ...
+
+func main() {
+	errors, err := script.Args().Concat().Match("error").First(10).Slice()
+	if err != nil {
+		log.Fatal(err)
+	}
+	for _, e := range errors {
+		fmt.Printf("ERROR: %s\n", e)
+	}
+}
diff --git a/sinks.go b/sinks.go
index c32c553..d33504c 100644
--- a/sinks.go
+++ b/sinks.go
@@ -54,17 +54,29 @@ func (p *Pipe) SHA256Sum() (string, error) {
 		return "", p.Error()
 	}
 
-	bytes, err := p.Bytes()
-	if err != nil {
+	h := sha256.New()
+	if _, err := io.Copy(h, p.Reader); err != nil {
 		p.SetError(err)
 		return "", p.Error()
 	}
 
-	sum := sha256.Sum256(bytes)
-	encodedCheckSum := hex.EncodeToString(sum[:])
+	encodedCheckSum := hex.EncodeToString(h.Sum(nil)[:])
 	return encodedCheckSum, nil
 }
 
+// Slice returns the contents of the pipe as a slice of strings, one element per line, or an error.
+// If there is an error reading the pipe, the pipe's error status is also set.
+func (p *Pipe) Slice() ([]string, error) {
+	if p == nil || p.Error() != nil {
+		return nil, p.Error()
+	}
+	result := []string{}
+	p.EachLine(func(line string, out *strings.Builder) {
+		result = append(result, line)
+	})
+	return result, p.Error()
+}
+
 // Stdout writes the contents of the pipe to the program's standard output. It
 // returns the number of bytes successfully written, plus a non-nil error if the
 // write failed or if there was an error reading from the pipe. If the pipe has


Test patch:
diff --git a/pipes_test.go b/pipes_test.go
index 12a11b9..bd053f3 100644
--- a/pipes_test.go
+++ b/pipes_test.go
@@ -107,10 +107,6 @@ func doMethodsOnPipe(t *testing.T, p *Pipe, kind string) {
 	p.Concat()
 	action = "CountLines()"
 	p.CountLines()
-	action = "SHA256Sums()"
-	p.SHA256Sums()
-	action = "SHA256Sum()"
-	p.SHA256Sum()
 	action = "Dirname()"
 	p.Dirname()
 	action = "EachLine()"
@@ -141,6 +137,12 @@ func doMethodsOnPipe(t *testing.T, p *Pipe, kind string) {
 	p.ReplaceRegexp(regexp.MustCompile(".*"), "")
 	action = "SetError()"
 	p.SetError(nil)
+	action = "SHA256Sums()"
+	p.SHA256Sums()
+	action = "SHA256Sum()"
+	p.SHA256Sum()
+	action = "Slice()"
+	p.Slice()
 	action = "Stdout()"
 	// Ensure we don't clash with TestStdout
 	stdoutM.Lock()
diff --git a/sinks_test.go b/sinks_test.go
index 76aa560..7767850 100644
--- a/sinks_test.go
+++ b/sinks_test.go
@@ -2,6 +2,7 @@ package script
 
 import (
 	"bytes"
+	"github.com/google/go-cmp/cmp"
 	"io/ioutil"
 	"os"
 	"testing"
@@ -31,6 +32,11 @@ func doSinksOnPipe(t *testing.T, p *Pipe, kind string) {
 	if err != nil {
 		t.Error(err)
 	}
+	action = "Slice()"
+	_, err = p.Slice()
+	if err != nil {
+		t.Error(err)
+	}
 	action = "WriteFile()"
 	_, err = p.WriteFile("testdata/tmp" + kind)
 	defer os.Remove("testdata/tmp" + kind)
@@ -166,6 +172,61 @@ func TestSHA256Sum(t *testing.T) {
 	}
 }
 
+func TestSliceSink(t *testing.T) {
+	tests := []struct {
+		name    string
+		fields  *Pipe
+		want    []string
+		wantErr bool
+	}{
+		{
+			name: "Multiple lines pipe",
+			fields: Echo("testdata/multiple_files/1.txt\ntestdata/multiple_files/2.txt\ntestdata/multiple_files/3.tar.zip\n"),
+			want: []string{
+				"testdata/multiple_files/1.txt",
+				"testdata/multiple_files/2.txt",
+				"testdata/multiple_files/3.tar.zip",
+			},
+			wantErr: false,
+		},
+		{
+			name: "Empty pipe",
+			fields: Echo(""),
+			want: []string{},
+			wantErr: false,
+		},
+		{
+			name: "Single newline",
+			fields: Echo("\n"),
+			want: []string{""},
+			wantErr: false,
+		},
+		{
+			name: "Empty line between two existing lines",
+			fields: Echo("testdata/multiple_files/1.txt\n\ntestdata/multiple_files/3.tar.zip"),
+			want: []string{
+				"testdata/multiple_files/1.txt",
+				"",
+				"testdata/multiple_files/3.tar.zip",
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			p := tt.fields
+			got, err := p.Slice()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Slice() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !cmp.Equal(tt.want, got) {
+				t.Error(cmp.Diff(tt.want, got))
+			}
+		})
+	}
+}
+
 func TestStdout(t *testing.T) {
 	t.Parallel()
 	// Temporarily point os.Stdout to a file so that we can capture it for