Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package console

import (
"github.com/spf13/cobra"

"github.com/reeflective/console/internal/command"
)

const (
// CommandFilterKey should be used as a key to in a cobra.Annotation map.
// The value will be used as a filter to disable commands when the console
// calls the Filter("name") method on the console.
// The string value will be comma-splitted, with each split being a filter.
CommandFilterKey = "console-hidden"
CommandFilterKey = command.FilterKey
)

// Commands is a simple function a root cobra command containing an arbitrary tree
Expand Down
19 changes: 14 additions & 5 deletions completer.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
completer "github.com/carapace-sh/carapace/pkg/x"
"github.com/reeflective/readline"

"github.com/reeflective/console/internal/command"
"github.com/reeflective/console/internal/completion"
"github.com/reeflective/console/internal/line"
)
Expand All @@ -18,10 +19,12 @@ func (c *Console) complete(input []rune, pos int) readline.Completions {
// Ensure the carapace library is called so that the function
// completer.Complete() variable is correctly initialized before use.
carapace.Gen(menu.Command)
command.HideCarapace(menu.Command)

// Split the line as shell words, only using
// what the right buffer (up to the cursor)
args, prefixComp, prefixLine := completion.SplitArgs(input, pos)
command.ResetCompletionFlagState(menu.Command, args)

// Prepare arguments for the carapace completer
// (we currently need those two dummies for avoiding a panic).
Expand All @@ -32,10 +35,14 @@ func (c *Console) complete(input []rune, pos int) readline.Completions {

// The completions are never nil: fill out our own object
// with everything it contains, regardless of errors.
raw := make([]readline.Completion, len(completions.Values))
raw := make([]readline.Completion, 0, len(completions.Values))

for idx, val := range completions.Values {
raw[idx] = readline.Completion{
for _, val := range completions.Values {
if strings.TrimSpace(val.Value) == "_carapace" {
continue
}

comp := readline.Completion{
Value: line.UnescapeValue(prefixComp, prefixLine, val.Value),
Display: val.Display,
Description: val.Description,
Expand All @@ -44,15 +51,17 @@ func (c *Console) complete(input []rune, pos int) readline.Completions {
}

if !completions.Nospace.Matches(val.Value) {
raw[idx].Value = val.Value + " "
comp.Value = val.Value + " "
}

// Remove short/long flags grouping
// join to single tag group for classic zsh side-by-side view
switch val.Tag {
case "shorthand flags", "longhand flags":
raw[idx].Tag = "flags"
comp.Tag = "flags"
}

raw = append(raw, comp)
}

// Assign both completions and command/flags/args usage strings.
Expand Down
88 changes: 88 additions & 0 deletions completer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package console

import (
"strings"
"testing"

"github.com/reeflective/readline"
"github.com/spf13/cobra"
)

func TestCompleteHidesCarapaceCommand(t *testing.T) {
c := New("test")
root := &cobra.Command{Use: "root"}
internal := &cobra.Command{Use: "_carapace"}
root.AddCommand(internal, &cobra.Command{Use: "visible"})
c.activeMenu().Command = root

comps := c.complete(nil, 0)

if !internal.Hidden {
t.Fatal("_carapace command was not hidden")
}

for _, value := range completionValues(comps) {
if strings.TrimSpace(value) == "_carapace" {
t.Fatalf("completion values include internal command: %v", completionValues(comps))
}
}
}

func TestCompleteResetsFlagDefaults(t *testing.T) {
c := New("test")
root := &cobra.Command{Use: "root"}
cmd := &cobra.Command{Use: "serve"}
cmd.Flags().Bool("verbose", false, "")
root.AddCommand(cmd)
c.activeMenu().Command = root

if err := cmd.Flags().Set("verbose", "true"); err != nil {
t.Fatal(err)
}

_ = c.complete([]rune("serve "), len("serve "))

flag := cmd.Flags().Lookup("verbose")
if flag == nil {
t.Fatal("missing verbose flag")
}
if flag.Changed {
t.Fatal("completion did not clear flag Changed state")
}
if flag.Value.String() != "false" {
t.Fatalf("flag value = %q, want false", flag.Value.String())
}
}

func TestCompleteResetsArgsLenAtDash(t *testing.T) {
c := New("test")
root := &cobra.Command{Use: "root"}
cmd := &cobra.Command{Use: "serve"}
cmd.Flags().Bool("verbose", false, "")
root.AddCommand(cmd)
c.activeMenu().Command = root

if err := cmd.Flags().Parse([]string{"--", "positional"}); err != nil {
t.Fatal(err)
}
if got := cmd.Flags().ArgsLenAtDash(); got < 0 {
t.Fatalf("test setup did not set ArgsLenAtDash: %d", got)
}

_ = c.complete([]rune("serve "), len("serve "))

if got := cmd.Flags().ArgsLenAtDash(); got != -1 {
t.Fatalf("ArgsLenAtDash = %d, want -1", got)
}
}

func completionValues(comps readline.Completions) []string {
var values []string

comps.EachValue(func(comp readline.Completion) readline.Completion {
values = append(values, comp.Value)
return comp
})

return values
}
198 changes: 198 additions & 0 deletions internal/command/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Package command provides pure utilities for manipulating cobra command
// trees: matching commands against console filters, hiding filtered or internal
// commands, resetting reused flag state, and locating the command targeted by a
// line of input. None of these functions depend on console state, so they can be
// tested in isolation; the root console package wraps them in its own methods.
package command

import (
"encoding/csv"
"strings"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

// FilterKey is the cobra annotation key whose comma-separated value marks a
// command with the filters that hide it. The console re-exports this as
// CommandFilterKey for application use.
const FilterKey = "console-hidden"

// ActiveFilters returns the console filters that cmd (or its nearest annotated
// ancestor) declares itself incompatible with. A non-empty result means the
// command is currently hidden/unavailable under the given console filters.
func ActiveFilters(cmd *cobra.Command, consoleFilters []string) []string {
if cmd.Annotations == nil {
if cmd.HasParent() {
return ActiveFilters(cmd.Parent(), consoleFilters)
}

return nil
}

// Get the filters declared on the command.
filterStr := cmd.Annotations[FilterKey]
var filters []string

for _, cmdFilter := range strings.Split(filterStr, ",") {
for _, filter := range consoleFilters {
if cmdFilter != "" && cmdFilter == filter {
filters = append(filters, cmdFilter)
}
}
}

if len(filters) > 0 || !cmd.HasParent() {
return filters
}

// Any parent that is hidden makes its whole subtree hidden also.
return ActiveFilters(cmd.Parent(), consoleFilters)
}

// HideFiltered hides every subcommand of root that matches an active console
// filter, so it is not shown in help strings or offered as a completion.
// Commands already hidden are left untouched.
func HideFiltered(root *cobra.Command, consoleFilters []string) {
for _, cmd := range root.Commands() {
// Don't override commands if they are already hidden.
if cmd.Hidden {
continue
}

if filters := ActiveFilters(cmd, consoleFilters); len(filters) > 0 {
cmd.Hidden = true
}
}
}

// HideCarapace recursively hides carapace's internal _carapace completion
// command so it is never offered as a normal user command.
func HideCarapace(root *cobra.Command) {
if root == nil {
return
}

for _, cmd := range root.Commands() {
if cmd.Name() == "_carapace" {
cmd.Hidden = true
continue
}

HideCarapace(cmd)
}
}

// ResetFlagsDefaults resets every flag on target back to its registered default
// value and clears its Changed state. Console reuses cobra command trees across
// completions and executions; when the application supplies a command tree
// directly (no generator), flag state parsed by an earlier run would otherwise
// leak into later ones.
func ResetFlagsDefaults(target *cobra.Command) {
if target == nil {
return
}

target.Flags().VisitAll(func(flag *pflag.Flag) {
flag.Changed = false

switch value := flag.Value.(type) {
case pflag.SliceValue:
_ = value.Replace(parseSliceDefault(flag.DefValue))
default:
_ = flag.Value.Set(flag.DefValue)
}
})
}

// parseSliceDefault turns a pflag slice flag's DefValue string representation
// (e.g. "[a,b]") back into the individual default elements.
func parseSliceDefault(defValue string) []string {
if defValue == "" || defValue == "[]" {
return nil
}
if strings.HasPrefix(defValue, "[") && strings.HasSuffix(defValue, "]") {
defValue = defValue[1 : len(defValue)-1]
}
if defValue == "" {
return nil
}

values, err := csv.NewReader(strings.NewReader(defValue)).Read()
if err != nil {
return []string{defValue}
}

return values
}

// ResetCompletionFlagState clears flag state left over from a previous
// completion or execution on a reused command tree, before carapace parses the
// current input. It restores the target command's flag defaults (shared with
// the execution path) and resets ArgsLenAtDash along the command's lineage.
func ResetCompletionFlagState(root *cobra.Command, args []string) {
if root == nil {
return
}

target := findCompletionTarget(root, args)

// Force cobra to merge persistent/inherited flags into the full flag set
// so ResetFlagsDefaults sees them all.
_ = target.LocalFlags()

ResetFlagsDefaults(target)
resetArgsLenAtDash(target)
}

// resetArgsLenAtDash clears the "-- seen at index" bookkeeping on the target
// command and every parent, which a previous parse may have left set.
func resetArgsLenAtDash(target *cobra.Command) {
for cmd := target; cmd != nil; cmd = cmd.Parent() {
resetFlagSetArgsLenAtDash(cmd.Flags(), cmd.DisplayName())
resetFlagSetArgsLenAtDash(cmd.PersistentFlags(), cmd.DisplayName())
}
}

func resetFlagSetArgsLenAtDash(fs *pflag.FlagSet, name string) {
if fs == nil {
return
}

// FlagSet.Init resets argsLenAtDash to -1 without discarding registered
// flags; it is the only exported way to clear that internal state.
fs.Init(name, pflag.ContinueOnError)
}

// findCompletionTarget walks the command tree following the positional words in
// args, stopping at the first flag or "--", to locate the command being completed.
func findCompletionTarget(root *cobra.Command, args []string) *cobra.Command {
cmd := root
for _, arg := range args {
if arg == "--" || strings.HasPrefix(arg, "-") {
break
}

next := findSubcommand(cmd, arg)
if next == nil {
break
}
cmd = next
}

return cmd
}

func findSubcommand(cmd *cobra.Command, name string) *cobra.Command {
if cmd == nil {
return nil
}

for _, sub := range cmd.Commands() {
if sub.Name() == name || sub.HasAlias(name) {
return sub
}
}

return nil
}
Loading
Loading