Skip to content

Commit

Permalink
Merge branch 'master' into exec-with-context
Browse files Browse the repository at this point in the history
  • Loading branch information
sk91 authored Aug 29, 2024
2 parents 3c3977e + 0daf4b2 commit eefb8ae
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 6 deletions.
1 change: 1 addition & 0 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: Security audit
on:
pull_request:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ If you're already familiar with shell scripting and the Unix toolset, here is a
| `>` | [`WriteFile`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WriteFile) |
| `>>` | [`AppendFile`](https://pkg.go.dev/github.com/bitfield/script#Pipe.AppendFile) |
| `$*` | [`Args`](https://pkg.go.dev/github.com/bitfield/script#Args) |
| `base64` | [`DecodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.DecodeBase64) / [`EncodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.EncodeBase64) |
| `basename` | [`Basename`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Basename) |
| `cat` | [`File`](https://pkg.go.dev/github.com/bitfield/script#File) / [`Concat`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Concat) |
| `curl` | [`Do`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Do) / [`Get`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Get) / [`Post`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Post) |
Expand Down Expand Up @@ -290,9 +291,11 @@ Filters are methods on an existing pipe that also return a pipe, allowing you to
| [`Basename`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Basename) | removes leading path components from each line, leaving only the filename |
| [`Column`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Column) | Nth column of input |
| [`Concat`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Concat) | contents of multiple files |
| [`DecodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.DecodeBase64) | input decoded from base64 |
| [`Dirname`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Dirname) | removes filename from each line, leaving only leading path components |
| [`Do`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Do) | response to supplied HTTP request |
| [`Echo`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Echo) | all input replaced by given string |
| [`EncodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.EncodeBase64) | input encoded to base64 |
| [`Exec`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Exec) | filtered through external command |
| [`ExecForEach`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ExecForEach) | execute given command template for each line of input |
| [`Filter`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Filter) | user-supplied function filtering a reader to a writer |
Expand Down Expand Up @@ -330,13 +333,14 @@ Sinks are methods that return some data from a pipe, ending the pipeline and ext
| [`Slice`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Slice) | | data as `[]string`, error |
| [`Stdout`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Stdout) | standard output | bytes written, error |
| [`String`](https://pkg.go.dev/github.com/bitfield/script#Pipe.String) | | data as `string`, error |
| [`Wait`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Wait) | | none |
| [`Wait`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Wait) | | error |
| [`WriteFile`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WriteFile) | specified file, truncating if it exists | bytes written, error |

# What's new

| Version | New |
| ----------- | ------- |
| _next_ | [`DecodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.DecodeBase64) / [`EncodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.EncodeBase64) |
| v0.22.0 | [`Tee`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Tee), [`WithStderr`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithStderr) |
| v0.21.0 | HTTP support: [`Do`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Do), [`Get`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Get), [`Post`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Post) |
| v0.20.0 | [`JQ`](https://pkg.go.dev/github.com/bitfield/script#Pipe.JQ) |
Expand All @@ -347,7 +351,7 @@ See the [contributor's guide](CONTRIBUTING.md) for some helpful tips if you'd li

# Links

- [Scripting with Go](https://bitfieldconsulting.com/golang/scripting)
- [Scripting with Go](https://bitfieldconsulting.com/posts/scripting)
- [Code Club: Script](https://www.youtube.com/watch?v=6S5EqzVwpEg)
- [Bitfield Consulting](https://bitfieldconsulting.com/)
- [Go books by John Arundel](https://bitfieldconsulting.com/books)
Expand Down
38 changes: 34 additions & 4 deletions script.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"container/ring"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -286,6 +287,18 @@ func (p *Pipe) CountLines() (lines int, err error) {
return lines, p.Error()
}

// DecodeBase64 produces the string represented by the base64 encoded input.
func (p *Pipe) DecodeBase64() *Pipe {
return p.Filter(func(r io.Reader, w io.Writer) error {
decoder := base64.NewDecoder(base64.StdEncoding, r)
_, err := io.Copy(w, decoder)
if err != nil {
return err
}
return nil
})
}

// Dirname reads paths from the pipe, one per line, and produces only the
// parent directories of each path. For example, /usr/local/bin/foo would
// become just /usr/local/bin. This is the complementary operation to
Expand Down Expand Up @@ -358,7 +371,23 @@ func (p *Pipe) Echo(s string) *Pipe {
return p.WithReader(strings.NewReader(s))
}

// EncodeBase64 produces the base64 encoding of the input.
func (p *Pipe) EncodeBase64() *Pipe {
return p.Filter(func(r io.Reader, w io.Writer) error {
encoder := base64.NewEncoder(base64.StdEncoding, w)
defer encoder.Close()
_, err := io.Copy(encoder, r)
if err != nil {
return err
}
return nil
})
}

// Error returns any error present on the pipe, or nil otherwise.
// Error is not a sink and does not wait until the pipe reaches
// completion. To wait for completion before returning the error,
// see [Pipe.Wait].
func (p *Pipe) Error() error {
if p.mu == nil { // uninitialised pipe
return nil
Expand Down Expand Up @@ -948,14 +977,15 @@ func (p *Pipe) Tee(writers ...io.Writer) *Pipe {
return p.WithReader(io.TeeReader(p.Reader, teeWriter))
}

// Wait reads the pipe to completion and discards the result. This is mostly
// useful for waiting until concurrent filters have completed (see
// [Pipe.Filter]).
func (p *Pipe) Wait() {
// Wait reads the pipe to completion and returns any error present on
// the pipe, or nil otherwise. This is mostly useful for waiting until
// concurrent filters have completed (see [Pipe.Filter]).
func (p *Pipe) Wait() error {
_, err := io.Copy(io.Discard, p)
if err != nil {
p.SetError(err)
}
return p.Error()
}

// WithError sets the error err on the pipe.
Expand Down
133 changes: 133 additions & 0 deletions script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1867,6 +1867,127 @@ func TestReadReturnsErrorGivenReadErrorOnPipe(t *testing.T) {
}
}

func TestWait_ReturnsErrorPresentOnPipe(t *testing.T) {
t.Parallel()
p := script.Echo("a\nb\nc\n").ExecForEach("{{invalid template syntax}}")
if p.Wait() == nil {
t.Error("want error, got nil")
}
}

func TestWait_DoesNotReturnErrorForValidExecution(t *testing.T) {
t.Parallel()
p := script.Echo("a\nb\nc\n").ExecForEach("echo \"{{.}}\"")
if err := p.Wait(); err != nil {
t.Fatal(err)
}
}

var base64Cases = []struct {
name string
decoded string
encoded string
}{
{
name: "empty string",
decoded: "",
encoded: "",
},
{
name: "single line string",
decoded: "hello world",
encoded: "aGVsbG8gd29ybGQ=",
},
{
name: "multi line string",
decoded: "hello\nthere\nworld\n",
encoded: "aGVsbG8KdGhlcmUKd29ybGQK",
},
}

func TestEncodeBase64_CorrectlyEncodes(t *testing.T) {
t.Parallel()
for _, tc := range base64Cases {
t.Run(tc.name, func(t *testing.T) {
got, err := script.Echo(tc.decoded).EncodeBase64().String()
if err != nil {
t.Fatal(err)
}
if got != tc.encoded {
t.Logf("input %q incorrectly encoded:", tc.decoded)
t.Error(cmp.Diff(tc.encoded, got))
}
})
}
}

func TestDecodeBase64_CorrectlyDecodes(t *testing.T) {
t.Parallel()
for _, tc := range base64Cases {
t.Run(tc.name, func(t *testing.T) {
got, err := script.Echo(tc.encoded).DecodeBase64().String()
if err != nil {
t.Fatal(err)
}
if got != tc.decoded {
t.Logf("input %q incorrectly decoded:", tc.encoded)
t.Error(cmp.Diff(tc.decoded, got))
}
})
}
}

func TestEncodeBase64_FollowedByDecodeRecoversOriginal(t *testing.T) {
t.Parallel()
for _, tc := range base64Cases {
t.Run(tc.name, func(t *testing.T) {
decoded, err := script.Echo(tc.decoded).EncodeBase64().DecodeBase64().String()
if err != nil {
t.Fatal(err)
}
if decoded != tc.decoded {
t.Error("encode-decode round trip failed:", cmp.Diff(tc.decoded, decoded))
}
encoded, err := script.Echo(tc.encoded).DecodeBase64().EncodeBase64().String()
if err != nil {
t.Fatal(err)
}
if encoded != tc.encoded {
t.Error("decode-encode round trip failed:", cmp.Diff(tc.encoded, encoded))
}
})
}
}

func TestDecodeBase64_CorrectlyDecodesInputToBytes(t *testing.T) {
t.Parallel()
input := "CAAAEA=="
got, err := script.Echo(input).DecodeBase64().Bytes()
if err != nil {
t.Fatal(err)
}
want := []byte{8, 0, 0, 16}
if !bytes.Equal(want, got) {
t.Logf("input %#v incorrectly decoded:", input)
t.Error(cmp.Diff(want, got))
}
}

func TestEncodeBase64_CorrectlyEncodesInputBytes(t *testing.T) {
t.Parallel()
input := []byte{8, 0, 0, 16}
reader := bytes.NewReader(input)
want := "CAAAEA=="
got, err := script.NewPipe().WithReader(reader).EncodeBase64().String()
if err != nil {
t.Fatal(err)
}
if got != want {
t.Logf("input %#v incorrectly encoded:", input)
t.Error(cmp.Diff(want, got))
}
}

func ExampleArgs() {
script.Args().Stdout()
// prints command-line arguments
Expand Down Expand Up @@ -1986,6 +2107,12 @@ func ExamplePipe_CountLines() {
// 3
}

func ExamplePipe_DecodeBase64() {
script.Echo("SGVsbG8sIHdvcmxkIQ==").DecodeBase64().Stdout()
// Output:
// Hello, world!
}

func ExamplePipe_Do() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
Expand Down Expand Up @@ -2021,6 +2148,12 @@ func ExamplePipe_Echo() {
// Hello, world!
}

func ExamplePipe_EncodeBase64() {
script.Echo("Hello, world!").EncodeBase64().Stdout()
// Output:
// SGVsbG8sIHdvcmxkIQ==
}

func ExamplePipe_ExitStatus() {
p := script.Exec("echo")
fmt.Println(p.ExitStatus())
Expand Down

0 comments on commit eefb8ae

Please sign in to comment.