Repository: bitfield/script
Pull request #11 (2019-07-01 17:27:17)

Add Column() filter and Visitors example
Finishing touches needed for #7.


Patch:
diff --git a/.gitignore b/.gitignore
index 69bc36b..d5b7e86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,6 @@ examples/cat/cat
 examples/grep/grep
 examples/cat2/cat2
 examples/echo/echo
+examples/head/head
+examples/visitors/visitors
+.vscode/settings.json
diff --git a/README.md b/README.md
index e76753d..b43a73d 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 [![GoDoc](https://godoc.org/github.com/bitfield/script?status.png)](http://godoc.org/github.com/bitfield/script)[![Go Report Card](https://goreportcard.com/badge/github.com/bitfield/script)](https://goreportcard.com/report/github.com/bitfield/script)[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go)[![CircleCI](https://circleci.com/gh/bitfield/script.svg?style=svg)](https://circleci.com/gh/bitfield/script)
 
-# script
+# What is `script`?
 
 `script` is a Go library for doing the kind of tasks that shell scripts are good at: reading files, executing subprocesses, counting lines, matching strings, and so on.
 
@@ -8,7 +8,13 @@ Why shouldn't it be as easy to write system administration programs in Go as it
 
 Shell scripts often compose a sequence of operations on a stream of data (a _pipeline_). This is how `script` works, too.
 
-## What can I do with it?
+# How do I import it?
+
+```go
+import github.com/bitfield/script
+```
+
+# What can I do with it?
 
 Let's see a simple example. Suppose you want to read the contents of a file as a string:
 
@@ -52,7 +58,63 @@ What's that? You want to append that output to a file instead of printing it to
 script.Args().Concat().Match("Error").First(10).AppendFile("/var/log/errors.txt")
 ```
 
-## How does it work?
+# Table of contents
+
+- [What is `script`?](#What-is-script)
+- [How do I import it?](#How-do-I-import-it)
+- [What can I do with it?](#What-can-I-do-with-it)
+- [Table of contents](#Table-of-contents)
+- [How does it work?](#How-does-it-work)
+- [Everything is a pipe](#Everything-is-a-pipe)
+- [What use is a pipe?](#What-use-is-a-pipe)
+- [Handling errors](#Handling-errors)
+- [Getting output](#Getting-output)
+- [Errors](#Errors)
+- [Closing pipes](#Closing-pipes)
+- [Why not just use shell?](#Why-not-just-use-shell)
+- [A real-world example](#A-real-world-example)
+- [Quick start: Unix equivalents](#Quick-start-Unix-equivalents)
+- [Sources, filters, and sinks](#Sources-filters-and-sinks)
+- [Sources](#Sources)
+	- [Args](#Args)
+	- [Echo](#Echo)
+	- [Exec](#Exec)
+		- [Exit status](#Exit-status)
+		- [Error output](#Error-output)
+	- [File](#File)
+	- [Stdin](#Stdin)
+- [Filters](#Filters)
+	- [Column](#Column)
+	- [Concat](#Concat)
+	- [EachLine](#EachLine)
+	- [Exec](#Exec-1)
+	- [First](#First)
+	- [Freq](#Freq)
+	- [Join](#Join)
+	- [Match](#Match)
+	- [MatchRegexp](#MatchRegexp)
+	- [Reject](#Reject)
+	- [RejectRegexp](#RejectRegexp)
+- [Sinks](#Sinks)
+	- [AppendFile](#AppendFile)
+	- [Bytes](#Bytes)
+	- [CountLines](#CountLines)
+	- [Read](#Read)
+	- [Stdout](#Stdout)
+	- [String](#String)
+	- [WriteFile](#WriteFile)
+- [Writing your own pipe operations](#Writing-your-own-pipe-operations)
+	- [Writing a source](#Writing-a-source)
+	- [Writing a filter](#Writing-a-filter)
+	- [Writing a sink](#Writing-a-sink)
+- [Ideas](#Ideas)
+	- [Sources](#Sources-1)
+	- [Filters](#Filters-1)
+	- [Sinks](#Sinks-1)
+- [Examples](#Examples)
+- [Use cases](#Use-cases)
+
+# How does it work?
 
 Those chained function calls look a bit weird. What's going on there?
 
@@ -68,7 +130,7 @@ By comparison, writing shell-like scripts in raw Go is much less convenient, bec
 
 In scripts for system administration we often want to compose different operations like this in a quick and convenient way. If an error occurs somewhere along the pipeline, we would like to check this just once at the end, rather than after every operation.
 
-## Everything is a pipe
+# Everything is a pipe
 
 The `script` library allows us to do this because everything is a pipe (specifically, a `script.Pipe`). To create a pipe, start with a _source_ like `File()`:
 
@@ -88,7 +150,7 @@ if p.Error() != nil {
 }
 ```
 
-## What use is a pipe?
+# What use is a pipe?
 
 Now, what can you do with this pipe? You can call a method on it:
 
@@ -104,7 +166,7 @@ var q script.Pipe
 q = script.File("test.txt").Match("Error")
 ```
 
-## Handling errors
+# Handling errors
 
 Woah, woah! Just a minute! What if there was an error opening the file in the first place? Won't `Match` blow up if it tries to read from a non-existent file?
 
@@ -112,7 +174,7 @@ No, it won't. As soon as an error status is set on a pipe, all operations on the
 
 (Seasoned Gophers will recognise this as the `errWriter` pattern described by Rob Pike in the blog post [Errors are values](https://blog.golang.org/errors-are-values).)
 
-## Getting output
+# Getting output
 
 A pipe is useless if we can't get some output from it. To do this, you can use a _sink_, such as `String()`:
 
@@ -124,7 +186,7 @@ if err != nil {
 fmt.Println(result)
 ```
 
-## Errors
+# Errors
 
 Note that sinks return an error value in addition to the data. This is the same value you would get by calling `p.Error()`. If the pipe had an error in any operation along the pipeline, the pipe's error status will be set, and a sink operation which gets output will return the zero value, plus the error.
 
@@ -140,7 +202,7 @@ if err != nil {
 
 `CountLines()` is another useful sink, which simply returns the number of lines read from the pipe.
 
-## Closing pipes
+# Closing pipes
 
 If you've dealt with files in Go before, you'll know that you need to _close_ the file once you've finished with it. Otherwise, the program will retain what's called a _file handle_ (the kernel data structure which represents an open file). There is a limit to the total number of open file handles for a given program, and for the system as a whole, so a program which leaks file handles will eventually crash, and will waste resources in the meantime.
 
@@ -157,7 +219,78 @@ This is implemented using a type called `ReadAutoCloser`, which takes an `io.Rea
 
 _It is your responsibility to close a pipe if you do not read it to completion_.
 
-## Sources, filters, and sinks
+# Why not just use shell?
+
+It's a fair question. Shell scripts and one-liners are perfectly adequate for building one-off tasks, initialization scripts, and the kind of 'glue code' that holds the internet together. I speak as someone who's spent at least thirty years doing this for a living. But in many ways they're not ideal for important, non-trivial programs:
+
+* Trying to build portable shell scripts is a nightmare. The exact syntax and options of Unix commands varies from one distribution to another. Although in theory POSIX is a workable common subset of functionality, in practice it's usually precisely the non-POSIX behaviour that you need.
+
+* Shell scripts are hard to test (though test frameworks have been written, and if you're seriously putting mission-critical shell scripts into production, you should be using them, or reconsidering your technology choices).
+
+* Shell scripts don't scale. Because there are very limited facilities for logic and abstraction, and because any successful program tends to grow remorselessly over time, shell scripts can become an unreadable mess of special cases and spaghetti code. We've all seen it, if not, indeed, done it.
+
+* Shell syntax is awkward: quoting, whitespace, and brackets can require a lot of fiddling to get right, and so many characters are magic to the shell (`*`, `?`, `>` and so on) that this can lead to subtle bugs. Scripts can work fine for years until you suddenly encounter a file whose name contains whitespace, and then everything breaks horribly.
+
+* Deploying shell scripts obviously requires at least a (sizable) shell binary in addition to the source code, but it usually also requires an unknown and variable number of extra userland programs (`cut`, `grep`, `head`, and friends). If you're building container images, for example, you effectively need to include a whole Unix distribution with your program, which runs to hundreds of megabytes, and is not at all in the spirit of containers.
+
+To be fair to the shell, this kind of thing is not what it was ever intended for. Shell is an interactive job control tool for launching programs, connecting programs together, and to a limited extent, manipulating text. It's not for building portable, scalable, reliable, and elegant programs. That's what Go is for.
+
+Go has a superb testing framework built right into the standard library. It has a superb standard library, and thousands of high-quality third-party packages for just about any functionality you can imagine. It is compiled, so it's fast, and statically typed, so it's reliable. It's efficient and memory-safe. Go programs can be distributed as a single binary. Go scales to enormous projects (Kubernetes, for example).
+
+The `script` library is implemented entirely in Go, and does not require any userland programs (or any other dependencies) to be present. Thus you can build your `script` program as a container image containing a single (very small) binary, which is quick to build, quick to upload, quick to deploy, quick to run, and economical with resources.
+
+If you've ever struggled to get a shell script containing a simple `if` statement to work (and who hasn't?), then the `script` library is dedicated to you.
+
+# A real-world example
+
+Let's use `script` to write a program which system administrators might actually need. One thing I often find myself doing is counting the most frequent visitors to a website over a given period of time. Given an Apache log in the Common Log Format like this:
+
+```
+212.205.21.11 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 2028 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36"
+```
+
+we would like to extract the visitor's IP address (the first column in the logfile), and count the number of times this IP address occurs in the file. Finally, we might like to list the top 10 visitors by frequency. In a shell script we might do something like:
+
+```sh
+cut -d' ' -f 1 access.log |sort |uniq -c |sort -rn |head
+```
+
+There's a lot going on there, and it's pleasing to find that the equivalent `script` program is quite brief:
+
+```go
+package main
+
+import (
+	"github.com/bitfield/script"
+)
+
+func main() {
+	script.Stdin().Column(1).Freq().First(10).Stdout()
+}
+```
+
+(Thanks to [Lucas Bremgartner](https://github.com/breml) for suggesting this example. You can find the complete [program](examples/visitors/main.go), along with a sample [logfile](examples/visitors/access.log), in the [`examples/visitors/`](examples/visitors) directory.)
+
+# Quick start: Unix equivalents
+
+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)                             |
+
+# Sources, filters, and sinks
 
 `script` provides three types of pipe operations: sources, filters, and sinks.
 
@@ -167,11 +300,11 @@ _It is your responsibility to close a pipe if you do not read it to completion_.
 
 Let's look at the source, filter, and sink options that `script` provides.
 
-## Sources
+# Sources
 
 These are operations which create a pipe.
 
-### Args
+## Args
 
 `Args()` creates a pipe containing the program's command-line arguments, one per line.
 
@@ -182,7 +315,7 @@ fmt.Println(output)
 // Output: command-line arguments
 ```
 
-### Echo
+## Echo
 
 `Echo()` creates a pipe containing a given string:
 
@@ -193,7 +326,7 @@ fmt.Println(output)
 // Output: Hello, world!
 ```
 
-### Exec
+## Exec
 
 `Exec()` runs a given command and creates a pipe containing its combined output (`stdout` and `stderr`). If there was an error running the command, the pipe's error status will be set.
 
@@ -206,7 +339,7 @@ fmt.Println(output)
 
 Note that `Exec()` can also be used as a filter, in which case the given command will read from the pipe as its standard input.
 
-#### Exit status
+### Exit status
 
 If the command returns a non-zero exit status, the pipe's error status will be set to the string "exit status X", where X is the integer exit status.
 
@@ -229,7 +362,7 @@ fmt.Println(exit)
 
 The value of `ExitStatus()` will be zero unless the pipe's error status matches the string "exit status X", where X is a non-zero integer.
 
-#### Error output
+### Error output
 
 Even in the event of a non-zero exit status, the command's output will still be available in the pipe. This is often helpful for debugging. However, because `String()` is a no-op if the pipe's error status is set, if you want output you will need to reset the error status before calling `String()`:
 
@@ -241,7 +374,7 @@ fmt.Println(output)
 // Output: No manual entry for bogus
 ```
 
-### File
+## File
 
 `File()` creates a pipe that reads from a file.
 
@@ -252,7 +385,7 @@ fmt.Println(output)
 // Output: contents of file
 ```
 
-### Stdin
+## Stdin
 
 `Stdin()` creates a pipe which reads from the program's standard input.
 
@@ -263,11 +396,39 @@ fmt.Println(output)
 // Output: [contents of standard input]
 ```
 
-## Filters
+# Filters
 
 Filters are operations on an existing pipe that also return a pipe, allowing you to chain filters indefinitely.
 
-### Concat
+## Column
+
+`Column()` reads input tabulated by whitespace, and outputs only the Nth column of each input line (like Unix `cut`). Lines containing less than N columns will be ignored.
+
+For example, given this input:
+
+```
+  PID   TT  STAT      TIME COMMAND
+    1   ??  Ss   873:17.62 /sbin/launchd
+   50   ??  Ss    13:18.13 /usr/libexec/UserEventAgent (System)
+   51   ??  Ss    22:56.75 /usr/sbin/syslogd
+```
+
+and this program:
+
+```go
+script.Stdin().Column(1).Stdout()
+```
+
+this will be the output:
+
+```
+PID
+1
+50
+51
+```
+
+## Concat
 
 `Concat()` reads a list of filenames from the pipe, one per line, and creates a pipe which concatenates the contents of those files. For example, if you have files `a`, `b`, and `c`:
 
@@ -302,7 +463,7 @@ p := Exec("ls /var/app/config/").Concat().Stdout()
 
 Each input file will be closed once it has been fully read.
 
-### EachLine
+## EachLine
 
 `EachLine()` lets you create custom filters. You provide a function, and it will be called once for each line of input. If you want to produce output, your function can write to a supplied `strings.Builder`. The return value from EachLine is a pipe containing your output.
 
@@ -315,7 +476,7 @@ output, err := q.String()
 fmt.Println(output)
 ```
 
-### Exec
+## Exec
 
 `Exec()` runs a given command, which will read from the pipe as its standard input, and returns a pipe containing the command's combined output (`stdout` and `stderr`). If there was an error running the command, the pipe's error status will be set.
 
@@ -329,7 +490,7 @@ fmt.Println(output)
 // Output: hello world
 ```
 
-### First
+## First
 
 `First()` reads its input and passes on the first N lines of it (like Unix [`head`](examples/head/main.go)):
 
@@ -337,7 +498,7 @@ fmt.Println(output)
 script.Stdin().First(10).Stdout()
 ```
 
-### Freq
+## Freq
 
 `Freq()` counts the frequencies of input lines, and outputs only the unique lines in the input, each prefixed with a count of its frequency, in descending order of frequency (that is, most frequent lines first). Lines with the same frequency will be sorted alphabetically. For example, given this input:
 
@@ -375,7 +536,16 @@ sort testdata/freq.input.txt |uniq -c |sort -rn
 script.Stdin().Freq().First(10).Stdout()
 ```
 
-### Join
+Like `uniq -c`, `Freq()` left-pads its count values if necessary to make them easier to read:
+
+```
+10 apple
+ 4 banana
+ 2 orange
+ 1 kumquat
+```
+
+## Join
 
 `Join()` reads its input and replaces newlines with spaces, preserving a terminating newline if there is one.
 
@@ -386,7 +556,7 @@ fmt.Println(output)
 // Output: hello world\n
 ```
 
-### Match
+## Match
 
 `Match()` returns a pipe containing only the input lines which match the supplied string:
 
@@ -394,7 +564,7 @@ fmt.Println(output)
 p := script.File("test.txt").Match("Error")
 ```
 
-### MatchRegexp
+## MatchRegexp
 
 `MatchRegexp()` is like `Match()`, but takes a compiled regular expression instead of a string.
 
@@ -402,7 +572,7 @@ p := script.File("test.txt").Match("Error")
 p := script.File("test.txt").MatchRegexp(regexp.MustCompile(`E.*r`))
 ```
 
-### Reject
+## Reject
 
 `Reject()` is the inverse of `Match()`. Its pipe produces only lines which _don't_ contain the given string:
 
@@ -410,7 +580,7 @@ p := script.File("test.txt").MatchRegexp(regexp.MustCompile(`E.*r`))
 p := script.File("test.txt").Match("Error").Reject("false alarm")
 ```
 
-### RejectRegexp
+## RejectRegexp
 
 `RejectRegexp()` is like `Reject()`, but takes a compiled regular expression instead of a string.
 
@@ -418,11 +588,11 @@ p := script.File("test.txt").Match("Error").Reject("false alarm")
 p := script.File("test.txt").Match("Error").RejectRegexp(regexp.MustCompile(`false|bogus`))
 ```
 
-## Sinks
+# Sinks
 
 Sinks are operations which return some data from a pipe, ending the pipeline.
 
-### AppendFile
+## AppendFile
 
 `AppendFile()` is like `WriteFile()`, but appends to the destination file instead of overwriting it. It returns the number of bytes written, or an error:
 
@@ -431,7 +601,7 @@ var wrote int
 wrote, err := script.Echo("Got this far!").AppendFile("logfile.txt")
 ```
 
-### Bytes
+## Bytes
 
 `Bytes()` returns the contents of the pipe as a slice of byte, plus an error:
 
@@ -440,7 +610,7 @@ var data []byte
 data, err := script.File("test.bin").Bytes()
 ```
 
-### CountLines
+## CountLines
 
 `CountLines()`, as the name suggests, counts lines in its input, and returns the number of lines as an integer, plus an error:
 
@@ -449,7 +619,7 @@ var numLines int
 numLines, err := script.File("test.txt").CountLines()
 ```
 
-### Read
+## Read
 
 `Read()` behaves just like the standard `Read()` method on any `io.Reader`:
 
@@ -462,7 +632,7 @@ Because a Pipe is an `io.Reader`, you can use it anywhere you would use a file,
 
 Unlike most sinks, `Read()` does not read the whole contents of the pipe (unless the supplied buffer is big enough to hold them).
 
-### Stdout
+## Stdout
 
 `Stdout()` writes the contents of the pipe to the program's standard output. It returns the number of bytes written, or an error:
 
@@ -487,7 +657,7 @@ func main() {
 }
 ```
 
-### String
+## String
 
 `String()` returns the contents of the pipe as a string, plus an error:
 
@@ -505,7 +675,7 @@ fmt.Println(err)
 // Output: read test.txt: file already closed
 ```
 
-### WriteFile
+## WriteFile
 
 `WriteFile()` writes the contents of the pipe to a named file. It returns the number of bytes written, or an error:
 
@@ -514,11 +684,11 @@ var wrote int
 wrote, err := script.File("source.txt").WriteFile("destination.txt")
 ```
 
-## Writing your own pipe operations
+# Writing your own pipe operations
 
 There's nothing to stop you writing your own sources, sinks, or filters (in fact, that would be excellent. Please submit a pull request if you want to add them to the standard operations supplied with `script`.)
 
-### Writing a source
+## Writing a source
 
 All a pipe source has to do is return a pointer to a `script.Pipe`. To be useful, a pipe needs to have a reader (a data source, such as a file) associated with it.
 
@@ -552,7 +722,7 @@ func File(name string) *script.Pipe {
 }
 ```
 
-### Writing a filter
+## Writing a filter
 
 Filters are methods on pipes, that return pipes. For example, here's a simple filter which just reads and rejects all input, returning an empty pipe:
 
@@ -581,7 +751,7 @@ As you can see from the example, the pipe's reader is available to you as `p.Rea
 
 If your method modifies the pipe (for example if it can set an error on the pipe), it must take a pointer receiver, as in this example. Otherwise, it can take a value receiver.
 
-### Writing a sink
+## Writing a sink
 
 Any method on a pipe which returns something other than a pipe is a sink. For example, here's an implementation of `String()`:
 
@@ -605,11 +775,11 @@ We then try to read the whole contents of the pipe. If we get an error on readin
 
 Otherwise, we return the result of reading the pipe, and a nil error.
 
-## Ideas
+# Ideas
 
 These are some ideas I'm playing with for additional features. If you feel like working on one of them, send a pull request. If you have ideas for other features, open an issue (or, better, a pull request).
 
-### Sources
+## Sources
 
 * `Get()` makes a web request, like `curl`, and pipes the result
 * `Net()` makes a network connection to a specified address and port, and reads the connection until it's closed
@@ -617,16 +787,15 @@ These are some ideas I'm playing with for additional features. If you feel like
 * `Find()` pipes a list of files matching various criteria (name, modified time, and so on)
 * `Processes()` pipes the list of running processes, like `ps`.
 
-### Filters
+## Filters
 
-* `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).
+* [Ideas welcome!](https://github.com/bitfield/script/issues/new)
 
-### Sinks
+## Sinks
 
 * [Ideas welcome!](https://github.com/bitfield/script/issues/new)
 
-### Examples
+# Examples
 
 Since `script` is designed to help you write system administration programs, a few simple examples of such programs are included in the [examples](examples/) directory:
 
@@ -635,10 +804,11 @@ 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)
+* [visitors](examples/visitors/main.go)
 
 [More examples would be welcome!](https://github.com/bitfield/script/pulls)
 
-### Use cases
+# 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](https://github.com/bitfield/script/issues/new) - I'd love to hear from you.
 
diff --git a/examples/visitors/access.log b/examples/visitors/access.log
new file mode 100644
index 0000000..5049388
--- /dev/null
+++ b/examples/visitors/access.log
@@ -0,0 +1,25 @@
+212.205.21.11 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 2028 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36"
+212.205.21.11 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 162544 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36"
+212.205.21.11 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 9419 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36"
+212.205.21.11 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 2058 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36"
+212.205.21.11 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 343743 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36"
+212.205.21.11 - - [30/Jun/2019:17:06:16 +0000] "GET / HTTP/1.1" 200 1150 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36"
+212.205.21.11 - - [30/Jun/2019:17:06:16 +0000] "GET / HTTP/1.1" 200 2946 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36"
+176.182.2.191 - - [30/Jun/2019:17:06:17 +0000] "GET / HTTP/1.1" 200 13278 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 29474 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 29349 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 48271 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 1380 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 2028 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 91819 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:19 +0000] "GET / HTTP/1.1" 200 305667 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 13194 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 12935 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 14598 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 22458 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 200 15737 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:20 +0000] "GET / HTTP/1.1" 404 17679 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+176.182.2.191 - - [30/Jun/2019:17:06:23 +0000] "GET / HTTP/1.1" 200 5995 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+190.253.121.1 - - [30/Jun/2019:17:06:23 +0000] "GET / HTTP/1.1" 200 8809 "-" "Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-J415FN Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.2 Chrome/67.0.3396.87 Mobile Safari/537.36"
+176.182.2.191 - - [30/Jun/2019:17:06:24 +0000] "GET / HTTP/1.1" 200 162544 "https://example.com/ "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1"
+90.53.111.17 - - [30/Jun/2019:17:06:24 +0000] "GET / HTTP/1.1" 200 - "https://example.com/ "Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-J415FN Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.2 Chrome/67.0.3396.87 Mobile Safari/537.36"
\ No newline at end of file
diff --git a/examples/visitors/go.mod b/examples/visitors/go.mod
new file mode 100644
index 0000000..4de0cd2
--- /dev/null
+++ b/examples/visitors/go.mod
@@ -0,0 +1,5 @@
+module visitors
+
+go 1.12
+
+require github.com/bitfield/script v0.9.0
\ No newline at end of file
diff --git a/examples/visitors/main.go b/examples/visitors/main.go
new file mode 100644
index 0000000..767c464
--- /dev/null
+++ b/examples/visitors/main.go
@@ -0,0 +1,24 @@
+/*
+This program reads an Apache logfile in Common Log Format, like this:
+
+212.205.21.11 - - [30/Jun/2019:17:06:15 +0000] "GET / HTTP/1.1" 200 2028 "https://example.com/ "Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX1 Build/HUAWEIFIG-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.156 Mobile Safari/537.36"
+
+It extracts the first column of each line (the visitor IP address), counts the
+frequency of each unique IP address in the log, and outputs the 10 most frequent
+visitors in the log. Example output:
+
+16 176.182.2.191
+7 212.205.21.11
+1 190.253.121.1
+1 90.53.111.17
+
+*/
+package main
+
+import (
+	"github.com/bitfield/script"
+)
+
+func main() {
+	script.Stdin().Column(1).Freq().First(10).Stdout()
+}
diff --git a/filters.go b/filters.go
index 8095180..d4e289a 100644
--- a/filters.go
+++ b/filters.go
@@ -9,6 +9,7 @@ import (
 	"os/exec"
 	"regexp"
 	"sort"
+	"strconv"
 	"strings"
 )
 
@@ -19,7 +20,7 @@ func (p *Pipe) Match(s string) *Pipe {
 	return p.EachLine(func(line string, out *strings.Builder) {
 		if strings.Contains(line, s) {
 			out.WriteString(line)
-			out.WriteByte('\n')
+			out.WriteRune('\n')
 		}
 	})
 }
@@ -31,7 +32,7 @@ func (p *Pipe) MatchRegexp(re *regexp.Regexp) *Pipe {
 	return p.EachLine(func(line string, out *strings.Builder) {
 		if re.MatchString(line) {
 			out.WriteString(line)
-			out.WriteByte('\n')
+			out.WriteRune('\n')
 		}
 	})
 }
@@ -43,7 +44,7 @@ func (p *Pipe) Reject(s string) *Pipe {
 	return p.EachLine(func(line string, out *strings.Builder) {
 		if !strings.Contains(line, s) {
 			out.WriteString(line)
-			out.WriteByte('\n')
+			out.WriteRune('\n')
 		}
 	})
 }
@@ -55,7 +56,7 @@ func (p *Pipe) RejectRegexp(re *regexp.Regexp) *Pipe {
 	return p.EachLine(func(line string, out *strings.Builder) {
 		if !re.MatchString(line) {
 			out.WriteString(line)
-			out.WriteByte('\n')
+			out.WriteRune('\n')
 		}
 	})
 }
@@ -153,6 +154,7 @@ 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)
@@ -178,8 +180,12 @@ func (p *Pipe) Freq() *Pipe {
 		count int
 	}
 	var freqs = make([]frequency, 0, len(freq))
+	var maxCount int
 	for line, count := range freq {
 		freqs = append(freqs, frequency{line, count})
+		if count > maxCount {
+			maxCount = count
+		}
 	}
 	sort.Slice(freqs, func(i, j int) bool {
 		if freqs[i].count == freqs[j].count {
@@ -187,10 +193,26 @@ func (p *Pipe) Freq() *Pipe {
 		}
 		return freqs[i].count > freqs[j].count
 	})
+	fieldWidth := len(strconv.Itoa(maxCount))
 	var output strings.Builder
 	for _, item := range freqs {
-		output.WriteString(fmt.Sprintf("%d %s", item.count, item.line))
+		output.WriteString(fmt.Sprintf("%*d %s", fieldWidth, item.count, item.line))
 		output.WriteRune('\n')
 	}
 	return Echo(output.String())
 }
+
+// Column reads from the pipe, and returns a new pipe containing only the Nth
+// column of each line in the input, where '1' means the first column, and
+// columns are delimited by whitespace. Specifically, whatever Unicode defines
+// as whitespace ('WSpace=yes'). If there is an error reading the pipe, the
+// pipe's error status is also set.
+func (p *Pipe) Column(col int) *Pipe {
+	return p.EachLine(func(line string, out *strings.Builder) {
+		columns := strings.Fields(line)
+		if col <= len(columns) {
+			out.WriteString(columns[col-1])
+			out.WriteRune('\n')
+		}
+	})
+}


Test patch:
diff --git a/filters_test.go b/filters_test.go
index 7d51b01..5d7c752 100644
--- a/filters_test.go
+++ b/filters_test.go
@@ -226,23 +226,30 @@ func TestFirst(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	got, err := File("testdata/first.input.txt").First(10).Bytes()
+	input := File("testdata/first.input.txt")
+	got, err := input.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()
+	_, err = ioutil.ReadAll(input.Reader)
+	if err == nil {
+		t.Error("input not closed after reading")
+	}
+	input = File("testdata/first.input.txt")
+	gotZero, err := input.First(0).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.
+	_, 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)
@@ -270,3 +277,18 @@ func TestFreq(t *testing.T) {
 		t.Errorf("want %q, got %q", want, got)
 	}
 }
+
+func TestColumn(t *testing.T) {
+	t.Parallel()
+	want, err := ioutil.ReadFile("testdata/column.golden.txt")
+	if err != nil {
+		t.Fatal(err)
+	}
+	got, err := File("testdata/column.input.txt").Column(3).Bytes()
+	if err != nil {
+		t.Error(err)
+	}
+	if !bytes.Equal(got, want) {
+		t.Errorf("want %q, got %q", want, got)
+	}
+}
diff --git a/pipes_test.go b/pipes_test.go
index 01febbd..949d776 100644
--- a/pipes_test.go
+++ b/pipes_test.go
@@ -104,6 +104,8 @@ func doMethodsOnPipe(t *testing.T, p *Pipe, kind string) {
 	p.First(1)
 	action = "Freq()"
 	p.Freq()
+	action = "Column()"
+	p.Column(2)
 }
 
 func TestNilPipes(t *testing.T) {
diff --git a/testdata/column.golden.txt b/testdata/column.golden.txt
new file mode 100644
index 0000000..6acf666
--- /dev/null
+++ b/testdata/column.golden.txt
@@ -0,0 +1,8 @@
+Ss+
+R+
+Ss
+Ss+
+Ss+
+Ss+
+Ss+
+Ss
diff --git a/testdata/column.input.txt b/testdata/column.input.txt
new file mode 100644
index 0000000..7335a81
--- /dev/null
+++ b/testdata/column.input.txt
@@ -0,0 +1,9 @@
+60916 s003  Ss+    0:00.51 /bin/bash -l
+ 6653 s004  R+     0:00.01 ps ax
+bogus line
+80159 s004  Ss     0:00.56 /bin/bash -l
+60942 s006  Ss+    0:00.53 /bin/bash -l
+60943 s007  Ss+    0:00.51 /bin/bash -l
+60977 s009  Ss+  	0:00.52 /bin/bash -l
+	60978 s010  Ss+    0:00.53 /bin/bash -l
+61356 s011	Ss     0:00.54 /bin/bash -l
diff --git a/testdata/freq.golden.txt b/testdata/freq.golden.txt
index ca7d8b2..ae81bc0 100644
--- a/testdata/freq.golden.txt
+++ b/testdata/freq.golden.txt
@@ -1,4 +1,4 @@
-4 apple
-4 banana
-2 orange
-1 kumquat
+10 apple
+ 4 banana
+ 2 orange
+ 1 kumquat
diff --git a/testdata/freq.input.txt b/testdata/freq.input.txt
index e15ba3e..67762b1 100644
--- a/testdata/freq.input.txt
+++ b/testdata/freq.input.txt
@@ -8,4 +8,10 @@ kumquat
 apple
 apple
 banana
-banana
\ No newline at end of file
+banana
+apple
+apple
+apple
+apple
+apple
+apple