Skip to content

Commit

Permalink
Merge pull request #437 from rsteube/preinvoke
Browse files Browse the repository at this point in the history
added PreInvoke
  • Loading branch information
rsteube authored Apr 29, 2022
2 parents 954d3d9 + 620446e commit 4edc77a
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 110 deletions.
70 changes: 15 additions & 55 deletions action.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package carapace

import (
"fmt"
"os"
"regexp"
"runtime"
"strings"
"time"

"github.com/rsteube/carapace/internal/cache"
Expand All @@ -27,26 +24,6 @@ type ActionMap map[string]Action
// CompletionCallback is executed during completion of associated flag or positional argument
type CompletionCallback func(c Context) Action

// Context provides information during completion
type Context struct {
// CallbackValue contains the (partial) value (or part of it during an ActionMultiParts) currently being completed
CallbackValue string
// Args contains the positional arguments of current (sub)command (exclusive the one currently being completed)
Args []string
// Parts contains the splitted CallbackValue during an ActionMultiParts (exclusive the part currently being completed)
Parts []string
// Env contains environment variables for current context (implicitly passed to `exec.Cmd` during ActionExecCommand)
Env []string
}

// Setenv sets the value of the environment variable named by the key.
func (c *Context) Setenv(key, value string) {
if c.Env == nil {
c.Env = []string{}
}
c.Env = append(c.Env, fmt.Sprintf("%v=%v", key, value))
}

// Cache cashes values of a CompletionCallback for given duration and keys
func (a Action) Cache(timeout time.Duration, keys ...pkgcache.Key) Action {
// TODO static actions are using callback now as well (for performance) - probably best to add a `static` bool to Action for this and check that here
Expand Down Expand Up @@ -108,6 +85,18 @@ func (a Action) Style(style string) Action {
})
}

// Style sets the style using a reference
// ActionValues("value").StyleR(&style.Carapace.Value)
// ActionValues("description").StyleR(&style.Carapace.Value)
func (a Action) StyleR(style *string) Action {
return ActionCallback(func(c Context) Action {
if style != nil {
return a.Style(*style)
}
return a
})
}

// Style sets the style using a function
// ActionValues("dir/", "test.txt").StyleF(style.ForPathExt)
// ActionValues("true", "false").StyleF(style.ForKeyword)
Expand All @@ -124,41 +113,12 @@ func (a Action) StyleF(f func(s string) string) Action {
// Chdir changes the current working directory to the named directory during invocation.
func (a Action) Chdir(dir string) Action {
return ActionCallback(func(c Context) Action {
if dir == "" || dir == "." {
return a // do nothing on current dir
}

if strings.HasPrefix(dir, "~") {
home, err := os.UserHomeDir()
if err != nil {
return ActionMessage(err.Error())
}
dir = strings.Replace(dir, "~", home, 1)
}

file, err := os.Stat(dir)
if err != nil {
return ActionMessage(err.Error())
}
if !file.IsDir() {
return ActionMessage(fmt.Sprintf("%v is not a directory", dir))
}

current, err := os.Getwd()
abs, err := c.Abs(dir)
if err != nil {
return ActionMessage(err.Error())
}

if err := os.Chdir(dir); err != nil {
return ActionMessage(err.Error())
}

a := a.Invoke(c).ToA()

if err := os.Chdir(current); err != nil {
return ActionMessage(err.Error())
}
return a
c.Dir = abs
return a.Invoke(c).ToA()
})
}

Expand Down
8 changes: 4 additions & 4 deletions action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ func TestCache(t *testing.T) {
f := func() Action {
return ActionCallback(func(c Context) Action {
return ActionValues(time.Now().String())
}).Cache(10 * time.Millisecond)
}).Cache(15 * time.Millisecond)
}

a1 := f().Invoke(Context{})
a2 := f().Invoke(Context{})
assertEqual(t, a1, a2)

time.Sleep(12 * time.Millisecond)
time.Sleep(16 * time.Millisecond)
a3 := f().Invoke(Context{})
assertNotEqual(t, a1, a3)
}
Expand Down Expand Up @@ -173,12 +173,12 @@ func TestActionFilesChdir(t *testing.T) {
oldWd, _ := os.Getwd()

assertEqual(t,
ActionValuesDescribed("ERR", "stat nonexistent: no such file or directory", "_", "").noSpace(true).skipCache(true).Invoke(Context{}),
ActionValuesDescribed("ERR", fmt.Sprintf("open %v: no such file or directory", wd("nonexistent")), "_", "").noSpace(true).Style("fg-default bg-default").Invoke(Context{}),
ActionFiles(".md").Chdir("nonexistent").Invoke(Context{CallbackValue: ""}),
)

assertEqual(t,
ActionValuesDescribed("ERR", "go.mod is not a directory", "_", "").noSpace(true).skipCache(true).Invoke(Context{}),
ActionValuesDescribed("ERR", fmt.Sprintf("readdirent %v/go.mod: not a directory", wd("")), "_", "").noSpace(true).Style("fg-default bg-default").Invoke(Context{}),
ActionFiles(".md").Chdir("go.mod").Invoke(Context{CallbackValue: ""}),
)

Expand Down
23 changes: 21 additions & 2 deletions carapace.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ func Gen(cmd *cobra.Command) *Carapace {
}
}

// PreInvoke TODO experimental
func (c Carapace) PreInvoke(f func(cmd *cobra.Command, flag *pflag.Flag, action Action) Action) {
if entry := storage.get(c.cmd); entry.preinvoke != nil {
_f := entry.preinvoke
entry.preinvoke = func(cmd *cobra.Command, flag *pflag.Flag, action Action) Action {
return f(cmd, flag, _f(cmd, flag, action)) // TODO verify if this is correct
}
} else {
entry.preinvoke = f
}
}

