github.com/criteo/command-launcher@v0.0.0-20230407142452-fb616f546e98/internal/command/default-command.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "html/template" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "runtime" 10 "strings" 11 12 "github.com/criteo/command-launcher/internal/helper" 13 log "github.com/sirupsen/logrus" 14 ) 15 16 const ( 17 CACHE_DIR_PATTERN = "#CACHE#" 18 OS_PATTERN = "#OS#" 19 ARCH_PATTERN = "#ARCH#" 20 BINARY_PATTERN = "#BINARY#" 21 SCRIPT_PATTERN = "#SCRIPT#" 22 EXT_PATTERN = "#EXT#" 23 SCRIPT_EXT_PATTERN = "#SCRIPT_EXT#" 24 ) 25 26 /* 27 DefaultCommand implements the command.Command interface 28 29 There are two types of cdt command: 30 1. group command 31 2. executable command 32 33 A group command doesn't do any thing but contain other executable commands. An executable 34 command must be under a group command, the default one is the cdt root (group = "") 35 36 for example, command: cdt hotfix create 37 38 hotfix is a group command, and create is a command under the "hotfix" group command 39 40 Another example: cdt ls, here ls is an executable command under the root "" group command 41 42 Note: nested group command is not supported! It is not a good practice to have to much level 43 of nested commands like: cdt workspace create moab. 44 45 The group field of group command is ignored. 46 47 An additional "category" field is reserved in case we have too much first level commands, 48 we can use it to category them in the cdt help output. 49 */ 50 type DefaultCommand struct { 51 CmdID string 52 CmdPackageName string 53 CmdRepositoryID string 54 CmdRuntimeGroup string 55 CmdRuntimeName string 56 CmdName string `json:"name" yaml:"name"` 57 CmdCategory string `json:"category" yaml:"category"` 58 CmdType string `json:"type" yaml:"type"` 59 CmdGroup string `json:"group" yaml:"group"` 60 CmdArgsUsage string `json:"argsUsage" yaml:"argsUsage"` // optional, set this field will custom the one line usage 61 CmdExamples []ExampleEntry `json:"examples" yaml:"examples"` 62 CmdShortDescription string `json:"short" yaml:"short"` 63 CmdLongDescription string `json:"long" yaml:"long"` 64 CmdExecutable string `json:"executable" yaml:"executable"` 65 CmdArguments []string `json:"args" yaml:"args"` 66 CmdDocFile string `json:"docFile" yaml:"docFile"` 67 CmdDocLink string `json:"docLink" yaml:"docLink"` 68 CmdValidArgs []string `json:"validArgs" yaml:"validArgs"` // the valid argument options 69 CmdValidArgsCmd []string `json:"validArgsCmd" yaml:"validArgsCmd"` // the command to call to get the args for autocompletion 70 CmdRequiredFlags []string `json:"requiredFlags" yaml:"requiredFlags"` // the required flags -- deprecated in 1.9.0, see flags, exclusiveFlags, and groupFlags 71 CmdFlags []Flag `json:"flags" yaml:"flags"` 72 CmdExclusiveFlags [][]string `json:"exclusiveFlags" yaml:"exclusiveFlags"` 73 CmdGroupFlags [][]string `json:"groupFlags" yaml:"groupFlags"` 74 CmdFlagValuesCmd []string `json:"flagValuesCmd" yaml:"flagValuesCmd"` // the command to call flag values for autocompletion 75 CmdCheckFlags bool `json:"checkFlags" yaml:"checkFlags"` // whether parse the flags and check them before execution 76 CmdRequestedResources []string `json:"requestedResources" yaml:"requestedResources"` 77 78 PkgDir string `json:"pkgDir"` 79 } 80 81 func NewDefaultCommandFromCopy(cmd Command, pkgDir string) *DefaultCommand { 82 return &DefaultCommand{ 83 CmdID: cmd.ID(), 84 CmdPackageName: cmd.PackageName(), 85 CmdRepositoryID: cmd.RepositoryID(), 86 CmdRuntimeGroup: cmd.RuntimeGroup(), 87 CmdRuntimeName: cmd.RuntimeName(), 88 89 CmdName: cmd.Name(), 90 CmdCategory: cmd.Category(), 91 CmdType: cmd.Type(), 92 CmdGroup: cmd.Group(), 93 CmdArgsUsage: cmd.ArgsUsage(), 94 CmdExamples: cmd.Examples(), 95 CmdShortDescription: cmd.ShortDescription(), 96 CmdLongDescription: cmd.LongDescription(), 97 CmdExecutable: cmd.Executable(), 98 CmdArguments: cmd.Arguments(), 99 CmdDocFile: cmd.DocFile(), 100 CmdDocLink: cmd.DocLink(), 101 CmdValidArgs: cmd.ValidArgs(), 102 CmdValidArgsCmd: cmd.ValidArgsCmd(), 103 CmdRequiredFlags: cmd.RequiredFlags(), 104 CmdFlags: cmd.Flags(), 105 CmdExclusiveFlags: cmd.ExclusiveFlags(), 106 CmdGroupFlags: cmd.GroupFlags(), 107 CmdFlagValuesCmd: cmd.FlagValuesCmd(), 108 CmdCheckFlags: cmd.CheckFlags(), 109 CmdRequestedResources: cmd.RequestedResources(), 110 PkgDir: pkgDir, 111 } 112 } 113 114 func CmdID(repo, pkg, group, name string) string { 115 return fmt.Sprintf("%s>%s>%s>%s", repo, pkg, group, name) 116 } 117 func CmdReverseID(repo, pkg, group, name string) string { 118 return fmt.Sprintf("%s@%s@%s@%s", name, group, pkg, repo) 119 } 120 121 func (cmd *DefaultCommand) Execute(envVars []string, args ...string) (int, error) { 122 arguments := append(cmd.CmdArguments, args...) 123 cmd.interpolateArray(&arguments) 124 command := cmd.interpolateCmd() 125 126 log.Debug("Command line: ", command, " ", arguments) 127 128 ctx := exec.Command(command, arguments...) 129 // inject additional environments 130 env := append(os.Environ(), envVars...) 131 ctx.Env = env 132 133 ctx.Stdout = os.Stdout 134 ctx.Stderr = os.Stderr 135 ctx.Stdin = os.Stdin 136 137 log.Debug("Command start executing") 138 if err := ctx.Run(); err != nil { 139 log.Debug("Command execution err: ", err) 140 if exitError, ok := err.(*exec.ExitError); ok { 141 log.Debug("Exit code: ", exitError.ExitCode()) 142 return exitError.ExitCode(), err 143 } else { 144 exitcode := ctx.ProcessState.ExitCode() 145 return exitcode, err 146 } 147 } 148 149 exitcode := ctx.ProcessState.ExitCode() 150 log.Debug("Command executed successfully with exit code: ", exitcode) 151 return exitcode, nil 152 } 153 154 func (cmd *DefaultCommand) ExecuteWithOutput(envVars []string, args ...string) (int, string, error) { 155 wd, err := os.Getwd() 156 if err != nil { 157 return 1, "", err 158 } 159 arguments := append(cmd.CmdArguments, args...) 160 cmd.interpolateArray(&arguments) 161 command := cmd.interpolateCmd() 162 163 env := append(os.Environ(), envVars...) 164 165 log.Debug("Execute command line with output: ", command, " ", arguments) 166 167 return helper.CallExternalWithOutput(env, wd, command, arguments...) 168 } 169 170 func (cmd *DefaultCommand) ExecuteValidArgsCmd(envVars []string, args ...string) (int, string, error) { 171 return cmd.executeArrayCmd(envVars, cmd.CmdValidArgsCmd, args...) 172 } 173 174 func (cmd *DefaultCommand) ExecuteFlagValuesCmd(envVars []string, flagCmd []string, args ...string) (int, string, error) { 175 return cmd.executeArrayCmd(envVars, flagCmd, args...) 176 } 177 178 func (cmd *DefaultCommand) executeArrayCmd(envVars []string, cmdArray []string, args ...string) (int, string, error) { 179 wd, err := os.Getwd() 180 if err != nil { 181 return 1, "", err 182 } 183 validCmd := "" 184 validArgs := []string{} 185 if cmdArray != nil { 186 argsLen := len(cmdArray) 187 if argsLen >= 2 { 188 validCmd = cmdArray[0] 189 validArgs = cmdArray[1:argsLen] 190 } else if argsLen >= 1 { 191 validCmd = cmdArray[0] 192 } 193 } 194 if validCmd == "" { 195 return 0, "", nil 196 } 197 cmd.interpolateArray(&validArgs) 198 // Should we interpolate the argumments too??? 199 return helper.CallExternalWithOutput(envVars, wd, cmd.interpolate(validCmd), append(validArgs, args...)...) 200 } 201 202 func (cmd *DefaultCommand) ID() string { 203 return CmdID(cmd.CmdRepositoryID, cmd.CmdPackageName, cmd.CmdGroup, cmd.CmdName) 204 } 205 206 func (cmd *DefaultCommand) PackageName() string { 207 return cmd.CmdPackageName 208 } 209 210 func (cmd *DefaultCommand) RepositoryID() string { 211 return cmd.CmdRepositoryID 212 } 213 214 func (cmd *DefaultCommand) RuntimeGroup() string { 215 if cmd.CmdRuntimeGroup == "" { 216 return cmd.CmdGroup 217 } 218 return cmd.CmdRuntimeGroup 219 } 220 221 func (cmd *DefaultCommand) RuntimeName() string { 222 if cmd.CmdRuntimeName == "" { 223 return cmd.CmdName 224 } 225 return cmd.CmdRuntimeName 226 } 227 228 // Full group name in form of group name @ [empty] @ package @ repo 229 // Read as a group command named [name] in root group (empty) from package [package] managed by repo [repo] 230 func (cmd *DefaultCommand) FullGroup() string { 231 return CmdReverseID(cmd.CmdRepositoryID, cmd.CmdPackageName, "", cmd.CmdGroup) 232 } 233 234 // Full command name in form of name @ group @ package @ repo 235 // Read as a command named [name] in group [group] from package [package] managed by repo [repo] 236 func (cmd *DefaultCommand) FullName() string { 237 return CmdReverseID(cmd.CmdRepositoryID, cmd.CmdPackageName, cmd.CmdGroup, cmd.CmdName) 238 } 239 240 func (cmd *DefaultCommand) Name() string { 241 return cmd.CmdName 242 } 243 244 func (cmd *DefaultCommand) Type() string { 245 if cmd.CmdType != "group" && 246 cmd.CmdType != "executable" && 247 cmd.CmdType != "system" { 248 // for invalid cmd type, set it to group to make it do nothing 249 return "group" 250 } 251 return cmd.CmdType 252 } 253 254 func (cmd *DefaultCommand) Category() string { 255 return cmd.CmdCategory 256 } 257 258 func (cmd *DefaultCommand) Group() string { 259 return cmd.CmdGroup 260 } 261 262 // custom the usage message for the arguments format 263 // this is useful to name your arguments and show argument orders 264 // this will replace the one-line usage message in help 265 // NOTE: there is no need to provide the command name in the usage 266 // it will be added by command launcher automatically 267 func (cmd *DefaultCommand) ArgsUsage() string { 268 return cmd.CmdArgsUsage 269 } 270 271 func (cmd *DefaultCommand) Examples() []ExampleEntry { 272 if cmd.CmdExamples == nil { 273 return []ExampleEntry{} 274 } 275 return cmd.CmdExamples 276 } 277 278 func (cmd *DefaultCommand) LongDescription() string { 279 return cmd.CmdLongDescription 280 } 281 282 func (cmd *DefaultCommand) ShortDescription() string { 283 return cmd.CmdShortDescription 284 } 285 286 func (cmd *DefaultCommand) Executable() string { 287 return cmd.CmdExecutable 288 } 289 290 func (cmd *DefaultCommand) Arguments() []string { 291 if cmd.CmdArguments == nil { 292 return []string{} 293 } 294 return cmd.CmdArguments 295 } 296 297 func (cmd *DefaultCommand) DocFile() string { 298 return cmd.interpolate(cmd.CmdDocFile) 299 } 300 301 func (cmd *DefaultCommand) DocLink() string { 302 return cmd.CmdDocLink 303 } 304 305 func (cmd *DefaultCommand) RequestedResources() []string { 306 if cmd.CmdRequestedResources == nil { 307 return []string{} 308 } 309 return cmd.CmdRequestedResources 310 } 311 312 func (cmd *DefaultCommand) ValidArgs() []string { 313 if cmd.CmdValidArgs != nil && len(cmd.CmdValidArgs) > 0 { 314 return cmd.CmdValidArgs 315 } 316 return []string{} 317 } 318 319 func (cmd *DefaultCommand) ValidArgsCmd() []string { 320 if cmd.CmdValidArgsCmd != nil && len(cmd.CmdValidArgsCmd) > 0 { 321 return cmd.CmdValidArgsCmd 322 } 323 return []string{} 324 } 325 326 func (cmd *DefaultCommand) RequiredFlags() []string { 327 if cmd.CmdRequiredFlags != nil && len(cmd.CmdRequiredFlags) > 0 { 328 return cmd.CmdRequiredFlags 329 } 330 return []string{} 331 } 332 333 func (cmd *DefaultCommand) Flags() []Flag { 334 if cmd.CmdFlags != nil && len(cmd.CmdFlags) > 0 { 335 return cmd.CmdFlags 336 } 337 return []Flag{} 338 } 339 340 func (cmd *DefaultCommand) ExclusiveFlags() [][]string { 341 if cmd.CmdExclusiveFlags == nil { 342 return [][]string{} 343 } 344 return cmd.CmdExclusiveFlags 345 } 346 347 func (cmd *DefaultCommand) GroupFlags() [][]string { 348 if cmd.CmdGroupFlags == nil { 349 return [][]string{} 350 } 351 return cmd.CmdGroupFlags 352 } 353 354 func (cmd *DefaultCommand) FlagValuesCmd() []string { 355 if cmd.CmdFlagValuesCmd != nil && len(cmd.CmdFlagValuesCmd) > 0 { 356 return cmd.CmdFlagValuesCmd 357 } 358 return []string{} 359 } 360 361 func (cmd *DefaultCommand) CheckFlags() bool { 362 return cmd.CmdCheckFlags 363 } 364 365 func (cmd *DefaultCommand) PackageDir() string { 366 return cmd.PkgDir 367 } 368 369 func (cmd *DefaultCommand) SetPackageDir(pkgDir string) { 370 cmd.PkgDir = pkgDir 371 } 372 373 func (cmd *DefaultCommand) SetNamespace(repoId string, pkgName string) { 374 cmd.CmdRepositoryID = repoId 375 cmd.CmdPackageName = pkgName 376 cmd.CmdID = fmt.Sprintf("%s:%s:%s:%s", repoId, pkgName, cmd.Group(), cmd.Name()) 377 } 378 379 func (cmd *DefaultCommand) SetRuntimeGroup(name string) { 380 cmd.CmdRuntimeGroup = name 381 } 382 383 func (cmd *DefaultCommand) SetRuntimeName(name string) { 384 cmd.CmdRuntimeName = name 385 } 386 387 func (cmd *DefaultCommand) copyArray(src []string) []string { 388 if len(src) == 0 { 389 return []string{} 390 } 391 return append([]string{}, src...) 392 } 393 394 func (cmd *DefaultCommand) copyExamples() []ExampleEntry { 395 ret := []ExampleEntry{} 396 if cmd.CmdExamples == nil || len(cmd.CmdExamples) == 0 { 397 return ret 398 } 399 for _, v := range cmd.CmdExamples { 400 ret = append(ret, v.Clone()) 401 } 402 return ret 403 } 404 405 func (cmd *DefaultCommand) interpolateArray(values *[]string) { 406 for i := range *values { 407 (*values)[i] = cmd.interpolate((*values)[i]) 408 } 409 } 410 411 func (cmd *DefaultCommand) interpolateCmd() string { 412 return cmd.interpolate(cmd.CmdExecutable) 413 } 414 415 func (cmd *DefaultCommand) binary(os string) string { 416 if os == "windows" { 417 return fmt.Sprintf("%s.exe", cmd.CmdName) 418 } 419 return cmd.CmdName 420 } 421 422 func (cmd *DefaultCommand) extension(os string) string { 423 if os == "windows" { 424 return ".exe" 425 } 426 return "" 427 } 428 429 func (cmd *DefaultCommand) script(os string) string { 430 return fmt.Sprintf("%s%s", cmd.CmdName, cmd.script_ext(os)) 431 } 432 433 func (cmd *DefaultCommand) script_ext(os string) string { 434 if os == "windows" { 435 return ".bat" 436 } 437 return "" 438 } 439 440 func (cmd *DefaultCommand) interpolate(text string) string { 441 return cmd.doInterpolate(runtime.GOOS, runtime.GOARCH, text) 442 } 443 444 func (cmd *DefaultCommand) doInterpolate(os string, arch string, text string) string { 445 output := strings.ReplaceAll(text, CACHE_DIR_PATTERN, filepath.ToSlash(cmd.PkgDir)) 446 output = strings.ReplaceAll(output, OS_PATTERN, os) 447 output = strings.ReplaceAll(output, ARCH_PATTERN, arch) 448 output = strings.ReplaceAll(output, BINARY_PATTERN, cmd.binary(os)) 449 output = strings.ReplaceAll(output, EXT_PATTERN, cmd.extension(os)) 450 output = strings.ReplaceAll(output, SCRIPT_PATTERN, cmd.script(os)) 451 output = strings.ReplaceAll(output, SCRIPT_EXT_PATTERN, cmd.script_ext(os)) 452 output = cmd.render(output) 453 return output 454 } 455 456 // Support golang built-in text/template engine 457 type TemplateContext struct { 458 Os string 459 Arch string 460 Cache string 461 Root string 462 PackageDir string 463 Binary string 464 Script string 465 Extension string 466 ScriptExtension string 467 } 468 469 func (cmd *DefaultCommand) render(text string) string { 470 ctx := TemplateContext{ 471 Os: runtime.GOOS, 472 Arch: runtime.GOARCH, 473 Cache: filepath.ToSlash(cmd.PkgDir), 474 Root: filepath.ToSlash(cmd.PkgDir), 475 PackageDir: filepath.ToSlash(cmd.PkgDir), 476 Binary: cmd.binary(runtime.GOOS), 477 Script: cmd.script(runtime.GOOS), 478 Extension: cmd.extension(runtime.GOOS), 479 ScriptExtension: cmd.script_ext(runtime.GOOS), 480 } 481 482 t, err := template.New("command-template").Parse(text) 483 if err != nil { 484 return text 485 } 486 487 builder := strings.Builder{} 488 err = t.Execute(&builder, ctx) 489 if err != nil { 490 return text 491 } 492 493 return builder.String() 494 }