package completers import ( "os/exec" "reflect" "strings" "unicode" "github.com/jessevdk/go-flags" ) // These functions are just shorthands for checking various conditions on the input line. // They make the main function more readable, which might be useful, should a logic error pop somewhere. // [ Parser Commands & Options ] -------------------------------------------------------------------------- // ArgumentByName Get the name of a detected command's argument func argumentByName(command *flags.Command, name string) *flags.Arg { args := command.Args() for _, arg := range args { if arg.Name == name { return arg } } return nil } // optionByName - Returns an option for a command or a subcommand, identified by name func optionByName(cmd *flags.Command, option string) *flags.Option { if cmd == nil { return nil } // Get all (root) option groups. groups := cmd.Groups() // For each group, build completions for _, grp := range groups { // Add each option to completion group for _, opt := range grp.Options() { if opt.LongName == option { return opt } } } return nil } // [ Menus ] -------------------------------------------------------------------------------------------- // Is the input line is either empty, or without any detected command ? func noCommandOrEmpty(args []string, last []rune, command *flags.Command) bool { if len(args) == 0 || len(args) == 1 && command == nil { return true } return false } // [ Commands ] ------------------------------------------------------------------------------------- // detectedCommand - Returns the base command from parser if detected, depending on context func (c *CommandCompleter) detectedCommand(args []string) (command *flags.Command) { arg := strings.TrimSpace(args[0]) command = c.parser.Find(arg) return } // is the command a special command, usually not handled by parser ? func isSpecialCommand(args []string, command *flags.Command) bool { // If command is not nil, return if command == nil { // Shell if args[0] == "!" { return true } // Exit if args[0] == "exit" { return true } return false } return false } // The commmand has been found func commandFound(command *flags.Command) bool { if command != nil { return true } return false } // Search for input in $PATH func commandFoundInPath(input string) bool { _, err := exec.LookPath(input) if err != nil { return false } return true } // [ SubCommands ]------------------------------------------------------------------------------------- // Does the command have subcommands ? func hasSubCommands(command *flags.Command, args []string) bool { if len(args) < 2 || command == nil { return false } if len(command.Commands()) != 0 { return true } return false } // Does the input has a subcommand in it ? func subCommandFound(lastWord string, raw []string, command *flags.Command) (sub *flags.Command, ok bool) { // First, filter redundant spaces. This does not modify the actual line args := ignoreRedundantSpaces(raw) if len(args) <= 1 || command == nil { return nil, false } sub = command.Find(args[1]) if sub != nil { return sub, true } return nil, false } // Is the last input PRECISELY a subcommand. This is used as a brief hint for the subcommand func lastIsSubCommand(lastWord string, command *flags.Command) bool { if sub := command.Find(lastWord); sub != nil { return true } return false } // [ Arguments ]------------------------------------------------------------------------------------- // Does the command have arguments ? func hasArgs(command *flags.Command) bool { if len(command.Args()) != 0 { return true } return false } // commandArgumentRequired - Analyses input and sends back the next argument name to provide completion for func commandArgumentRequired(lastWord string, raw []string, command *flags.Command) (name string, yes bool) { // First, filter redundant spaces. This does not modify the actual line args := ignoreRedundantSpaces(raw) // Trim command and subcommand args var remain []string if args[0] == command.Name { remain = args[1:] } if len(args) > 1 && args[1] == command.Name { remain = args[2:] } // The remain may include a "" as a last element, // which we don't consider as a real remain, so we move it away switch lastWord { case "": case command.Name: return "", false } // Trim all --option flags and their arguments if they have remain = filterOptions(remain, command) // For each argument, check if needs completion. If not continue, if yes return. // The arguments remainder is popped according to the number of values expected. for i, arg := range command.Args() { // If it's required and has one argument, check filled. if arg.Required == 1 && arg.RequiredMaximum == 1 { // If last word is the argument, and we are // last arg in: line keep completing. if len(remain) < 1 { return arg.Name, true } // If the we are still writing the argument if len(remain) == 1 { if lastWord != "" { return arg.Name, true } } // If filed and we are not last arg, continue if len(remain) > 1 && i < (len(command.Args())-1) { remain = remain[1:] continue } continue } // If we need more than one value and we knwo the maximum, // either return or pop the remain. if arg.Required > 0 && arg.RequiredMaximum > 1 { // Pop the corresponding amount of arguments. var found int for i := 0; i < len(remain) && i < arg.RequiredMaximum; i++ { remain = remain[1:] found++ } // If we still need values: if len(remain) == 0 && found <= arg.RequiredMaximum { if lastWord == "" { // We are done, no more completions. break } else { return arg.Name, true } } // Else go on with the next argument continue } // If has required arguments, with no limit of needs, return true if arg.Required > 0 && arg.RequiredMaximum == -1 { return arg.Name, true } // Else, if no requirements and the command has subcommands, // return so that we complete subcommands if arg.Required == -1 && len(command.Commands()) > 0 { continue } // Else, return this argument // NOTE: This block is after because we always use []type arguments // AFTER individual argument fields. Thus blocks any args that have // not been processed. if arg.Required == -1 { return arg.Name, true } } // Once we exited the loop, it means that none of the arguments require completion: // They are all either optional, or fullfiled according to their required numbers. // Thus we return none return "", false } // getRemainingArgs - Filters the input slice from commands and detected option:value pairs, and returns args func getRemainingArgs(args []string, last []rune, command *flags.Command) (remain []string) { var input []string // Clean subcommand name if args[0] == command.Name && len(args) >= 2 { input = args[1:] } else if len(args) == 1 { input = args } // For each each argument for i := 0; i < len(input); i++ { // Check option prefix if strings.HasPrefix(input[i], "-") || strings.HasPrefix(input[i], "--") { // Clean it cur := strings.TrimPrefix(input[i], "--") cur = strings.TrimPrefix(cur, "-") // Check if option matches any command option if opt := command.FindOptionByLongName(cur); opt != nil { boolean := true if opt.Field().Type == reflect.TypeOf(boolean) { continue // If option is boolean, don't skip an argument } i++ // Else skip next arg in input continue } } // Safety check if input[i] == "" || input[i] == " " { continue } remain = append(remain, input[i]) } return } // [ Options ]------------------------------------------------------------------------------------- // commandOptionsAsked - Does the user asks for options in a root command ? func commandOptionsAsked(args []string, lastWord string, command *flags.Command) bool { if len(args) >= 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) { return true } return false } // commandOptionsAsked - Does the user asks for options in a subcommand ? func subCommandOptionsAsked(args []string, lastWord string, command *flags.Command) bool { if len(args) > 2 && (strings.HasPrefix(lastWord, "-") || strings.HasPrefix(lastWord, "--")) { return true } return false } // Is the last input argument is a dash ? func isOptionDash(args []string, last []rune) bool { if len(args) > 2 && (strings.HasPrefix(string(last), "-") || strings.HasPrefix(string(last), "--")) { return true } return false } // optionIsAlreadySet - Detects in input if an option is already set func optionIsAlreadySet(args []string, lastWord string, opt *flags.Option) bool { return false } // Check if option type allows for repetition func optionNotRepeatable(opt *flags.Option) bool { return true } // [ Option Values ]------------------------------------------------------------------------------------- // Is the last input word an option name (--option) ? func optionArgRequired(args []string, last []rune, group *flags.Group) (opt *flags.Option, yes bool) { var lastItem string var lastOption string var option *flags.Option // If there is argument required we must have 1) command 2) --option inputs at least. if len(args) <= 2 { return nil, false } // Check for last two arguments in input if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") { // Long opts if strings.HasPrefix(args[len(args)-2], "--") { lastOption = strings.TrimPrefix(args[len(args)-2], "--") if opt := group.FindOptionByLongName(lastOption); opt != nil { option = opt } // Short opts } else if strings.HasPrefix(args[len(args)-2], "-") { lastOption = strings.TrimPrefix(args[len(args)-2], "-") if len(lastOption) > 0 { if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil { option = opt } } } } // If option is found, and we still are in writing the argument if (lastItem == "" && option != nil) || option != nil { // Check if option is a boolean, if yes return false boolean := true if option.Field().Type == reflect.TypeOf(boolean) { return nil, false } return option, true } // Check for previous argument if lastItem != "" && option == nil { if strings.HasPrefix(args[len(args)-2], "-") || strings.HasPrefix(args[len(args)-2], "--") { // Long opts if strings.HasPrefix(args[len(args)-2], "--") { lastOption = strings.TrimPrefix(args[len(args)-2], "--") if opt := group.FindOptionByLongName(lastOption); opt != nil { option = opt return option, true } // Short opts } else if strings.HasPrefix(args[len(args)-2], "-") { lastOption = strings.TrimPrefix(args[len(args)-2], "-") if opt := group.FindOptionByShortName(rune(lastOption[0])); opt != nil { option = opt return option, true } } } } return nil, false } // [ Other ]------------------------------------------------------------------------------------- // Does the user asks for Environment variables ? func envVarAsked(args []string, lastWord string) bool { // Check if the current word is an environment variable, or if the last part of it is a variable if len(lastWord) > 1 && strings.HasPrefix(lastWord, "$") { if strings.LastIndex(lastWord, "/") < strings.LastIndex(lastWord, "$") { return true } return false } // Check if env var is asked in a path or something if len(lastWord) > 1 { // If last is a path, it cannot be an env var anymore if lastWord[len(lastWord)-1] == '/' { return false } if lastWord[len(lastWord)-1] == '$' { return true } } // If we are at the beginning of an env var if len(lastWord) > 0 && lastWord[len(lastWord)-1] == '$' { return true } return false } // filterOptions - Check various elements of an option and return a list func filterOptions(args []string, command *flags.Command) (processed []string) { for i := 0; i < len(args); i++ { arg := args[i] // --long-name options if strings.HasPrefix(arg, "--") { name := strings.TrimPrefix(arg, "--") if opt := optionByName(command, name); opt != nil { var boolean = true if opt.Field().Type == reflect.TypeOf(boolean) { continue } // Else skip the option argument (next item) i++ } continue } // -s short options if strings.HasPrefix(arg, "-") { name := strings.TrimPrefix(arg, "-") if opt := optionByName(command, name); opt != nil { var boolean = true if opt.Field().Type == reflect.TypeOf(boolean) { continue } // Else skip the option argument (next item) i++ } continue } processed = append(processed, arg) } return } // Other Functions -------------------------------------------------------------------------------------------------------------// // formatInput - Formats & sanitize the command line input func formatInput(line []rune) (args []string, last []rune, lastWord string) { args = strings.Split(string(line), " ") // The readline input as a []string last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input lastWord = string(last) return } // FormatInput - Formats & sanitize the command line input func formatInputHighlighter(line []rune) (args []string, last []rune, lastWord string) { args = strings.SplitN(string(line), " ", -1) last = trimSpaceLeft([]rune(args[len(args)-1])) // The last char in input lastWord = string(last) return } // ignoreRedundantSpaces - We might have several spaces between each real arguments. // However these indivual spaces are counted as args themselves. // For each space arg found, verify that no space args follow, // and if some are found, delete them. func ignoreRedundantSpaces(raw []string) (args []string) { for i := 0; i < len(raw); i++ { // Catch a space argument. if raw[i] == "" { // The arg evaulated is always kept, because we just adjusted // the indexing to avoid the ones we don't need // args = append(args, raw[i]) for y, next := range raw[i:] { if next != "" { i += y - 1 break } // If we come to the end while not breaking // we push the outer loop straight to the end. if y == len(raw[i:])-1 { i += y } } } else { // The arg evaulated is always kept, because we just adjusted // the indexing to avoid the ones we don't need args = append(args, raw[i]) } } return } func trimSpaceLeft(in []rune) []rune { firstIndex := len(in) for i, r := range in { if unicode.IsSpace(r) == false { firstIndex = i break } } return in[firstIndex:] } func equal(a, b []rune) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if a[i] != b[i] { return false } } return true } func hasPrefix(r, prefix []rune) bool { if len(r) < len(prefix) { return false } return equal(r[:len(prefix)], prefix) }