Skip to content

Commit 2d95834

Browse files
add WithEnv for setting command environment (#208)
Co-authored-by: John Arundel <[email protected]>
1 parent 0edd895 commit 2d95834

File tree

3 files changed

+93
-15
lines changed

3 files changed

+93
-15
lines changed

README.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -269,18 +269,31 @@ These are functions that create a pipe with a given contents:
269269

270270
| Source | Contents |
271271
| -------- | ------------- |
272-
| [`Args`](https://pkg.go.dev/github.com/bitfield/script#Args) | command-line arguments
273-
| [`Do`](https://pkg.go.dev/github.com/bitfield/script#Do) | HTTP response
274-
| [`Echo`](https://pkg.go.dev/github.com/bitfield/script#Echo) | a string
275-
| [`Exec`](https://pkg.go.dev/github.com/bitfield/script#Exec) | command output
276-
| [`File`](https://pkg.go.dev/github.com/bitfield/script#File) | file contents
277-
| [`FindFiles`](https://pkg.go.dev/github.com/bitfield/script#FindFiles) | recursive file listing
278-
| [`Get`](https://pkg.go.dev/github.com/bitfield/script#Get) | HTTP response
279-
| [`IfExists`](https://pkg.go.dev/github.com/bitfield/script#IfExists) | do something only if some file exists
280-
| [`ListFiles`](https://pkg.go.dev/github.com/bitfield/script#ListFiles) | file listing (including wildcards)
281-
| [`Post`](https://pkg.go.dev/github.com/bitfield/script#Post) | HTTP response
282-
| [`Slice`](https://pkg.go.dev/github.com/bitfield/script#Slice) | slice elements, one per line
283-
| [`Stdin`](https://pkg.go.dev/github.com/bitfield/script#Stdin) | standard input
272+
| [`Args`](https://pkg.go.dev/github.com/bitfield/script#Args) | command-line arguments |
273+
| [`Do`](https://pkg.go.dev/github.com/bitfield/script#Do) | HTTP response |
274+
| [`Echo`](https://pkg.go.dev/github.com/bitfield/script#Echo) | a string |
275+
| [`Exec`](https://pkg.go.dev/github.com/bitfield/script#Exec) | command output |
276+
| [`File`](https://pkg.go.dev/github.com/bitfield/script#File) | file contents |
277+
| [`FindFiles`](https://pkg.go.dev/github.com/bitfield/script#FindFiles) | recursive file listing |
278+
| [`Get`](https://pkg.go.dev/github.com/bitfield/script#Get) | HTTP response |
279+
| [`IfExists`](https://pkg.go.dev/github.com/bitfield/script#IfExists) | do something only if some file exists |
280+
| [`ListFiles`](https://pkg.go.dev/github.com/bitfield/script#ListFiles) | file listing (including wildcards) |
281+
| [`Post`](https://pkg.go.dev/github.com/bitfield/script#Post) | HTTP response |
282+
| [`Slice`](https://pkg.go.dev/github.com/bitfield/script#Slice) | slice elements, one per line |
283+
| [`Stdin`](https://pkg.go.dev/github.com/bitfield/script#Stdin) | standard input |
284+
285+
## Modifiers
286+
287+
These are methods on a pipe that change its configuration:
288+
289+
| Source | Modifies |
290+
| -------- | ------------- |
291+
| [`WithEnv`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithEnv) | environment for commands |
292+
| [`WithError`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithError) | pipe error status |
293+
| [`WithHTTPClient`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithHTTPClient) | client for HTTP requests |
294+
| [`WithReader`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithReader) | pipe source |
295+
| [`WithStderr`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithStderr) | standard error output stream for command |
296+
| [`WithStdout`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithStdout) | standard output stream for pipe |
284297

285298
## Filters
286299

@@ -340,7 +353,8 @@ Sinks are methods that return some data from a pipe, ending the pipeline and ext
340353

341354
| Version | New |
342355
| ----------- | ------- |
343-
| _next_ | [`DecodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.DecodeBase64) / [`EncodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.EncodeBase64) |
356+
| _next_ | [`WithEnv`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WithEnv) |
357+
| | [`DecodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.DecodeBase64) / [`EncodeBase64`](https://pkg.go.dev/github.com/bitfield/script#Pipe.EncodeBase64) |
344358
| 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) |
345359
| 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) |
346360
| v0.20.0 | [`JQ`](https://pkg.go.dev/github.com/bitfield/script#Pipe.JQ) |

script.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ type Pipe struct {
3232
stdout io.Writer
3333
httpClient *http.Client
3434

35-
// because pipe stages are concurrent, protect 'err' and 'stderr'
3635
mu *sync.Mutex
3736
err error
3837
stderr io.Writer
38+
env []string
3939
}
4040

4141
// Args creates a pipe containing the program's command-line arguments from
@@ -168,6 +168,7 @@ func NewPipe() *Pipe {
168168
mu: new(sync.Mutex),
169169
stdout: os.Stdout,
170170
httpClient: http.DefaultClient,
171+
env: nil,
171172
}
172173
}
173174

@@ -374,6 +375,12 @@ func (p *Pipe) EncodeBase64() *Pipe {
374375
})
375376
}
376377

378+
func (p *Pipe) environment() []string {
379+
p.mu.Lock()
380+
defer p.mu.Unlock()
381+
return p.env
382+
}
383+
377384
// Error returns any error present on the pipe, or nil otherwise.
378385
// Error is not a sink and does not wait until the pipe reaches
379386
// completion. To wait for completion before returning the error,
@@ -392,6 +399,11 @@ func (p *Pipe) Error() error {
392399
// error output). The effect of this is to filter the contents of the pipe
393400
// through the external command.
394401
//
402+
// # Environment
403+
//
404+
// The command inherits the current process's environment, optionally modified
405+
// by [Pipe.WithEnv].
406+
//
395407
// # Error handling
396408
//
397409
// If the command had a non-zero exit status, the pipe's error status will also
@@ -419,6 +431,10 @@ func (p *Pipe) Exec(cmdLine string) *Pipe {
419431
if pipeStderr != nil {
420432
cmd.Stderr = pipeStderr
421433
}
434+
pipeEnv := p.environment()
435+
if pipeEnv != nil {
436+
cmd.Env = pipeEnv
437+
}
422438
err = cmd.Start()
423439
if err != nil {
424440
fmt.Fprintln(cmd.Stderr, err)
@@ -430,7 +446,8 @@ func (p *Pipe) Exec(cmdLine string) *Pipe {
430446

431447
// ExecForEach renders cmdLine as a Go template for each line of input, running
432448
// the resulting command, and produces the combined output of all these
433-
// commands in sequence. See [Pipe.Exec] for error handling details.
449+
// commands in sequence. See [Pipe.Exec] for details on error handling and
450+
// environment variables.
434451
//
435452
// This is mostly useful for substituting data into commands using Go template
436453
// syntax. For example:
@@ -460,6 +477,9 @@ func (p *Pipe) ExecForEach(cmdLine string) *Pipe {
460477
if pipeStderr != nil {
461478
cmd.Stderr = pipeStderr
462479
}
480+
if p.env != nil {
481+
cmd.Env = p.env
482+
}
463483
err = cmd.Start()
464484
if err != nil {
465485
fmt.Fprintln(cmd.Stderr, err)
@@ -903,6 +923,16 @@ func (p *Pipe) Wait() error {
903923
return p.Error()
904924
}
905925

926+
// WithEnv sets the environment for subsequent [Pipe.Exec] and [Pipe.ExecForEach]
927+
// commands to the string slice env, using the same format as [os/exec.Cmd.Env].
928+
// An empty slice unsets all existing environment variables.
929+
func (p *Pipe) WithEnv(env []string) *Pipe {
930+
p.mu.Lock()
931+
defer p.mu.Unlock()
932+
p.env = env
933+
return p
934+
}
935+
906936
// WithError sets the error err on the pipe.
907937
func (p *Pipe) WithError(err error) *Pipe {
908938
p.SetError(err)

script_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1768,6 +1768,40 @@ func TestWithStdout_SetsSpecifiedWriterAsStdout(t *testing.T) {
17681768
}
17691769
}
17701770

1771+
func TestWithEnv_UnsetsAllEnvVarsGivenEmptySlice(t *testing.T) {
1772+
t.Parallel()
1773+
p := script.NewPipe().WithEnv([]string{"ENV1=test1"}).Exec("sh -c 'echo ENV1=$ENV1'")
1774+
want := "ENV1=test1\n"
1775+
got, err := p.String()
1776+
if err != nil {
1777+
t.Fatal(err)
1778+
}
1779+
if got != want {
1780+
t.Fatalf("want %q, got %q", want, got)
1781+
}
1782+
got, err = p.Echo("").WithEnv([]string{}).Exec("sh -c 'echo ENV1=$ENV1'").String()
1783+
if err != nil {
1784+
t.Fatal(err)
1785+
}
1786+
want = "ENV1=\n"
1787+
if got != want {
1788+
t.Errorf("want %q, got %q", want, got)
1789+
}
1790+
}
1791+
1792+
func TestWithEnv_SetsGivenVariablesForSubsequentExec(t *testing.T) {
1793+
t.Parallel()
1794+
env := []string{"ENV1=test1", "ENV2=test2"}
1795+
got, err := script.NewPipe().WithEnv(env).Exec("sh -c 'echo ENV1=$ENV1 ENV2=$ENV2'").String()
1796+
if err != nil {
1797+
t.Fatal(err)
1798+
}
1799+
want := "ENV1=test1 ENV2=test2\n"
1800+
if got != want {
1801+
t.Errorf("want %q, got %q", want, got)
1802+
}
1803+
}
1804+
17711805
func TestErrorReturnsErrorSetByPreviousPipeStage(t *testing.T) {
17721806
t.Parallel()
17731807
p := script.File("testdata/nonexistent.txt")

0 commit comments

Comments
 (0)