@@ -12,29 +12,152 @@ import (
1212 "github.com/magefile/mage/mg"
1313)
1414
15+ // runOptions is a set of options to be applied with ExecSh.
16+ type runOptions struct {
17+ cmd string
18+ args []string
19+ dir string
20+ env map [string ]string
21+ stderr , stdout io.Writer
22+ }
23+
24+ // RunOpt applies an option to a runOptions set.
25+ type RunOpt func (* runOptions )
26+
27+ // WithV sets stderr and stdout the standard streams
28+ func WithV () RunOpt {
29+ return func (options * runOptions ) {
30+ options .stdout = os .Stdout
31+ options .stderr = os .Stderr
32+ }
33+ }
34+
35+ // WithEnv sets the env passed in env vars.
36+ func WithEnv (env map [string ]string ) RunOpt {
37+ return func (options * runOptions ) {
38+ if options .env == nil {
39+ options .env = make (map [string ]string )
40+ }
41+ for k , v := range env {
42+ options .env [k ] = v
43+ }
44+ }
45+ }
46+
47+ // WithStderr sets the stderr stream.
48+ func WithStderr (w io.Writer ) RunOpt {
49+ return func (options * runOptions ) {
50+ options .stderr = w
51+ }
52+ }
53+
54+ // WithStdout sets the stdout stream.
55+ func WithStdout (w io.Writer ) RunOpt {
56+ return func (options * runOptions ) {
57+ options .stdout = w
58+ }
59+ }
60+
61+ // WithDir sets the working directory for the command.
62+ func WithDir (dir string ) RunOpt {
63+ return func (options * runOptions ) {
64+ options .dir = dir
65+ }
66+ }
67+
68+ // WithArgs appends command arguments.
69+ func WithArgs (args ... string ) RunOpt {
70+ return func (options * runOptions ) {
71+ if options .args == nil {
72+ options .args = make ([]string , 0 , len (args ))
73+ }
74+ options .args = append (options .args , args ... )
75+ }
76+ }
77+
78+ // RunSh returns a function that calls ExecSh, only returning errors.
79+ func RunSh (cmd string , options ... RunOpt ) func (args ... string ) error {
80+ run := ExecSh (cmd , options ... )
81+ return func (args ... string ) error {
82+ _ , err := run ()
83+ return err
84+ }
85+ }
86+
87+ // ExecSh returns a function that executes the command, piping its stdout and
88+ // stderr according to the config options. If the command fails, it will return
89+ // an error that, if returned from a target or mg.Deps call, will cause mage to
90+ // exit with the same code as the command failed with.
91+ //
92+ // ExecSh takes a variable list of RunOpt objects to configure how the command
93+ // is executed. See RunOpt docs for more details.
94+ //
95+ // Env vars configured on the command override the current environment variables
96+ // set (which are also passed to the command). The cmd and args may include
97+ // references to environment variables in $FOO format, in which case these will be
98+ // expanded before the command is run.
99+ //
100+ // Ran reports if the command ran (rather than was not found or not executable).
101+ // Code reports the exit code the command returned if it ran. If err == nil, ran
102+ // is always true and code is always 0.
103+ func ExecSh (cmd string , options ... RunOpt ) func (args ... string ) (bool , error ) {
104+ opts := runOptions {
105+ cmd : cmd ,
106+ }
107+ for _ , o := range options {
108+ o (& opts )
109+ }
110+
111+ if opts .stdout == nil && mg .Verbose () {
112+ opts .stdout = os .Stdout
113+ }
114+
115+ return func (args ... string ) (bool , error ) {
116+ expand := func (s string ) string {
117+ s2 , ok := opts .env [s ]
118+ if ok {
119+ return s2
120+ }
121+ return os .Getenv (s )
122+ }
123+ cmd = os .Expand (cmd , expand )
124+ finalArgs := append (opts .args , args ... )
125+ for i := range finalArgs {
126+ finalArgs [i ] = os .Expand (finalArgs [i ], expand )
127+ }
128+ ran , code , err := run (opts .dir , opts .env , opts .stdout , opts .stderr , cmd , finalArgs ... )
129+
130+ if err == nil {
131+ return ran , nil
132+ }
133+ if ran {
134+ return ran , mg .Fatalf (code , `running "%s %s" failed with exit code %d` , cmd , strings .Join (args , " " ), code )
135+ }
136+ return ran , fmt .Errorf (`failed to run "%s %s: %v"` , cmd , strings .Join (args , " " ), err )
137+ }
138+ }
139+
15140// RunCmd returns a function that will call Run with the given command. This is
16141// useful for creating command aliases to make your scripts easier to read, like
17142// this:
18143//
19- // // in a helper file somewhere
20- // var g0 = sh.RunCmd("go") // go is a keyword :(
144+ // // in a helper file somewhere
145+ // var g0 = sh.RunCmd("go") // go is a keyword :(
21146//
22- // // somewhere in your main code
23- // if err := g0("install", "github.com/gohugo/hugo"); err != nil {
24- // return err
25- // }
147+ // // somewhere in your main code
148+ // if err := g0("install", "github.com/gohugo/hugo"); err != nil {
149+ // return err
150+ // }
26151//
27152// Args passed to command get baked in as args to the command when you run it.
28153// Any args passed in when you run the returned function will be appended to the
29154// original args. For example, this is equivalent to the above:
30155//
31- // var goInstall = sh.RunCmd("go", "install") goInstall("github.com/gohugo/hugo")
156+ // var goInstall = sh.RunCmd("go", "install") goInstall("github.com/gohugo/hugo")
32157//
33158// RunCmd uses Exec underneath, so see those docs for more details.
34159func RunCmd (cmd string , args ... string ) func (args ... string ) error {
35- return func (args2 ... string ) error {
36- return Run (cmd , append (args , args2 ... )... )
37- }
160+ return RunSh (cmd , WithArgs (args ... ))
38161}
39162
40163// OutCmd is like RunCmd except the command returns the output of the
@@ -47,45 +170,38 @@ func OutCmd(cmd string, args ...string) func(args ...string) (string, error) {
47170
48171// Run is like RunWith, but doesn't specify any environment variables.
49172func Run (cmd string , args ... string ) error {
50- return RunWith ( nil , cmd , args ... )
173+ return RunSh ( cmd , WithArgs ( args ... ))( )
51174}
52175
53176// RunV is like Run, but always sends the command's stdout to os.Stdout.
54177func RunV (cmd string , args ... string ) error {
55- _ , err := Exec (nil , os .Stdout , os .Stderr , cmd , args ... )
56- return err
178+ return RunSh (cmd , WithV (), WithArgs (args ... ))()
57179}
58180
59181// RunWith runs the given command, directing stderr to this program's stderr and
60182// printing stdout to stdout if mage was run with -v. It adds adds env to the
61183// environment variables for the command being run. Environment variables should
62184// be in the format name=value.
63185func RunWith (env map [string ]string , cmd string , args ... string ) error {
64- var output io.Writer
65- if mg .Verbose () {
66- output = os .Stdout
67- }
68- _ , err := Exec (env , output , os .Stderr , cmd , args ... )
69- return err
186+ return RunSh (cmd , WithEnv (env ), WithArgs (args ... ))()
70187}
71188
72189// RunWithV is like RunWith, but always sends the command's stdout to os.Stdout.
73190func RunWithV (env map [string ]string , cmd string , args ... string ) error {
74- _ , err := Exec (env , os .Stdout , os .Stderr , cmd , args ... )
75- return err
191+ return RunSh (cmd , WithV (), WithEnv (env ), WithArgs (args ... ))()
76192}
77193
78194// Output runs the command and returns the text from stdout.
79195func Output (cmd string , args ... string ) (string , error ) {
80196 buf := & bytes.Buffer {}
81- _ , err := Exec ( nil , buf , os .Stderr , cmd , args ... )
197+ err := RunSh ( cmd , WithStderr ( os .Stderr ), WithStdout ( buf ), WithArgs ( args ... ))( )
82198 return strings .TrimSuffix (buf .String (), "\n " ), err
83199}
84200
85201// OutputWith is like RunWith, but returns what is written to stdout.
86202func OutputWith (env map [string ]string , cmd string , args ... string ) (string , error ) {
87203 buf := & bytes.Buffer {}
88- _ , err := Exec ( env , buf , os .Stderr , cmd , args ... )
204+ err := RunSh ( cmd , WithEnv ( env ), WithStderr ( os .Stderr ), WithStdout ( buf ), WithArgs ( args ... ))( )
89205 return strings .TrimSuffix (buf .String (), "\n " ), err
90206}
91207
@@ -102,40 +218,23 @@ func OutputWith(env map[string]string, cmd string, args ...string) (string, erro
102218// Code reports the exit code the command returned if it ran. If err == nil, ran
103219// is always true and code is always 0.
104220func Exec (env map [string ]string , stdout , stderr io.Writer , cmd string , args ... string ) (ran bool , err error ) {
105- expand := func (s string ) string {
106- s2 , ok := env [s ]
107- if ok {
108- return s2
109- }
110- return os .Getenv (s )
111- }
112- cmd = os .Expand (cmd , expand )
113- for i := range args {
114- args [i ] = os .Expand (args [i ], expand )
115- }
116- ran , code , err := run (env , stdout , stderr , cmd , args ... )
117- if err == nil {
118- return true , nil
119- }
120- if ran {
121- return ran , mg .Fatalf (code , `running "%s %s" failed with exit code %d` , cmd , strings .Join (args , " " ), code )
122- }
123- return ran , fmt .Errorf (`failed to run "%s %s: %v"` , cmd , strings .Join (args , " " ), err )
221+ return ExecSh (cmd , WithArgs (args ... ), WithStderr (stderr ), WithStdout (stdout ), WithEnv (env ))()
124222}
125223
126- func run (env map [string ]string , stdout , stderr io.Writer , cmd string , args ... string ) (ran bool , code int , err error ) {
224+ func run (dir string , env map [string ]string , stdout , stderr io.Writer , cmd string , args ... string ) (ran bool , code int , err error ) {
127225 c := exec .Command (cmd , args ... )
128226 c .Env = os .Environ ()
129227 for k , v := range env {
130228 c .Env = append (c .Env , k + "=" + v )
131229 }
230+ c .Dir = dir
132231 c .Stderr = stderr
133232 c .Stdout = stdout
134233 c .Stdin = os .Stdin
135234
136- var quoted []string
235+ var quoted []string
137236 for i := range args {
138- quoted = append (quoted , fmt .Sprintf ("%q" , args [i ]));
237+ quoted = append (quoted , fmt .Sprintf ("%q" , args [i ]))
139238 }
140239 // To protect against logging from doing exec in global variables
141240 if mg .Verbose () {
@@ -144,6 +243,7 @@ func run(env map[string]string, stdout, stderr io.Writer, cmd string, args ...st
144243 err = c .Run ()
145244 return CmdRan (err ), ExitStatus (err ), err
146245}
246+
147247// CmdRan examines the error to determine if it was generated as a result of a
148248// command running via os/exec.Command. If the error is nil, or the command ran
149249// (even if it exited with a non-zero exit code), CmdRan reports true. If the
0 commit comments