1// Copyright 2019 CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package exec
16
17import (
18 "fmt"
19 "os/exec"
20 "strings"
21
22 "cuelang.org/go/cue"
23 "cuelang.org/go/cue/errors"
24 "cuelang.org/go/internal/task"
25)
26
27func init() {
28 task.Register("tool/exec.Run", newExecCmd)
29
30 // For backwards compatibility.
31 task.Register("exec", newExecCmd)
32}
33
34type execCmd struct{}
35
36func newExecCmd(v cue.Value) (task.Runner, error) {
37 return &execCmd{}, nil
38}
39
40func (c *execCmd) Run(ctx *task.Context) (res interface{}, err error) {
41 cmd, doc, err := mkCommand(ctx)
42 if err != nil {
43 return cue.Value{}, err
44 }
45
46 // TODO: set environment variables, if defined.
47 stream := func(name string) (stream cue.Value, ok bool) {
48 c := ctx.Obj.LookupPath(cue.ParsePath(name))
49 if c.Err() != nil || c.IsNull() {
50 return
51 }
52 return c, true
53 }
54
55 if v, ok := stream("stdin"); !ok {
56 cmd.Stdin = ctx.Stdin
57 } else if cmd.Stdin, err = v.Reader(); err != nil {
58 return nil, errors.Wrapf(err, v.Pos(), "invalid input")
59 }
60 _, captureOut := stream("stdout")
61 if !captureOut {
62 cmd.Stdout = ctx.Stdout
63 }
64 _, captureErr := stream("stderr")
65 if !captureErr {
66 cmd.Stderr = ctx.Stderr
67 }
68
69 v := ctx.Obj.LookupPath(cue.ParsePath("mustSucceed"))
70 mustSucceed, err := v.Bool()
71 if err != nil {
72 return nil, errors.Wrapf(err, v.Pos(), "invalid bool value")
73 }
74
75 update := map[string]interface{}{}
76 if captureOut {
77 var stdout []byte
78 stdout, err = cmd.Output()
79 update["stdout"] = string(stdout)
80 } else {
81 err = cmd.Run()
82 }
83 update["success"] = err == nil
84
85 if err == nil {
86 return update, nil
87 }
88
89 if captureErr {
90 if exit := (*exec.ExitError)(nil); errors.As(err, &exit) {
91 update["stderr"] = string(exit.Stderr)
92 } else {
93 update["stderr"] = err.Error()
94 }
95 }
96
97 if !mustSucceed {
98 return update, nil
99 }
100
101 return nil, fmt.Errorf("command %q failed: %v", doc, err)
102}
103
104// mkCommand builds an [exec.Cmd] from a CUE task value,
105// also returning the full list of arguments as a string slice
106// so that it can be used in error messages.
107func mkCommand(ctx *task.Context) (c *exec.Cmd, doc []string, err error) {
108 v := ctx.Lookup("cmd")
109 if ctx.Err != nil {
110 return nil, nil, ctx.Err
111 }
112
113 var bin string
114 var args []string
115 switch v.Kind() {
116 case cue.StringKind:
117 str, _ := v.String()
118 list := strings.Fields(str)
119 bin, args = list[0], list[1:]
120
121 case cue.ListKind:
122 list, _ := v.List()
123 if !list.Next() {
124 return nil, nil, errors.New("empty command list")
125 }
126 bin, err = list.Value().String()
127 if err != nil {
128 return nil, nil, err
129 }
130 for list.Next() {
131 str, err := list.Value().String()
132 if err != nil {
133 return nil, nil, err
134 }
135 args = append(args, str)
136 }
137 }
138
139 if bin == "" {
140 return nil, nil, errors.New("empty command")
141 }
142
143 cmd := exec.CommandContext(ctx.Context, bin, args...)
144
145 cmd.Dir, _ = ctx.Obj.LookupPath(cue.ParsePath("dir")).String()
146
147 env := ctx.Obj.LookupPath(cue.ParsePath("env"))
148
149 // List case.
150 for iter, _ := env.List(); iter.Next(); {
151 v, _ := iter.Value().Default()
152 str, err := v.String()
153 if err != nil {
154 return nil, nil, errors.Wrapf(err, v.Pos(),
155 "invalid environment variable value %q", v)
156 }
157 cmd.Env = append(cmd.Env, str)
158 }
159
160 // Struct case.
161 for iter, _ := env.Fields(); iter.Next(); {
162 label := iter.Selector().Unquoted()
163 v, _ := iter.Value().Default()
164 var str string
165 switch v.Kind() {
166 case cue.StringKind:
167 str, _ = v.String()
168 case cue.IntKind, cue.FloatKind, cue.NumberKind:
169 str = fmt.Sprint(v)
170 default:
171 return nil, nil, errors.Newf(v.Pos(),
172 "invalid environment variable value %q", v)
173 }
174 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", label, str))
175 }
176
177 return cmd, append([]string{bin}, args...), nil
178}