diff --git a/README.md b/README.md index 269c8af..710d308 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,16 @@ That was almost too easy! So let's pass in a list of files on the command line, script.Args().Concat().Match("Error").Stdout() ``` -What's that? You want to append that output to a file instead of printing it to the terminal? No problem: +Maybe we're only interested in the first 10 matches. No problem: ```go -script.Args().Concat().Match("Error").AppendFile("/var/log/errors.txt") +script.Args().Concat().Match("Error").First(10).Stdout() +``` + +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_. + +```go +script.Args().Concat().Match("Error").First(10).AppendFile("/var/log/errors.txt") ``` ## How does it work? @@ -323,6 +329,14 @@ fmt.Println(output) // Output: hello world ``` +### First + +`First()` reads its input and passes on the first N lines of it (like Unix [`head`](examples/head/main.go)): + +```go +Stdin().First(10).Stdout() +``` + ### Join `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 ### Filters -* Ideas welcome! +* `Column()` reads columnar (whitespace-separated) data and cuts the specified column, like Unix `cut` +* `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). ### Sinks -* Ideas equally welcome! +* [Ideas welcome!](https://github.com/bitfield/script/issues/new) ### Examples @@ -580,10 +595,13 @@ Since `script` is designed to help you write system administration programs, a f * [cat](examples/cat/main.go) (copies stdin to stdout) * [cat 2](examples/cat2/main.go) (takes a list of files on the command line and concatenates their contents to stdout) * [grep](examples/grep/main.go) +* [head](examples/head/main.go) * [echo](examples/echo/main.go) -More examples would be welcome! +[More examples would be welcome!](https://github.com/bitfield/script/pulls) ### Use cases -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. +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. + +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 john@bitfieldconsulting.com and tell me how you're using `script` and what you think of it! \ No newline at end of file diff --git a/examples/head/main.go b/examples/head/main.go new file mode 100644 index 0000000..bf15466 --- /dev/null +++ b/examples/head/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().First(lines).Stdout() +} diff --git a/filters.go b/filters.go index 1826adb..87b3ba6 100644 --- a/filters.go +++ b/filters.go @@ -134,3 +134,26 @@ func (p *Pipe) Concat() *Pipe { }) return p.WithReader(io.MultiReader(readers...)) } + +// First reads from the pipe, and returns a new pipe containing only the first N +// lines. If there is an error reading the pipe, the pipe's error status is also +// set. +func (p *Pipe) First(lines int) *Pipe { + if p == nil || p.Error() != nil { + return p + } + scanner := bufio.NewScanner(p.Reader) + output := strings.Builder{} + for i := 0; i < lines; i++ { + if !scanner.Scan() { + break + } + output.WriteString(scanner.Text()) + output.WriteRune('\n') + } + err := scanner.Err() + if err != nil { + p.SetError(err) + } + return Echo(output.String()) +} diff --git a/filters_test.go b/filters_test.go index cda92b0..15380c4 100644 --- a/filters_test.go +++ b/filters_test.go @@ -219,3 +219,39 @@ func TestConcat(t *testing.T) { t.Errorf("want %q, got %q", want, got) } } + +func TestFirst(t *testing.T) { + t.Parallel() + want, err := ioutil.ReadFile("testdata/first10.golden.txt") + if err != nil { + t.Fatal(err) + } + got, err := File("testdata/first.input.txt").First(10).Bytes() + if err != nil { + t.Error(err) + } + if !bytes.Equal(got, want) { + t.Errorf("want %q, got %q", want, got) + } + // First(0) should return zero lines + zero := File("testdata/first.input.txt").First(0) + gotZero, err := zero.CountLines() + if err != nil { + t.Fatal(err) + } + if gotZero != 0 { + t.Errorf("want 0 lines, got %d lines", gotZero) + } + // First(N) where the input has less than N lines, should just return the input. + want, err = File("testdata/first.input.txt").Bytes() + if err != nil { + t.Fatal(err) + } + got, err = File("testdata/first.input.txt").First(100).Bytes() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Errorf("want %q, got %q", want, got) + } +} diff --git a/pipes.go b/pipes.go index 467650a..22399d0 100644 --- a/pipes.go +++ b/pipes.go @@ -56,8 +56,9 @@ func (p *Pipe) ExitStatus() int { return status } -// WithReader takes an io.Reader which does not need to be closed after reading, -// and associates the pipe with that reader. +// WithReader takes an io.Reader, and associates the pipe with that reader. If +// necessary, the reader will be automatically closed once it has been +// completely read. func (p *Pipe) WithReader(r io.Reader) *Pipe { if p == nil { return p diff --git a/pipes_test.go b/pipes_test.go index 5f03340..5e93668 100644 --- a/pipes_test.go +++ b/pipes_test.go @@ -100,6 +100,8 @@ func doMethodsOnPipe(t *testing.T, p *Pipe, kind string) { p.WithError(nil) action = "Read()" p.Read([]byte{}) + action = "First()" + p.First(1) } func TestNilPipes(t *testing.T) { diff --git a/testdata/first.input.txt b/testdata/first.input.txt new file mode 100644 index 0000000..9522f81 --- /dev/null +++ b/testdata/first.input.txt @@ -0,0 +1,18 @@ +The tao that can be told +is not the eternal Tao +The name that can be named +is not the eternal Name. + +The unnamable is the eternally real. +Naming is the origin +of all particular things. + +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. diff --git a/testdata/first10.golden.txt b/testdata/first10.golden.txt new file mode 100644 index 0000000..cc2da8c --- /dev/null +++ b/testdata/first10.golden.txt @@ -0,0 +1,10 @@ +The tao that can be told +is not the eternal Tao +The name that can be named +is not the eternal Name. + +The unnamable is the eternally real. +Naming is the origin +of all particular things. + +Free from desire, you realize the mystery.