diff --git a/Dockerfile.bash b/Dockerfile.bash index ec5a00784..532fb5fc0 100644 --- a/Dockerfile.bash +++ b/Dockerfile.bash @@ -1,7 +1,11 @@ FROM golang +RUN apt-get update \ + && apt-get install -y bash-completion + RUN echo "\n\ PS1=$'\e[0;36mcarapace \e[0m'\n\ +source /usr/share/bash-completion/bash_completion \n\ source <(example _carapace bash)" \ > /root/.bashrc diff --git a/Dockerfile.oil b/Dockerfile.oil index 485b26c6f..967619adb 100644 --- a/Dockerfile.oil +++ b/Dockerfile.oil @@ -1,7 +1,7 @@ FROM golang RUN apt-get update \ - && apt install -y build-essential libreadline-dev + && apt install -y build-essential libreadline-dev bash-completion RUN curl https://www.oilshell.org/download/oil-0.8.0.tar.gz | tar -xvz \ && cd oil-*/ \ @@ -12,6 +12,7 @@ RUN curl https://www.oilshell.org/download/oil-0.8.0.tar.gz | tar -xvz \ RUN mkdir -p ~/.config/oil \ && echo "\n\ PS1=$'\e[0;36mcarapace \e[0m'\n\ +source /usr/share/bash-completion/bash_completion \n\ source <(example _carapace bash)" \ > ~/.config/oil/oshrc diff --git a/README.md b/README.md index 9b2072db4..298678323 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Uids are generated to identify corresponding completions: ## Action -An [action](#action) indicates how to complete a flag or a positional argument. See [action.go](./action.go) and the examples below for current implementations. +An [action](#action) indicates how to complete a flag or a positional argument. See [action.go](./action.go) and the examples below for current implementations. A range of custom actions can be found at [rsteube/carapace-bin](https://github.com/rsteube/carapace-bin/tree/master/actions) ### ActionMessage @@ -135,32 +135,24 @@ Since callbacks are simply invocations of the program they can be tested directl ### ActionMultiParts -> This is an initial version which still got some quirks, expect some changes here (in the long term this shall return Action as well) - ActionMultiParts is a [callback action](#actioncallback) where parts of an argument can be completed separately (e.g. user:group from [chown](https://github.com/rsteube/carapace-completers/blob/master/completers/chown_completer/cmd/root.go)). Divider can be empty as well, but note that `bash` and `fish` will add the space suffix for anything other than `/=@:.,` (it still works, but after each selection backspace is needed to continue the completion). ```go -carapace.ActionMultiParts(":", func(args []string, parts []string) []string { - switch len(parts) { - case 0: - return []{"user1:", "user2:", "user3:"} - case 1: - return []{"groupA", "groupB", "groupC"} - default: - return []string{} - } -}) +func ActionUserGroup() carapace.Action { + return carapace.ActionMultiParts(":", func(args []string, parts []string) carapace.Action { + switch len(parts) { + case 0: + return ActionUsers().Suffix(":", args) + case 1: + return ActionGroups() + default: + return carapace.ActionValues() + } + }) +} ``` -### Custom Action - -For [actions](#action) that aren't implemented or missing required options, a custom action can be defined. - -```go -carapace.Action{Zsh: "_most_recent_file 2"} - -// #./example action --custom -``` +### Shell Completion Documentation Additional information can be found at: - Bash: [bash-programmable-completion-tutorial](https://iridakos.com/programming/2018/03/01/bash-programmable-completion-tutorial) and [Programmable-Completion-Builtins](https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html#Programmable-Completion-Builtins) diff --git a/action.go b/action.go index 4fcfb8bb1..99a6b0350 100644 --- a/action.go +++ b/action.go @@ -1,12 +1,10 @@ package carapace import ( - "io/ioutil" - "regexp" "strings" - "github.com/mitchellh/go-homedir" "github.com/rsteube/carapace/bash" + "github.com/rsteube/carapace/common" "github.com/rsteube/carapace/elvish" "github.com/rsteube/carapace/fish" "github.com/rsteube/carapace/powershell" @@ -16,12 +14,13 @@ import ( ) type Action struct { - Bash string - Elvish string - Fish string - Powershell string - Xonsh string - Zsh string + rawValues []common.Candidate + bash func() string + elvish func() string + fish func() string + powershell func() string + xonsh func() string + zsh func() string Callback CompletionCallback } type ActionMap map[string]Action @@ -30,52 +29,83 @@ type CompletionCallback func(args []string) Action // finalize replaces value if a callback function is set func (a Action) finalize(cmd *cobra.Command, uid string) Action { if a.Callback != nil { - if a.Bash == "" { - a.Bash = bash.Callback(cmd.Root().Name(), uid) + if a.bash == nil { + a.bash = func() string { return bash.Callback(cmd.Root().Name(), uid) } } - if a.Elvish == "" { - a.Elvish = elvish.Callback(cmd.Root().Name(), uid) + if a.elvish == nil { + a.elvish = func() string { return elvish.Callback(cmd.Root().Name(), uid) } } - if a.Fish == "" { - a.Fish = fish.Callback(cmd.Root().Name(), uid) + if a.fish == nil { + a.fish = func() string { return fish.Callback(cmd.Root().Name(), uid) } } - if a.Powershell == "" { - a.Powershell = powershell.Callback(cmd.Root().Name(), uid) + if a.powershell == nil { + a.powershell = func() string { return powershell.Callback(cmd.Root().Name(), uid) } } - if a.Xonsh == "" { - a.Xonsh = xonsh.Callback(cmd.Root().Name(), uid) + if a.xonsh == nil { + a.xonsh = func() string { return xonsh.Callback(cmd.Root().Name(), uid) } } - if a.Zsh == "" { - a.Zsh = zsh.Callback(uid) + if a.zsh == nil { + a.zsh = func() string { return zsh.Callback(cmd.Root().Name(), uid) } } } return a } +// TODO maybe use Invoke(args) and work on []Candidate +func (a Action) Prefix(prefix string, args []string) Action { + if nestedAction := a.NestedAction(args, 5); nestedAction.rawValues != nil { + for index, val := range nestedAction.rawValues { + nestedAction.rawValues[index].Value = prefix + val.Value // TODO check if val.Value can be assigned directly + } + return nestedAction + } else { + return ActionMessage("TODO Prefix(str) failed") + } +} + +// TODO maybe use Invoke(args) and work on []Candidate +func (a Action) Suffix(suffix string, args []string) Action { + if nestedAction := a.NestedAction(args, 5); nestedAction.rawValues != nil { + for index, val := range nestedAction.rawValues { + nestedAction.rawValues[index].Value = val.Value + suffix // TODO check if val.Value can be assigned directly + } + return nestedAction + } else { + return ActionMessage("TODO Prefix(str) failed") + } +} + +// TODO maybe rename to Invoke(args) +func (a Action) NestedAction(args []string, maxDepth int) Action { + if a.rawValues == nil && a.Callback != nil && maxDepth > 0 { + return a.Callback(args).NestedAction(args, maxDepth-1) + } else { + return a + } +} + func (a Action) Value(shell string) string { + var f func() string switch shell { case "bash": - return a.Bash + f = a.bash case "fish": - return a.Fish + f = a.fish case "elvish": - return a.Elvish + f = a.elvish case "powershell": - return a.Powershell + f = a.powershell case "xonsh": - return a.Xonsh + f = a.xonsh case "zsh": - return a.Zsh - default: - return "" + f = a.zsh } -} -func (a Action) NestedValue(args []string, shell string, maxDepth int) string { - if value := a.Value(shell); value == "" && a.Callback != nil && maxDepth > 0 { - return a.Callback(args).NestedValue(args, shell, maxDepth-1) + if f == nil { + // TODO "{}" for xonsh? + return "" } else { - return value + return f() } } @@ -92,18 +122,6 @@ func ActionCallback(callback CompletionCallback) Action { return Action{Callback: callback} } -// ActionExecute uses command substitution to invoke a command and evalues it's result as Action -func ActionExecute(command string) Action { - return Action{ - Bash: bash.ActionExecute(command), - Elvish: elvish.ActionExecute(command), - Fish: fish.ActionExecute(command), - Powershell: powershell.ActionExecute(command), - Xonsh: xonsh.ActionExecute(command), - Zsh: zsh.ActionExecute(command), - } -} - // ActionBool completes true/false func ActionBool() Action { return ActionValues("true", "false") @@ -111,186 +129,69 @@ func ActionBool() Action { func ActionDirectories() Action { return Action{ - Bash: bash.ActionDirectories(), - Elvish: elvish.ActionDirectories(), - Fish: fish.ActionDirectories(), - Powershell: powershell.ActionDirectories(), - Xonsh: xonsh.ActionDirectories(), - Zsh: zsh.ActionDirectories(), + bash: func() string { return bash.ActionDirectories() }, + elvish: func() string { return elvish.ActionDirectories() }, + fish: func() string { return fish.ActionDirectories() }, + powershell: func() string { return powershell.ActionDirectories() }, + xonsh: func() string { return xonsh.ActionDirectories() }, + zsh: func() string { return zsh.ActionDirectories() }, + // TODO add Callback so that the action can be used in ActionMultiParts as well } } func ActionFiles(suffix string) Action { return Action{ - Bash: bash.ActionFiles(suffix), - Elvish: elvish.ActionFiles(suffix), - Fish: fish.ActionFiles(suffix), - Powershell: powershell.ActionFiles(suffix), - Xonsh: xonsh.ActionFiles(suffix), - Zsh: zsh.ActionFiles("*" + suffix), - } -} - -// ActionNetInterfaces completes network interface names -func ActionNetInterfaces() Action { - return Action{ - Bash: bash.ActionNetInterfaces(), - Elvish: elvish.ActionNetInterfaces(), - Fish: fish.ActionNetInterfaces(), - Powershell: powershell.ActionNetInterfaces(), - Xonsh: xonsh.ActionNetInterfaces(), - Zsh: zsh.ActionNetInterfaces(), - } -} - -// ActionUsers completes user names -func ActionUsers() Action { - return Action{ - Bash: bash.ActionUsers(), - Fish: fish.ActionUsers(), - Zsh: zsh.ActionUsers(), - Callback: func(args []string) Action { - return ActionValues(users()...) - }, - } -} - -// ActionGroups completes group names -func ActionGroups() Action { - return Action{ - Bash: bash.ActionGroups(), - Fish: fish.ActionGroups(), - Zsh: zsh.ActionGroups(), - Callback: func(args []string) Action { - return ActionValues(groups()...) - }, - } -} - -// ActionUserGroup completes user:group separately -func ActionUserGroup() Action { - return ActionMultiParts(":", func(args []string, parts []string) []string { - switch len(parts) { - case 0: - users := users() - usersWithSuffix := make([]string, len(users)) - for index, user := range users { - usersWithSuffix[index] = user + ":" - } - return usersWithSuffix - case 1: - return groups() - default: - return []string{} - } - }) -} - -// TODO windows -func users() []string { - users := []string{} - if content, err := ioutil.ReadFile("/etc/passwd"); err == nil { - for _, entry := range strings.Split(string(content), "\n") { - user := strings.Split(entry, ":")[0] - if len(strings.TrimSpace(user)) > 0 { - users = append(users, user) - } - } - } - return users -} - -// TODO windows -func groups() []string { - users := []string{} - if content, err := ioutil.ReadFile("/etc/group"); err == nil { - for _, entry := range strings.Split(string(content), "\n") { - group := strings.Split(entry, ":")[0] - if len(strings.TrimSpace(group)) > 0 { - users = append(users, group) - } - } - } - return users -} - -// ActionHosts completes host names -func ActionHosts() Action { - return Action{ - Bash: bash.ActionHosts(), - Fish: fish.ActionHosts(), - Zsh: zsh.ActionHosts(), - Callback: func(args []string) Action { - hosts := []string{} - if file, err := homedir.Expand("~/.ssh/known_hosts"); err == nil { - if content, err := ioutil.ReadFile(file); err == nil { - r := regexp.MustCompile(`^(?P[^ ,#]+)`) - for _, entry := range strings.Split(string(content), "\n") { - if r.MatchString(entry) { - hosts = append(hosts, r.FindStringSubmatch(entry)[0]) - } - } - } else { - return ActionValues(err.Error()) - } - } - return ActionValues(hosts...) - }, + bash: func() string { return bash.ActionFiles(suffix) }, + elvish: func() string { return elvish.ActionFiles(suffix) }, + fish: func() string { return fish.ActionFiles(suffix) }, + powershell: func() string { return powershell.ActionFiles(suffix) }, + xonsh: func() string { return xonsh.ActionFiles(suffix) }, + zsh: func() string { return zsh.ActionFiles("*" + suffix) }, + // TODO add Callback so that the action can be used in ActionMultiParts as well } } // ActionValues completes arbitrary keywords (values) func ActionValues(values ...string) Action { - return Action{ - Bash: bash.ActionValues(values...), - Elvish: elvish.ActionValues(values...), - Fish: fish.ActionValues(values...), - Powershell: powershell.ActionValues(values...), - Xonsh: xonsh.ActionValues(values...), - Zsh: zsh.ActionValues(values...), + vals := make([]string, len(values)*2) + for index, val := range values { + vals[index*2] = val + vals[(index*2)+1] = "" } + return ActionValuesDescribed(vals...) } // ActionValuesDescribed completes arbitrary key (values) with an additional description (value, description pairs) func ActionValuesDescribed(values ...string) Action { - return Action{ - Bash: bash.ActionValuesDescribed(values...), - Elvish: elvish.ActionValuesDescribed(values...), - Fish: fish.ActionValuesDescribed(values...), - Powershell: powershell.ActionValuesDescribed(values...), - Xonsh: xonsh.ActionValuesDescribed(values...), - Zsh: zsh.ActionValuesDescribed(values...), + vals := make([]common.Candidate, len(values)/2) + for index, val := range values { + if index%2 == 0 { + vals[index/2] = common.Candidate{Value: val, Display: val, Description: values[index+1]} + } } -} - -// ActionMessage displays a help messages in places where no completions can be generated -func ActionMessage(msg string) Action { return Action{ - Bash: bash.ActionMessage(msg), - Elvish: elvish.ActionMessage(msg), - Fish: fish.ActionMessage(msg), - Powershell: powershell.ActionMessage(msg), - Xonsh: xonsh.ActionMessage(msg), - Zsh: zsh.ActionMessage(msg), + rawValues: vals, + bash: func() string { return bash.ActionCandidates(vals...) }, + elvish: func() string { return elvish.ActionCandidates(vals...) }, + fish: func() string { return fish.ActionCandidates(vals...) }, + powershell: func() string { return powershell.ActionCandidates(vals...) }, + xonsh: func() string { return xonsh.ActionCandidates(vals...) }, + zsh: func() string { return zsh.ActionCandidates(vals...) }, } } -func ActionPrefixValues(prefix string, values ...string) Action { - return Action(Action{ - Bash: bash.ActionPrefixValues(prefix, values...), - Elvish: elvish.ActionPrefixValues(prefix, values...), - Fish: fish.ActionPrefixValues(prefix, values...), - Powershell: powershell.ActionPrefixValues(prefix, values...), - Xonsh: xonsh.ActionPrefixValues(prefix, values...), - Zsh: zsh.ActionPrefixValues(prefix, values...), - }) +// ActionMessage displays a help messages in places where no completions can be generated +func ActionMessage(msg string) Action { // TODO somehow handle this differently for Prefix/Suffix + return ActionValuesDescribed("_", "", "ERR", msg) + // TODO muss not be filtered if value already contains a submatch (so that it is always shown) + // TODO zsh is the only one with actual message function zsh: func() string { return zsh.ActionMessage(msg) }, } // TODO find a better solution for this var CallbackValue string // ActionMultiParts completes multiple parts of words separately where each part is separated by some char -func ActionMultiParts(divider string, callback func(args []string, parts []string) []string) Action { +func ActionMultiParts(divider string, callback func(args []string, parts []string) Action) Action { return ActionCallback(func(args []string) Action { // TODO multiple dividers by splitting on each char index := strings.LastIndex(CallbackValue, string(divider)) @@ -305,42 +206,6 @@ func ActionMultiParts(divider string, callback func(args []string, parts []strin parts = parts[0 : len(parts)-1] } - return ActionPrefixValues(prefix, callback(args, parts)...) + return callback(args, parts).Prefix(prefix, args) }) } - -func ActionKillSignals() Action { - return ActionValuesDescribed( - "ABRT", "Abnormal termination", - "ALRM", "Virtual alarm clock", - "BUS", "BUS error", - "CHLD", "Child status has changed", - "CONT", "Continue stopped process", - "FPE", "Floating-point exception", - "HUP", "Hangup detected on controlling terminal", - "ILL", "Illegal instruction", - "INT", "Interrupt from keyboard", - "KILL", "Kill, unblockable", - "PIPE", "Broken pipe", - "POLL", "Pollable event occurred", - "PROF", "Profiling alarm clock timer expired", - "PWR", "Power failure restart", - "QUIT", "Quit from keyboard", - "SEGV", "Segmentation violation", - "STKFLT", "Stack fault on coprocessor", - "STOP", "Stop process, unblockable", - "SYS", "Bad system call", - "TERM", "Termination request", - "TRAP", "Trace/breakpoint trap", - "TSTP", "Stop typed at keyboard", - "TTIN", "Background read from tty", - "TTOU", "Background write to tty", - "URG", "Urgent condition on socket", - "USR1", "User-defined signal 1", - "USR2", "User-defined signal 2", - "VTALRM", "Virtual alarm clock", - "WINCH", "Window size change", - "XCPU", "CPU time limit exceeded", - "XFSZ", "File size limit exceeded", - ) -} diff --git a/bash/action.go b/bash/action.go index 639bff3bc..083d61c88 100644 --- a/bash/action.go +++ b/bash/action.go @@ -3,6 +3,8 @@ package bash import ( "fmt" "strings" + + "github.com/rsteube/carapace/common" ) var sanitizer = strings.NewReplacer( @@ -34,79 +36,23 @@ func Callback(prefix string, uid string) string { return fmt.Sprintf(`eval $(_%v_callback '%v')`, prefix, uid) } -func ActionExecute(command string) string { - return fmt.Sprintf(`$(%v)`, command) -} - func ActionDirectories() string { - return `compgen -S / -d -- "$last"` + return `compgen -S / -d -- "$cur"` } func ActionFiles(suffix string) string { - return fmt.Sprintf(`compgen -S / -d -- "$last"; compgen -f -X '!*%v' -- "$last"`, suffix) -} - -func ActionNetInterfaces() string { - return `compgen -W "$(ifconfig -a | grep -o '^[^ :]\+')" -- "$last"` -} - -func ActionUsers() string { - return `compgen -u -- "${last//[\"\|\']/}"` -} - -func ActionGroups() string { - return `compgen -g -- "${last//[\"\|\']/}"` -} - -func ActionHosts() string { - return `compgen -W "$(cut -d ' ' -f1 < ~/.ssh/known_hosts | cut -d ',' -f1)" -- "$last"` -} - -func ActionValues(values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - // TODO escape special characters - //vals[index] = strings.Replace(val, " ", `\ `, -1) - vals[index] = strings.Replace(val, ` `, `\\\ `, -1) - } - return fmt.Sprintf(`compgen -W $'%v' -- "$last"`, strings.Join(vals, `\n`)) + return fmt.Sprintf(`compgen -S / -d -- "$cur"; compgen -f -X '!*%v' -- "$cur"`, suffix) } -func ActionValuesDescribed(values ...string) string { - // TODO verify length (description always exists) - vals := make([]string, len(values)/2) +func ActionCandidates(values ...common.Candidate) string { + vals := make([]string, len(values)) for index, val := range values { - if index%2 == 0 { - vals[index/2] = val + if val.Description == "" { + vals[index] = strings.Replace(sanitizer.Replace(val.Value), ` `, `\\\ `, -1) + } else { + vals[index] = fmt.Sprintf(`%v (%v)`, strings.Replace(sanitizer.Replace(val.Value), ` `, `\\\ `, -1), sanitizer.Replace(val.Description)) } } - return ActionValues(vals...) -} - -func ActionMessage(msg string) string { - return ActionValues("ERR", Sanitize(msg)[0]) -} - -func ActionPrefixValues(prefix string, values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - // TODO escape special characters - vals[index] = strings.Replace(val, ` `, `\\\ `, -1) - } - - if index := strings.LastIndexAny(prefix, ":="); index > -1 { - // COMP_WORD will split on these characters, so $last does not contain the full argument - prefix = prefix[index:] - } - return fmt.Sprintf(`compgen -W $'%v' -- "${last/%v/}"`, strings.Join(vals, `\n`), prefix) + return fmt.Sprintf(`compgen -W $'%v' -- "$cur" | sed "s!^$curprefix!!"`, strings.Join(vals, `\n`)) } diff --git a/bash/snippet.go b/bash/snippet.go index 749258978..9be79e5c5 100644 --- a/bash/snippet.go +++ b/bash/snippet.go @@ -14,38 +14,32 @@ func Snippet(cmd *cobra.Command, actions map[string]string) string { result := fmt.Sprintf(`#!/bin/bash _%v_callback() { local compline="${COMP_LINE:0:${COMP_POINT}}" - local last="${COMP_WORDS[${COMP_CWORD}]}" - if [[ $last =~ ^[\"\'] ]] && ! echo "$last" | xargs echo 2>/dev/null >/dev/null ; then - compline="${compline}${last:0:1}" - last="${last// /\\\\ }" - fi + # TODO + #if [[ $last =~ ^[\"\'] ]] && ! echo "$last" | xargs echo 2>/dev/null >/dev/null ; then + # compline="${compline}${last:0:1}" + # last="${last// /\\\\ }" + #fi echo "$compline" | sed -e "s/ $/ ''/" -e 's/"/\"/g' | xargs %v _carapace bash "$1" } _%v_completions() { + local cur prev #words cword split + _init_completion -n := + local curprefix + curprefix="$(echo "$cur" | sed -r 's_^(.*[:/=])?.*_\1_')" local compline="${COMP_LINE:0:${COMP_POINT}}" - local last="${COMP_WORDS[${COMP_CWORD}]}" - - if [[ $last =~ ^[\"\'] ]] && ! echo "$last" | xargs echo 2>/dev/null >/dev/null ; then - compline="${compline}${last:0:1}" - last="${last// /\\\\ }" - else - last="${last// /\\\ }" - fi + + # TODO + #if [[ $last =~ ^[\"\'] ]] && ! echo "$last" | xargs echo 2>/dev/null >/dev/null ; then + # compline="${compline}${last:0:1}" + # last="${last// /\\\\ }" + #else + # last="${last// /\\\ }" + #fi local state state="$(echo "$compline" | sed -e "s/ \$/ ''/" -e 's/"/\"/g' | xargs %v _carapace bash state)" - local previous="${COMP_WORDS[$((COMP_CWORD-1))]}" - - # crude optarg patch - won't work with --optarg=key=value - local previous="${COMP_WORDS[$((COMP_CWORD-1))]}" - if [[ $previous == '=' ]]; then - previous="${COMP_WORDS[$((COMP_CWORD-2))]}=" - elif [[ $last == '=' ]]; then - last='' - previous="$previous=" - fi local IFS=$'\n' @@ -53,7 +47,8 @@ _%v_completions() { %v esac - [[ $last =~ ^[\"\'] ]] && COMPREPLY=("${COMPREPLY[@]//\\ /\ }") + [[ $cur =~ ^[\"\'] ]] && COMPREPLY=("${COMPREPLY[@]//\\ /\ }") + [[ ${#COMPREPLY[*]} -eq 1 ]] && COMPREPLY=( ${COMPREPLY[0]%% (*} ) # https://stackoverflow.com/a/10130007 [[ ${COMPREPLY[0]} == *[/=@:.,] ]] && compopt -o nospace } @@ -66,10 +61,15 @@ complete -F _%v_completions %v func snippetFunctions(cmd *cobra.Command, actions map[string]string) string { function_pattern := ` '%v' ) - if [[ $last == -* ]]; then - COMPREPLY=($(%v)) + if [[ $cur == -* ]]; then + case $cur in +%v + *) + COMPREPLY=($(%v)) + ;; + esac else - case $previous in + case $prev in %v *) COMPREPLY=($(%v)) @@ -79,37 +79,54 @@ func snippetFunctions(cmd *cobra.Command, actions map[string]string) string { ;; ` + optArgflags := make([]string, 0) + cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { + if !f.Hidden { + var s string + if action, ok := actions[uid.Flag(cmd, f)]; ok { + s = snippetFlagCompletion(f, action, true) + } else { + s = snippetFlagCompletion(f, "", true) + } + if s != "" { + optArgflags = append(optArgflags, s) + } + } + }) + flags := make([]string, 0) cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { if !f.Hidden { var s string if action, ok := actions[uid.Flag(cmd, f)]; ok { - s = snippetFlagCompletion(f, action) + s = snippetFlagCompletion(f, action, false) } else { - s = snippetFlagCompletion(f, "") + s = snippetFlagCompletion(f, "", false) + } + if s != "" { + flags = append(flags, s) } - flags = append(flags, s) } }) var positionalAction string if cmd.HasAvailableSubCommands() { - subcommands := make([]string, 0) + subcommands := make([]common.Candidate, 0) for _, c := range cmd.Commands() { if !c.Hidden { - subcommands = append(subcommands, c.Name()) + subcommands = append(subcommands, common.Candidate{Value: c.Name(), Description: c.Short}) for _, alias := range c.Aliases { - subcommands = append(subcommands, alias) + subcommands = append(subcommands, common.Candidate{Value: alias, Description: c.Short}) } } } - positionalAction = ActionValues(subcommands...) + positionalAction = ActionCandidates(subcommands...) } else { positionalAction = Callback(cmd.Root().Name(), "_") } result := make([]string, 0) - result = append(result, fmt.Sprintf(function_pattern, uid.Command(cmd), snippetFlagList(cmd.LocalFlags()), strings.Join(flags, "\n"), positionalAction)) + result = append(result, fmt.Sprintf(function_pattern, uid.Command(cmd), strings.Join(optArgflags, "\n"), snippetFlagList(cmd.LocalFlags()), strings.Join(flags, "\n"), positionalAction)) for _, subcmd := range cmd.Commands() { if !subcmd.Hidden { result = append(result, snippetFunctions(subcmd, actions)) @@ -119,29 +136,42 @@ func snippetFunctions(cmd *cobra.Command, actions map[string]string) string { } func snippetFlagList(flags *pflag.FlagSet) string { - flagValues := make([]string, 0) + flagValues := make([]common.Candidate, 0) flags.VisitAll(func(flag *pflag.Flag) { if !flag.Hidden { if !common.IsShorthandOnly(flag) { - flagValues = append(flagValues, "--"+flag.Name) + //flagValues = append(flagValues, "--"+flag.Name) + flagValues = append(flagValues, common.Candidate{Value: "--" + flag.Name, Description: flag.Usage}) } if flag.Shorthand != "" { - flagValues = append(flagValues, "-"+flag.Shorthand) + //flagValues = append(flagValues, "-"+flag.Shorthand) + flagValues = append(flagValues, common.Candidate{Value: "-" + flag.Shorthand, Description: flag.Usage}) } } }) if len(flagValues) > 0 { - return ActionValues(flagValues...) + return ActionCandidates(flagValues...) // TODO use candidatas } else { return "" } } -func snippetFlagCompletion(flag *pflag.Flag, action string) (snippet string) { +func snippetFlagCompletion(flag *pflag.Flag, action string, optArgFlag bool) (snippet string) { + // TODO cleanup this mess + if flag.Value.Type() == "bool" { + return + } + if flag.NoOptDefVal != "" && !optArgFlag { + return + } + if flag.NoOptDefVal == "" && optArgFlag { + return + } + optArgSuffix := "" if flag.NoOptDefVal != "" { - optArgSuffix = "=" + optArgSuffix = "=*" } var names string @@ -155,8 +185,18 @@ func snippetFlagCompletion(flag *pflag.Flag, action string) (snippet string) { names = "--" + flag.Name + optArgSuffix } - return fmt.Sprintf(` %v) + if optArgFlag { + return fmt.Sprintf(` %v) + cur=${cur#*=} + curprefix=${curprefix#*=} COMPREPLY=($(%v)) ;; `, names, action) + + } else { + return fmt.Sprintf(` %v) + COMPREPLY=($(%v)) + ;; +`, names, action) + } } diff --git a/carapace.go b/carapace.go index 4e06b1e6e..a2aafecb2 100644 --- a/carapace.go +++ b/carapace.go @@ -60,30 +60,6 @@ func (c Carapace) FlagCompletion(actions ActionMap) { } } -func (c Carapace) Bash() string { - return c.Snippet("bash") -} - -func (c Carapace) Elvish() string { - return c.Snippet("elvish") -} - -func (c Carapace) Fish() string { - return c.Snippet("fish") -} - -func (c Carapace) Powershell() string { - return c.Snippet("powershell") -} - -func (c Carapace) Zsh() string { - return c.Snippet("zsh") -} - -func (c Carapace) Xonsh() string { - return c.Snippet("xonsh") -} - func (c Carapace) Standalone() { // TODO probably needs to be done for each subcommand if c.cmd.Root().Flag("help") != nil { @@ -159,14 +135,14 @@ func addCompletionCommand(cmd *cobra.Command) { if action.Callback == nil { fmt.Println(action.Value(shell)) } else { - fmt.Println(action.Callback(targetArgs).NestedValue(targetArgs, shell, 1)) + fmt.Println(action.Callback(targetArgs).NestedAction(targetArgs, 2).Value(shell)) } } case "state": fmt.Println(uid.Command(targetCmd)) default: CallbackValue = uid.Value(targetCmd, targetArgs, id) - fmt.Println(completions.invokeCallback(id, targetArgs).NestedValue(targetArgs, shell, 1)) + fmt.Println(completions.invokeCallback(id, targetArgs).NestedAction(targetArgs, 2).Value(shell)) } } } diff --git a/common/value.go b/common/value.go new file mode 100644 index 000000000..0e7569956 --- /dev/null +++ b/common/value.go @@ -0,0 +1,15 @@ +package common + +type Candidate struct { + Value string + Display string + Description string +} + +func CandidateFromValues(values ...string) []Candidate { + candidates := make([]Candidate, len(values)) + for index, val := range values { + candidates[index] = Candidate{Value: val, Display: val} + } + return candidates +} diff --git a/elvish/action.go b/elvish/action.go index 5df04d708..642f27e45 100644 --- a/elvish/action.go +++ b/elvish/action.go @@ -3,6 +3,8 @@ package elvish import ( "fmt" "strings" + + "github.com/rsteube/carapace/common" ) var sanitizer = strings.NewReplacer( @@ -34,10 +36,6 @@ func Callback(prefix string, uid string) string { return fmt.Sprintf(`_%v_callback '%v'`, prefix, uid) } -func ActionExecute(command string) string { - return `` // TODO -} - func ActionDirectories() string { return `edit:complete-filename $arg[-1]` // TODO } @@ -46,59 +44,14 @@ func ActionFiles(suffix string) string { return `edit:complete-filename $arg[-1]` // TODO } -func ActionNetInterfaces() string { - return `` // TODO -} - -func ActionUsers() string { - return `` // TODO -} - -func ActionHosts() string { - return `` // TODO -} - -func ActionValues(values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - // TODO escape special characters - //vals[index] = fmt.Sprintf(`edit:complex-candidate %v`, val) - vals[index] = fmt.Sprintf(`%v`, val) - } - return fmt.Sprintf(`put %v`, strings.Join(vals, " ")) -} - -func ActionValuesDescribed(values ...string) string { - // TODO verify length (description always exists) - sanitized := Sanitize(values...) - vals := make([]string, len(values)/2) - for index, val := range sanitized { - if index%2 == 0 { - vals[index/2] = fmt.Sprintf(`edit:complex-candidate '%v' &display='%v (%v)'`, val, val, sanitized[index+1]) +func ActionCandidates(values ...common.Candidate) string { + vals := make([]string, len(values)) + for index, val := range values { + if val.Description == "" { + vals[index] = fmt.Sprintf(`edit:complex-candidate '%v' &display='%v'`, sanitizer.Replace(val.Value), sanitizer.Replace(val.Display)) + } else { + vals[index] = fmt.Sprintf(`edit:complex-candidate '%v' &display='%v (%v)'`, sanitizer.Replace(val.Value), sanitizer.Replace(val.Display), sanitizer.Replace(val.Description)) } } return strings.Join(vals, "\n") } - -func ActionMessage(msg string) string { - return ActionValuesDescribed("ERR", Sanitize(msg)[0], "_", "") -} - -func ActionPrefixValues(prefix string, values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - // TODO escape special characters - vals[index] = fmt.Sprintf(`edit:complex-candidate '%v' &display='%v'`, prefix+val, val) - } - return strings.Join(vals, "\n") -} diff --git a/elvish/snippet.go b/elvish/snippet.go index 6d04ffdc9..f95982675 100644 --- a/elvish/snippet.go +++ b/elvish/snippet.go @@ -75,16 +75,16 @@ func snippetFunctions(cmd *cobra.Command, actions map[string]string) string { var positionals []string if cmd.HasAvailableSubCommands() { - subcommands := make([]string, 0) + subcommands := make([]common.Candidate, 0) for _, c := range cmd.Commands() { if !c.Hidden { - subcommands = append(subcommands, c.Name(), c.Short) + subcommands = append(subcommands, common.Candidate{Value: c.Name(), Display: c.Name(), Description: c.Short}) for _, alias := range c.Aliases { - subcommands = append(subcommands, alias, c.Short) + subcommands = append(subcommands, common.Candidate{Value: alias, Display: alias, Description: c.Short}) } } } - positionals = []string{" " + snippetPositionalCompletion(ActionValuesDescribed(subcommands...))} + positionals = []string{" " + snippetPositionalCompletion(ActionCandidates(subcommands...))} } else { pos := 1 for { @@ -101,7 +101,7 @@ func snippetFunctions(cmd *cobra.Command, actions map[string]string) string { } if len(positionals) == 0 { if cmd.ValidArgs != nil { - positionals = []string{" " + snippetPositionalCompletion(ActionValues(cmd.ValidArgs...))} + positionals = []string{" " + snippetPositionalCompletion(ActionCandidates(common.CandidateFromValues(cmd.ValidArgs...)...))} } } } diff --git a/example/cmd/action.go b/example/cmd/action.go index 73a4ed70d..ad92b6f41 100644 --- a/example/cmd/action.go +++ b/example/cmd/action.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/rsteube/carapace" + "github.com/rsteube/carapace/example/cmd/action" "github.com/spf13/cobra" ) @@ -24,7 +25,6 @@ func init() { actionCmd.Flags().StringP("users", "u", "", "users flag") actionCmd.Flags().StringP("values", "v", "", "values flag") actionCmd.Flags().StringP("values_described", "d", "", "values with description flag") - actionCmd.Flags().StringP("custom", "c", "", "custom flag") //actionCmd.Flags().StringS("shorthandonly", "s", "", "shorthandonly flag") actionCmd.Flags().StringP("kill", "k", "", "kill signals") actionCmd.Flags().StringP("optarg", "o", "", "optional arg with default value blue") @@ -33,16 +33,15 @@ func init() { carapace.Gen(actionCmd).FlagCompletion(carapace.ActionMap{ "files": carapace.ActionFiles(".go"), "directories": carapace.ActionDirectories(), - "groups": carapace.ActionGroups(), - "hosts": carapace.ActionHosts(), + "groups": action.ActionGroups(), + "hosts": action.ActionHosts(), "message": carapace.ActionMessage("message example"), - "net_interfaces": carapace.ActionNetInterfaces(), - "usergroup": carapace.ActionUserGroup(), - "users": carapace.ActionUsers(), + "net_interfaces": action.ActionNetInterfaces(), + "usergroup": action.ActionUserGroup(), + "users": action.ActionUsers(), "values": carapace.ActionValues("values", "example"), "values_described": carapace.ActionValuesDescribed("values", "valueDescription", "example", "exampleDescription"), - "custom": carapace.Action{Zsh: "_most_recent_file 2", Xonsh: "{}"}, - "kill": carapace.ActionKillSignals(), + "kill": action.ActionKillSignals(), "optarg": carapace.ActionValues("blue", "red", "green", "yellow"), }) diff --git a/example/cmd/action/net.go b/example/cmd/action/net.go new file mode 100644 index 000000000..918c14098 --- /dev/null +++ b/example/cmd/action/net.go @@ -0,0 +1,47 @@ +package action + +import ( + "io/ioutil" + "os/exec" + "regexp" + "strings" + + "github.com/mitchellh/go-homedir" + + "github.com/rsteube/carapace" +) + +func ActionHosts() carapace.Action { + return carapace.ActionCallback(func(args []string) carapace.Action { + hosts := []string{} + if file, err := homedir.Expand("~/.ssh/known_hosts"); err == nil { + if content, err := ioutil.ReadFile(file); err == nil { + r := regexp.MustCompile(`^(?P[^ ,#]+)`) + for _, entry := range strings.Split(string(content), "\n") { + if r.MatchString(entry) { + hosts = append(hosts, r.FindStringSubmatch(entry)[0]) + } + } + } else { + return carapace.ActionMessage(err.Error()) + } + } + return carapace.ActionValues(hosts...) + }) +} + +func ActionNetInterfaces() carapace.Action { + return carapace.ActionCallback(func(args []string) carapace.Action { + if output, err := exec.Command("ifconfig").Output(); err != nil { + return carapace.ActionMessage(err.Error()) + } else { + interfaces := []string{} + for _, line := range strings.Split(string(output), "\n") { + if matches, _ := regexp.MatchString("^[0-9a-zA-Z]", line); matches { + interfaces = append(interfaces, strings.Split(line, ":")[0]) + } + } + return carapace.ActionValues(interfaces...) + } + }) +} diff --git a/example/cmd/action/os.go b/example/cmd/action/os.go new file mode 100644 index 000000000..5c81d98d8 --- /dev/null +++ b/example/cmd/action/os.go @@ -0,0 +1,148 @@ +package action + +import ( + "io/ioutil" + "os" + "os/exec" + "strings" + + ps "github.com/mitchellh/go-ps" + "github.com/rsteube/carapace" +) + +func ActionEnvironmentVariables() carapace.Action { + return carapace.ActionCallback(func(args []string) carapace.Action { + env := os.Environ() + vars := make([]string, len(env)) + for index, e := range os.Environ() { + pair := strings.SplitN(e, "=", 2) + vars[index] = pair[0] + } + return carapace.ActionValues(vars...) + }) +} + +func ActionGroups() carapace.Action { + return carapace.ActionCallback(func(args []string) carapace.Action { + groups := []string{} + if content, err := ioutil.ReadFile("/etc/group"); err == nil { + for _, entry := range strings.Split(string(content), "\n") { + splitted := strings.Split(entry, ":") + if len(splitted) > 2 { + group := splitted[0] + id := splitted[2] + if len(strings.TrimSpace(group)) > 0 { + groups = append(groups, group, id) + } + } + } + } + return carapace.ActionValuesDescribed(groups...) + }) +} + +func ActionKillSignals() carapace.Action { + return carapace.ActionValuesDescribed( + "ABRT", "Abnormal termination", + "ALRM", "Virtual alarm clock", + "BUS", "BUS error", + "CHLD", "Child status has changed", + "CONT", "Continue stopped process", + "FPE", "Floating-point exception", + "HUP", "Hangup detected on controlling terminal", + "ILL", "Illegal instruction", + "INT", "Interrupt from keyboard", + "KILL", "Kill, unblockable", + "PIPE", "Broken pipe", + "POLL", "Pollable event occurred", + "PROF", "Profiling alarm clock timer expired", + "PWR", "Power failure restart", + "QUIT", "Quit from keyboard", + "SEGV", "Segmentation violation", + "STKFLT", "Stack fault on coprocessor", + "STOP", "Stop process, unblockable", + "SYS", "Bad system call", + "TERM", "Termination request", + "TRAP", "Trace/breakpoint trap", + "TSTP", "Stop typed at keyboard", + "TTIN", "Background read from tty", + "TTOU", "Background write to tty", + "URG", "Urgent condition on socket", + "USR1", "User-defined signal 1", + "USR2", "User-defined signal 2", + "VTALRM", "Virtual alarm clock", + "WINCH", "Window size change", + "XCPU", "CPU time limit exceeded", + "XFSZ", "File size limit exceeded", + ) +} + +func ActionProcessExecutables() carapace.Action { + return carapace.ActionCallback(func(args []string) carapace.Action { + if processes, err := ps.Processes(); err != nil { + return carapace.ActionMessage(err.Error()) + } else { + executables := make([]string, 0) + for _, process := range processes { + executables = append(executables, process.Executable()) + } + return carapace.ActionValues(executables...) + } + }) +} + +func ActionProcessStates() carapace.Action { + return carapace.ActionValuesDescribed( + "D", "uninterruptible sleep (usually IO)", + "I", "Idle kernel thread", + "R", "running or runnable (on run queue)", + "S", "interruptible sleep (waiting for an event to complete)", + "T", "stopped by job control signal", + "W", "paging (not valid since the 2.6.xx kernel)", + "X", "dead (should never be seen)", + "Z", "defunct (zombie) process, terminated but not reaped by its parent", + "t", "stopped by debugger during the tracing", + ) +} + +func ActionUsers() carapace.Action { + return carapace.ActionCallback(func(args []string) carapace.Action { + users := []string{} + if content, err := ioutil.ReadFile("/etc/passwd"); err == nil { + for _, entry := range strings.Split(string(content), "\n") { + splitted := strings.Split(entry, ":") + if len(splitted) > 2 { + user := splitted[0] + id := splitted[2] + if len(strings.TrimSpace(user)) > 0 { + users = append(users, user, id) + } + } + } + } + return carapace.ActionValuesDescribed(users...) + }) +} + +func ActionUserGroup() carapace.Action { + return carapace.ActionMultiParts(":", func(args []string, parts []string) carapace.Action { + switch len(parts) { + case 0: + return ActionUsers().Suffix(":", args) + case 1: + return ActionGroups() + default: + return carapace.ActionValues() + } + }) +} + +func ActionShells() carapace.Action { + return carapace.ActionCallback(func(args []string) carapace.Action { + if output, err := exec.Command("chsh", "--list-shells").Output(); err != nil { + return carapace.ActionMessage(err.Error()) + } else { + return carapace.ActionValues(strings.Split(string(output), "\n")...) + } + }) +} diff --git a/example/cmd/callback.go b/example/cmd/callback.go index 789ca0433..3642a5690 100644 --- a/example/cmd/callback.go +++ b/example/cmd/callback.go @@ -30,21 +30,21 @@ func init() { carapace.ActionCallback(func(args []string) carapace.Action { return carapace.ActionValues("callback1", "callback2") }), - carapace.ActionMultiParts("=", func(args []string, parts []string) []string { + carapace.ActionMultiParts("=", func(args []string, parts []string) carapace.Action { switch len(parts) { case 0: - return []string{"alpha=", "beta=", "gamma"} + return carapace.ActionValues("alpha=", "beta=", "gamma") case 1: switch parts[0] { case "alpha": - return []string{"one", "two", "three"} + return carapace.ActionValues("one", "two", "three") case "beta": - return []string{"1", "2", "3"} + return carapace.ActionValues("1", "2", "3") default: - return []string{} + return carapace.ActionValues() } default: - return []string{} + return carapace.ActionValues() } }), ) diff --git a/example/cmd/root_test.go b/example/cmd/root_test.go index c5b9392f5..661a1b4ea 100644 --- a/example/cmd/root_test.go +++ b/example/cmd/root_test.go @@ -11,62 +11,58 @@ func TestBash(t *testing.T) { expected := `#!/bin/bash _example_callback() { local compline="${COMP_LINE:0:${COMP_POINT}}" - local last="${COMP_WORDS[${COMP_CWORD}]}" - if [[ $last =~ ^[\"\'] ]] && ! echo "$last" | xargs echo 2>/dev/null >/dev/null ; then - compline="${compline}${last:0:1}" - last="${last// /\\\\ }" - fi + # TODO + #if [[ $last =~ ^[\"\'] ]] && ! echo "$last" | xargs echo 2>/dev/null >/dev/null ; then + # compline="${compline}${last:0:1}" + # last="${last// /\\\\ }" + #fi echo "$compline" | sed -e "s/ $/ ''/" -e 's/"/\"/g' | xargs example _carapace bash "$1" } _example_completions() { + local cur prev #words cword split + _init_completion -n := + local curprefix + curprefix="$(echo "$cur" | sed -r 's_^(.*[:/=])?.*_\1_')" local compline="${COMP_LINE:0:${COMP_POINT}}" - local last="${COMP_WORDS[${COMP_CWORD}]}" - - if [[ $last =~ ^[\"\'] ]] && ! echo "$last" | xargs echo 2>/dev/null >/dev/null ; then - compline="${compline}${last:0:1}" - last="${last// /\\\\ }" - else - last="${last// /\\\ }" - fi + + # TODO + #if [[ $last =~ ^[\"\'] ]] && ! echo "$last" | xargs echo 2>/dev/null >/dev/null ; then + # compline="${compline}${last:0:1}" + # last="${last// /\\\\ }" + #else + # last="${last// /\\\ }" + #fi local state state="$(echo "$compline" | sed -e "s/ \$/ ''/" -e 's/"/\"/g' | xargs example _carapace bash state)" - local previous="${COMP_WORDS[$((COMP_CWORD-1))]}" - - # crude optarg patch - won't work with --optarg=key=value - local previous="${COMP_WORDS[$((COMP_CWORD-1))]}" - if [[ $previous == '=' ]]; then - previous="${COMP_WORDS[$((COMP_CWORD-2))]}=" - elif [[ $last == '=' ]]; then - last='' - previous="$previous=" - fi local IFS=$'\n' case $state in '_example' ) - if [[ $last == -* ]]; then - COMPREPLY=($(compgen -W $'--array\n-a\n--persistentFlag\n-p\n--toggle\n-t' -- "$last")) - else - case $previous in - -a | --array) + if [[ $cur == -* ]]; then + case $cur in + -p=* | --persistentFlag=*) + cur=${cur#*=} + curprefix=${curprefix#*=} COMPREPLY=($()) ;; - -p= | --persistentFlag=) - COMPREPLY=($()) + *) + COMPREPLY=($(compgen -W $'--array (multiflag)\n-a (multiflag)\n--persistentFlag (Help message for persistentFlag)\n-p (Help message for persistentFlag)\n--toggle (Help message for toggle)\n-t (Help message for toggle)' -- "$cur" | sed "s!^$curprefix!!")) ;; - - -t= | --toggle=) - COMPREPLY=($(compgen -W $'true\nfalse' -- "$last")) + esac + else + case $prev in + -a | --array) + COMPREPLY=($()) ;; *) - COMPREPLY=($(compgen -W $'action\nalias\ncallback\ncondition\nhelp\ninjection' -- "$last")) + COMPREPLY=($(compgen -W $'action (action example)\nalias (action example)\ncallback (callback example)\ncondition (condition example)\nhelp (Help about any command)\ninjection (just trying to break things)' -- "$cur" | sed "s!^$curprefix!!")) ;; esac fi @@ -74,44 +70,46 @@ _example_completions() { '_example__action' ) - if [[ $last == -* ]]; then - COMPREPLY=($(compgen -W $'--custom\n-c\n--directories\n--files\n-f\n--groups\n-g\n--hosts\n--kill\n-k\n--message\n-m\n--net_interfaces\n-n\n--optarg\n-o\n--usergroup\n--users\n-u\n--values\n-v\n--values_described\n-d' -- "$last")) - else - case $previous in - -c | --custom) - COMPREPLY=($()) + if [[ $cur == -* ]]; then + case $cur in + -o=* | --optarg=*) + cur=${cur#*=} + curprefix=${curprefix#*=} + COMPREPLY=($(compgen -W $'blue\nred\ngreen\nyellow' -- "$cur" | sed "s!^$curprefix!!")) ;; + *) + COMPREPLY=($(compgen -W $'--directories (files flag)\n--files (files flag)\n-f (files flag)\n--groups (groups flag)\n-g (groups flag)\n--hosts (hosts flag)\n--kill (kill signals)\n-k (kill signals)\n--message (message flag)\n-m (message flag)\n--net_interfaces (net_interfaces flag)\n-n (net_interfaces flag)\n--optarg (optional arg with default value blue)\n-o (optional arg with default value blue)\n--usergroup (user:group flag)\n--users (users flag)\n-u (users flag)\n--values (values flag)\n-v (values flag)\n--values_described (values with description flag)\n-d (values with description flag)' -- "$cur" | sed "s!^$curprefix!!")) + ;; + esac + else + case $prev in --directories) - COMPREPLY=($(compgen -S / -d -- "$last")) + COMPREPLY=($(compgen -S / -d -- "$cur")) ;; -f | --files) - COMPREPLY=($(compgen -S / -d -- "$last"; compgen -f -X '!*.go' -- "$last")) + COMPREPLY=($(compgen -S / -d -- "$cur"; compgen -f -X '!*.go' -- "$cur")) ;; -g | --groups) - COMPREPLY=($(compgen -g -- "${last//[\"\|\']/}")) + COMPREPLY=($(eval $(_example_callback '_example__action##groups'))) ;; --hosts) - COMPREPLY=($(compgen -W "$(cut -d ' ' -f1 < ~/.ssh/known_hosts | cut -d ',' -f1)" -- "$last")) + COMPREPLY=($(eval $(_example_callback '_example__action##hosts'))) ;; -k | --kill) - COMPREPLY=($(compgen -W $'ABRT\nALRM\nBUS\nCHLD\nCONT\nFPE\nHUP\nILL\nINT\nKILL\nPIPE\nPOLL\nPROF\nPWR\nQUIT\nSEGV\nSTKFLT\nSTOP\nSYS\nTERM\nTRAP\nTSTP\nTTIN\nTTOU\nURG\nUSR1\nUSR2\nVTALRM\nWINCH\nXCPU\nXFSZ' -- "$last")) + COMPREPLY=($(compgen -W $'ABRT (Abnormal termination)\nALRM (Virtual alarm clock)\nBUS (BUS error)\nCHLD (Child status has changed)\nCONT (Continue stopped process)\nFPE (Floating-point exception)\nHUP (Hangup detected on controlling terminal)\nILL (Illegal instruction)\nINT (Interrupt from keyboard)\nKILL (Kill, unblockable)\nPIPE (Broken pipe)\nPOLL (Pollable event occurred)\nPROF (Profiling alarm clock timer expired)\nPWR (Power failure restart)\nQUIT (Quit from keyboard)\nSEGV (Segmentation violation)\nSTKFLT (Stack fault on coprocessor)\nSTOP (Stop process, unblockable)\nSYS (Bad system call)\nTERM (Termination request)\nTRAP (Trace/breakpoint trap)\nTSTP (Stop typed at keyboard)\nTTIN (Background read from tty)\nTTOU (Background write to tty)\nURG (Urgent condition on socket)\nUSR1 (User-defined signal 1)\nUSR2 (User-defined signal 2)\nVTALRM (Virtual alarm clock)\nWINCH (Window size change)\nXCPU (CPU time limit exceeded)\nXFSZ (File size limit exceeded)' -- "$cur" | sed "s!^$curprefix!!")) ;; -m | --message) - COMPREPLY=($(compgen -W $'ERR\nmessage\\\ example' -- "$last")) + COMPREPLY=($(compgen -W $'_\nERR (message example)' -- "$cur" | sed "s!^$curprefix!!")) ;; -n | --net_interfaces) - COMPREPLY=($(compgen -W "$(ifconfig -a | grep -o '^[^ :]\+')" -- "$last")) - ;; - - -o= | --optarg=) - COMPREPLY=($(compgen -W $'blue\nred\ngreen\nyellow' -- "$last")) + COMPREPLY=($(eval $(_example_callback '_example__action##net_interfaces'))) ;; --usergroup) @@ -119,15 +117,15 @@ _example_completions() { ;; -u | --users) - COMPREPLY=($(compgen -u -- "${last//[\"\|\']/}")) + COMPREPLY=($(eval $(_example_callback '_example__action##users'))) ;; -v | --values) - COMPREPLY=($(compgen -W $'values\nexample' -- "$last")) + COMPREPLY=($(compgen -W $'values\nexample' -- "$cur" | sed "s!^$curprefix!!")) ;; -d | --values_described) - COMPREPLY=($(compgen -W $'values\nexample' -- "$last")) + COMPREPLY=($(compgen -W $'values (valueDescription)\nexample (exampleDescription)' -- "$cur" | sed "s!^$curprefix!!")) ;; *) @@ -139,10 +137,15 @@ _example_completions() { '_example__callback' ) - if [[ $last == -* ]]; then - COMPREPLY=($(compgen -W $'--callback\n-c' -- "$last")) + if [[ $cur == -* ]]; then + case $cur in + + *) + COMPREPLY=($(compgen -W $'--callback (Help message for callback)\n-c (Help message for callback)' -- "$cur" | sed "s!^$curprefix!!")) + ;; + esac else - case $previous in + case $prev in -c | --callback) COMPREPLY=($(eval $(_example_callback '_example__callback##callback'))) ;; @@ -156,12 +159,17 @@ _example_completions() { '_example__condition' ) - if [[ $last == -* ]]; then - COMPREPLY=($(compgen -W $'--required\n-r' -- "$last")) + if [[ $cur == -* ]]; then + case $cur in + + *) + COMPREPLY=($(compgen -W $'--required (required flag)\n-r (required flag)' -- "$cur" | sed "s!^$curprefix!!")) + ;; + esac else - case $previous in + case $prev in -r | --required) - COMPREPLY=($(compgen -W $'valid\ninvalid' -- "$last")) + COMPREPLY=($(compgen -W $'valid\ninvalid' -- "$cur" | sed "s!^$curprefix!!")) ;; *) @@ -173,10 +181,15 @@ _example_completions() { '_example__help' ) - if [[ $last == -* ]]; then - COMPREPLY=($()) + if [[ $cur == -* ]]; then + case $cur in + + *) + COMPREPLY=($()) + ;; + esac else - case $previous in + case $prev in *) COMPREPLY=($(eval $(_example_callback '_'))) @@ -187,10 +200,15 @@ _example_completions() { '_example__injection' ) - if [[ $last == -* ]]; then - COMPREPLY=($()) + if [[ $cur == -* ]]; then + case $cur in + + *) + COMPREPLY=($()) + ;; + esac else - case $previous in + case $prev in *) COMPREPLY=($(eval $(_example_callback '_'))) @@ -201,14 +219,15 @@ _example_completions() { esac - [[ $last =~ ^[\"\'] ]] && COMPREPLY=("${COMPREPLY[@]//\\ /\ }") + [[ $cur =~ ^[\"\'] ]] && COMPREPLY=("${COMPREPLY[@]//\\ /\ }") + [[ ${#COMPREPLY[*]} -eq 1 ]] && COMPREPLY=( ${COMPREPLY[0]% (*} ) # https://stackoverflow.com/a/10130007 [[ ${COMPREPLY[0]} == *[/=@:.,] ]] && compopt -o nospace } complete -F _example_completions example ` rootCmd.InitDefaultHelpCmd() - assert.Equal(t, expected, carapace.Gen(rootCmd).Bash()) + assert.Equal(t, expected, carapace.Gen(rootCmd).Snippet("bash")) } func TestElvish(t *testing.T) { @@ -235,7 +254,8 @@ edit:completion:arg-completer[example] = [@arg]{ opt-specs = [ [&long='array' &short='a' &desc='multiflag' &arg-required=$true &completer=[_]{ }] [&long='persistentFlag' &short='p' &desc='Help message for persistentFlag' &arg-optional=$true &completer=[_]{ }] - [&long='toggle' &short='t' &desc='Help message for toggle' &arg-optional=$true &completer=[_]{ put true false }] + [&long='toggle' &short='t' &desc='Help message for toggle' &arg-optional=$true &completer=[_]{ edit:complex-candidate 'true' &display='true' +edit:complex-candidate 'false' &display='false' }] ] arg-handlers = [ [_]{ edit:complex-candidate 'action' &display='action (action example)' @@ -251,7 +271,6 @@ edit:complex-candidate 'injection' &display='injection (just trying to break thi } } elif (eq $state '_example__action') { opt-specs = [ - [&long='custom' &short='c' &desc='custom flag' &arg-required=$true &completer=[_]{ }] [&long='directories' &desc='files flag' &arg-required=$true &completer=[_]{ edit:complete-filename $arg[-1] }] [&long='files' &short='f' &desc='files flag' &arg-required=$true &completer=[_]{ edit:complete-filename $arg[-1] }] [&long='groups' &short='g' &desc='groups flag' &arg-required=$true &completer=[_]{ _example_callback '_example__action##groups' }] @@ -287,19 +306,25 @@ edit:complex-candidate 'VTALRM' &display='VTALRM (Virtual alarm clock)' edit:complex-candidate 'WINCH' &display='WINCH (Window size change)' edit:complex-candidate 'XCPU' &display='XCPU (CPU time limit exceeded)' edit:complex-candidate 'XFSZ' &display='XFSZ (File size limit exceeded)' }] - [&long='message' &short='m' &desc='message flag' &arg-required=$true &completer=[_]{ edit:complex-candidate 'ERR' &display='ERR (message example)' -edit:complex-candidate '_' &display='_ ()' }] - [&long='net_interfaces' &short='n' &desc='net_interfaces flag' &arg-required=$true &completer=[_]{ }] - [&long='optarg' &short='o' &desc='optional arg with default value blue' &arg-optional=$true &completer=[_]{ put blue red green yellow }] + [&long='message' &short='m' &desc='message flag' &arg-required=$true &completer=[_]{ edit:complex-candidate '_' &display='_' +edit:complex-candidate 'ERR' &display='ERR (message example)' }] + [&long='net_interfaces' &short='n' &desc='net_interfaces flag' &arg-required=$true &completer=[_]{ _example_callback '_example__action##net_interfaces' }] + [&long='optarg' &short='o' &desc='optional arg with default value blue' &arg-optional=$true &completer=[_]{ edit:complex-candidate 'blue' &display='blue' +edit:complex-candidate 'red' &display='red' +edit:complex-candidate 'green' &display='green' +edit:complex-candidate 'yellow' &display='yellow' }] [&long='usergroup' &desc='user\:group flag' &arg-required=$true &completer=[_]{ _example_callback '_example__action##usergroup' }] [&long='users' &short='u' &desc='users flag' &arg-required=$true &completer=[_]{ _example_callback '_example__action##users' }] - [&long='values' &short='v' &desc='values flag' &arg-required=$true &completer=[_]{ put values example }] + [&long='values' &short='v' &desc='values flag' &arg-required=$true &completer=[_]{ edit:complex-candidate 'values' &display='values' +edit:complex-candidate 'example' &display='example' }] [&long='values_described' &short='d' &desc='values with description flag' &arg-required=$true &completer=[_]{ edit:complex-candidate 'values' &display='values (valueDescription)' edit:complex-candidate 'example' &display='example (exampleDescription)' }] ] arg-handlers = [ - [_]{ put positional1 p1 } - [_]{ put positional2 p2 } + [_]{ edit:complex-candidate 'positional1' &display='positional1' +edit:complex-candidate 'p1' &display='p1' } + [_]{ edit:complex-candidate 'positional2' &display='positional2' +edit:complex-candidate 'p2' &display='p2' } ] subargs = $arg[(subindex action):] if (> (count $subargs) 0) { @@ -321,7 +346,8 @@ edit:complex-candidate 'example' &display='example (exampleDescription)' }] } } elif (eq $state '_example__condition') { opt-specs = [ - [&long='required' &short='r' &desc='required flag' &arg-required=$true &completer=[_]{ put valid invalid }] + [&long='required' &short='r' &desc='required flag' &arg-required=$true &completer=[_]{ edit:complex-candidate 'valid' &display='valid' +edit:complex-candidate 'invalid' &display='invalid' }] ] arg-handlers = [ [_]{ _example_callback '_example__condition#1' } @@ -346,16 +372,15 @@ edit:complex-candidate 'example' &display='example (exampleDescription)' }] ] arg-handlers = [ - [_]{ put echo fail } - [_]{ put echo fail } - [_]{ put echo fail } - [_]{ put echo fail } - [_]{ put echo fail } - [_]{ put echo fail } - [_]{ put echo fail } - [_]{ edit:complex-candidate 'ERR' &display='ERR (no values to complete)' -edit:complex-candidate '_' &display='_ ()' } - [_]{ put LAST POSITIONAL VALUE } + [_]{ edit:complex-candidate 'echo fail' &display='echo fail' } + [_]{ edit:complex-candidate 'echo fail' &display='echo fail' } + [_]{ edit:complex-candidate 'echo fail' &display='echo fail' } + [_]{ edit:complex-candidate ' echo fail ' &display=' echo fail ' } + [_]{ edit:complex-candidate ' echo fail ' &display=' echo fail ' } + [_]{ edit:complex-candidate ' echo fail ' &display=' echo fail ' } + [_]{ edit:complex-candidate 'echo fail' &display='echo fail' } + [_]{ edit:complex-candidate '' &display='' } + [_]{ edit:complex-candidate 'LAST POSITIONAL VALUE' &display='LAST POSITIONAL VALUE' } ] subargs = $arg[(subindex injection):] if (> (count $subargs) 0) { @@ -365,7 +390,7 @@ edit:complex-candidate '_' &display='_ ()' } } ` rootCmd.InitDefaultHelpCmd() - assert.Equal(t, expected, carapace.Gen(rootCmd).Elvish()) + assert.Equal(t, expected, carapace.Gen(rootCmd).Snippet("elvish")) } func TestFish(t *testing.T) { @@ -400,7 +425,7 @@ complete -c example -f complete -c 'example' -f -n '_example_state _example' -l 'array' -s 'a' -d 'multiflag' -r complete -c 'example' -f -n '_example_state _example' -l 'persistentFlag' -s 'p' -d 'Help message for persistentFlag' -complete -c 'example' -f -n '_example_state _example' -l 'toggle' -s 't' -d 'Help message for toggle' -a '(echo -e "true\nfalse")' +complete -c 'example' -f -n '_example_state _example' -l 'toggle' -s 't' -d 'Help message for toggle' -a '(echo -e "true \nfalse ")' complete -c 'example' -f -n '_example_state _example ' -a 'action alias' -d 'action example' complete -c 'example' -f -n '_example_state _example ' -a 'callback ' -d 'callback example' complete -c 'example' -f -n '_example_state _example ' -a 'condition ' -d 'condition example' @@ -408,19 +433,18 @@ complete -c 'example' -f -n '_example_state _example ' -a 'help ' -d 'Help about complete -c 'example' -f -n '_example_state _example ' -a 'injection ' -d 'just trying to break things' -complete -c 'example' -f -n '_example_state _example__action' -l 'custom' -s 'c' -d 'custom flag' -a '()' -r complete -c 'example' -f -n '_example_state _example__action' -l 'directories' -d 'files flag' -a '(__fish_complete_directories)' -r complete -c 'example' -f -n '_example_state _example__action' -l 'files' -s 'f' -d 'files flag' -a '(__fish_complete_suffix ".go")' -r -complete -c 'example' -f -n '_example_state _example__action' -l 'groups' -s 'g' -d 'groups flag' -a '(__fish_complete_groups)' -r -complete -c 'example' -f -n '_example_state _example__action' -l 'hosts' -d 'hosts flag' -a '(__fish_print_hostnames)' -r -complete -c 'example' -f -n '_example_state _example__action' -l 'kill' -s 'k' -d 'kill signals' -a '(echo -e "ABRT\tAbnormal termination\nALRM\tVirtual alarm clock\nBUS\tBUS error\nCHLD\tChild status has changed\nCONT\tContinue stopped process\nFPE\tFloating-point exception\nHUP\tHangup detected on controlling terminal\nILL\tIllegal instruction\nINT\tInterrupt from keyboard\nKILL\tKill, unblockable\nPIPE\tBroken pipe\nPOLL\tPollable event occurred\nPROF\tProfiling alarm clock timer expired\nPWR\tPower failure restart\nQUIT\tQuit from keyboard\nSEGV\tSegmentation violation\nSTKFLT\tStack fault on coprocessor\nSTOP\tStop process, unblockable\nSYS\tBad system call\nTERM\tTermination request\nTRAP\tTrace/breakpoint trap\nTSTP\tStop typed at keyboard\nTTIN\tBackground read from tty\nTTOU\tBackground write to tty\nURG\tUrgent condition on socket\nUSR1\tUser-defined signal 1\nUSR2\tUser-defined signal 2\nVTALRM\tVirtual alarm clock\nWINCH\tWindow size change\nXCPU\tCPU time limit exceeded\nXFSZ\tFile size limit exceeded")' -r -complete -c 'example' -f -n '_example_state _example__action' -l 'message' -s 'm' -d 'message flag' -a '(echo -e "ERR\tmessage example\n_")' -r -complete -c 'example' -f -n '_example_state _example__action' -l 'net_interfaces' -s 'n' -d 'net_interfaces flag' -a '(__fish_print_interfaces)' -r -complete -c 'example' -f -n '_example_state _example__action' -l 'optarg' -s 'o' -d 'optional arg with default value blue' -a '(echo -e "blue\nred\ngreen\nyellow")' +complete -c 'example' -f -n '_example_state _example__action' -l 'groups' -s 'g' -d 'groups flag' -a '(_example_callback _example__action##groups)' -r +complete -c 'example' -f -n '_example_state _example__action' -l 'hosts' -d 'hosts flag' -a '(_example_callback _example__action##hosts)' -r +complete -c 'example' -f -n '_example_state _example__action' -l 'kill' -s 'k' -d 'kill signals' -a '(echo -e "ABRT Abnormal termination\nALRM Virtual alarm clock\nBUS BUS error\nCHLD Child status has changed\nCONT Continue stopped process\nFPE Floating-point exception\nHUP Hangup detected on controlling terminal\nILL Illegal instruction\nINT Interrupt from keyboard\nKILL Kill, unblockable\nPIPE Broken pipe\nPOLL Pollable event occurred\nPROF Profiling alarm clock timer expired\nPWR Power failure restart\nQUIT Quit from keyboard\nSEGV Segmentation violation\nSTKFLT Stack fault on coprocessor\nSTOP Stop process, unblockable\nSYS Bad system call\nTERM Termination request\nTRAP Trace/breakpoint trap\nTSTP Stop typed at keyboard\nTTIN Background read from tty\nTTOU Background write to tty\nURG Urgent condition on socket\nUSR1 User-defined signal 1\nUSR2 User-defined signal 2\nVTALRM Virtual alarm clock\nWINCH Window size change\nXCPU CPU time limit exceeded\nXFSZ File size limit exceeded")' -r +complete -c 'example' -f -n '_example_state _example__action' -l 'message' -s 'm' -d 'message flag' -a '(echo -e "_ \nERR message example")' -r +complete -c 'example' -f -n '_example_state _example__action' -l 'net_interfaces' -s 'n' -d 'net_interfaces flag' -a '(_example_callback _example__action##net_interfaces)' -r +complete -c 'example' -f -n '_example_state _example__action' -l 'optarg' -s 'o' -d 'optional arg with default value blue' -a '(echo -e "blue \nred \ngreen \nyellow ")' complete -c 'example' -f -n '_example_state _example__action' -l 'usergroup' -d 'user\:group flag' -a '(_example_callback _example__action##usergroup)' -r -complete -c 'example' -f -n '_example_state _example__action' -l 'users' -s 'u' -d 'users flag' -a '(__fish_complete_users)' -r -complete -c 'example' -f -n '_example_state _example__action' -l 'values' -s 'v' -d 'values flag' -a '(echo -e "values\nexample")' -r -complete -c 'example' -f -n '_example_state _example__action' -l 'values_described' -s 'd' -d 'values with description flag' -a '(echo -e "values\tvalueDescription\nexample\texampleDescription")' -r +complete -c 'example' -f -n '_example_state _example__action' -l 'users' -s 'u' -d 'users flag' -a '(_example_callback _example__action##users)' -r +complete -c 'example' -f -n '_example_state _example__action' -l 'values' -s 'v' -d 'values flag' -a '(echo -e "values \nexample ")' -r +complete -c 'example' -f -n '_example_state _example__action' -l 'values_described' -s 'd' -d 'values with description flag' -a '(echo -e "values valueDescription\nexample exampleDescription")' -r complete -c 'example' -f -n '_example_state _example__action' -a '(_example_callback _)' @@ -428,7 +452,7 @@ complete -c 'example' -f -n '_example_state _example__callback' -l 'callback' -s complete -c 'example' -f -n '_example_state _example__callback' -a '(_example_callback _)' -complete -c 'example' -f -n '_example_state _example__condition' -l 'required' -s 'r' -d 'required flag' -a '(echo -e "valid\ninvalid")' -r +complete -c 'example' -f -n '_example_state _example__condition' -l 'required' -s 'r' -d 'required flag' -a '(echo -e "valid \ninvalid ")' -r complete -c 'example' -f -n '_example_state _example__condition' -a '(_example_callback _)' @@ -438,7 +462,7 @@ complete -c 'example' -f -n '_example_state _example__help' -a '(_example_callba complete -c 'example' -f -n '_example_state _example__injection' -a '(_example_callback _)' ` rootCmd.InitDefaultHelpCmd() - assert.Equal(t, expected, carapace.Gen(rootCmd).Fish()) + assert.Equal(t, expected, carapace.Gen(rootCmd).Snippet("fish")) } func TestPowershell(t *testing.T) { @@ -504,10 +528,6 @@ Register-ArgumentCompleter -Native -CommandName 'example' -ScriptBlock { '_example__action' { switch -regex ($previous) { - '^(-c|--custom)$' { - - break - } '^(--directories)$' { [CompletionResult]::new('', '', [CompletionResultType]::ParameterValue, '') break @@ -525,46 +545,46 @@ Register-ArgumentCompleter -Native -CommandName 'example' -ScriptBlock { break } '^(-k|--kill)$' { - [CompletionResult]::new('ABRT ', 'ABRT', [CompletionResultType]::ParameterValue, 'Abnormal termination ') - [CompletionResult]::new('ALRM ', 'ALRM', [CompletionResultType]::ParameterValue, 'Virtual alarm clock ') - [CompletionResult]::new('BUS ', 'BUS', [CompletionResultType]::ParameterValue, 'BUS error ') - [CompletionResult]::new('CHLD ', 'CHLD', [CompletionResultType]::ParameterValue, 'Child status has changed ') - [CompletionResult]::new('CONT ', 'CONT', [CompletionResultType]::ParameterValue, 'Continue stopped process ') - [CompletionResult]::new('FPE ', 'FPE', [CompletionResultType]::ParameterValue, 'Floating-point exception ') - [CompletionResult]::new('HUP ', 'HUP', [CompletionResultType]::ParameterValue, 'Hangup detected on controlling terminal ') - [CompletionResult]::new('ILL ', 'ILL', [CompletionResultType]::ParameterValue, 'Illegal instruction ') - [CompletionResult]::new('INT ', 'INT', [CompletionResultType]::ParameterValue, 'Interrupt from keyboard ') - [CompletionResult]::new('KILL ', 'KILL', [CompletionResultType]::ParameterValue, 'Kill, unblockable ') - [CompletionResult]::new('PIPE ', 'PIPE', [CompletionResultType]::ParameterValue, 'Broken pipe ') - [CompletionResult]::new('POLL ', 'POLL', [CompletionResultType]::ParameterValue, 'Pollable event occurred ') - [CompletionResult]::new('PROF ', 'PROF', [CompletionResultType]::ParameterValue, 'Profiling alarm clock timer expired ') - [CompletionResult]::new('PWR ', 'PWR', [CompletionResultType]::ParameterValue, 'Power failure restart ') - [CompletionResult]::new('QUIT ', 'QUIT', [CompletionResultType]::ParameterValue, 'Quit from keyboard ') - [CompletionResult]::new('SEGV ', 'SEGV', [CompletionResultType]::ParameterValue, 'Segmentation violation ') - [CompletionResult]::new('STKFLT ', 'STKFLT', [CompletionResultType]::ParameterValue, 'Stack fault on coprocessor ') - [CompletionResult]::new('STOP ', 'STOP', [CompletionResultType]::ParameterValue, 'Stop process, unblockable ') - [CompletionResult]::new('SYS ', 'SYS', [CompletionResultType]::ParameterValue, 'Bad system call ') - [CompletionResult]::new('TERM ', 'TERM', [CompletionResultType]::ParameterValue, 'Termination request ') - [CompletionResult]::new('TRAP ', 'TRAP', [CompletionResultType]::ParameterValue, 'Trace/breakpoint trap ') - [CompletionResult]::new('TSTP ', 'TSTP', [CompletionResultType]::ParameterValue, 'Stop typed at keyboard ') - [CompletionResult]::new('TTIN ', 'TTIN', [CompletionResultType]::ParameterValue, 'Background read from tty ') - [CompletionResult]::new('TTOU ', 'TTOU', [CompletionResultType]::ParameterValue, 'Background write to tty ') - [CompletionResult]::new('URG ', 'URG', [CompletionResultType]::ParameterValue, 'Urgent condition on socket ') - [CompletionResult]::new('USR1 ', 'USR1', [CompletionResultType]::ParameterValue, 'User-defined signal 1 ') - [CompletionResult]::new('USR2 ', 'USR2', [CompletionResultType]::ParameterValue, 'User-defined signal 2 ') - [CompletionResult]::new('VTALRM ', 'VTALRM', [CompletionResultType]::ParameterValue, 'Virtual alarm clock ') - [CompletionResult]::new('WINCH ', 'WINCH', [CompletionResultType]::ParameterValue, 'Window size change ') - [CompletionResult]::new('XCPU ', 'XCPU', [CompletionResultType]::ParameterValue, 'CPU time limit exceeded ') - [CompletionResult]::new('XFSZ ', 'XFSZ', [CompletionResultType]::ParameterValue, 'File size limit exceeded ') + [CompletionResult]::new('ABRT', 'ABRT ', [CompletionResultType]::ParameterValue, 'Abnormal termination ') + [CompletionResult]::new('ALRM', 'ALRM ', [CompletionResultType]::ParameterValue, 'Virtual alarm clock ') + [CompletionResult]::new('BUS', 'BUS ', [CompletionResultType]::ParameterValue, 'BUS error ') + [CompletionResult]::new('CHLD', 'CHLD ', [CompletionResultType]::ParameterValue, 'Child status has changed ') + [CompletionResult]::new('CONT', 'CONT ', [CompletionResultType]::ParameterValue, 'Continue stopped process ') + [CompletionResult]::new('FPE', 'FPE ', [CompletionResultType]::ParameterValue, 'Floating-point exception ') + [CompletionResult]::new('HUP', 'HUP ', [CompletionResultType]::ParameterValue, 'Hangup detected on controlling terminal ') + [CompletionResult]::new('ILL', 'ILL ', [CompletionResultType]::ParameterValue, 'Illegal instruction ') + [CompletionResult]::new('INT', 'INT ', [CompletionResultType]::ParameterValue, 'Interrupt from keyboard ') + [CompletionResult]::new('KILL', 'KILL ', [CompletionResultType]::ParameterValue, 'Kill, unblockable ') + [CompletionResult]::new('PIPE', 'PIPE ', [CompletionResultType]::ParameterValue, 'Broken pipe ') + [CompletionResult]::new('POLL', 'POLL ', [CompletionResultType]::ParameterValue, 'Pollable event occurred ') + [CompletionResult]::new('PROF', 'PROF ', [CompletionResultType]::ParameterValue, 'Profiling alarm clock timer expired ') + [CompletionResult]::new('PWR', 'PWR ', [CompletionResultType]::ParameterValue, 'Power failure restart ') + [CompletionResult]::new('QUIT', 'QUIT ', [CompletionResultType]::ParameterValue, 'Quit from keyboard ') + [CompletionResult]::new('SEGV', 'SEGV ', [CompletionResultType]::ParameterValue, 'Segmentation violation ') + [CompletionResult]::new('STKFLT', 'STKFLT ', [CompletionResultType]::ParameterValue, 'Stack fault on coprocessor ') + [CompletionResult]::new('STOP', 'STOP ', [CompletionResultType]::ParameterValue, 'Stop process, unblockable ') + [CompletionResult]::new('SYS', 'SYS ', [CompletionResultType]::ParameterValue, 'Bad system call ') + [CompletionResult]::new('TERM', 'TERM ', [CompletionResultType]::ParameterValue, 'Termination request ') + [CompletionResult]::new('TRAP', 'TRAP ', [CompletionResultType]::ParameterValue, 'Trace/breakpoint trap ') + [CompletionResult]::new('TSTP', 'TSTP ', [CompletionResultType]::ParameterValue, 'Stop typed at keyboard ') + [CompletionResult]::new('TTIN', 'TTIN ', [CompletionResultType]::ParameterValue, 'Background read from tty ') + [CompletionResult]::new('TTOU', 'TTOU ', [CompletionResultType]::ParameterValue, 'Background write to tty ') + [CompletionResult]::new('URG', 'URG ', [CompletionResultType]::ParameterValue, 'Urgent condition on socket ') + [CompletionResult]::new('USR1', 'USR1 ', [CompletionResultType]::ParameterValue, 'User-defined signal 1 ') + [CompletionResult]::new('USR2', 'USR2 ', [CompletionResultType]::ParameterValue, 'User-defined signal 2 ') + [CompletionResult]::new('VTALRM', 'VTALRM ', [CompletionResultType]::ParameterValue, 'Virtual alarm clock ') + [CompletionResult]::new('WINCH', 'WINCH ', [CompletionResultType]::ParameterValue, 'Window size change ') + [CompletionResult]::new('XCPU', 'XCPU ', [CompletionResultType]::ParameterValue, 'CPU time limit exceeded ') + [CompletionResult]::new('XFSZ', 'XFSZ ', [CompletionResultType]::ParameterValue, 'File size limit exceeded ') break } '^(-m|--message)$' { - [CompletionResult]::new('_ ', '_', [CompletionResultType]::ParameterValue, 'message example ') - [CompletionResult]::new('ERR ', 'ERR', [CompletionResultType]::ParameterValue, 'message example ') + [CompletionResult]::new('_', '_ ', [CompletionResultType]::ParameterValue, ' ') + [CompletionResult]::new('ERR', 'ERR ', [CompletionResultType]::ParameterValue, 'message example ') break } '^(-n|--net_interfaces)$' { - $(Get-NetAdapter).Name + _example_callback '_example__action##net_interfaces' break } '^(--usergroup)$' { @@ -576,23 +596,23 @@ Register-ArgumentCompleter -Native -CommandName 'example' -ScriptBlock { break } '^(-v|--values)$' { - [CompletionResult]::new('values ', 'values', [CompletionResultType]::ParameterValue, ' ') - [CompletionResult]::new('example ', 'example', [CompletionResultType]::ParameterValue, ' ') + [CompletionResult]::new('values', 'values ', [CompletionResultType]::ParameterValue, ' ') + [CompletionResult]::new('example', 'example ', [CompletionResultType]::ParameterValue, ' ') break } '^(-d|--values_described)$' { - [CompletionResult]::new('values ', 'values', [CompletionResultType]::ParameterValue, 'valueDescription ') - [CompletionResult]::new('example ', 'example', [CompletionResultType]::ParameterValue, 'exampleDescription ') + [CompletionResult]::new('values', 'values ', [CompletionResultType]::ParameterValue, 'valueDescription ') + [CompletionResult]::new('example', 'example ', [CompletionResultType]::ParameterValue, 'exampleDescription ') break } default { switch -regex ($wordToComplete) { '^(-o=*|--optarg=*)$' { @( - [CompletionResult]::new('blue ', 'blue', [CompletionResultType]::ParameterValue, ' ') - [CompletionResult]::new('red ', 'red', [CompletionResultType]::ParameterValue, ' ') - [CompletionResult]::new('green ', 'green', [CompletionResultType]::ParameterValue, ' ') - [CompletionResult]::new('yellow ', 'yellow', [CompletionResultType]::ParameterValue, ' ') + [CompletionResult]::new('blue', 'blue ', [CompletionResultType]::ParameterValue, ' ') + [CompletionResult]::new('red', 'red ', [CompletionResultType]::ParameterValue, ' ') + [CompletionResult]::new('green', 'green ', [CompletionResultType]::ParameterValue, ' ') + [CompletionResult]::new('yellow', 'yellow ', [CompletionResultType]::ParameterValue, ' ') ) | ForEach-Object{ [CompletionResult]::new($wordToComplete.split("=")[0] + "=" + $_.CompletionText, $_.ListItemText, $_.ResultType, $_.ToolTip) } break } @@ -600,8 +620,6 @@ Register-ArgumentCompleter -Native -CommandName 'example' -ScriptBlock { default { if ($wordToComplete -like "-*") { - [CompletionResult]::new('-c ', '-c', [CompletionResultType]::ParameterName, 'custom flag') - [CompletionResult]::new('--custom ', '--custom', [CompletionResultType]::ParameterName, 'custom flag') [CompletionResult]::new('--directories ', '--directories', [CompletionResultType]::ParameterName, 'files flag') [CompletionResult]::new('-f ', '-f', [CompletionResultType]::ParameterName, 'files flag') [CompletionResult]::new('--files ', '--files', [CompletionResultType]::ParameterName, 'files flag') @@ -661,8 +679,8 @@ Register-ArgumentCompleter -Native -CommandName 'example' -ScriptBlock { '_example__condition' { switch -regex ($previous) { '^(-r|--required)$' { - [CompletionResult]::new('valid ', 'valid', [CompletionResultType]::ParameterValue, ' ') - [CompletionResult]::new('invalid ', 'invalid', [CompletionResultType]::ParameterValue, ' ') + [CompletionResult]::new('valid', 'valid ', [CompletionResultType]::ParameterValue, ' ') + [CompletionResult]::new('invalid', 'invalid ', [CompletionResultType]::ParameterValue, ' ') break } default { @@ -734,7 +752,7 @@ Register-ArgumentCompleter -Native -CommandName 'example' -ScriptBlock { Sort-Object -Property ListItemText }` rootCmd.InitDefaultHelpCmd() - assert.Equal(t, expected, carapace.Gen(rootCmd).Powershell()) + assert.Equal(t, expected, carapace.Gen(rootCmd).Snippet("powershell")) } func TestXonsh(t *testing.T) { @@ -815,9 +833,6 @@ def _example_completer(prefix, line, begidx, endidx, ctx): elif state == '_example__action': if False: # switch previous pass - elif re.search('^(-c|--custom)$',previous): - result = {} - elif re.search('^(--directories)$',previous): result = { RichCompletion(f, display=pathlib.PurePath(f).name, description='', prefix_len=0) for f in complete_dir(prefix, line, begidx, endidx, ctx, True)[0]} @@ -867,12 +882,12 @@ def _example_completer(prefix, line, begidx, endidx, ctx): elif re.search('^(-m|--message)$',previous): result = { - RichCompletion('_', display='_', description='message example', prefix_len=0), + RichCompletion('_', display='_', description='', prefix_len=0), RichCompletion('ERR', display='ERR', description='message example', prefix_len=0), } elif re.search('^(-n|--net_interfaces)$',previous): - result = {} + result = _example_callback('_example__action##net_interfaces') elif re.search('^(--usergroup)$',previous): result = _example_callback('_example__action##usergroup') @@ -907,8 +922,6 @@ def _example_completer(prefix, line, begidx, endidx, ctx): elif re.search("-.*",current): result = { - RichCompletion('-c', display='-c', description='custom flag', prefix_len=0), - RichCompletion('--custom', display='--custom', description='custom flag', prefix_len=0), RichCompletion('--directories', display='--directories', description='files flag', prefix_len=0), RichCompletion('-f', display='-f', description='files flag', prefix_len=0), RichCompletion('--files', display='--files', description='files flag', prefix_len=0), @@ -1021,11 +1034,15 @@ _add_one_completer('example', _example_completer, 'start') ` rootCmd.InitDefaultHelpCmd() - assert.Equal(t, expected, carapace.Gen(rootCmd).Xonsh()) + assert.Equal(t, expected, carapace.Gen(rootCmd).Snippet("xonsh")) } func TestZsh(t *testing.T) { expected := `#compdef example +function _example_callback { + # shellcheck disable=SC2086 + eval "$(example _carapace zsh "$5" ${os_args})" +} function _example { local -a commands # shellcheck disable=SC2206 @@ -1034,7 +1051,7 @@ function _example { _arguments -C \ "(*-a *--array)"{\*-a,\*--array}"[multiflag]: :" \ "(-p --persistentFlag)"{-p=-,--persistentFlag=-}"[Help message for persistentFlag]::" \ - "(-t --toggle)"{-t=-,--toggle=-}"[Help message for toggle]:: :_values '' true false" \ + "(-t --toggle)"{-t=-,--toggle=-}"[Help message for toggle]:: :{local _comp_desc=('true' 'false');compadd -S '' -d _comp_desc 'true' 'false'}" \ "1: :->cmnds" \ "*::arg:->args" @@ -1078,35 +1095,34 @@ function _example { function _example__action { _arguments -C \ - "(-c --custom)"{-c,--custom}"[custom flag]: :_most_recent_file 2" \ "--directories[files flag]: :_files -/" \ "(-f --files)"{-f,--files}"[files flag]: :_files -g '*.go'" \ - "(-g --groups)"{-g,--groups}"[groups flag]: :_groups" \ - "--hosts[hosts flag]: :_hosts" \ - "(-k --kill)"{-k,--kill}"[kill signals]: :_values '' 'ABRT[Abnormal\ termination]' 'ALRM[Virtual\ alarm\ clock]' 'BUS[BUS\ error]' 'CHLD[Child\ status\ has\ changed]' 'CONT[Continue\ stopped\ process]' 'FPE[Floating-point\ exception]' 'HUP[Hangup\ detected\ on\ controlling\ terminal]' 'ILL[Illegal\ instruction]' 'INT[Interrupt\ from\ keyboard]' 'KILL[Kill,\ unblockable]' 'PIPE[Broken\ pipe]' 'POLL[Pollable\ event\ occurred]' 'PROF[Profiling\ alarm\ clock\ timer\ expired]' 'PWR[Power\ failure\ restart]' 'QUIT[Quit\ from\ keyboard]' 'SEGV[Segmentation\ violation]' 'STKFLT[Stack\ fault\ on\ coprocessor]' 'STOP[Stop\ process,\ unblockable]' 'SYS[Bad\ system\ call]' 'TERM[Termination\ request]' 'TRAP[Trace/breakpoint\ trap]' 'TSTP[Stop\ typed\ at\ keyboard]' 'TTIN[Background\ read\ from\ tty]' 'TTOU[Background\ write\ to\ tty]' 'URG[Urgent\ condition\ on\ socket]' 'USR1[User-defined\ signal\ 1]' 'USR2[User-defined\ signal\ 2]' 'VTALRM[Virtual\ alarm\ clock]' 'WINCH[Window\ size\ change]' 'XCPU[CPU\ time\ limit\ exceeded]' 'XFSZ[File\ size\ limit\ exceeded]' " \ - "(-m --message)"{-m,--message}"[message flag]: : _message -r 'message example'" \ - "(-n --net_interfaces)"{-n,--net_interfaces}"[net_interfaces flag]: :_net_interfaces" \ - "(-o --optarg)"{-o=-,--optarg=-}"[optional arg with default value blue]:: :_values '' blue red green yellow" \ - "--usergroup[user\:group flag]: : eval \$(example _carapace zsh '_example__action##usergroup' ${${os_args:1:gs/\"/\\\"}:gs/\'/\\\"})" \ - "(-u --users)"{-u,--users}"[users flag]: :_users" \ - "(-v --values)"{-v,--values}"[values flag]: :_values '' values example" \ - "(-d --values_described)"{-d,--values_described}"[values with description flag]: :_values '' 'values[valueDescription]' 'example[exampleDescription]' " \ - "1: :_values '' positional1 p1" \ - "2: :_values '' positional2 p2" + "(-g --groups)"{-g,--groups}"[groups flag]: :_example_callback '_example__action##groups'" \ + "--hosts[hosts flag]: :_example_callback '_example__action##hosts'" \ + "(-k --kill)"{-k,--kill}"[kill signals]: :{local _comp_desc=('ABRT (Abnormal termination)' 'ALRM (Virtual alarm clock)' 'BUS (BUS error)' 'CHLD (Child status has changed)' 'CONT (Continue stopped process)' 'FPE (Floating-point exception)' 'HUP (Hangup detected on controlling terminal)' 'ILL (Illegal instruction)' 'INT (Interrupt from keyboard)' 'KILL (Kill, unblockable)' 'PIPE (Broken pipe)' 'POLL (Pollable event occurred)' 'PROF (Profiling alarm clock timer expired)' 'PWR (Power failure restart)' 'QUIT (Quit from keyboard)' 'SEGV (Segmentation violation)' 'STKFLT (Stack fault on coprocessor)' 'STOP (Stop process, unblockable)' 'SYS (Bad system call)' 'TERM (Termination request)' 'TRAP (Trace/breakpoint trap)' 'TSTP (Stop typed at keyboard)' 'TTIN (Background read from tty)' 'TTOU (Background write to tty)' 'URG (Urgent condition on socket)' 'USR1 (User-defined signal 1)' 'USR2 (User-defined signal 2)' 'VTALRM (Virtual alarm clock)' 'WINCH (Window size change)' 'XCPU (CPU time limit exceeded)' 'XFSZ (File size limit exceeded)');compadd -S '' -d _comp_desc 'ABRT' 'ALRM' 'BUS' 'CHLD' 'CONT' 'FPE' 'HUP' 'ILL' 'INT' 'KILL' 'PIPE' 'POLL' 'PROF' 'PWR' 'QUIT' 'SEGV' 'STKFLT' 'STOP' 'SYS' 'TERM' 'TRAP' 'TSTP' 'TTIN' 'TTOU' 'URG' 'USR1' 'USR2' 'VTALRM' 'WINCH' 'XCPU' 'XFSZ'}" \ + "(-m --message)"{-m,--message}"[message flag]: :{local _comp_desc=('_' 'ERR (message example)');compadd -S '' -d _comp_desc '_' 'ERR'}" \ + "(-n --net_interfaces)"{-n,--net_interfaces}"[net_interfaces flag]: :_example_callback '_example__action##net_interfaces'" \ + "(-o --optarg)"{-o=-,--optarg=-}"[optional arg with default value blue]:: :{local _comp_desc=('blue' 'red' 'green' 'yellow');compadd -S '' -d _comp_desc 'blue' 'red' 'green' 'yellow'}" \ + "--usergroup[user\:group flag]: :_example_callback '_example__action##usergroup'" \ + "(-u --users)"{-u,--users}"[users flag]: :_example_callback '_example__action##users'" \ + "(-v --values)"{-v,--values}"[values flag]: :{local _comp_desc=('values' 'example');compadd -S '' -d _comp_desc 'values' 'example'}" \ + "(-d --values_described)"{-d,--values_described}"[values with description flag]: :{local _comp_desc=('values (valueDescription)' 'example (exampleDescription)');compadd -S '' -d _comp_desc 'values' 'example'}" \ + "1: :{local _comp_desc=('positional1' 'p1');compadd -S '' -d _comp_desc 'positional1' 'p1'}" \ + "2: :{local _comp_desc=('positional2' 'p2');compadd -S '' -d _comp_desc 'positional2' 'p2'}" } function _example__callback { _arguments -C \ - "(-c --callback)"{-c,--callback}"[Help message for callback]: : eval \$(example _carapace zsh '_example__callback##callback' ${${os_args:1:gs/\"/\\\"}:gs/\'/\\\"})" \ - "1: : eval \$(example _carapace zsh '_example__callback#1' ${${os_args:1:gs/\"/\\\"}:gs/\'/\\\"})" \ - "2: : eval \$(example _carapace zsh '_example__callback#2' ${${os_args:1:gs/\"/\\\"}:gs/\'/\\\"})" \ - "*: : eval \$(example _carapace zsh '_example__callback#0' ${${os_args:1:gs/\"/\\\"}:gs/\'/\\\"})" + "(-c --callback)"{-c,--callback}"[Help message for callback]: :_example_callback '_example__callback##callback'" \ + "1: :_example_callback '_example__callback#1'" \ + "2: :_example_callback '_example__callback#2'" \ + "*: :_example_callback '_example__callback#0'" } function _example__condition { _arguments -C \ - "(-r --required)"{-r,--required}"[required flag]: :_values '' valid invalid" \ - "1: : eval \$(example _carapace zsh '_example__condition#1' ${${os_args:1:gs/\"/\\\"}:gs/\'/\\\"})" + "(-r --required)"{-r,--required}"[required flag]: :{local _comp_desc=('valid' 'invalid');compadd -S '' -d _comp_desc 'valid' 'invalid'}" \ + "1: :_example_callback '_example__condition#1'" } function _example__help { @@ -1116,18 +1132,18 @@ function _example__help { function _example__injection { _arguments -C \ - "1: :_values '' echo\ fail" \ - "2: :_values '' echo\ fail" \ - "3: :_values '' echo\ fail" \ - "4: :_values '' \ echo\ fail\ " \ - "5: :_values '' \ echo\ fail\ " \ - "6: :_values '' \ echo\ fail\ " \ - "7: :_values '' echo\ fail" \ - "8: : _message -r 'no values to complete'" \ - "9: :_values '' LAST\ POSITIONAL\ VALUE" + "1: :{local _comp_desc=('echo fail');compadd -S '' -d _comp_desc 'echo fail'}" \ + "2: :{local _comp_desc=('echo fail');compadd -S '' -d _comp_desc 'echo fail'}" \ + "3: :{local _comp_desc=('echo fail');compadd -S '' -d _comp_desc 'echo fail'}" \ + "4: :{local _comp_desc=(' echo fail ');compadd -S '' -d _comp_desc ' echo fail '}" \ + "5: :{local _comp_desc=(' echo fail ');compadd -S '' -d _comp_desc ' echo fail '}" \ + "6: :{local _comp_desc=(' echo fail ');compadd -S '' -d _comp_desc ' echo fail '}" \ + "7: :{local _comp_desc=('echo fail');compadd -S '' -d _comp_desc 'echo fail'}" \ + "8: :{local _comp_desc=('');compadd -S '' -d _comp_desc ''}" \ + "9: :{local _comp_desc=('LAST POSITIONAL VALUE');compadd -S '' -d _comp_desc 'LAST POSITIONAL VALUE'}" } if compquote '' 2>/dev/null; then _example; else compdef _example example; fi ` rootCmd.InitDefaultHelpCmd() - assert.Equal(t, expected, carapace.Gen(rootCmd).Zsh()) + assert.Equal(t, expected, carapace.Gen(rootCmd).Snippet("zsh")) } diff --git a/fish/action.go b/fish/action.go index 26954721a..7e3b588ab 100644 --- a/fish/action.go +++ b/fish/action.go @@ -3,6 +3,8 @@ package fish import ( "fmt" "strings" + + "github.com/rsteube/carapace/common" ) var sanitizer = strings.NewReplacer( @@ -25,11 +27,7 @@ func Sanitize(values ...string) []string { } func Callback(prefix string, uid string) string { - return ActionExecute(fmt.Sprintf(`_%v_callback %v`, prefix, uid)) -} - -func ActionExecute(command string) string { - return fmt.Sprintf(`%v`, command) + return fmt.Sprintf(`_%v_callback %v`, prefix, uid) } func ActionDirectories() string { @@ -37,71 +35,15 @@ func ActionDirectories() string { } func ActionFiles(suffix string) string { - return ActionExecute(fmt.Sprintf(`__fish_complete_suffix "%v"`, suffix)) -} - -func ActionNetInterfaces() string { - return ActionExecute("__fish_print_interfaces") -} - -func ActionUsers() string { - return ActionExecute("__fish_complete_users") -} - -func ActionGroups() string { - return ActionExecute("__fish_complete_groups") -} - -func ActionHosts() string { - return ActionExecute("__fish_print_hostnames") -} - -func ActionValues(values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - // TODO escape special characters - //vals[index] = strings.Replace(val, " ", `\ `, -1) - vals[index] = val - } - return ActionExecute(fmt.Sprintf(`echo -e "%v"`, strings.Join(vals, `\n`))) -} - -func ActionValuesDescribed(values ...string) string { - sanitized := Sanitize(values...) - // TODO verify length (description always exists) - vals := make([]string, len(sanitized)/2) - for index, val := range sanitized { - if index%2 == 0 { - if sanitized[index+1] == "" { - vals[index/2] = fmt.Sprintf(`%v`, val) - } else { - vals[index/2] = fmt.Sprintf(`%v\t%v`, val, sanitized[index+1]) - } - } - } - return ActionExecute(fmt.Sprintf(`echo -e "%v"`, strings.Join(vals, `\n`))) -} - -func ActionMessage(msg string) string { - return ActionExecute(fmt.Sprintf(`echo -e "ERR\t%v\n_"`, Sanitize(msg)[0])) + return fmt.Sprintf(`__fish_complete_suffix "%v"`, suffix) } -func ActionPrefixValues(prefix string, values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - // TODO escape special characters +func ActionCandidates(values ...common.Candidate) string { + vals := make([]string, len(values)) + for index, val := range values { + // TODO sanitize //vals[index] = strings.Replace(val, " ", `\ `, -1) - vals[index] = prefix + val + vals[index] = sanitizer.Replace(val.Value) + "\t" + sanitizer.Replace(val.Description) } - return ActionExecute(fmt.Sprintf(`echo -e "%v"`, strings.Join(vals, `\n`))) + return fmt.Sprintf(`echo -e "%v"`, strings.Join(vals, `\n`)) } diff --git a/powershell/action.go b/powershell/action.go index fbed3a078..8a096992b 100644 --- a/powershell/action.go +++ b/powershell/action.go @@ -3,6 +3,8 @@ package powershell import ( "fmt" "strings" + + "github.com/rsteube/carapace/common" ) var sanitizer = strings.NewReplacer( // TODO @@ -38,10 +40,6 @@ func Callback(prefix string, uid string) string { return fmt.Sprintf("_%v_callback '%v'", prefix, uid) } -func ActionExecute(command string) string { - return fmt.Sprintf(`"%v" | Out-String | InvokeExpression`, strings.Replace(command, "\n", "`n", -1)) -} - func ActionDirectories() string { return `[CompletionResult]::new('', '', [CompletionResultType]::ParameterValue, '')` } @@ -50,60 +48,12 @@ func ActionFiles(suffix string) string { return `[CompletionResult]::new('', '', [CompletionResultType]::ParameterValue, '')` } -func ActionNetInterfaces() string { - return `$(Get-NetAdapter).Name` // TODO test this -} - -func ActionUsers() string { - return `$(Get-LocalUser).Name` // TODO test this -} - -func ActionGroups() string { - return `$(Get-Localgroup).Name` // TODO test this -} - -func ActionHosts() string { - return `` // TODO -} - -func ActionValues(values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - vals[index] = fmt.Sprintf(`[CompletionResult]::new('%v ', '%v', [CompletionResultType]::ParameterValue, ' ')`, EscapeSpace(val), val) - } - return strings.Join(vals, "\n") -} - -func ActionValuesDescribed(values ...string) string { - sanitized := Sanitize(values...) - // TODO verify length (description always exists) - vals := make([]string, len(sanitized)/2) - for index, val := range sanitized { - if index%2 == 0 { - vals[index/2] = fmt.Sprintf(`[CompletionResult]::new('%v ', '%v', [CompletionResultType]::ParameterValue, '%v ')`, EscapeSpace(val), val, sanitized[index+1]) +func ActionCandidates(values ...common.Candidate) string { + vals := make([]string, len(values)) + for index, val := range values { + if val.Value != "" { // must not be empty - any empty `''` parameter in CompletionResult causes an error + vals[index] = fmt.Sprintf(`[CompletionResult]::new('%v', '%v ', [CompletionResultType]::ParameterValue, '%v ')`, EscapeSpace(sanitizer.Replace(val.Value)), sanitizer.Replace(val.Display), sanitizer.Replace(val.Description)) } } return strings.Join(vals, "\n") } - -func ActionMessage(msg string) string { - return ActionValuesDescribed("_", msg, "ERR", msg) -} - -func ActionPrefixValues(prefix string, values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - vals[index] = fmt.Sprintf(`[CompletionResult]::new('%v', '%v', [CompletionResultType]::ParameterValue, ' ')`, EscapeSpace(prefix+val), val) - } - return strings.Join(vals, "\n") -} diff --git a/xonsh/action.go b/xonsh/action.go index b95e97c63..04ee7da57 100644 --- a/xonsh/action.go +++ b/xonsh/action.go @@ -3,6 +3,8 @@ package xonsh import ( "fmt" "strings" + + "github.com/rsteube/carapace/common" ) var sanitizer = strings.NewReplacer( // TODO @@ -34,11 +36,6 @@ func Callback(prefix string, uid string) string { return fmt.Sprintf("_%v_callback('%v')", strings.Replace(prefix, "-", "__", -1), uid) } -func ActionExecute(command string) string { - return `{}` - //return fmt.Sprintf(`"%v" | Out-String | InvokeExpression`, strings.Replace(command, "\n", "`n", -1)) -} - func ActionDirectories() string { return `{ RichCompletion(f, display=pathlib.PurePath(f).name, description='', prefix_len=0) for f in complete_dir(prefix, line, begidx, endidx, ctx, True)[0]}` } @@ -48,56 +45,10 @@ func ActionFiles(suffix string) string { return `{ RichCompletion(f, display=pathlib.PurePath(f).name, description='', prefix_len=0) for f in complete_path(prefix, line, begidx, endidx, ctx)[0]}` } -func ActionNetInterfaces() string { - return `{}` -} - -func ActionUsers() string { - return `{}` -} - -func ActionGroups() string { - return `{}` -} - -func ActionHosts() string { - return `{}` -} - -func ActionValues(values ...string) string { - vals := make([]string, len(values)*2) +func ActionCandidates(values ...common.Candidate) string { + vals := make([]string, len(values)) for index, val := range values { - vals[index*2] = val - vals[(index*2)+1] = "" - } - return ActionValuesDescribed(vals...) -} - -func ActionValuesDescribed(values ...string) string { - sanitized := Sanitize(values...) - // TODO verify length (description always exists) - vals := make([]string, len(sanitized)/2) - for index, val := range sanitized { - if index%2 == 0 { - vals[index/2] = fmt.Sprintf(` RichCompletion('%v', display='%v', description='%v', prefix_len=0),`, val, val, sanitized[index+1]) - } - } - return fmt.Sprintf("{\n%v\n}", strings.Join(vals, "\n")) -} - -func ActionMessage(msg string) string { - return ActionValuesDescribed("_", msg, "ERR", msg) -} - -func ActionPrefixValues(prefix string, values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - vals[index] = fmt.Sprintf(` RichCompletion('%v', display='%v', description='%v', prefix_len=0),`, prefix+val, val, "") + vals[index] = fmt.Sprintf(` RichCompletion('%v', display='%v', description='%v', prefix_len=0),`, sanitizer.Replace(val.Value), sanitizer.Replace(val.Display), sanitizer.Replace(val.Description)) } return fmt.Sprintf("{\n%v\n}", strings.Join(vals, "\n")) } diff --git a/zsh/action.go b/zsh/action.go index bc46df3b9..8f9e66123 100644 --- a/zsh/action.go +++ b/zsh/action.go @@ -2,7 +2,7 @@ package zsh import ( "fmt" - "github.com/rsteube/carapace/uid" + "github.com/rsteube/carapace/common" "strings" ) @@ -24,7 +24,6 @@ var sanitizer = strings.NewReplacer( `#`, ``, `[`, `\[`, `]`, `\]`, - `:`, `\:`, ) func Sanitize(values ...string) []string { @@ -35,13 +34,8 @@ func Sanitize(values ...string) []string { return sanitized } -func Callback(cuid string) string { - return ActionExecute(fmt.Sprintf(`%v _carapace zsh '%v' ${${os_args:1:gs/\"/\\\"}:gs/\'/\\\"}`, uid.Executable(), cuid)) -} - -// ActionExecute uses command substitution to invoke a command and evalues it's result as Action -func ActionExecute(command string) string { - return fmt.Sprintf(` eval \$(%v)`, command) // {EVAL-STRING} action did not handle space escaping ('\ ') as expected (separate arguments), this one works +func Callback(prefix string, cuid string) string { + return fmt.Sprintf(`_%v_callback '%v'`, prefix, cuid) } func ActionDirectories() string { @@ -58,92 +52,17 @@ func ActionFiles(pattern string) string { } } -// ActionNetInterfaces completes network interface names -func ActionNetInterfaces() string { - return "_net_interfaces" -} - -// ActionUsers completes user names -func ActionUsers() string { - return "_users" -} - -// ActionGroups completes group names -func ActionGroups() string { - return "_groups" -} - -// ActionHosts completes host names -func ActionHosts() string { - return "_hosts" -} - -// ActionValues completes arbitrary keywords (values) -func ActionValues(values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - // TODO escape special characters - vals[index] = strings.Replace(val, " ", `\ `, -1) - } - return fmt.Sprintf(`_values '' %v`, strings.Join(vals, " ")) -} - -// ActionValuesDescribed completes arbitrary key (values) with an additional description (value, description pairs) -func ActionValuesDescribed(values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - // TODO verify length (description always exists) - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - if index%2 == 0 { - vals[index/2] = fmt.Sprintf("'%v[%v]'", strings.Replace(val, " ", `\ `, -1), strings.Replace(sanitized[index+1], " ", `\ `, -1)) +func ActionCandidates(values ...common.Candidate) string { + vals := make([]string, len(values)) + displays := make([]string, len(values)) + for index, val := range values { + // TODO sanitize + vals[index] = fmt.Sprintf("'%v'", sanitizer.Replace(val.Value)) + if strings.TrimSpace(val.Description) == "" { + displays[index] = fmt.Sprintf("'%v'", sanitizer.Replace(val.Display)) + } else { + displays[index] = fmt.Sprintf("'%v (%v)'", sanitizer.Replace(val.Display), sanitizer.Replace(val.Description)) } } - return fmt.Sprintf(`_values '' %v`, strings.Join(vals, " ")) -} - -// ActionMessage displays a help messages in places where no completions can be generated -func ActionMessage(msg string) string { - return fmt.Sprintf(" _message -r '%v'", msg) // space before _message is necessary -} - -func ActionPrefixValues(prefix string, values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - // TODO escape special characters - //vals[index] = fmt.Sprintf("'%v'" ,strings.Replace(val, " ", `\ `, -1)) - vals[index] = fmt.Sprintf("'%v'", val) - } - //return fmt.Sprintf("compadd -S '' -P '%v' %v", currentValue, strings.Join(vals, " ")) - return fmt.Sprintf("compadd -S '' -p '%v' %v", prefix, strings.Join(vals, " ")) -} - -func ActionPrefixValuesDescribed(prefix string, values ...string) string { - sanitized := Sanitize(values...) - if len(strings.TrimSpace(strings.Join(sanitized, ""))) == 0 { - return ActionMessage("no values to complete") - } - - vals := make([]string, len(sanitized)) - for index, val := range sanitized { - // TODO escape special characters - //vals[index] = fmt.Sprintf("'%v'" ,strings.Replace(val, " ", `\ `, -1)) - vals[index] = fmt.Sprintf("'%v'", val) - } - //return fmt.Sprintf("compadd -S '' -P '%v' %v", currentValue, strings.Join(vals, " ")) - // TODO - return fmt.Sprintf("local _comp_desc(desc1 desc2 desc3);compadd -S '' -l -d _comp_desc -p '%v' %v", prefix, strings.Join(vals, " ")) + return fmt.Sprintf("{local _comp_desc=(%v);compadd -S '' -d _comp_desc %v}", strings.Join(displays, " "), strings.Join(vals, " ")) } diff --git a/zsh/snippet.go b/zsh/snippet.go index d35e91f2e..fb2a92f3b 100644 --- a/zsh/snippet.go +++ b/zsh/snippet.go @@ -21,6 +21,11 @@ var replacer = strings.NewReplacer( func Snippet(cmd *cobra.Command, actions map[string]string) string { result := fmt.Sprintf("#compdef %v\n", cmd.Name()) + result += fmt.Sprintf(`function _%v_callback { + # shellcheck disable=SC2086 + eval "$(%v _carapace zsh "$5" ${os_args})" +} +`, cmd.Name(), uid.Executable()) result += snippetFunctions(cmd, actions) result += fmt.Sprintf("if compquote '' 2>/dev/null; then _%v; else compdef _%v %v; fi\n", cmd.Name(), cmd.Name(), cmd.Name()) // check if withing completion function and enable direct sourcing @@ -76,7 +81,7 @@ func snippetFunctions(cmd *cobra.Command, actions map[string]string) string { } if len(positionals) == 0 { if cmd.ValidArgs != nil { - positionals = []string{" " + snippetPositionalCompletion(1, ActionValues(cmd.ValidArgs...))} + positionals = []string{" " + snippetPositionalCompletion(1, ActionCandidates(common.CandidateFromValues(cmd.ValidArgs...)...))} } positionals = append(positionals, ` "*::arg:->args"`) } diff --git a/zsh/snippet_test.go b/zsh/snippet_test.go deleted file mode 100644 index 401ce1d13..000000000 --- a/zsh/snippet_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package zsh - -import ( - "testing" - - "github.com/rsteube/carapace/assert" - "github.com/spf13/cobra" -) - -func TestSnippetFlagCompletion(t *testing.T) { - root := &cobra.Command{ - Use: "root", - } - root.Flags().Bool("simple", false, "simple flag") - root.Flags().String("values", "b", "values action flag") - root.Flags().BoolP("shorthand", "s", false, "shorthand flag") - root.Flags().StringArrayP("stringarray", "a", []string{"a"}, "stringarray flag") - root.Flags().StringSlice("stringslice", []string{"a"}, "stringslice flag") - - assert.Equal(t, `"--simple=-[simple flag]::"`, snippetFlagCompletion(root.Flag("simple"), nil)) - - valuesAction := ActionValues("a", "b", "c") - assert.Equal(t, `"--values[values action flag]: :_values '' a b c"`, snippetFlagCompletion(root.Flag("values"), &valuesAction)) - - assert.Equal(t, `"(-s --shorthand)"{-s=-,--shorthand=-}"[shorthand flag]::"`, snippetFlagCompletion(root.Flag("shorthand"), nil)) - - assert.Equal(t, `"(*-a *--stringarray)"{\*-a,\*--stringarray}"[stringarray flag]: :"`, snippetFlagCompletion(root.Flag("stringarray"), nil)) - assert.Equal(t, `"*--stringslice[stringslice flag]: :"`, snippetFlagCompletion(root.Flag("stringslice"), nil)) -} - -func TestSnippetPositionalCompletion(t *testing.T) { - pos1 := snippetPositionalCompletion(1, ActionValues("a", "b", "c")) - assert.Equal(t, `"1: :_values '' a b c"`, pos1) - - pos2 := snippetPositionalCompletion(2, ActionMessage("test")) - assert.Equal(t, `"2: : _message -r 'test'"`, pos2) -} - -func TestSnippetSubcommands(t *testing.T) { - root := &cobra.Command{ - Use: "root", - Run: func(cmd *cobra.Command, args []string) {}, - } - sub1 := &cobra.Command{ - Use: "sub1", - Run: func(cmd *cobra.Command, args []string) {}, - Aliases: []string{"alias1", "alias2"}, - } - sub2 := &cobra.Command{ - Use: "sub2", - Run: func(cmd *cobra.Command, args []string) {}, - Short: "short description", - } - hidden := &cobra.Command{ - Use: "hidden", - Run: func(cmd *cobra.Command, args []string) {}, - Hidden: true, - } - root.AddCommand(sub1) - root.AddCommand(sub2) - root.AddCommand(hidden) - - expected := ` - - # shellcheck disable=SC2154 - case $state in - cmnds) - # shellcheck disable=SC2034 - commands=( - "sub1:" - "alias1:" - "alias2:" - "sub2:short description" - ) - _describe "command" commands - ;; - esac - - case "${words[1]}" in - sub1) - _root__sub1 - ;; - alias1) - _root__sub1 - ;; - alias2) - _root__sub1 - ;; - sub2) - _root__sub2 - ;; - esac` - assert.Equal(t, expected, snippetSubcommands(root)) -}