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