1// Copyright 2018 The 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 cmd
16
17import (
18	"context"
19	"io"
20	"os"
21	"strings"
22
23	"github.com/spf13/cobra"
24
25	"cuelang.org/go/cue/errors"
26	"cuelang.org/go/cue/token"
27)
28
29// TODO: commands
30//   fix:      rewrite/refactor configuration files
31//             -i interactive: open diff and ask to update
32//   serve:    like cmd, but for servers
33//   get:      convert cue from other languages, like proto and go.
34//   gen:      generate files for other languages
35//   generate  like go generate (also convert cue to go doc)
36//   test      load and fully evaluate test files.
37//
38// TODO: documentation of concepts
39//   tasks     the key element for cmd, serve, and fix
40
41type runFunction func(cmd *Command, args []string) error
42
43func mkRunE(c *Command, f runFunction) func(*cobra.Command, []string) error {
44	return func(cmd *cobra.Command, args []string) error {
45		c.Command = cmd
46		err := f(c, args)
47		if err != nil {
48			exitOnErr(c, err, true)
49		}
50		return err
51	}
52}
53
54// newRootCmd creates the base command when called without any subcommands
55func newRootCmd() *Command {
56	cmd := &cobra.Command{
57		Use:   "cue",
58		Short: "cue emits configuration files to user-defined commands.",
59		Long: `cue evaluates CUE files, an extension of JSON, and sends them
60to user-defined commands for processing.
61
62Commands are defined in CUE as follows:
63
64	import "tool/exec"
65	command: deploy: {
66		exec.Run
67		cmd:   "kubectl"
68		args:  [ "-f", "deploy" ]
69		in:    json.Encode(userValue) // encode the emitted configuration.
70	}
71
72cue can also combine the results of http or grpc request with the input
73configuration for further processing. For more information on defining commands
74run 'cue help cmd' or go to cuelang.org/pkg/cmd.
75
76For more information on writing CUE configuration files see cuelang.org.`,
77		// Uncomment the following line if your bare application
78		// has an action associated with it:
79		//	Run: func(cmd *cobra.Command, args []string) { },
80
81		SilenceUsage: true,
82	}
83
84	c := &Command{Command: cmd, root: cmd}
85
86	cmdCmd := newCmdCmd(c)
87	c.cmd = cmdCmd
88
89	subCommands := []*cobra.Command{
90		cmdCmd,
91		newCompletionCmd(c),
92		newEvalCmd(c),
93		newDefCmd(c),
94		newExportCmd(c),
95		newFixCmd(c),
96		newFmtCmd(c),
97		newGetCmd(c),
98		newImportCmd(c),
99		newModCmd(c),
100		newTrimCmd(c),
101		newVersionCmd(c),
102		newVetCmd(c),
103
104		// Hidden
105		newAddCmd(c),
106	}
107	subCommands = append(subCommands, newHelpTopics(c)...)
108
109	addGlobalFlags(cmd.PersistentFlags())
110
111	for _, sub := range subCommands {
112		cmd.AddCommand(sub)
113	}
114
115	return c
116}
117
118// MainTest is like Main, runs the cue tool and returns the code for passing to os.Exit.
119func MainTest() int {
120	inTest = true
121	return Main()
122}
123
124// Main runs the cue tool and returns the code for passing to os.Exit.
125func Main() int {
126	cwd, _ := os.Getwd()
127	err := mainErr(context.Background(), os.Args[1:])
128	if err != nil {
129		if err != ErrPrintedError {
130			errors.Print(os.Stderr, err, &errors.Config{
131				Cwd:     cwd,
132				ToSlash: inTest,
133			})
134		}
135		return 1
136	}
137	return 0
138}
139
140func mainErr(ctx context.Context, args []string) error {
141	cmd, err := New(args)
142	if err != nil {
143		return err
144	}
145	return cmd.Run(ctx)
146}
147
148type Command struct {
149	// The currently active command.
150	*cobra.Command
151
152	root *cobra.Command
153
154	// Subcommands
155	cmd *cobra.Command
156
157	hasErr bool
158}
159
160type errWriter Command
161
162func (w *errWriter) Write(b []byte) (int, error) {
163	c := (*Command)(w)
164	c.hasErr = true
165	return c.Command.OutOrStderr().Write(b)
166}
167
168// Hint: search for uses of OutOrStderr other than the one here to see
169// which output does not trigger a non-zero exit code. os.Stderr may never
170// be used directly.
171
172// Stderr returns a writer that should be used for error messages.
173func (c *Command) Stderr() io.Writer {
174	return (*errWriter)(c)
175}
176
177// TODO: add something similar for Stdout. The output model of Cobra isn't
178// entirely clear, and such a change seems non-trivial.
179
180// Consider overriding these methods from Cobra using OutOrStdErr.
181// We don't use them currently, but may be safer to block. Having them
182// will encourage their usage, and the naming is inconsistent with other CUE APIs.
183// PrintErrf(format string, args ...interface{})
184// PrintErrln(args ...interface{})
185// PrintErr(args ...interface{})
186
187func (c *Command) SetOutput(w io.Writer) {
188	c.root.SetOut(w)
189}
190
191func (c *Command) SetInput(r io.Reader) {
192	c.root.SetIn(r)
193}
194
195// ErrPrintedError indicates error messages have been printed to stderr.
196var ErrPrintedError = errors.New("terminating because of errors")
197
198func (c *Command) Run(ctx context.Context) (err error) {
199	// Three categories of commands:
200	// - normal
201	// - user defined
202	// - help
203	// For the latter two, we need to use the default loading.
204	defer recoverError(&err)
205
206	if err := c.root.Execute(); err != nil {
207		return err
208	}
209	if c.hasErr {
210		return ErrPrintedError
211	}
212	return nil
213}
214
215func recoverError(err *error) {
216	switch e := recover().(type) {
217	case nil:
218	case panicError:
219		*err = e.Err
220	default:
221		panic(e)
222	}
223	// We use panic to escape, instead of os.Exit
224}
225
226func New(args []string) (cmd *Command, err error) {
227	defer recoverError(&err)
228
229	cmd = newRootCmd()
230	rootCmd := cmd.root
231	if len(args) == 0 {
232		return cmd, nil
233	}
234	rootCmd.SetArgs(args)
235
236	var sub = map[string]*subSpec{
237		"cmd": {commandSection, cmd.cmd},
238		// "serve": {"server", nil},
239		// "fix":   {"fix", nil},
240	}
241
242	// handle help, --help and -h on root 'cue' command
243	if args[0] == "help" || args[0] == "--help" || args[0] == "-h" {
244		// Allow errors.
245		_ = addSubcommands(cmd, sub, args[1:], true)
246		return cmd, nil
247	}
248
249	if _, ok := sub[args[0]]; ok {
250		return cmd, addSubcommands(cmd, sub, args, false)
251	}
252
253	// TODO: clean this up once we either use Cobra properly or when we remove
254	// it.
255	err = cmd.cmd.ParseFlags(args)
256	if err != nil {
257		return nil, err
258	}
259
260	args = cmd.cmd.Flags().Args()
261	rootCmd.SetArgs(args)
262
263	if c, _, err := rootCmd.Find(args); err == nil && c != nil {
264		return cmd, nil
265	}
266
267	if !isCommandName(args[0]) {
268		return cmd, nil // Forces unknown command message from Cobra.
269	}
270
271	tools, err := buildTools(cmd, args[1:])
272	if err != nil {
273		return cmd, err
274	}
275	_, err = addCustom(cmd, rootCmd, commandSection, args[0], tools)
276	if err != nil {
277		err = errors.Newf(token.NoPos,
278			`%s %q is not defined
279Ensure commands are defined in a "_tool.cue" file.
280Run 'cue help' to show available commands.`,
281			commandSection, args[0],
282		)
283		return cmd, err
284	}
285	return cmd, nil
286}
287
288type subSpec struct {
289	name string
290	cmd  *cobra.Command
291}
292
293func addSubcommands(cmd *Command, sub map[string]*subSpec, args []string, isHelp bool) error {
294	if len(args) > 0 {
295		if _, ok := sub[args[0]]; ok {
296			oldargs := []string{args[0]}
297			args = args[1:]
298
299			// Check for 'cue cmd --help|-h'
300			// it is behaving differently for this one command, cobra does not seem to pick it up
301			// See issue #340
302			if len(args) == 1 && (args[0] == "--help" || args[0] == "-h") {
303				return cmd.Usage()
304			}
305
306			if !isHelp {
307				err := cmd.cmd.ParseFlags(args)
308				if err != nil {
309					return err
310				}
311				args = cmd.cmd.Flags().Args()
312				cmd.root.SetArgs(append(oldargs, args...))
313			}
314		}
315	}
316
317	if len(args) > 0 {
318		if !isCommandName(args[0]) {
319			return nil // Forces unknown command message from Cobra.
320		}
321		args = args[1:]
322	}
323
324	tools, err := buildTools(cmd, args)
325	if err != nil {
326		return err
327	}
328
329	// TODO: for now we only allow one instance. Eventually, we can allow
330	// more if they all belong to the same package and we merge them
331	// before computing commands.
332	for _, spec := range sub {
333		commands := tools.Lookup(spec.name)
334		if !commands.Exists() {
335			return nil
336		}
337		i, err := commands.Fields()
338		if err != nil {
339			return errors.Newf(token.NoPos, "could not create command definitions: %v", err)
340		}
341		for i.Next() {
342			_, _ = addCustom(cmd, spec.cmd, spec.name, i.Label(), tools)
343		}
344	}
345	return nil
346}
347
348func isCommandName(s string) bool {
349	return !strings.Contains(s, `/\`) &&
350		!strings.HasPrefix(s, ".") &&
351		!strings.HasSuffix(s, ".cue")
352}
353
354type panicError struct {
355	Err error
356}
357
358func exit() {
359	panic(panicError{ErrPrintedError})
360}
361