Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ script.Args().Concat().Match("Error").First(10).AppendFile("/var/log/errors.txt"
- [Slice](#slice)
- [Stdin](#stdin)
- [Filters](#filters)
- [Basename](#basename)
- [Column](#column)
- [Concat](#concat)
- [Dirname](#dirname)
- [EachLine](#eachline)
- [Exec](#exec-1)
- [First](#first)
Expand Down Expand Up @@ -278,8 +280,10 @@ If you're already familiar with shell scripting and the Unix toolset, here is a
| `>` | [`WriteFile()`](#writefile) |
| `>>` | [`AppendFile()`](#appendfile) |
| `$*` | [`Args()`](#args) |
| `basename` | [`Basename()`](#basename) |
| `cat` | [`File()`](#file) / [`Concat()`](#concat) |
| `cut` | [`Column()`](#column) |
| `dirname` | [`Dirname()`](#dirname) |
| `echo` | [`Echo()`](#echo) |
| `grep` | [`Match()`](#match) / [`MatchRegexp()`](#matchregexp) |
| `grep -v` | [`Reject()`](#reject) / [`RejectRegexp()`](#rejectregexp) |
Expand Down Expand Up @@ -445,6 +449,24 @@ fmt.Println(output)

Filters are operations on an existing pipe that also return a pipe, allowing you to chain filters indefinitely.

## Basename

`Basename()` reads a list of filepaths from the pipe, one per line, and removes any leading directory components from each line (so, for example, `/usr/local/bin/foo` would become just `foo`). This is the complement of [Dirname](#dirname).

If a line is empty, `Basename()` will produce a single dot: `.`. Trailing slashes are removed.

Examples:

| Input | `Basename` output |
| ------------------ | ----------------- |
| | `.` |
| `/` | `.` |
| `/root` | `root` |
| `/tmp/example.php` | `example.php` |
| `/var/tmp/` | `tmp` |
| `./src/filters` | `filters` |
| `C:/Program Files` | `Program Files` |

## 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.
Expand Down Expand Up @@ -508,6 +530,26 @@ p := Exec("ls /var/app/config/").Concat().Stdout()

Each input file will be closed once it has been fully read.

## Dirname

`Dirname()` reads a list of pathnames from the pipe, one per line, and returns a pipe which contains only the parent directories of each pathname (so, for example, `/usr/local/bin/foo` would become just `/usr/local/bin`). This is the complement of [Basename](#basename).

If a line is empty, `Dirname()` will convert it to a single dot: `.` (this is the behaviour of Unix `dirname` and the Go standard library's `filepath.Dir`).

Trailing slashes are removed, unless `Dirname()` returns the root folder.

Examples:

| Input | `Dirname` output |
| ------------------ | ---------------- |
| | `.` |
| `/` | `/` |
| `/root` | `/` |
| `/tmp/example.php` | `/tmp` |
| `/var/tmp/` | `/var` |
| `./src/filters` | `./src` |
| `C:/Program Files` | `C:` |

## 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.
Expand Down
37 changes: 37 additions & 0 deletions filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,26 @@ import (
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)

// Basename reads a list of filepaths from the pipe, one per line, and removes
// any leading directory components from each line. If a line is empty, Basename
// will produce '.'. Trailing slashes are removed.
func (p *Pipe) Basename() *Pipe {
if p == nil || p.Error() != nil {
return p
}
return p.EachLine(func(line string, out *strings.Builder) {
out.WriteString(filepath.Base(line))
out.WriteRune('\n')
})
}

// 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
Expand Down Expand Up @@ -49,6 +63,29 @@ func (p *Pipe) Concat() *Pipe {
return p.WithReader(io.MultiReader(readers...))
}

// Dirname reads a list of pathnames from the pipe, one per line, and returns a
// pipe which contains only the parent directories of each pathname. If a line
// is empty, Dirname will produce a '.'. Trailing slashes are removed, unless
// Dirname returns the root folder.
func (p *Pipe) Dirname() *Pipe {
if p == nil || p.Error() != nil {
return p
}
return p.EachLine(func(line string, out *strings.Builder) {
// filepath.Dir() does not handle trailing slashes correctly
if len(line) > 1 && strings.HasSuffix(line, "/") {
line = line[0 : len(line)-1]
}
dirname := filepath.Dir(line)
// filepath.Dir() does not preserve a leading './'
if len(dirname) > 1 && strings.HasPrefix(line, "./") {
dirname = "./" + dirname
}
out.WriteString(dirname)
out.WriteRune('\n')
})
}

// EachLine calls the specified function for each line of input, passing it the
// line as a string, and a *strings.Builder to write its output to. The return
// value from EachLine is a pipe containing the contents of the strings.Builder.
Expand Down
149 changes: 68 additions & 81 deletions filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,87 +9,6 @@ import (
"testing"
)

// doFiltersOnPipe calls every kind of filter method on the supplied pipe and
// tries to trigger a panic.
func doFiltersOnPipe(t *testing.T, p *Pipe, kind string) {
var action string
defer func() {
if r := recover(); r != nil {
t.Errorf("panic: %s on %s pipe", action, kind)
}
}()
// also tests methods that wrap EachLine, such as Match*/Reject*
action = "EachLine()"
output, err := p.EachLine(func(string, *strings.Builder) {}).String()
if err != nil {
t.Error(err)
}
if output != "" {
t.Errorf("want zero output from %s on %s pipe, but got %q", action, kind, output)
}
action = "Exec()"
output, err = p.Exec("bogus").String()
if err != nil && kind == "nil" {
t.Errorf("%s on %s pipe: %v", action, kind, err)
}
if err == nil && kind == "zero" {
t.Errorf("want error from %s on %s pipe, but got nil", action, kind)
}
if output != "" {
t.Errorf("want zero output from %s on %s pipe, but got %q", action, kind, output)
}
action = "Concat()"
output, err = p.Concat().String()
if err != nil {
t.Errorf("%s on %s pipe: %v", action, kind, err)
}
if output != "" {
t.Errorf("want zero output from %s on %s pipe, but got %q", action, kind, output)
}
action = "First()"
output, err = p.First(1).String()
if err != nil {
t.Errorf("%s on %s pipe: %v", action, kind, err)
}
if output != "" {
t.Errorf("want zero output from %s on %s pipe, but got %q", action, kind, output)
}
action = "Freq()"
output, err = p.Freq().String()
if err != nil {
t.Errorf("%s on %s pipe: %v", action, kind, err)
}
if output != "" {
t.Errorf("want zero output from %s on %s pipe, but got %q", action, kind, output)
}
action = "Join()"
output, err = p.Join().String()
if err != nil {
t.Errorf("%s on %s pipe: %v", action, kind, err)
}
if output != "" {
t.Errorf("want zero output from %s on %s pipe, but got %q", action, kind, output)
}
action = "Last()"
output, err = p.Last(1).String()
if err != nil {
t.Errorf("%s on %s pipe: %v", action, kind, err)
}
if output != "" {
t.Errorf("want zero output from %s on %s pipe, but got %q", action, kind, output)
}
}

func TestNilPipeFilters(t *testing.T) {
t.Parallel()
doFiltersOnPipe(t, nil, "nil")
}

func TestZeroPipeFilters(t *testing.T) {
t.Parallel()
doFiltersOnPipe(t, &Pipe{}, "zero")
}

func TestMatch(t *testing.T) {
t.Parallel()
testCases := []struct {
Expand Down Expand Up @@ -424,3 +343,71 @@ func TestColumn(t *testing.T) {
t.Errorf("want %q, got %q", want, got)
}
}

func TestBasename(t *testing.T) {
t.Parallel()
testCases := []struct {
testFileName string
want string
}{
{"\n", ".\n"},
{"/", "/\n"},
{"/root", "root\n"},
{"/tmp/example.php", "example.php\n"},
{"./src/filters", "filters\n"},
{"/var/tmp/example.php", "example.php\n"},
{"/tmp/script-21345.txt\n/tmp/script-5371253.txt", "script-21345.txt\nscript-5371253.txt\n"},
{"C:/Program Files", "Program Files\n"},
{"C:/Program Files/", "Program Files\n"},
}
for _, tc := range testCases {
got, err := Echo(tc.testFileName).Basename().String()
if err != nil {
t.Error(err)
}
if got != tc.want {
t.Errorf("%q: want %q, got %q", tc.testFileName, tc.want, got)
}
}
}

func TestDirname(t *testing.T) {
t.Parallel()
testCases := []struct {
testFileName string
want string
}{
{"/", "/\n"},
{"\n", ".\n"},
{"/root", "/\n"},
{"/tmp/example.php", "/tmp\n"},
{"/var/tmp/example.php", "/var/tmp\n"},
{"/var/tmp", "/var\n"},
{"/var/tmp/", "/var\n"},
{"./src/filters", "./src\n"},
{"./src/filters/", "./src\n"},
{"/tmp/script-21345.txt\n/tmp/script-5371253.txt", "/tmp\n/tmp\n"},
{"C:/Program Files/PHP", "C:/Program Files\n"},
{"C:/Program Files/PHP/", "C:/Program Files\n"},
{"C:/Program Files", "C:\n"},

// NOTE:
// there are no tests for Windows-style directory separators,
// because these are not supported by filepath at this time
//
// the following test data currently does not work with the
// Golang filepath library:
//
// {"C:\\Program Files\\PHP", "C:\\Program Files\n"},
// {"C:\\Program Files\\PHP\\", "C:\\Program Files\n"},
}
for _, tc := range testCases {
got, err := Echo(tc.testFileName).Dirname().String()
if err != nil {
t.Error(err)
}
if got != tc.want {
t.Errorf("%q: want %q, got %q", tc.testFileName, tc.want, got)
}
}
}
8 changes: 8 additions & 0 deletions pipes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ func doMethodsOnPipe(t *testing.T, p *Pipe, kind string) {
defer os.Remove("testdata/bogus.txt")
action = "AppendFile()"
p.AppendFile("testdata/bogus.txt")
action = "Basename()"
p.Basename()
action = "Bytes()"
p.Bytes()
action = "Close()"
Expand All @@ -101,6 +103,10 @@ func doMethodsOnPipe(t *testing.T, p *Pipe, kind string) {
p.Concat()
action = "CountLines()"
p.CountLines()
action = "Dirname()"
p.Dirname()
action = "EachLine()"
p.EachLine(func(string, *strings.Builder) {})
action = "Error()"
p.Error()
action = "Exec()"
Expand All @@ -113,6 +119,8 @@ func doMethodsOnPipe(t *testing.T, p *Pipe, kind string) {
p.Freq()
action = "Join()"
p.Join()
action = "Last()"
p.Last(1)
action = "Match()"
p.Match("foo")
action = "MatchRegexp()"
Expand Down