Skip to content

Commit f44dc1d

Browse files
authored
Merge pull request #87 from reeflective/dev
Completion & execution fixes + internal/command refactor
2 parents 0199308 + 0e1ae89 commit f44dc1d

9 files changed

Lines changed: 600 additions & 45 deletions

File tree

command.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ package console
22

33
import (
44
"github.com/spf13/cobra"
5+
6+
"github.com/reeflective/console/internal/command"
57
)
68

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

1517
// Commands is a simple function a root cobra command containing an arbitrary tree

completer.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
completer "github.com/carapace-sh/carapace/pkg/x"
99
"github.com/reeflective/readline"
1010

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

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

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

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

37-
for idx, val := range completions.Values {
38-
raw[idx] = readline.Completion{
40+
for _, val := range completions.Values {
41+
if strings.TrimSpace(val.Value) == "_carapace" {
42+
continue
43+
}
44+
45+
comp := readline.Completion{
3946
Value: line.UnescapeValue(prefixComp, prefixLine, val.Value),
4047
Display: val.Display,
4148
Description: val.Description,
@@ -44,15 +51,17 @@ func (c *Console) complete(input []rune, pos int) readline.Completions {
4451
}
4552

4653
if !completions.Nospace.Matches(val.Value) {
47-
raw[idx].Value = val.Value + " "
54+
comp.Value = val.Value + " "
4855
}
4956

5057
// Remove short/long flags grouping
5158
// join to single tag group for classic zsh side-by-side view
5259
switch val.Tag {
5360
case "shorthand flags", "longhand flags":
54-
raw[idx].Tag = "flags"
61+
comp.Tag = "flags"
5562
}
63+
64+
raw = append(raw, comp)
5665
}
5766

5867
// Assign both completions and command/flags/args usage strings.

completer_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package console
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/reeflective/readline"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
func TestCompleteHidesCarapaceCommand(t *testing.T) {
12+
c := New("test")
13+
root := &cobra.Command{Use: "root"}
14+
internal := &cobra.Command{Use: "_carapace"}
15+
root.AddCommand(internal, &cobra.Command{Use: "visible"})
16+
c.activeMenu().Command = root
17+
18+
comps := c.complete(nil, 0)
19+
20+
if !internal.Hidden {
21+
t.Fatal("_carapace command was not hidden")
22+
}
23+
24+
for _, value := range completionValues(comps) {
25+
if strings.TrimSpace(value) == "_carapace" {
26+
t.Fatalf("completion values include internal command: %v", completionValues(comps))
27+
}
28+
}
29+
}
30+
31+
func TestCompleteResetsFlagDefaults(t *testing.T) {
32+
c := New("test")
33+
root := &cobra.Command{Use: "root"}
34+
cmd := &cobra.Command{Use: "serve"}
35+
cmd.Flags().Bool("verbose", false, "")
36+
root.AddCommand(cmd)
37+
c.activeMenu().Command = root
38+
39+
if err := cmd.Flags().Set("verbose", "true"); err != nil {
40+
t.Fatal(err)
41+
}
42+
43+
_ = c.complete([]rune("serve "), len("serve "))
44+
45+
flag := cmd.Flags().Lookup("verbose")
46+
if flag == nil {
47+
t.Fatal("missing verbose flag")
48+
}
49+
if flag.Changed {
50+
t.Fatal("completion did not clear flag Changed state")
51+
}
52+
if flag.Value.String() != "false" {
53+
t.Fatalf("flag value = %q, want false", flag.Value.String())
54+
}
55+
}
56+
57+
func TestCompleteResetsArgsLenAtDash(t *testing.T) {
58+
c := New("test")
59+
root := &cobra.Command{Use: "root"}
60+
cmd := &cobra.Command{Use: "serve"}
61+
cmd.Flags().Bool("verbose", false, "")
62+
root.AddCommand(cmd)
63+
c.activeMenu().Command = root
64+
65+
if err := cmd.Flags().Parse([]string{"--", "positional"}); err != nil {
66+
t.Fatal(err)
67+
}
68+
if got := cmd.Flags().ArgsLenAtDash(); got < 0 {
69+
t.Fatalf("test setup did not set ArgsLenAtDash: %d", got)
70+
}
71+
72+
_ = c.complete([]rune("serve "), len("serve "))
73+
74+
if got := cmd.Flags().ArgsLenAtDash(); got != -1 {
75+
t.Fatalf("ArgsLenAtDash = %d, want -1", got)
76+
}
77+
}
78+
79+
func completionValues(comps readline.Completions) []string {
80+
var values []string
81+
82+
comps.EachValue(func(comp readline.Completion) readline.Completion {
83+
values = append(values, comp.Value)
84+
return comp
85+
})
86+
87+
return values
88+
}

internal/command/command.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Package command provides pure utilities for manipulating cobra command
2+
// trees: matching commands against console filters, hiding filtered or internal
3+
// commands, resetting reused flag state, and locating the command targeted by a
4+
// line of input. None of these functions depend on console state, so they can be
5+
// tested in isolation; the root console package wraps them in its own methods.
6+
package command
7+
8+
import (
9+
"encoding/csv"
10+
"strings"
11+
12+
"github.com/spf13/cobra"
13+
"github.com/spf13/pflag"
14+
)
15+
16+
// FilterKey is the cobra annotation key whose comma-separated value marks a
17+
// command with the filters that hide it. The console re-exports this as
18+
// CommandFilterKey for application use.
19+
const FilterKey = "console-hidden"
20+
21+
// ActiveFilters returns the console filters that cmd (or its nearest annotated
22+
// ancestor) declares itself incompatible with. A non-empty result means the
23+
// command is currently hidden/unavailable under the given console filters.
24+
func ActiveFilters(cmd *cobra.Command, consoleFilters []string) []string {
25+
if cmd.Annotations == nil {
26+
if cmd.HasParent() {
27+
return ActiveFilters(cmd.Parent(), consoleFilters)
28+
}
29+
30+
return nil
31+
}
32+
33+
// Get the filters declared on the command.
34+
filterStr := cmd.Annotations[FilterKey]
35+
var filters []string
36+
37+
for _, cmdFilter := range strings.Split(filterStr, ",") {
38+
for _, filter := range consoleFilters {
39+
if cmdFilter != "" && cmdFilter == filter {
40+
filters = append(filters, cmdFilter)
41+
}
42+
}
43+
}
44+
45+
if len(filters) > 0 || !cmd.HasParent() {
46+
return filters
47+
}
48+
49+
// Any parent that is hidden makes its whole subtree hidden also.
50+
return ActiveFilters(cmd.Parent(), consoleFilters)
51+
}
52+
53+
// HideFiltered hides every subcommand of root that matches an active console
54+
// filter, so it is not shown in help strings or offered as a completion.
55+
// Commands already hidden are left untouched.
56+
func HideFiltered(root *cobra.Command, consoleFilters []string) {
57+
for _, cmd := range root.Commands() {
58+
// Don't override commands if they are already hidden.
59+
if cmd.Hidden {
60+
continue
61+
}
62+
63+
if filters := ActiveFilters(cmd, consoleFilters); len(filters) > 0 {
64+
cmd.Hidden = true
65+
}
66+
}
67+
}
68+
69+
// HideCarapace recursively hides carapace's internal _carapace completion
70+
// command so it is never offered as a normal user command.
71+
func HideCarapace(root *cobra.Command) {
72+
if root == nil {
73+
return
74+
}
75+
76+
for _, cmd := range root.Commands() {
77+
if cmd.Name() == "_carapace" {
78+
cmd.Hidden = true
79+
continue
80+
}
81+
82+
HideCarapace(cmd)
83+
}
84+
}
85+
86+
// ResetFlagsDefaults resets every flag on target back to its registered default
87+
// value and clears its Changed state. Console reuses cobra command trees across
88+
// completions and executions; when the application supplies a command tree
89+
// directly (no generator), flag state parsed by an earlier run would otherwise
90+
// leak into later ones.
91+
func ResetFlagsDefaults(target *cobra.Command) {
92+
if target == nil {
93+
return
94+
}
95+
96+
target.Flags().VisitAll(func(flag *pflag.Flag) {
97+
flag.Changed = false
98+
99+
switch value := flag.Value.(type) {
100+
case pflag.SliceValue:
101+
_ = value.Replace(parseSliceDefault(flag.DefValue))
102+
default:
103+
_ = flag.Value.Set(flag.DefValue)
104+
}
105+
})
106+
}
107+
108+
// parseSliceDefault turns a pflag slice flag's DefValue string representation
109+
// (e.g. "[a,b]") back into the individual default elements.
110+
func parseSliceDefault(defValue string) []string {
111+
if defValue == "" || defValue == "[]" {
112+
return nil
113+
}
114+
if strings.HasPrefix(defValue, "[") && strings.HasSuffix(defValue, "]") {
115+
defValue = defValue[1 : len(defValue)-1]
116+
}
117+
if defValue == "" {
118+
return nil
119+
}
120+
121+
values, err := csv.NewReader(strings.NewReader(defValue)).Read()
122+
if err != nil {
123+
return []string{defValue}
124+
}
125+
126+
return values
127+
}
128+
129+
// ResetCompletionFlagState clears flag state left over from a previous
130+
// completion or execution on a reused command tree, before carapace parses the
131+
// current input. It restores the target command's flag defaults (shared with
132+
// the execution path) and resets ArgsLenAtDash along the command's lineage.
133+
func ResetCompletionFlagState(root *cobra.Command, args []string) {
134+
if root == nil {
135+
return
136+
}
137+
138+
target := findCompletionTarget(root, args)
139+
140+
// Force cobra to merge persistent/inherited flags into the full flag set
141+
// so ResetFlagsDefaults sees them all.
142+
_ = target.LocalFlags()
143+
144+
ResetFlagsDefaults(target)
145+
resetArgsLenAtDash(target)
146+
}
147+
148+
// resetArgsLenAtDash clears the "-- seen at index" bookkeeping on the target
149+
// command and every parent, which a previous parse may have left set.
150+
func resetArgsLenAtDash(target *cobra.Command) {
151+
for cmd := target; cmd != nil; cmd = cmd.Parent() {
152+
resetFlagSetArgsLenAtDash(cmd.Flags(), cmd.DisplayName())
153+
resetFlagSetArgsLenAtDash(cmd.PersistentFlags(), cmd.DisplayName())
154+
}
155+
}
156+
157+
func resetFlagSetArgsLenAtDash(fs *pflag.FlagSet, name string) {
158+
if fs == nil {
159+
return
160+
}
161+
162+
// FlagSet.Init resets argsLenAtDash to -1 without discarding registered
163+
// flags; it is the only exported way to clear that internal state.
164+
fs.Init(name, pflag.ContinueOnError)
165+
}
166+
167+
// findCompletionTarget walks the command tree following the positional words in
168+
// args, stopping at the first flag or "--", to locate the command being completed.
169+
func findCompletionTarget(root *cobra.Command, args []string) *cobra.Command {
170+
cmd := root
171+
for _, arg := range args {
172+
if arg == "--" || strings.HasPrefix(arg, "-") {
173+
break
174+
}
175+
176+
next := findSubcommand(cmd, arg)
177+
if next == nil {
178+
break
179+
}
180+
cmd = next
181+
}
182+
183+
return cmd
184+
}
185+
186+
func findSubcommand(cmd *cobra.Command, name string) *cobra.Command {
187+
if cmd == nil {
188+
return nil
189+
}
190+
191+
for _, sub := range cmd.Commands() {
192+
if sub.Name() == name || sub.HasAlias(name) {
193+
return sub
194+
}
195+
}
196+
197+
return nil
198+
}

0 commit comments

Comments
 (0)