Skip to content

Commit b7ef548

Browse files
authored
Merge pull request #9 from bitfield/first
Add First filter
2 parents 2a59d1b + 37c241e commit b7ef548

File tree

8 files changed

+133
-8
lines changed

8 files changed

+133
-8
lines changed

README.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,16 @@ That was almost too easy! So let's pass in a list of files on the command line,
4040
script.Args().Concat().Match("Error").Stdout()
4141
```
4242

43-
What's that? You want to append that output to a file instead of printing it to the terminal? No problem:
43+
Maybe we're only interested in the first 10 matches. No problem:
4444

4545
```go
46-
script.Args().Concat().Match("Error").AppendFile("/var/log/errors.txt")
46+
script.Args().Concat().Match("Error").First(10).Stdout()
47+
```
48+
49+
What's that? You want to append that output to a file instead of printing it to the terminal? _You've got some attitude, mister_.
50+
51+
```go
52+
script.Args().Concat().Match("Error").First(10).AppendFile("/var/log/errors.txt")
4753
```
4854

4955
## How does it work?
@@ -323,6 +329,14 @@ fmt.Println(output)
323329
// Output: hello world
324330
```
325331

332+
### First
333+
334+
`First()` reads its input and passes on the first N lines of it (like Unix [`head`](examples/head/main.go)):
335+
336+
```go
337+
Stdin().First(10).Stdout()
338+
```
339+
326340
### Join
327341

328342
`Join()` reads its input and replaces newlines with spaces, preserving a terminating newline if there is one.
@@ -567,11 +581,12 @@ These are some ideas I'm playing with for additional features. If you feel like
567581

568582
### Filters
569583

570-
* Ideas welcome!
584+
* `Column()` reads columnar (whitespace-separated) data and cuts the specified column, like Unix `cut`
585+
* `CountFreq()` counts the frequency of input lines, and prepends each unique line with its frequency (like Unix `uniq -c`). The results are sorted in descending numerical order (that is, most frequent lines first).
571586

572587
### Sinks
573588

574-
* Ideas equally welcome!
589+
* [Ideas welcome!](https://github.com/bitfield/script/issues/new)
575590

576591
### Examples
577592

@@ -580,10 +595,13 @@ Since `script` is designed to help you write system administration programs, a f
580595
* [cat](examples/cat/main.go) (copies stdin to stdout)
581596
* [cat 2](examples/cat2/main.go) (takes a list of files on the command line and concatenates their contents to stdout)
582597
* [grep](examples/grep/main.go)
598+
* [head](examples/head/main.go)
583599
* [echo](examples/echo/main.go)
584600

585-
More examples would be welcome!
601+
[More examples would be welcome!](https://github.com/bitfield/script/pulls)
586602

587603
### Use cases
588604

589-
The best libraries are designed to satisfy real use cases. If you have a sysadmin task which you'd like to implement with `script`, let me know by opening an issue.
605+
The best libraries are designed to satisfy real use cases. If you have a sysadmin task which you'd like to implement with `script`, let me know by [opening an issue](https://github.com/bitfield/script/issues/new) - I'd love to hear from you.
606+
607+
If you use `script` for real work (or, for that matter, real play), I'm always very interested to hear about it. Drop me a line to [email protected] and tell me how you're using `script` and what you think of it!

examples/head/main.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
"strconv"
7+
8+
"github.com/bitfield/script"
9+
)
10+
11+
func main() {
12+
lines, err := strconv.Atoi(os.Args[1])
13+
if err != nil {
14+
log.Fatal(err)
15+
}
16+
script.Stdin().First(lines).Stdout()
17+
}

filters.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,26 @@ func (p *Pipe) Concat() *Pipe {
134134
})
135135
return p.WithReader(io.MultiReader(readers...))
136136
}
137+
138+
// First reads from the pipe, and returns a new pipe containing only the first N
139+
// lines. If there is an error reading the pipe, the pipe's error status is also
140+
// set.
141+
func (p *Pipe) First(lines int) *Pipe {
142+
if p == nil || p.Error() != nil {
143+
return p
144+
}
145+
scanner := bufio.NewScanner(p.Reader)
146+
output := strings.Builder{}
147+
for i := 0; i < lines; i++ {
148+
if !scanner.Scan() {
149+
break
150+
}
151+
output.WriteString(scanner.Text())
152+
output.WriteRune('\n')
153+
}
154+
err := scanner.Err()
155+
if err != nil {
156+
p.SetError(err)
157+
}
158+
return Echo(output.String())
159+
}

filters_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,39 @@ func TestConcat(t *testing.T) {
219219
t.Errorf("want %q, got %q", want, got)
220220
}
221221
}
222+
223+
func TestFirst(t *testing.T) {
224+
t.Parallel()
225+
want, err := ioutil.ReadFile("testdata/first10.golden.txt")
226+
if err != nil {
227+
t.Fatal(err)
228+
}
229+
got, err := File("testdata/first.input.txt").First(10).Bytes()
230+
if err != nil {
231+
t.Error(err)
232+
}
233+
if !bytes.Equal(got, want) {
234+
t.Errorf("want %q, got %q", want, got)
235+
}
236+
// First(0) should return zero lines
237+
zero := File("testdata/first.input.txt").First(0)
238+
gotZero, err := zero.CountLines()
239+
if err != nil {
240+
t.Fatal(err)
241+
}
242+
if gotZero != 0 {
243+
t.Errorf("want 0 lines, got %d lines", gotZero)
244+
}
245+
// First(N) where the input has less than N lines, should just return the input.
246+
want, err = File("testdata/first.input.txt").Bytes()
247+
if err != nil {
248+
t.Fatal(err)
249+
}
250+
got, err = File("testdata/first.input.txt").First(100).Bytes()
251+
if err != nil {
252+
t.Fatal(err)
253+
}
254+
if !bytes.Equal(got, want) {
255+
t.Errorf("want %q, got %q", want, got)
256+
}
257+
}

pipes.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ func (p *Pipe) ExitStatus() int {
5656
return status
5757
}
5858

59-
// WithReader takes an io.Reader which does not need to be closed after reading,
60-
// and associates the pipe with that reader.
59+
// WithReader takes an io.Reader, and associates the pipe with that reader. If
60+
// necessary, the reader will be automatically closed once it has been
61+
// completely read.
6162
func (p *Pipe) WithReader(r io.Reader) *Pipe {
6263
if p == nil {
6364
return p

pipes_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ func doMethodsOnPipe(t *testing.T, p *Pipe, kind string) {
100100
p.WithError(nil)
101101
action = "Read()"
102102
p.Read([]byte{})
103+
action = "First()"
104+
p.First(1)
103105
}
104106

105107
func TestNilPipes(t *testing.T) {

testdata/first.input.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
The tao that can be told
2+
is not the eternal Tao
3+
The name that can be named
4+
is not the eternal Name.
5+
6+
The unnamable is the eternally real.
7+
Naming is the origin
8+
of all particular things.
9+
10+
Free from desire, you realize the mystery.
11+
Caught in desire, you see only the manifestations.
12+
13+
Yet mystery and manifestations
14+
arise from the same source.
15+
This source is called darkness.
16+
17+
Darkness within darkness.
18+
The gateway to all understanding.

testdata/first10.golden.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
The tao that can be told
2+
is not the eternal Tao
3+
The name that can be named
4+
is not the eternal Name.
5+
6+
The unnamable is the eternally real.
7+
Naming is the origin
8+
of all particular things.
9+
10+
Free from desire, you realize the mystery.

0 commit comments

Comments
 (0)