github.com/jfrog/jfrog-cli-core/v2@v2.51.0/plugins/components/conversionlayer.go (about) 1 package components 2 3 import ( 4 "errors" 5 "fmt" 6 "strings" 7 8 "github.com/jfrog/gofrog/datastructures" 9 "github.com/jfrog/jfrog-cli-core/v2/docs/common" 10 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 11 "github.com/jfrog/jfrog-client-go/utils/errorutils" 12 "github.com/urfave/cli" 13 ) 14 15 func ConvertApp(jfrogApp App) (*cli.App, error) { 16 var err error 17 app := cli.NewApp() 18 app.Name = jfrogApp.Name 19 app.Description = jfrogApp.Description 20 app.Version = jfrogApp.Version 21 app.Commands, err = ConvertAppCommands(jfrogApp) 22 if err != nil { 23 return nil, err 24 } 25 // Defaults: 26 app.EnableBashCompletion = true 27 return app, nil 28 } 29 30 func ConvertAppCommands(jfrogApp App, commandPrefix ...string) (cmds []cli.Command, err error) { 31 cmds, err = convertCommands(jfrogApp.Commands, commandPrefix...) 32 if err != nil || len(jfrogApp.Subcommands) == 0 { 33 return 34 } 35 subcommands, err := convertSubcommands(jfrogApp.Subcommands, commandPrefix...) 36 if err != nil { 37 return 38 } 39 cmds = append(cmds, subcommands...) 40 return 41 } 42 43 func convertSubcommands(subcommands []Namespace, nameSpaces ...string) ([]cli.Command, error) { 44 var converted []cli.Command 45 for _, ns := range subcommands { 46 nameSpaceCommand := cli.Command{ 47 Name: ns.Name, 48 Usage: ns.Description, 49 Category: ns.Category, 50 } 51 nsCommands, err := convertCommands(ns.Commands, append(nameSpaces, ns.Name)...) 52 if err != nil { 53 return converted, err 54 } 55 nameSpaceCommand.Subcommands = nsCommands 56 converted = append(converted, nameSpaceCommand) 57 } 58 return converted, nil 59 } 60 61 func convertCommands(commands []Command, nameSpaces ...string) ([]cli.Command, error) { 62 var converted []cli.Command 63 for _, cmd := range commands { 64 cur, err := convertCommand(cmd, nameSpaces...) 65 if err != nil { 66 return converted, err 67 } 68 converted = append(converted, cur) 69 } 70 return converted, nil 71 } 72 73 func convertCommand(cmd Command, namespaces ...string) (cli.Command, error) { 74 convertedFlags, convertedStringFlags, err := convertFlags(cmd) 75 if err != nil { 76 return cli.Command{}, err 77 } 78 cmdUsages, err := createCommandUsages(cmd, convertedStringFlags, namespaces...) 79 if err != nil { 80 return cli.Command{}, err 81 } 82 return cli.Command{ 83 Name: cmd.Name, 84 Flags: convertedFlags, 85 Aliases: cmd.Aliases, 86 Category: cmd.Category, 87 Description: cmd.Description, 88 HelpName: common.CreateUsage(getCmdUsageString(cmd, namespaces...), cmd.Description, cmdUsages), 89 UsageText: createArgumentsSummary(cmd), 90 ArgsUsage: createEnvVarsSummary(cmd), 91 BashComplete: common.CreateBashCompletionFunc(), 92 SkipFlagParsing: cmd.SkipFlagParsing, 93 Hidden: cmd.Hidden, 94 // Passing any other interface than 'cli.ActionFunc' will fail the command. 95 Action: getActionFunc(cmd), 96 }, nil 97 } 98 99 func removeEmptyValues(slice []string) []string { 100 var result []string 101 for _, s := range slice { 102 if s != "" { 103 result = append(result, s) 104 } 105 } 106 return result 107 } 108 109 // Create the command usage strings that will be shown in the help. 110 func createCommandUsages(cmd Command, convertedStringFlags map[string]StringFlag, namespaces ...string) (usages []string, err error) { 111 // Handle manual usages provided. 112 if cmd.UsageOptions != nil { 113 for _, manualUsage := range cmd.UsageOptions.Usage { 114 usages = append(usages, fmt.Sprintf("%s %s", coreutils.GetCliExecutableName(), manualUsage)) 115 } 116 if cmd.UsageOptions.ReplaceAutoGeneratedUsage { 117 return 118 } 119 } 120 // Handle auto generated usages for the command. 121 generated, err := generateCommandUsages(getCmdUsageString(cmd, namespaces...), cmd, convertedStringFlags) 122 if err != nil { 123 return 124 } 125 usages = append(usages, generated...) 126 return 127 } 128 129 func getCmdUsageString(cmd Command, namespaces ...string) string { 130 return strings.Join(append(removeEmptyValues(namespaces), cmd.Name), " ") 131 } 132 133 // Generated usages are based on the command's flags and arguments: 134 // <cli-name> <command-name> [command options] --mandatory-opt1=<opt1-value-alias> --mandatory-opt2=<value>... <arg1> [optional-arg2] <arg3>... 135 func generateCommandUsages(usagePrefix string, cmd Command, convertedStringFlags map[string]StringFlag) (usages []string, err error) { 136 argumentsUsageParts, flagReplacements, err := getArgumentsUsageParts(cmd, convertedStringFlags) 137 if err != nil { 138 return 139 } 140 if len(argumentsUsageParts) == 0 { 141 // No arguments provided. 142 usages = append(usages, fmt.Sprintf("%s%s", usagePrefix, getFlagUsagePart(cmd, convertedStringFlags, nil))) 143 return 144 } 145 usages = append(usages, fmt.Sprintf("%s%s%s", usagePrefix, getFlagUsagePart(cmd, convertedStringFlags, nil), argumentsUsageParts[0])) 146 if len(argumentsUsageParts) == 1 { 147 // No flag replacements, return single usage. 148 return 149 } 150 // Add the usage with the flag replacements. 151 usages = append(usages, fmt.Sprintf("%s%s%s", usagePrefix, getFlagUsagePart(cmd, convertedStringFlags, flagReplacements), argumentsUsageParts[1])) 152 return 153 } 154 155 // Get the command usage parts that are related to arguments, if any. 156 // Mandatory arguments represented as <Arg-Name> and optional arguments represented as [Arg-Name]. 157 // If some arguments have flag replacements, creates two parts: with and without all replacements: 158 // 1) <arg1> [optional-arg2] <arg3> 159 // 2) --<optional-flag-replacement-2-3>=<value> <arg1> 160 func getArgumentsUsageParts(cmd Command, convertedStringFlags map[string]StringFlag) (usageParts []string, flagReplacements *datastructures.Set[string], err error) { 161 var usage string 162 if usage = getArgsUsagePart(cmd); usage != "" { 163 // No replacements arguments usage part. (1) 164 usageParts = append(usageParts, usage) 165 } 166 if usage, flagReplacements, err = getArgsUsagePartWithReplacements(cmd, convertedStringFlags); err != nil { 167 return 168 } 169 if usage != "" { 170 // With replacements arguments usage part. (2) 171 usageParts = append(usageParts, usage) 172 } 173 return 174 } 175 176 func getArgsUsagePart(cmd Command) (usage string) { 177 for _, argument := range cmd.Arguments { 178 usage += getArgumentUsage(argument) 179 } 180 return 181 } 182 183 func getArgumentUsage(argument Argument) string { 184 if argument.Optional { 185 return fmt.Sprintf(" [%s]", argument.Name) 186 } 187 return fmt.Sprintf(" <%s>", argument.Name) 188 } 189 190 func getArgsUsagePartWithReplacements(cmd Command, convertedStringFlags map[string]StringFlag) (usage string, flagReplacements *datastructures.Set[string], err error) { 191 flagReplacements = datastructures.MakeSet[string]() 192 for _, argument := range cmd.Arguments { 193 if argument.ReplaceWithFlag == "" { 194 usage += getArgumentUsage(argument) 195 continue 196 } 197 if flagReplacements.Exists(argument.ReplaceWithFlag) { 198 // Flag already exists in the replacements, skip. (Multiple arguments can have the same replacement flag) 199 continue 200 } 201 if _, exists := convertedStringFlags[argument.ReplaceWithFlag]; !exists { 202 err = fmt.Errorf("command '%s': argument '%s' has a defined replacement flag '%s' that does not exist", cmd.Name, argument.Name, argument.ReplaceWithFlag) 203 return 204 } 205 flagReplacements.Add(argument.ReplaceWithFlag) 206 } 207 if flagReplacements.Size() == 0 { 208 // No replacements, return empty string. 209 return "", nil, nil 210 } 211 for _, flagName := range flagReplacements.ToSlice() { 212 usage = getMandatoryFlagUsage(convertedStringFlags[flagName]) + usage 213 } 214 return 215 } 216 217 // Get the command usage part that is related to flags, if any. 218 // If some flags are optional, returns with general prefix `[command options]` followed by the mandatory flags. 219 // Mandatory flags are returned with their value alias, if provided. --<Name>=<ValueAlias> or --<Name>=<value> if no alias. 220 func getFlagUsagePart(cmd Command, convertedStringFlags map[string]StringFlag, flagReplacements *datastructures.Set[string]) (usage string) { 221 // Calculate flag counts. 222 totalFlagCount := len(cmd.Flags) 223 if totalFlagCount == 0 { 224 return 225 } 226 mandatoryFlagCount := getMandatoryFlagCount(cmd) 227 optionalFlagCount := totalFlagCount - mandatoryFlagCount 228 optionalFlagCountUsedAsArgReplacements := 0 229 if flagReplacements != nil { 230 optionalFlagCountUsedAsArgReplacements = flagReplacements.Size() 231 } 232 // Add general prefix. 233 if optionalFlagCount-optionalFlagCountUsedAsArgReplacements > 0 { 234 usage += " [command options]" 235 } 236 if mandatoryFlagCount == 0 { 237 return 238 } 239 // Add mandatory flags. 240 for flagName, flag := range convertedStringFlags { 241 if flag.Mandatory { 242 valueAlias := "value" 243 if flag.HelpValue != "" { 244 valueAlias = flag.HelpValue 245 } 246 usage += fmt.Sprintf(" --%s=<%s>", flagName, valueAlias) 247 } 248 } 249 return 250 } 251 252 func getMandatoryFlagCount(cmd Command) int { 253 count := 0 254 for _, flag := range cmd.Flags { 255 if flag.IsMandatory() { 256 count++ 257 } 258 } 259 return count 260 } 261 262 func getMandatoryFlagUsage(flag StringFlag) string { 263 valueAlias := "value" 264 if flag.HelpValue != "" { 265 valueAlias = flag.HelpValue 266 } 267 return fmt.Sprintf(" --%s=<%s>", flag.Name, valueAlias) 268 } 269 270 func createArgumentsSummary(cmd Command) string { 271 summary := "" 272 for i, argument := range cmd.Arguments { 273 if i > 0 { 274 summary += "\n" 275 } 276 optional := "" 277 if argument.Optional { 278 optional = " [Optional]" 279 } 280 summary += "\t" + argument.Name + optional + "\n\t\t" + argument.Description + "\n" 281 } 282 return summary 283 } 284 285 func createEnvVarsSummary(cmd Command) string { 286 var envVarsSummary []string 287 for i, env := range cmd.EnvVars { 288 summary := "" 289 if i > 0 { 290 summary += "\n" 291 } 292 summary += "\t" + env.Name + "\n" 293 if env.Default != "" { 294 summary += "\t\t[Default: " + env.Default + "]\n" 295 } 296 summary += "\t\t" + env.Description 297 envVarsSummary = append(envVarsSummary, summary) 298 } 299 return strings.Join(envVarsSummary, "\n") 300 } 301 302 func convertFlags(cmd Command) ([]cli.Flag, map[string]StringFlag, error) { 303 var convertedFlags []cli.Flag 304 convertedStringFlags := map[string]StringFlag{} 305 for _, flag := range cmd.Flags { 306 converted, convertedString, err := convertByType(flag) 307 if err != nil { 308 return convertedFlags, convertedStringFlags, fmt.Errorf("command '%s': %w", cmd.Name, err) 309 } 310 if converted != nil { 311 convertedFlags = append(convertedFlags, converted) 312 } 313 if convertedString != nil { 314 convertedStringFlags[flag.GetName()] = *convertedString 315 } 316 } 317 return convertedFlags, convertedStringFlags, nil 318 } 319 320 func convertByType(flag Flag) (cli.Flag, *StringFlag, error) { 321 switch actualType := flag.(type) { 322 case StringFlag: 323 return convertStringFlag(actualType), &actualType, nil 324 case BoolFlag: 325 return convertBoolFlag(actualType), nil, nil 326 } 327 return nil, nil, errorutils.CheckErrorf("flag '%s' does not match any known flag type", flag.GetName()) 328 } 329 330 func convertStringFlag(f StringFlag) cli.Flag { 331 stringFlag := cli.StringFlag{ 332 Name: f.Name, 333 Hidden: f.Hidden, 334 Usage: f.Description + "` `", 335 } 336 // If default is set, add its value and return. 337 if f.DefaultValue != "" { 338 stringFlag.Usage = fmt.Sprintf("[Default: %s] %s", f.DefaultValue, stringFlag.Usage) 339 return stringFlag 340 } 341 // Otherwise, mark as mandatory/optional accordingly. 342 if f.Mandatory { 343 stringFlag.Usage = "[Mandatory] " + stringFlag.Usage 344 } else { 345 stringFlag.Usage = "[Optional] " + stringFlag.Usage 346 } 347 return stringFlag 348 } 349 350 func convertBoolFlag(f BoolFlag) cli.Flag { 351 if f.DefaultValue { 352 return cli.BoolTFlag{ 353 Name: f.Name, 354 Hidden: f.Hidden, 355 Usage: "[Default: true] " + f.Description + "` `", 356 } 357 } 358 return cli.BoolFlag{ 359 Name: f.Name, 360 Hidden: f.Hidden, 361 Usage: "[Default: false] " + f.Description + "` `", 362 } 363 } 364 365 // Wrap the base's ActionFunc with our own, while retrieving needed information from the Context. 366 func getActionFunc(cmd Command) cli.ActionFunc { 367 return func(baseContext *cli.Context) error { 368 pluginContext, err := ConvertContext(baseContext, cmd.Flags...) 369 if err != nil { 370 return err 371 } 372 return cmd.Action(pluginContext) 373 } 374 } 375 376 func ConvertContext(baseContext *cli.Context, flagsToConvert ...Flag) (*Context, error) { 377 pluginContext := &Context{ 378 CommandName: baseContext.Command.Name, 379 Arguments: baseContext.Args(), 380 PrintCommandHelp: getPrintCommandHelpFunc(baseContext), 381 } 382 return pluginContext, fillFlagMaps(pluginContext, baseContext, flagsToConvert) 383 } 384 385 func getPrintCommandHelpFunc(c *cli.Context) func(commandName string) error { 386 return func(commandName string) error { 387 return cli.ShowCommandHelp(c, c.Command.Name) 388 } 389 } 390 391 func fillFlagMaps(c *Context, baseContext *cli.Context, originalFlags []Flag) error { 392 c.stringFlags = make(map[string]string) 393 c.boolFlags = make(map[string]bool) 394 395 // Loop over all plugin's known flags. 396 for _, flag := range originalFlags { 397 if stringFlag, ok := flag.(StringFlag); ok { 398 finalValue, err := getValueForStringFlag(stringFlag, baseContext.String(stringFlag.Name)) 399 if err != nil { 400 return err 401 } 402 c.stringFlags[stringFlag.Name] = finalValue 403 continue 404 } 405 406 if boolFlag, ok := flag.(BoolFlag); ok { 407 c.boolFlags[boolFlag.Name] = getValueForBoolFlag(boolFlag, baseContext) 408 } 409 } 410 return nil 411 } 412 413 func getValueForStringFlag(f StringFlag, receivedValue string) (finalValue string, err error) { 414 if receivedValue != "" { 415 return receivedValue, nil 416 } 417 // Empty but has a default value defined. 418 if f.DefaultValue != "" { 419 return f.DefaultValue, nil 420 } 421 // Empty but mandatory. 422 if f.Mandatory { 423 return "", errors.New("Mandatory flag '" + f.Name + "' is missing") 424 } 425 return "", nil 426 } 427 428 func getValueForBoolFlag(f BoolFlag, baseContext *cli.Context) bool { 429 if f.DefaultValue { 430 return baseContext.BoolT(f.Name) 431 } 432 return baseContext.Bool(f.Name) 433 }