Repository: bitfield/script
Pull request #1 (2019-06-06 13:54:33)

Add replace filter
Imitating simple replacement without regexps

SmilingNavern: Thanks for feedback i am going to re-read code and fix it accordingly.
SmilingNavern: @bitfield 
I have fixed code as you requested. I am not good at english so i have used your descriptions for Readme and comments for Replace functions.
SmilingNavern: Use case is simple it is really often that you need to perform sed or another type of substitute for text or for config files. So Replace function is imitating simple sed in a way
bitfield: > Use case is simple it is really often that you need to perform sed or another type of substitute for text or for config files

Yes, I understand that; but presumably you wanted to use `script` to write a specific program to do something like this. If you could give me a simple version of that program it would be very helpful.
bitfield: Hey @SmilingNavern—are you still interested in working on this? If not, that's fine, but would you mind if I picked up your PR and finished off the changes requested here?
SmilingNavern: Sorry, i am still interested and i will return to this a bit later but if you in hurry feel free to finish this)

Patch:
diff --git a/README.md b/README.md
index 9c11055..c217d54 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,8 @@ script.Args().Concat().Match("Error").First(10).AppendFile("/var/log/errors.txt"
 	- [MatchRegexp](#MatchRegexp)
 	- [Reject](#Reject)
 	- [RejectRegexp](#RejectRegexp)
+	- [Replace](#Replace)
+	- [ReplaceRegexp](#ReplaceRegexp)
 - [Sinks](#Sinks)
 	- [AppendFile](#AppendFile)
 	- [Bytes](#Bytes)
@@ -265,20 +267,21 @@ func main() {
 
 If you're already familiar with shell scripting and the Unix toolset, here is a rough guide to the equivalent `script` operation for each listed Unix command.
 
-| Unix / shell       | `script` equivalent                                       |
-| ------------------ | --------------------------------------------------------- |
-| (any program name) | [`Exec()`](#exec)                                         |
-| `>`                | [`WriteFile()`](#writefile)                               |
-| `>>`               | [`AppendFile()`](#appendfile)                             |
-| `$*`               | [`Args()`](#args)                                         |
-| `cat`              | [`File()`](#file) / [`Concat()`](#concat)                 |
-| `cut`              | [`Column()`](#column)                                     |
-| `echo`             | [`Echo()`](#echo)                                         |
-| `grep`             | [`Match()`](#match) / [`MatchRegexp()`](#matchregexp)     |
-| `grep -v`          | [`Reject()`](#reject) / [`RejectRegexp()`](#rejectregexp) |
-| `head`             | [`First()`](#first)                                       |
-| `uniq -c`          | [`Freq()`](#freq)                                         |
-| `wc -l`            | [`CountLines()`](#countlines)                             |
+| Unix / shell       | `script` equivalent                                           |
+| ------------------ | ------------------------------------------------------------- |
+| (any program name) | [`Exec()`](#exec)                                             |
+| `>`                | [`WriteFile()`](#writefile)                                   |
+| `>>`               | [`AppendFile()`](#appendfile)                                 |
+| `$*`               | [`Args()`](#args)                                             |
+| `cat`              | [`File()`](#file) / [`Concat()`](#concat)                     |
+| `cut`              | [`Column()`](#column)                                         |
+| `echo`             | [`Echo()`](#echo)                                             |
+| `grep`             | [`Match()`](#match) / [`MatchRegexp()`](#matchregexp)         |
+| `grep -v`          | [`Reject()`](#reject) / [`RejectRegexp()`](#rejectregexp)     |
+| `head`             | [`First()`](#first)                                           |
+| `sed`              | [`Replace()`](#replace) / [`ReplaceRegexp()`](#replaceregexp) |
+| `uniq -c`          | [`Freq()`](#freq)                                             |
+| `wc -l`            | [`CountLines()`](#countlines)                                 |
 
 # Sources, filters, and sinks
 
@@ -578,6 +581,22 @@ p := script.File("test.txt").Match("Error").Reject("false alarm")
 p := script.File("test.txt").Match("Error").RejectRegexp(regexp.MustCompile(`false|bogus`))
 ```
 
+## Replace
+
+`Replace()` returns a pipe which filters its input by replacing all occurrences of one string with another, like Unix `sed`:
+
+```go
+p := script.File("test.txt").Replace("old", "new")
+```
+
+## ReplaceRegexp
+
+`ReplaceRegexp()` returns a pipe which filters its input by replacing all matches of a compiled regular expression with a supplied replacement string, like Unix `sed`:
+
+```go
+p := script.File("test.txt").ReplaceRegexp(regexp.MustCompile("Gol[a-z]{1}ng"), "Go")
+```
+
 # Sinks
 
 Sinks are operations which return some data from a pipe, ending the pipeline.
diff --git a/filters.go b/filters.go
index c833805..2535269 100644
--- a/filters.go
+++ b/filters.go
@@ -216,3 +216,25 @@ func (p *Pipe) RejectRegexp(re *regexp.Regexp) *Pipe {
 		}
 	})
 }
+
+// Replace filters its input by replacing all occurrences of the string `search`
+// with the string `replace`. If there is an error reading the pipe, the pipe's
+// error status is also set.
+func (p *Pipe) Replace(search, replace string) *Pipe {
+	return p.EachLine(func(line string, out *strings.Builder) {
+		out.WriteString(strings.ReplaceAll(line, search, replace))
+		out.WriteRune('\n')
+	})
+}
+
+// ReplaceRegexp filters its input by replacing all matches of the compiled
+// regular expression `re` with the replacement string `replace`. Inside
+// `replace`, $ signs are interpreted as in regexp.Expand, so for instance "$1"
+// represents the text of the first submatch. If there is an error reading the
+// pipe, the pipe's error status is also set.
+func (p *Pipe) ReplaceRegexp(re *regexp.Regexp, replace string) *Pipe {
+	return p.EachLine(func(line string, out *strings.Builder) {
+		out.WriteString(re.ReplaceAllString(line, replace))
+		out.WriteRune('\n')
+	})
+}


Test patch:
diff --git a/filters_test.go b/filters_test.go
index 5d7c752..7bff3c3 100644
--- a/filters_test.go
+++ b/filters_test.go
@@ -95,6 +95,54 @@ func TestMatchRegexp(t *testing.T) {
 	}
 }
 
+func TestReplace(t *testing.T) {
+	t.Parallel()
+	testCases := []struct {
+		testFileName string
+		search       string
+		replace      string
+		want         string
+	}{
+		{"testdata/hello.txt", "hello", "bye", "bye world\n"},
+		{"testdata/hello.txt", "Does not exist in input", "Will not appear in output", "hello world\n"},
+		{"testdata/hello.txt", " world", " string with newline\n", "hello string with newline\n\n"},
+		{"testdata/hello.txt", "hello", "Ж9", "Ж9 world\n"},
+	}
+	for _, tc := range testCases {
+		got, err := File(tc.testFileName).Replace(tc.search, tc.replace).String()
+		if err != nil {
+			t.Error(err)
+		}
+		if got != tc.want {
+			t.Errorf("%s with %s in %s, want %s, got %s", tc.search, tc.replace, tc.testFileName, tc.want, got)
+		}
+	}
+}
+
+func TestReplaceRegexp(t *testing.T) {
+	t.Parallel()
+	testCases := []struct {
+		testFileName string
+		regexp       string
+		replace      string
+		want         string
+	}{
+		{"testdata/hello.txt", "hel+o", "bye", "bye world\n"},
+		{"testdata/hello.txt", "Does not .* in input", "Will not appear in output", "hello world\n"},
+		{"testdata/hello.txt", "^([a-z]+) ([a-z]+)", "$1 cruel $2", "hello cruel world\n"},
+		{"testdata/hello.txt", "hello{1}", "Ж9", "Ж9 world\n"},
+	}
+	for _, tc := range testCases {
+		got, err := File(tc.testFileName).ReplaceRegexp(regexp.MustCompile(tc.regexp), tc.replace).String()
+		if err != nil {
+			t.Error(err)
+		}
+		if got != tc.want {
+			t.Errorf("%s with %s in %s, want %s, got %s", tc.regexp, tc.replace, tc.testFileName, tc.want, got)
+		}
+	}
+}
+
 func TestReject(t *testing.T) {
 	t.Parallel()
 	testCases := []struct {
diff --git a/pipes_test.go b/pipes_test.go
index 016e542..f4735d9 100644
--- a/pipes_test.go
+++ b/pipes_test.go
@@ -119,6 +119,10 @@ func doMethodsOnPipe(t *testing.T, p *Pipe, kind string) {
 	p.MatchRegexp(regexp.MustCompile(".*"))
 	action = "Read()"
 	p.Read([]byte{})
+	action = "Replace()"
+	p.Replace("old", "new")
+	action = "ReplaceRegexp()"
+	p.ReplaceRegexp(regexp.MustCompile(".*"), "")
 	action = "SetError()"
 	p.SetError(nil)
 	action = "Stdout()"