mirror of https://github.com/Hilbis/Hilbish
549 lines
15 KiB
Go
549 lines
15 KiB
Go
|
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)
|
||
|
}
|