// PositionalCompletion defines completion for positional arguments using a list of Actions
func (c Carapace) PositionalCompletion(action ...Action) {
storage.get(c.cmd).positional = action
Expand Down Expand Up @@ -220,8 +232,6 @@ func addCompletionCommand(cmd *cobra.Command) {
}

func complete(cmd *cobra.Command, args []string) (string, error) {
logger.Println(os.Args) // TODO replace last with '' if empty

if len(args) == 0 {
if s, err := Gen(cmd).Snippet(ps.DetermineShell()); err != nil {
fmt.Fprintln(io.MultiWriter(cmd.OutOrStderr(), logger.Writer()), err.Error())
Expand All @@ -245,10 +255,19 @@ func complete(cmd *cobra.Command, args []string) (string, error) {
}

targetCmd, targetArgs, err := findTarget(cmd, args)
if err != nil {
return ActionMessage(err.Error()).Invoke(Context{CallbackValue: current}).value(shell, current), nil
}

wd, err := os.Getwd()
if err != nil {
return ActionMessage(err.Error()).Invoke(Context{CallbackValue: current}).value(shell, current), nil
}
context := Context{
CallbackValue: current,
Args: targetArgs,
Env: os.Environ(),
Dir: wd,
}
if err != nil {
if opts.LongShorthand {
Expand Down
9 changes: 9 additions & 0 deletions carapace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func TestContext(t *testing.T) {
Args: []string{},
Parts: []string{},
Env: []string{},
Dir: wd(""),
},
"")

Expand All @@ -91,6 +92,7 @@ func TestContext(t *testing.T) {
Args: []string{"pos1"},
Parts: []string{},
Env: []string{},
Dir: wd(""),
},
"pos1", "")

Expand All @@ -99,6 +101,7 @@ func TestContext(t *testing.T) {
Args: []string{"pos1", "pos2"},
Parts: []string{},
Env: []string{},
Dir: wd(""),
},
"pos1", "pos2", "po")

Expand All @@ -107,6 +110,7 @@ func TestContext(t *testing.T) {
Args: []string{},
Parts: []string{},
Env: []string{},
Dir: wd(""),
},
"--multiparts", "")

Expand All @@ -115,6 +119,7 @@ func TestContext(t *testing.T) {
Args: []string{},
Parts: []string{},
Env: []string{},
Dir: wd(""),
},
"--multiparts", "fir")

Expand All @@ -123,6 +128,7 @@ func TestContext(t *testing.T) {
Args: []string{"pos1"},
Parts: []string{"first"},
Env: []string{},
Dir: wd(""),
},
"pos1", "--multiparts", "first,seco")

Expand All @@ -131,6 +137,7 @@ func TestContext(t *testing.T) {
Args: []string{},
Parts: []string{},
Env: []string{},
Dir: wd(""),
},
"pos")

Expand All @@ -139,6 +146,7 @@ func TestContext(t *testing.T) {
Args: []string{},
Parts: []string{"first"},
Env: []string{},
Dir: wd(""),
},
"first:sec")

Expand All @@ -147,6 +155,7 @@ func TestContext(t *testing.T) {
Args: []string{"first:second"},
Parts: []string{},
Env: []string{},
Dir: wd(""),
},
"first:second", "thi")
}
Expand Down
10 changes: 0 additions & 10 deletions codecov.yml

This file was deleted.

88 changes: 88 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package carapace

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/rsteube/carapace/third_party/golang.org/x/sys/execabs"
)

// Context provides information during completion
type Context struct {
// CallbackValue contains the (partial) value (or part of it during an ActionMultiParts) currently being completed
CallbackValue string
// Args contains the positional arguments of current (sub)command (exclusive the one currently being completed)
Args []string
// Parts contains the splitted CallbackValue during an ActionMultiParts (exclusive the part currently being completed)
Parts []string
// Env contains environment variables for current context
Env []string
// Dir contains the working directory for current context
Dir string
}

// Setenv sets the value of the environment variable named by the key.
func (c *Context) Setenv(key, value string) {
if c.Env == nil {
c.Env = []string{}
}
c.Env = append(c.Env, fmt.Sprintf("%v=%v", key, value))
}

// Command returns the Cmd struct to execute the named program with the given arguments.
// Env and Dir are set using the Context.
// See exec.Command for most details.
func (c Context) Command(name string, arg ...string) *execabs.Cmd {
cmd := execabs.Command(name, arg...)
cmd.Env = c.Env
cmd.Dir = c.Dir
return cmd
}

func expandHome(s string) (string, error) {
if strings.HasPrefix(s, "~") {
home, err := os.UserHomeDir() // TODO duplicated code
if err != nil {
return "", err
}
s = strings.Replace(s, "~", home+"/", 1)
}
return s, nil
}

func (c Context) Abs(s string) (string, error) {
var path string
if strings.HasPrefix(s, "/") || strings.HasPrefix(s, "~/") {
path = s // path is absolute
} else {
expanded, err := expandHome(c.Dir)
if err != nil {
return "", err
}
abs, err := filepath.Abs(expanded)
if err != nil {
return "", err
}
path = abs + "/" + s
}

expanded, err := expandHome(path)
if err != nil {
return "", err
}
path = expanded

result, err := filepath.Abs(path)
if err != nil {
return "", err
}

if strings.HasSuffix(path, "/") && !strings.HasSuffix(result, "/") {
result += "/"
} else if strings.HasSuffix(path, "/.") && !strings.HasSuffix(result, "/.") {
result += "/."
}
return result, nil
}
Loading

0 comments on commit 4edc77a

Please sign in to comment.