From bd244b8ed055db0eea7a53bea1dd04f723210d30 Mon Sep 17 00:00:00 2001 From: Thomas Schmitt Date: Tue, 1 Oct 2024 12:18:43 +0300 Subject: [PATCH] Create command abstraction and split up command builder --- commandline/autocomplete_handler.go | 41 +- commandline/cli.go | 135 +++++- commandline/command_builder.go | 682 ++++++++++------------------ commandline/command_definition.go | 33 ++ commandline/flag_builder.go | 88 +++- commandline/flag_definition.go | 25 + commandline/flag_type.go | 26 ++ commandline/show_command_handler.go | 38 +- 8 files changed, 547 insertions(+), 521 deletions(-) create mode 100644 commandline/command_definition.go create mode 100644 commandline/flag_definition.go create mode 100644 commandline/flag_type.go diff --git a/commandline/autocomplete_handler.go b/commandline/autocomplete_handler.go index 953971b..49cbfa9 100644 --- a/commandline/autocomplete_handler.go +++ b/commandline/autocomplete_handler.go @@ -6,15 +6,13 @@ import ( "os" "path/filepath" "strings" - - "github.com/urfave/cli/v2" ) const directoryPermissions = 0755 const filePermissions = 0644 -const Powershell = "powershell" -const Bash = "bash" +const AutocompletePowershell = "powershell" +const AutocompleteBash = "bash" const completeHandlerEnabledCheck = "uipath_auto_complete" @@ -55,8 +53,8 @@ type autoCompleteHandler struct { } func (a autoCompleteHandler) EnableCompleter(shell string, filePath string) (string, error) { - if shell != Powershell && shell != Bash { - return "", fmt.Errorf("Invalid shell, supported values: %s, %s", Powershell, Bash) + if shell != AutocompletePowershell && shell != AutocompleteBash { + return "", fmt.Errorf("Invalid shell, supported values: %s, %s", AutocompletePowershell, AutocompleteBash) } profileFilePath, err := a.profileFilePath(shell, filePath) @@ -71,14 +69,14 @@ func (a autoCompleteHandler) profileFilePath(shell string, filePath string) (str if filePath != "" { return filePath, nil } - if shell == Powershell { + if shell == AutocompletePowershell { return PowershellProfilePath() } return BashrcPath() } func (a autoCompleteHandler) completeHandler(shell string) string { - if shell == Powershell { + if shell == AutocompletePowershell { return powershellCompleteHandler } return bashCompleteHandler @@ -136,17 +134,12 @@ func (a autoCompleteHandler) writeCompleterHandler(filePath string, completerHan return nil } -func (a autoCompleteHandler) Find(commandText string, commands []*cli.Command, exclude []string) []string { +func (a autoCompleteHandler) Find(commandText string, command *commandDefinition, exclude []string) []string { words := strings.Split(commandText, " ") if len(words) < 2 { return []string{} } - command := &cli.Command{ - Name: "uipath", - Subcommands: commands, - } - for _, word := range words[1 : len(words)-1] { if strings.HasPrefix(word, "-") { break @@ -164,7 +157,7 @@ func (a autoCompleteHandler) Find(commandText string, commands []*cli.Command, e return a.searchCommands(lastWord, command.Subcommands, exclude) } -func (a autoCompleteHandler) findCommand(name string, commands []*cli.Command) *cli.Command { +func (a autoCompleteHandler) findCommand(name string, commands []*commandDefinition) *commandDefinition { for _, command := range commands { if command.Name == name { return command @@ -173,7 +166,7 @@ func (a autoCompleteHandler) findCommand(name string, commands []*cli.Command) * return nil } -func (a autoCompleteHandler) searchCommands(word string, commands []*cli.Command, exclude []string) []string { +func (a autoCompleteHandler) searchCommands(word string, commands []*commandDefinition, exclude []string) []string { result := []string{} for _, command := range commands { if strings.HasPrefix(command.Name, word) { @@ -188,22 +181,16 @@ func (a autoCompleteHandler) searchCommands(word string, commands []*cli.Command return a.removeDuplicates(a.removeExcluded(result, exclude)) } -func (a autoCompleteHandler) searchFlags(word string, command *cli.Command, exclude []string) []string { +func (a autoCompleteHandler) searchFlags(word string, command *commandDefinition, exclude []string) []string { result := []string{} for _, flag := range command.Flags { - flagNames := flag.Names() - for _, flagName := range flagNames { - if strings.HasPrefix(flagName, word) { - result = append(result, "--"+flagName) - } + if strings.HasPrefix(flag.Name, word) { + result = append(result, "--"+flag.Name) } } for _, flag := range command.Flags { - flagNames := flag.Names() - for _, flagName := range flagNames { - if strings.Contains(flagName, word) { - result = append(result, "--"+flagName) - } + if strings.Contains(flag.Name, word) { + result = append(result, "--"+flag.Name) } } return a.removeDuplicates(a.removeExcluded(result, exclude)) diff --git a/commandline/cli.go b/commandline/cli.go index 018d45b..22dbe2b 100644 --- a/commandline/cli.go +++ b/commandline/cli.go @@ -40,7 +40,11 @@ func (c Cli) run(args []string, input utils.Stream) error { PluginExecutor: c.pluginExecutor, DefinitionProvider: c.definitionProvider, } - flags := CommandBuilder.CreateDefaultFlags(false) + + flagBuilder := newFlagBuilder() + flagBuilder.AddDefaultFlags(false) + flags := flagBuilder.ToList() + commands, err := CommandBuilder.Create(args) if err != nil { return err @@ -51,8 +55,8 @@ func (c Cli) run(args []string, input utils.Stream) error { Usage: "Command-Line Interface for UiPath Services", UsageText: "uipath --parameter", Version: "1.0", - Flags: flags, - Commands: commands, + Flags: c.convertFlags(flags...), + Commands: c.convertCommands(commands...), Writer: c.stdOut, ErrWriter: c.stdErr, HideVersion: true, @@ -89,3 +93,128 @@ func NewCli( ) *Cli { return &Cli{stdIn, stdOut, stdErr, colors, definitionProvider, configProvider, executor, pluginExecutor} } + +func (c Cli) convertCommand(command *commandDefinition) *cli.Command { + result := cli.Command{ + Name: command.Name, + Usage: command.Summary, + Description: command.Description, + Flags: c.convertFlags(command.Flags...), + Subcommands: c.convertCommands(command.Subcommands...), + CustomHelpTemplate: command.CustomHelpTemplate, + Hidden: command.Hidden, + HideHelp: true, + } + if command.Action != nil { + result.Action = func(context *cli.Context) error { + return command.Action(&CommandExecContext{context}) + } + } + return &result +} + +func (c Cli) convertCommands(commands ...*commandDefinition) []*cli.Command { + result := []*cli.Command{} + for _, command := range commands { + result = append(result, c.convertCommand(command)) + } + return result +} + +func (c Cli) convertStringSliceFlag(flag *flagDefinition) *cli.StringSliceFlag { + envVars := []string{} + if flag.EnvVarName != "" { + envVars = append(envVars, flag.EnvVarName) + } + var value *cli.StringSlice + if flag.Value != nil { + value = cli.NewStringSlice(flag.Value.([]string)...) + } + return &cli.StringSliceFlag{ + Name: flag.Name, + Usage: flag.Summary, + EnvVars: envVars, + Required: flag.Required, + Hidden: flag.Hidden, + Value: value, + } +} + +func (c Cli) convertIntFlag(flag *flagDefinition) *cli.IntFlag { + envVars := []string{} + if flag.EnvVarName != "" { + envVars = append(envVars, flag.EnvVarName) + } + var value int + if flag.Value != nil { + value = flag.Value.(int) + } + return &cli.IntFlag{ + Name: flag.Name, + Usage: flag.Summary, + EnvVars: envVars, + Required: flag.Required, + Hidden: flag.Hidden, + Value: value, + } +} + +func (c Cli) convertBoolFlag(flag *flagDefinition) *cli.BoolFlag { + envVars := []string{} + if flag.EnvVarName != "" { + envVars = append(envVars, flag.EnvVarName) + } + var value bool + if flag.Value != nil { + value = flag.Value.(bool) + } + return &cli.BoolFlag{ + Name: flag.Name, + Usage: flag.Summary, + EnvVars: envVars, + Required: flag.Required, + Hidden: flag.Hidden, + Value: value, + } +} + +func (c Cli) convertStringFlag(flag *flagDefinition) *cli.StringFlag { + envVars := []string{} + if flag.EnvVarName != "" { + envVars = append(envVars, flag.EnvVarName) + } + var value string + if flag.Value != nil { + value = flag.Value.(string) + } + return &cli.StringFlag{ + Name: flag.Name, + Usage: flag.Summary, + EnvVars: envVars, + Required: flag.Required, + Hidden: flag.Hidden, + Value: value, + } +} + +func (c Cli) convertFlag(flag *flagDefinition) cli.Flag { + switch flag.Type { + case FlagTypeStringArray: + return c.convertStringSliceFlag(flag) + case FlagTypeInteger: + return c.convertIntFlag(flag) + case FlagTypeBoolean: + return c.convertBoolFlag(flag) + case FlagTypeString: + return c.convertStringFlag(flag) + } + panic(fmt.Sprintf("Unknown flag type: %s", flag.Type.String())) +} + +func (c Cli) convertFlags(flags ...*flagDefinition) []cli.Flag { + result := []cli.Flag{} + for _, flag := range flags { + result = append(result, c.convertFlag(flag)) + } + return result +} diff --git a/commandline/command_builder.go b/commandline/command_builder.go index 8b85782..45fa863 100644 --- a/commandline/command_builder.go +++ b/commandline/command_builder.go @@ -17,43 +17,10 @@ import ( "github.com/UiPath/uipathcli/output" "github.com/UiPath/uipathcli/parser" "github.com/UiPath/uipathcli/utils" - "github.com/urfave/cli/v2" ) const FromStdIn = "-" -const insecureFlagName = "insecure" -const debugFlagName = "debug" -const profileFlagName = "profile" -const uriFlagName = "uri" -const identityUriFlagName = "identity-uri" -const organizationFlagName = "organization" -const tenantFlagName = "tenant" -const helpFlagName = "help" -const outputFormatFlagName = "output" -const queryFlagName = "query" -const waitFlagName = "wait" -const waitTimeoutFlagName = "wait-timeout" -const versionFlagName = "version" -const fileFlagName = "file" - -var predefinedFlags = []string{ - insecureFlagName, - debugFlagName, - profileFlagName, - uriFlagName, - identityUriFlagName, - organizationFlagName, - tenantFlagName, - helpFlagName, - outputFormatFlagName, - queryFlagName, - waitFlagName, - waitTimeoutFlagName, - versionFlagName, - fileFlagName, -} - const outputFormatJson = "json" const outputFormatText = "text" @@ -87,14 +54,14 @@ type CommandBuilder struct { DefinitionProvider DefinitionProvider } -func (b CommandBuilder) sort(commands []*cli.Command) { +func (b CommandBuilder) sort(commands []*commandDefinition) { sort.Slice(commands, func(i, j int) bool { return commands[i].Name < commands[j].Name }) } -func (b CommandBuilder) fileInput(context *cli.Context, parameters []parser.Parameter) utils.Stream { - value := context.String(fileFlagName) +func (b CommandBuilder) fileInput(context *CommandExecContext, parameters []parser.Parameter) utils.Stream { + value := context.String(FlagNameFile) if value == "" { return nil } @@ -102,14 +69,14 @@ func (b CommandBuilder) fileInput(context *cli.Context, parameters []parser.Para return b.Input } for _, param := range parameters { - if strings.EqualFold(param.FieldName, fileFlagName) { + if strings.EqualFold(param.FieldName, FlagNameFile) { return nil } } return utils.NewFileStream(value) } -func (b CommandBuilder) createExecutionParameters(context *cli.Context, config *config.Config, operation parser.Operation) (executor.ExecutionParameters, error) { +func (b CommandBuilder) createExecutionParameters(context *CommandExecContext, config *config.Config, operation parser.Operation) (executor.ExecutionParameters, error) { typeConverter := newTypeConverter() parameters := []executor.ExecutionParameter{} @@ -163,23 +130,16 @@ func (b CommandBuilder) formatAllowedValues(values []interface{}) string { return result } -func (b CommandBuilder) createFlags(parameters []parser.Parameter) []cli.Flag { - flags := []cli.Flag{} +func (b CommandBuilder) createFlags(parameters []parser.Parameter) []*flagDefinition { + flags := []*flagDefinition{} for _, parameter := range parameters { formatter := newParameterFormatter(parameter) + flagType := FlagTypeString if parameter.IsArray() { - flag := cli.StringSliceFlag{ - Name: parameter.Name, - Usage: formatter.Description(), - } - flags = append(flags, &flag) - } else { - flag := cli.StringFlag{ - Name: parameter.Name, - Usage: formatter.Description(), - } - flags = append(flags, &flag) + flagType = FlagTypeStringArray } + flag := newFlagDefinition(parameter.Name, formatter.Description(), "", flagType, "", nil, false, false) + flags = append(flags, flag) } return flags } @@ -196,8 +156,8 @@ func (b CommandBuilder) sortParameters(parameters []parser.Parameter) { }) } -func (b CommandBuilder) outputFormat(config config.Config, context *cli.Context) (string, error) { - outputFormat := context.String(outputFormatFlagName) +func (b CommandBuilder) outputFormat(config config.Config, context *CommandExecContext) (string, error) { + outputFormat := context.String(FlagNameOutputFormat) if outputFormat == "" { outputFormat = config.Output } @@ -210,7 +170,7 @@ func (b CommandBuilder) outputFormat(config config.Config, context *cli.Context) return outputFormat, nil } -func (b CommandBuilder) createBaseUri(operation parser.Operation, config config.Config, context *cli.Context) (url.URL, error) { +func (b CommandBuilder) createBaseUri(operation parser.Operation, config config.Config, context *CommandExecContext) (url.URL, error) { uriArgument, err := b.parseUriArgument(context) if err != nil { return operation.BaseUri, err @@ -222,12 +182,12 @@ func (b CommandBuilder) createBaseUri(operation parser.Operation, config config. return builder.Uri(), nil } -func (b CommandBuilder) createIdentityUri(context *cli.Context, config config.Config, baseUri url.URL) (*url.URL, error) { - uri := context.String(identityUriFlagName) +func (b CommandBuilder) createIdentityUri(context *CommandExecContext, config config.Config, baseUri url.URL) (*url.URL, error) { + uri := context.String(FlagNameIdentityUri) if uri != "" { identityUri, err := url.Parse(uri) if err != nil { - return nil, fmt.Errorf("Error parsing %s argument: %w", identityUriFlagName, err) + return nil, fmt.Errorf("Error parsing %s argument: %w", FlagNameIdentityUri, err) } return identityUri, nil } @@ -248,19 +208,19 @@ func (b CommandBuilder) createIdentityUri(context *cli.Context, config config.Co return identityUri, nil } -func (b CommandBuilder) parseUriArgument(context *cli.Context) (*url.URL, error) { - uriFlag := context.String(uriFlagName) +func (b CommandBuilder) parseUriArgument(context *CommandExecContext) (*url.URL, error) { + uriFlag := context.String(FlagNameUri) if uriFlag == "" { return nil, nil } uriArgument, err := url.Parse(uriFlag) if err != nil { - return nil, fmt.Errorf("Error parsing %s argument: %w", uriFlagName, err) + return nil, fmt.Errorf("Error parsing %s argument: %w", FlagNameUri, err) } return uriArgument, nil } -func (b CommandBuilder) getValue(parameter parser.Parameter, context *cli.Context, config config.Config) string { +func (b CommandBuilder) getValue(parameter parser.Parameter, context *CommandExecContext, config config.Config) string { value := context.String(parameter.Name) if value != "" { return value @@ -279,7 +239,7 @@ func (b CommandBuilder) getValue(parameter parser.Parameter, context *cli.Contex return "" } -func (b CommandBuilder) validateArguments(context *cli.Context, parameters []parser.Parameter, config config.Config) error { +func (b CommandBuilder) validateArguments(context *CommandExecContext, parameters []parser.Parameter, config config.Config) error { err := errors.New("Invalid arguments:") result := true for _, parameter := range parameters { @@ -334,91 +294,82 @@ func (b CommandBuilder) executeCommand(context executor.ExecutionContext, writer return b.Executor.Call(context, writer, logger) } -func (b CommandBuilder) createOperationCommand(operation parser.Operation) *cli.Command { +func (b CommandBuilder) createOperationCommand(operation parser.Operation) *commandDefinition { parameters := operation.Parameters b.sortParameters(parameters) flagBuilder := newFlagBuilder() flagBuilder.AddFlags(b.createFlags(parameters)) - flagBuilder.AddFlags(b.CreateDefaultFlags(true)) - flagBuilder.AddFlag(b.HelpFlag()) - - return &cli.Command{ - Name: operation.Name, - Usage: operation.Summary, - Description: operation.Description, - Flags: flagBuilder.ToList(), - CustomHelpTemplate: subcommandHelpTemplate, - Action: func(context *cli.Context) error { - profileName := context.String(profileFlagName) - config := b.ConfigProvider.Config(profileName) - if config == nil { - return fmt.Errorf("Could not find profile '%s'", profileName) - } - outputFormat, err := b.outputFormat(*config, context) - if err != nil { - return err - } - query := context.String(queryFlagName) - wait := context.String(waitFlagName) - waitTimeout := context.Int(waitTimeoutFlagName) - - baseUri, err := b.createBaseUri(operation, *config, context) - if err != nil { - return err - } + flagBuilder.AddDefaultFlags(true) + flagBuilder.AddHelpFlag() + + return newCommandDefinition(operation.Name, operation.Summary, operation.Description, flagBuilder.ToList(), nil, subcommandHelpTemplate, operation.Hidden, func(context *CommandExecContext) error { + profileName := context.String(FlagNameProfile) + config := b.ConfigProvider.Config(profileName) + if config == nil { + return fmt.Errorf("Could not find profile '%s'", profileName) + } + outputFormat, err := b.outputFormat(*config, context) + if err != nil { + return err + } + query := context.String(FlagNameQuery) + wait := context.String(FlagNameWait) + waitTimeout := context.Int(FlagNameWaitTimeout) - input := b.fileInput(context, operation.Parameters) - if input == nil { - err = b.validateArguments(context, operation.Parameters, *config) - if err != nil { - return err - } - } + baseUri, err := b.createBaseUri(operation, *config, context) + if err != nil { + return err + } - parameters, err := b.createExecutionParameters(context, config, operation) + input := b.fileInput(context, operation.Parameters) + if input == nil { + err = b.validateArguments(context, operation.Parameters, *config) if err != nil { return err } + } - organization := context.String(organizationFlagName) - if organization == "" { - organization = config.Organization - } - tenant := context.String(tenantFlagName) - if tenant == "" { - tenant = config.Tenant - } - insecure := context.Bool(insecureFlagName) || config.Insecure - debug := context.Bool(debugFlagName) || config.Debug - identityUri, err := b.createIdentityUri(context, *config, baseUri) - if err != nil { - return err - } + parameters, err := b.createExecutionParameters(context, config, operation) + if err != nil { + return err + } - executionContext := executor.NewExecutionContext( - organization, - tenant, - operation.Method, - baseUri, - operation.Route, - operation.ContentType, - input, - parameters, - config.Auth, - insecure, - debug, - *identityUri, - operation.Plugin) - - if wait != "" { - return b.executeWait(*executionContext, outputFormat, query, wait, waitTimeout) - } - return b.execute(*executionContext, outputFormat, query, nil) - }, - HideHelp: true, - Hidden: operation.Hidden, - } + organization := context.String(FlagNameOrganization) + if organization == "" { + organization = config.Organization + } + tenant := context.String(FlagNameTenant) + if tenant == "" { + tenant = config.Tenant + } + insecure := context.Bool(FlagNameInsecure) || config.Insecure + debug := context.Bool(FlagNameDebug) || config.Debug + identityUri, err := b.createIdentityUri(context, *config, baseUri) + if err != nil { + return err + } + + executionContext := executor.NewExecutionContext( + organization, + tenant, + operation.Method, + baseUri, + operation.Route, + operation.ContentType, + input, + parameters, + config.Auth, + insecure, + debug, + *identityUri, + operation.Plugin) + + if wait != "" { + return b.executeWait(*executionContext, outputFormat, query, wait, waitTimeout) + } + return b.execute(*executionContext, outputFormat, query, nil) + }) } func (b CommandBuilder) executeWait(executionContext executor.ExecutionContext, outputFormat string, query string, wait string, waitTimeout int) error { @@ -495,19 +446,15 @@ func (b CommandBuilder) execute(executionContext executor.ExecutionContext, outp return err } -func (b CommandBuilder) createCategoryCommand(operation parser.Operation) *cli.Command { - return &cli.Command{ - Name: operation.Category.Name, - Description: operation.Category.Description, - Flags: []cli.Flag{ - b.HelpFlag(), - b.VersionFlag(true), - }, - HideHelp: true, - } +func (b CommandBuilder) createCategoryCommand(operation parser.Operation) *commandDefinition { + flagBuilder := newFlagBuilder() + flagBuilder.AddHelpFlag() + flagBuilder.AddVersionFlag(true) + + return newCommandDefinition(operation.Category.Name, "", operation.Category.Description, flagBuilder.ToList(), nil, "", false, nil) } -func (b CommandBuilder) createServiceCommandCategory(operation parser.Operation, categories map[string]*cli.Command) (bool, *cli.Command) { +func (b CommandBuilder) createServiceCommandCategory(operation parser.Operation, categories map[string]*commandDefinition) (bool, *commandDefinition) { isNewCategory := false operationCommand := b.createOperationCommand(operation) command, found := categories[operation.Category.Name] @@ -520,9 +467,9 @@ func (b CommandBuilder) createServiceCommandCategory(operation parser.Operation, return isNewCategory, command } -func (b CommandBuilder) createServiceCommand(definition parser.Definition) *cli.Command { - categories := map[string]*cli.Command{} - commands := []*cli.Command{} +func (b CommandBuilder) createServiceCommand(definition parser.Definition) *commandDefinition { + categories := map[string]*commandDefinition{} + commands := []*commandDefinition{} for _, operation := range definition.Operations { if operation.Category == nil { command := b.createOperationCommand(operation) @@ -539,180 +486,119 @@ func (b CommandBuilder) createServiceCommand(definition parser.Definition) *cli. b.sort(command.Subcommands) } - return &cli.Command{ - Name: definition.Name, - Description: definition.Description, - Flags: []cli.Flag{ - b.HelpFlag(), - b.VersionFlag(true), - }, - Subcommands: commands, - HideHelp: true, - } + flagBuilder := newFlagBuilder() + flagBuilder.AddHelpFlag() + flagBuilder.AddVersionFlag(true) + + return newCommandDefinition(definition.Name, "", definition.Description, flagBuilder.ToList(), commands, "", false, nil) } -func (b CommandBuilder) createAutoCompleteEnableCommand() *cli.Command { +func (b CommandBuilder) createAutoCompleteEnableCommand() *commandDefinition { const shellFlagName = "shell" - const powershellFlagValue = "powershell" - const bashFlagValue = "bash" const fileFlagName = "file" - return &cli.Command{ - Name: "enable", - Description: "Enables auto complete in your shell", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: shellFlagName, - Usage: fmt.Sprintf("%s, %s", powershellFlagValue, bashFlagValue), - Required: true, - }, - &cli.StringFlag{ - Name: fileFlagName, - Hidden: true, - }, - b.HelpFlag(), - }, - Action: func(context *cli.Context) error { - shell := context.String(shellFlagName) - filePath := context.String(fileFlagName) - handler := newAutoCompleteHandler() - output, err := handler.EnableCompleter(shell, filePath) - if err != nil { - return err - } - fmt.Fprintln(b.StdOut, output) - return nil - }, - HideHelp: true, - } -} - -func (b CommandBuilder) createAutoCompleteCompleteCommand(version string) *cli.Command { - return &cli.Command{ - Name: "complete", - Description: "Returns the autocomplete suggestions", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "command", - Usage: "The command to autocomplete", - Required: true, - }, - b.HelpFlag(), - }, - Action: func(context *cli.Context) error { - commandText := context.String("command") - exclude := []string{} - for _, flagName := range predefinedFlags { - exclude = append(exclude, "--"+flagName) - } - args := strings.Split(commandText, " ") - definitions, err := b.loadAutocompleteDefinitions(args, version) - if err != nil { - return err - } - commands := b.createServiceCommands(definitions) - handler := newAutoCompleteHandler() - words := handler.Find(commandText, commands, exclude) - for _, word := range words { - fmt.Fprintln(b.StdOut, word) - } - return nil - }, - HideHelp: true, - } + flagBuilder := newFlagBuilder() + flagBuilder.AddFlag(newFlagDefinition(shellFlagName, fmt.Sprintf("%s, %s", AutocompletePowershell, AutocompleteBash), "", FlagTypeString, "", nil, false, true)) + flagBuilder.AddFlag(newFlagDefinition(fileFlagName, "", "", FlagTypeString, "", nil, true, false)) + flagBuilder.AddHelpFlag() + + return newCommandDefinition("enable", "", "Enables auto complete in your shell", flagBuilder.ToList(), nil, "", false, func(context *CommandExecContext) error { + shell := context.String(shellFlagName) + filePath := context.String(fileFlagName) + handler := newAutoCompleteHandler() + output, err := handler.EnableCompleter(shell, filePath) + if err != nil { + return err + } + fmt.Fprintln(b.StdOut, output) + return nil + }) } -func (b CommandBuilder) createAutoCompleteCommand(version string) *cli.Command { - return &cli.Command{ - Name: "autocomplete", - Description: "Commands for autocompletion", - Flags: []cli.Flag{ - b.HelpFlag(), - }, - Subcommands: []*cli.Command{ - b.createAutoCompleteEnableCommand(), - b.createAutoCompleteCompleteCommand(version), - }, - HideHelp: true, +func (b CommandBuilder) createAutoCompleteCompleteCommand(version string) *commandDefinition { + flagBuilder := newFlagBuilder() + flagBuilder.AddFlag(newFlagDefinition("command", "The command to autocomplete", "", FlagTypeString, "", nil, false, true)) + flagBuilder.AddHelpFlag() + + return newCommandDefinition("complete", "", "Returns the autocomplete suggestions", flagBuilder.ToList(), nil, "", false, func(context *CommandExecContext) error { + commandText := context.String("command") + exclude := []string{} + for _, flagName := range FlagNamesPredefined { + exclude = append(exclude, "--"+flagName) + } + args := strings.Split(commandText, " ") + definitions, err := b.loadAutocompleteDefinitions(args, version) + if err != nil { + return err + } + commands := b.createServiceCommands(definitions) + command := newCommandDefinition("uipath", "", "", nil, commands, "", false, nil) + handler := newAutoCompleteHandler() + words := handler.Find(commandText, command, exclude) + for _, word := range words { + fmt.Fprintln(b.StdOut, word) + } + return nil + }) +} + +func (b CommandBuilder) createAutoCompleteCommand(version string) *commandDefinition { + flagBuilder := newFlagBuilder() + flagBuilder.AddHelpFlag() + + subcommands := []*commandDefinition{ + b.createAutoCompleteEnableCommand(), + b.createAutoCompleteCompleteCommand(version), } + + return newCommandDefinition("autocomplete", "", "Commands for autocompletion", flagBuilder.ToList(), subcommands, "", false, nil) } -func (b CommandBuilder) createConfigCommand() *cli.Command { +func (b CommandBuilder) createConfigCommand() *commandDefinition { authFlagName := "auth" - flags := []cli.Flag{ - &cli.StringFlag{ - Name: authFlagName, - Usage: fmt.Sprintf("Authorization type: %s, %s, %s", CredentialsAuth, LoginAuth, PatAuth), - }, - &cli.StringFlag{ - Name: profileFlagName, - Usage: "Profile to configure", - EnvVars: []string{"UIPATH_PROFILE"}, - Value: config.DefaultProfile, - }, - b.HelpFlag(), - } - - return &cli.Command{ - Name: "config", - Description: "Interactive command to configure the CLI", - Flags: flags, - Subcommands: []*cli.Command{ - b.createConfigSetCommand(), - }, - Action: func(context *cli.Context) error { - auth := context.String(authFlagName) - profileName := context.String(profileFlagName) - handler := ConfigCommandHandler{ - StdIn: b.StdIn, - StdOut: b.StdOut, - ConfigProvider: b.ConfigProvider, - } - return handler.Configure(auth, profileName) - }, - HideHelp: true, + + flagBuilder := newFlagBuilder() + flagBuilder.AddFlag(newFlagDefinition(authFlagName, fmt.Sprintf("Authorization type: %s, %s, %s", CredentialsAuth, LoginAuth, PatAuth), "", FlagTypeString, "", nil, false, false)) + flagBuilder.AddFlag(newFlagDefinition(FlagNameProfile, "Profile to configure", "", FlagTypeString, "UIPATH_PROFILE", config.DefaultProfile, false, false)) + flagBuilder.AddHelpFlag() + + subcommands := []*commandDefinition{ + b.createConfigSetCommand(), } + + return newCommandDefinition("config", "", "Interactive command to configure the CLI", flagBuilder.ToList(), subcommands, "", false, func(context *CommandExecContext) error { + auth := context.String(authFlagName) + profileName := context.String(FlagNameProfile) + handler := ConfigCommandHandler{ + StdIn: b.StdIn, + StdOut: b.StdOut, + ConfigProvider: b.ConfigProvider, + } + return handler.Configure(auth, profileName) + }) } -func (b CommandBuilder) createConfigSetCommand() *cli.Command { +func (b CommandBuilder) createConfigSetCommand() *commandDefinition { keyFlagName := "key" valueFlagName := "value" - flags := []cli.Flag{ - &cli.StringFlag{ - Name: keyFlagName, - Usage: "The key", - Required: true, - }, - &cli.StringFlag{ - Name: valueFlagName, - Usage: "The value to set", - Required: true, - }, - &cli.StringFlag{ - Name: profileFlagName, - Usage: "Profile to configure", - EnvVars: []string{"UIPATH_PROFILE"}, - Value: config.DefaultProfile, - }, - b.HelpFlag(), - } - return &cli.Command{ - Name: "set", - Description: "Set config parameters", - Flags: flags, - Action: func(context *cli.Context) error { - profileName := context.String(profileFlagName) - key := context.String(keyFlagName) - value := context.String(valueFlagName) - handler := ConfigCommandHandler{ - StdIn: b.StdIn, - StdOut: b.StdOut, - ConfigProvider: b.ConfigProvider, - } - return handler.Set(key, value, profileName) - }, - HideHelp: true, - } + + flagBuilder := newFlagBuilder() + flagBuilder.AddFlag(newFlagDefinition(keyFlagName, "The key", "", FlagTypeString, "", nil, false, true)) + flagBuilder.AddFlag(newFlagDefinition(valueFlagName, "The value to set", "", FlagTypeString, "", nil, false, true)) + flagBuilder.AddFlag(newFlagDefinition(FlagNameProfile, "Profile to configure", "", FlagTypeString, "UIPATH_PROFILE", config.DefaultProfile, false, false)) + flagBuilder.AddHelpFlag() + + return newCommandDefinition("set", "", "Set config parameters", flagBuilder.ToList(), nil, "", false, func(context *CommandExecContext) error { + profileName := context.String(FlagNameProfile) + key := context.String(keyFlagName) + value := context.String(valueFlagName) + handler := ConfigCommandHandler{ + StdIn: b.StdIn, + StdOut: b.StdOut, + ConfigProvider: b.ConfigProvider, + } + return handler.Set(key, value, profileName) + }) } func (b CommandBuilder) loadDefinitions(args []string, version string) ([]parser.Definition, error) { @@ -754,45 +640,38 @@ func (b CommandBuilder) loadAutocompleteDefinitions(args []string, version strin return b.loadDefinitions(args, version) } -func (b CommandBuilder) createShowCommand(definitions []parser.Definition, commands []*cli.Command) *cli.Command { - return &cli.Command{ - Name: "commands", - Description: "Command to inspect available uipath CLI operations", - Flags: []cli.Flag{ - b.HelpFlag(), - }, - Subcommands: []*cli.Command{ - { - Name: "show", - Description: "Print available uipath CLI commands", - Flags: []cli.Flag{ - b.HelpFlag(), - }, - Action: func(context *cli.Context) error { - flagBuilder := newFlagBuilder() - flagBuilder.AddFlags(b.CreateDefaultFlags(false)) - flagBuilder.AddFlag(b.HelpFlag()) - flags := flagBuilder.ToList() - - handler := newShowCommandHandler() - output, err := handler.Execute(definitions, flags) - if err != nil { - return err - } - fmt.Fprintln(b.StdOut, output) - return nil - }, - HideHelp: true, - Hidden: true, - }, - }, - HideHelp: true, - Hidden: true, - } -} - -func (b CommandBuilder) createServiceCommands(definitions []parser.Definition) []*cli.Command { - commands := []*cli.Command{} +func (b CommandBuilder) createShowCommand(definitions []parser.Definition) *commandDefinition { + flagBuilder := newFlagBuilder() + flagBuilder.AddHelpFlag() + + return newCommandDefinition("show", "", "Print available uipath CLI commands", flagBuilder.ToList(), nil, "", true, func(context *CommandExecContext) error { + builder := newFlagBuilder() + builder.AddDefaultFlags(false) + builder.AddHelpFlag() + + handler := newShowCommandHandler() + output, err := handler.Execute(definitions, builder.ToList()) + if err != nil { + return err + } + fmt.Fprintln(b.StdOut, output) + return nil + }) +} + +func (b CommandBuilder) createInspectCommand(definitions []parser.Definition) *commandDefinition { + flagBuilder := newFlagBuilder() + flagBuilder.AddHelpFlag() + + subcommands := []*commandDefinition{ + b.createShowCommand(definitions), + } + + return newCommandDefinition("commands", "", "Command to inspect available uipath CLI operations", flagBuilder.ToList(), subcommands, "", true, nil) +} + +func (b CommandBuilder) createServiceCommands(definitions []parser.Definition) []*commandDefinition { + commands := []*commandDefinition{} for _, e := range definitions { command := b.createServiceCommand(e) commands = append(commands, command) @@ -819,9 +698,9 @@ func (b CommandBuilder) versionFromProfile(profile string) string { return config.Version } -func (b CommandBuilder) Create(args []string) ([]*cli.Command, error) { - version := b.parseArgument(args, versionFlagName) - profile := b.parseArgument(args, profileFlagName) +func (b CommandBuilder) Create(args []string) ([]*commandDefinition, error) { + version := b.parseArgument(args, FlagNameVersion) + profile := b.parseArgument(args, FlagNameProfile) if version == "" && profile != "" { version = b.versionFromProfile(profile) } @@ -832,108 +711,7 @@ func (b CommandBuilder) Create(args []string) ([]*cli.Command, error) { servicesCommands := b.createServiceCommands(definitions) autocompleteCommand := b.createAutoCompleteCommand(version) configCommand := b.createConfigCommand() - showCommand := b.createShowCommand(definitions, servicesCommands) - commands := append(servicesCommands, autocompleteCommand, configCommand, showCommand) + inspectCommand := b.createInspectCommand(definitions) + commands := append(servicesCommands, autocompleteCommand, configCommand, inspectCommand) return commands, nil } - -func (b CommandBuilder) CreateDefaultFlags(hidden bool) []cli.Flag { - return []cli.Flag{ - &cli.BoolFlag{ - Name: debugFlagName, - Usage: "Enable debug output", - EnvVars: []string{"UIPATH_DEBUG"}, - Value: false, - Hidden: hidden, - }, - &cli.StringFlag{ - Name: profileFlagName, - Usage: "Config profile to use", - EnvVars: []string{"UIPATH_PROFILE"}, - Value: config.DefaultProfile, - Hidden: hidden, - }, - &cli.StringFlag{ - Name: uriFlagName, - Usage: "Server Base-URI", - EnvVars: []string{"UIPATH_URI"}, - Hidden: hidden, - }, - &cli.StringFlag{ - Name: organizationFlagName, - Usage: "Organization name", - EnvVars: []string{"UIPATH_ORGANIZATION"}, - Hidden: hidden, - }, - &cli.StringFlag{ - Name: tenantFlagName, - Usage: "Tenant name", - EnvVars: []string{"UIPATH_TENANT"}, - Hidden: hidden, - }, - &cli.BoolFlag{ - Name: insecureFlagName, - Usage: "Disable HTTPS certificate check", - EnvVars: []string{"UIPATH_INSECURE"}, - Value: false, - Hidden: hidden, - }, - &cli.StringFlag{ - Name: outputFormatFlagName, - Usage: fmt.Sprintf("Set output format: %s (default), %s", outputFormatJson, outputFormatText), - EnvVars: []string{"UIPATH_OUTPUT"}, - Value: "", - Hidden: hidden, - }, - &cli.StringFlag{ - Name: queryFlagName, - Usage: "Perform JMESPath query on output", - Value: "", - Hidden: hidden, - }, - &cli.StringFlag{ - Name: waitFlagName, - Usage: "Waits for the provided condition (JMESPath expression)", - Value: "", - Hidden: hidden, - }, - &cli.IntFlag{ - Name: waitTimeoutFlagName, - Usage: "Time to wait in seconds for condition", - Value: 30, - Hidden: hidden, - }, - &cli.StringFlag{ - Name: fileFlagName, - Usage: "Provide input from file (use - for stdin)", - Value: "", - Hidden: hidden, - }, - &cli.StringFlag{ - Name: identityUriFlagName, - Usage: "Identity Server URI", - EnvVars: []string{"UIPATH_IDENTITY_URI"}, - Hidden: hidden, - }, - b.VersionFlag(hidden), - } -} - -func (b CommandBuilder) VersionFlag(hidden bool) cli.Flag { - return &cli.StringFlag{ - Name: versionFlagName, - Usage: "Specific service version", - EnvVars: []string{"UIPATH_VERSION"}, - Value: "", - Hidden: hidden, - } -} - -func (b CommandBuilder) HelpFlag() cli.Flag { - return &cli.BoolFlag{ - Name: helpFlagName, - Usage: "Show help", - Value: false, - Hidden: true, - } -} diff --git a/commandline/command_definition.go b/commandline/command_definition.go new file mode 100644 index 0000000..c9c6f6b --- /dev/null +++ b/commandline/command_definition.go @@ -0,0 +1,33 @@ +package commandline + +import "github.com/urfave/cli/v2" + +type CommandExecContext struct { + *cli.Context +} + +type CommandExecFunc func(*CommandExecContext) error + +type commandDefinition struct { + Name string + Summary string + Description string + Flags []*flagDefinition + Subcommands []*commandDefinition + CustomHelpTemplate string + Hidden bool + Action CommandExecFunc +} + +func newCommandDefinition(name string, summary string, description string, flags []*flagDefinition, subcommands []*commandDefinition, customHelpTemplate string, hidden bool, action CommandExecFunc) *commandDefinition { + return &commandDefinition{ + name, + summary, + description, + flags, + subcommands, + customHelpTemplate, + hidden, + action, + } +} diff --git a/commandline/flag_builder.go b/commandline/flag_builder.go index bca3eb3..df54883 100644 --- a/commandline/flag_builder.go +++ b/commandline/flag_builder.go @@ -1,36 +1,108 @@ package commandline import ( - "github.com/urfave/cli/v2" + "fmt" + + "github.com/UiPath/uipathcli/config" ) +const FlagNameDebug = "debug" +const FlagNameProfile = "profile" +const FlagNameUri = "uri" +const FlagNameOrganization = "organization" +const FlagNameTenant = "tenant" +const FlagNameInsecure = "insecure" +const FlagNameOutputFormat = "output" +const FlagNameQuery = "query" +const FlagNameWait = "wait" +const FlagNameWaitTimeout = "wait-timeout" +const FlagNameFile = "file" +const FlagNameIdentityUri = "identity-uri" +const FlagNameVersion = "version" +const FlagNameHelp = "help" + +var FlagNamesPredefined = []string{ + FlagNameDebug, + FlagNameProfile, + FlagNameUri, + FlagNameOrganization, + FlagNameTenant, + FlagNameInsecure, + FlagNameOutputFormat, + FlagNameQuery, + FlagNameWait, + FlagNameWaitTimeout, + FlagNameFile, + FlagNameIdentityUri, + FlagNameVersion, + FlagNameHelp, +} + type flagBuilder struct { - flags map[string]cli.Flag + flags map[string]*flagDefinition order []string } -func (b *flagBuilder) AddFlag(flag cli.Flag) { - name := flag.Names()[0] +func (b *flagBuilder) AddFlag(flag *flagDefinition) { + name := flag.Name if _, found := b.flags[name]; !found { b.flags[name] = flag b.order = append(b.order, name) } } -func (b *flagBuilder) AddFlags(flags []cli.Flag) { +func (b *flagBuilder) AddFlags(flags []*flagDefinition) { for _, flag := range flags { b.AddFlag(flag) } } -func (b flagBuilder) ToList() []cli.Flag { - flags := []cli.Flag{} +func (b *flagBuilder) AddDefaultFlags(hidden bool) { + b.AddFlags(b.defaultFlags(hidden)) +} + +func (b *flagBuilder) AddHelpFlag() { + b.AddFlag(b.helpFlag()) +} + +func (b *flagBuilder) AddVersionFlag(hidden bool) { + b.AddFlag(b.versionFlag(hidden)) +} + +func (b flagBuilder) ToList() []*flagDefinition { + flags := []*flagDefinition{} for _, name := range b.order { flags = append(flags, b.flags[name]) } return flags } +func (b flagBuilder) defaultFlags(hidden bool) []*flagDefinition { + return []*flagDefinition{ + newFlagDefinition(FlagNameDebug, "Enable debug output", "", FlagTypeBoolean, "UIPATH_DEBUG", false, hidden, false), + newFlagDefinition(FlagNameProfile, "Config profile to use", "", FlagTypeString, "UIPATH_PROFILE", config.DefaultProfile, hidden, false), + newFlagDefinition(FlagNameUri, "Server Base-URI", "", FlagTypeString, "UIPATH_URI", "", hidden, false), + newFlagDefinition(FlagNameOrganization, "Organization name", "", FlagTypeString, "UIPATH_ORGANIZATION", nil, hidden, false), + newFlagDefinition(FlagNameTenant, "Tenant name", "", FlagTypeString, "UIPATH_TENANT", "", hidden, false), + newFlagDefinition(FlagNameInsecure, "Disable HTTPS certificate check", "", FlagTypeBoolean, "UIPATH_INSECURE", false, hidden, false), + newFlagDefinition(FlagNameOutputFormat, fmt.Sprintf("Set output format: %s (default), %s", outputFormatJson, outputFormatText), "", FlagTypeString, "UIPATH_OUTPUT", "", hidden, false), + newFlagDefinition(FlagNameQuery, "Perform JMESPath query on output", "", FlagTypeString, "", "", hidden, false), + newFlagDefinition(FlagNameWait, "Waits for the provided condition (JMESPath expression)", "", FlagTypeString, "", "", hidden, false), + newFlagDefinition(FlagNameWaitTimeout, "Time to wait in seconds for condition", "", FlagTypeInteger, "", 30, hidden, false), + newFlagDefinition(FlagNameFile, "Provide input from file (use - for stdin)", "", FlagTypeString, "", "", hidden, false), + newFlagDefinition(FlagNameIdentityUri, "Identity Server URI", "", FlagTypeString, "UIPATH_IDENTITY_URI", "", hidden, false), + b.versionFlag(hidden), + } +} + +func (b flagBuilder) versionFlag(hidden bool) *flagDefinition { + return newFlagDefinition(FlagNameVersion, "Specific service version", "", FlagTypeString, "UIPATH_VERSION", "", hidden, false) +} + +func (b flagBuilder) helpFlag() *flagDefinition { + return newFlagDefinition(FlagNameHelp, "Show help", "", FlagTypeBoolean, "", false, true, false) +} + func newFlagBuilder() *flagBuilder { - return &flagBuilder{map[string]cli.Flag{}, []string{}} + return &flagBuilder{map[string]*flagDefinition{}, []string{}} } diff --git a/commandline/flag_definition.go b/commandline/flag_definition.go new file mode 100644 index 0000000..eb734bc --- /dev/null +++ b/commandline/flag_definition.go @@ -0,0 +1,25 @@ +package commandline + +type flagDefinition struct { + Name string + Summary string + Description string + Type FlagType + EnvVarName string + Value interface{} + Hidden bool + Required bool +} + +func newFlagDefinition(name string, summary string, description string, flagType FlagType, envVarName string, value interface{}, hidden bool, required bool) *flagDefinition { + return &flagDefinition{ + name, + summary, + description, + flagType, + envVarName, + value, + hidden, + required, + } +} diff --git a/commandline/flag_type.go b/commandline/flag_type.go new file mode 100644 index 0000000..68cd6c6 --- /dev/null +++ b/commandline/flag_type.go @@ -0,0 +1,26 @@ +package commandline + +import "fmt" + +type FlagType int + +const ( + FlagTypeString FlagType = iota + 1 + FlagTypeInteger + FlagTypeBoolean + FlagTypeStringArray +) + +func (t FlagType) String() string { + switch t { + case FlagTypeString: + return "string" + case FlagTypeInteger: + return "integer" + case FlagTypeBoolean: + return "boolean" + case FlagTypeStringArray: + return "stringArray" + } + panic(fmt.Sprintf("Unknown flag type: %d", int(t))) +} diff --git a/commandline/show_command_handler.go b/commandline/show_command_handler.go index ec26b86..25f9402 100644 --- a/commandline/show_command_handler.go +++ b/commandline/show_command_handler.go @@ -5,7 +5,6 @@ import ( "sort" "github.com/UiPath/uipathcli/parser" - "github.com/urfave/cli/v2" ) // showCommandHandler prints all available uipathcli commands @@ -29,7 +28,7 @@ type commandJson struct { Subcommands []commandJson `json:"subcommands"` } -func (h showCommandHandler) Execute(definitions []parser.Definition, globalFlags []cli.Flag) (string, error) { +func (h showCommandHandler) Execute(definitions []parser.Definition, globalFlags []*flagDefinition) (string, error) { result := commandJson{ Name: "uipath", Description: "Command line interface to simplify, script and automate API calls for UiPath services", @@ -104,7 +103,7 @@ func (h showCommandHandler) convertOperationToCommand(operation parser.Operation } } -func (h showCommandHandler) convertFlagsToCommandParameters(flags []cli.Flag) []parameterJson { +func (h showCommandHandler) convertFlagsToCommandParameters(flags []*flagDefinition) []parameterJson { result := []parameterJson{} for _, f := range flags { result = append(result, h.convertFlagToCommandParameter(f)) @@ -120,37 +119,14 @@ func (h showCommandHandler) convertParametersToCommandParameters(parameters []pa return result } -func (h showCommandHandler) convertFlagToCommandParameter(flag cli.Flag) parameterJson { - intFlag, ok := flag.(*cli.IntFlag) - if ok { - return parameterJson{ - Name: intFlag.Name, - Description: intFlag.Usage, - Type: "integer", - Required: false, - AllowedValues: []interface{}{}, - DefaultValue: intFlag.Value, - } - } - boolFlag, ok := flag.(*cli.BoolFlag) - if ok { - return parameterJson{ - Name: boolFlag.Name, - Description: boolFlag.Usage, - Type: "boolean", - Required: false, - AllowedValues: []interface{}{}, - DefaultValue: boolFlag.Value, - } - } - stringFlag := flag.(*cli.StringFlag) +func (h showCommandHandler) convertFlagToCommandParameter(flag *flagDefinition) parameterJson { return parameterJson{ - Name: stringFlag.Name, - Description: stringFlag.Usage, - Type: "string", + Name: flag.Name, + Description: flag.Summary, + Type: flag.Type.String(), Required: false, AllowedValues: []interface{}{}, - DefaultValue: stringFlag.Value, + DefaultValue: flag.Value, } }