Repository: rsteube/carapace
Pull request #437 (2022-03-06 22:23:54)

added PreInvoke


Patch:
diff --git a/action.go b/action.go
index 9c6880e..2edf376 100644
--- a/action.go
+++ b/action.go
@@ -1,11 +1,8 @@
 package carapace
 
 import (
-	"fmt"
-	"os"
 	"regexp"
 	"runtime"
-	"strings"
 	"time"
 
 	"github.com/rsteube/carapace/internal/cache"
@@ -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
@@ -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)
@@ -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()
 	})
 }
 
diff --git a/carapace.go b/carapace.go
index 252aebb..548f204 100644
--- a/carapace.go
+++ b/carapace.go
@@ -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
@@ -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())
@@ -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 {
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index 2ecdd97..0000000
--- a/codecov.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-ignore:
-  - "example/main.go"
-  - "example/cmd/action"
-  - "example/cmd/action.go"
-  - "example/cmd/batch.go"
-  - "example/cmd/callback.go"
-  - "example/cmd/condition.go"
-  - "example/cmd/injection.go"
-  - "example/cmd/multiparts.go"
-  - "example/cmd/root.go"
diff --git a/context.go b/context.go
new file mode 100644
index 0000000..509602a
--- /dev/null
+++ b/context.go
@@ -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
+}
diff --git a/defaultActions.go b/defaultActions.go
index c993d74..ebf17ec 100644
--- a/defaultActions.go
+++ b/defaultActions.go
@@ -14,7 +14,7 @@ import (
 	"github.com/rsteube/carapace/internal/config"
 	"github.com/rsteube/carapace/internal/shell/export"
 	"github.com/rsteube/carapace/pkg/style"
-	exec "github.com/rsteube/carapace/third_party/golang.org/x/sys/execabs"
+	"github.com/rsteube/carapace/third_party/github.com/acarl005/stripansi"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 )
@@ -33,13 +33,12 @@ func ActionExecCommand(name string, arg ...string) func(f func(output []byte) Ac
 	return func(f func(output []byte) Action) Action {
 		return ActionCallback(func(c Context) Action {
 			var stdout, stderr bytes.Buffer
-			cmd := exec.Command(name, arg...)
-			cmd.Env = c.Env
+			cmd := c.Command(name, arg...)
 			cmd.Stdout = &stdout
 			cmd.Stderr = &stderr
 			if err := cmd.Run(); err != nil {
 				if firstLine := strings.SplitN(stderr.String(), "\n", 2)[0]; strings.TrimSpace(firstLine) != "" {
-					return ActionMessage(stripAnsi(firstLine))
+					return ActionMessage(stripansi.Strip(firstLine))
 				}
 				return ActionMessage(err.Error())
 			}
@@ -77,11 +76,20 @@ func ActionImport(output []byte) Action {
 // TODO example
 func ActionExecute(cmd *cobra.Command) Action {
 	return ActionCallback(func(c Context) Action {
-		args := []string{"_carapace", "export", ""}
+		args := []string{"_carapace", "export", cmd.Name()}
 		args = append(args, c.Args...)
 		args = append(args, c.CallbackValue)
 		cmd.SetArgs(args)
 
+		Gen(cmd).PreInvoke(func(cmd *cobra.Command, flag *pflag.Flag, action Action) Action {
+			return ActionCallback(func(_c Context) Action {
+				// TODO verify
+				_c.Env = c.Env
+				_c.Dir = c.Dir
+				return action.Invoke(_c).ToA()
+			})
+		})
+
 		var stdout, stderr bytes.Buffer
 		cmd.SetOut(&stdout)
 		cmd.SetErr(&stderr)
@@ -93,51 +101,51 @@ func ActionExecute(cmd *cobra.Command) Action {
 	})
 }
 
-// strip ANSI color escape codes from string (source: https://github.com/acarl005/stripansi)
-func stripAnsi(str string) string {
-	re := regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
-	return re.ReplaceAllString(str, "")
-}
-
 // ActionDirectories completes directories
 func ActionDirectories() Action {
 	return ActionCallback(func(c Context) Action {
-		return actionPath([]string{""}, true).Invoke(c).ToMultiPartsA("/").noSpace(true).StyleF(style.ForPath)
+		return actionPath([]string{""}, true).Invoke(c).ToMultiPartsA("/").noSpace(true).StyleF(func(s string) string {
+			if abs, err := c.Abs(s); err == nil {
+				return style.ForPath(abs)
+			}
+			return ""
+		})
 	})
 }
 
 // ActionFiles completes files with optional suffix filtering
 func ActionFiles(suffix ...string) Action {
 	return ActionCallback(func(c Context) Action {
-		return actionPath(suffix, false).Invoke(c).ToMultiPartsA("/").noSpace(true).StyleF(style.ForPath)
+		return actionPath(suffix, false).Invoke(c).ToMultiPartsA("/").noSpace(true).StyleF(func(s string) string {
+			if abs, err := c.Abs(s); err == nil {
+				return style.ForPath(abs)
+			}
+			return ""
+		})
 	})
 }
 
 func actionPath(fileSuffixes []string, dirOnly bool) Action {
 	return ActionCallback(func(c Context) Action {
-		folder := filepath.Dir(c.CallbackValue)
-		expandedFolder := folder
-		if strings.HasPrefix(c.CallbackValue, "~") {
-			homedir, err := os.UserHomeDir()
-			if err != nil {
-				return ActionMessage(err.Error())
-			}
-			expandedFolder = filepath.Dir(homedir + "/" + c.CallbackValue[1:])
+		abs, err := c.Abs(c.CallbackValue)
+		if err != nil {
+			return ActionMessage(err.Error())
+		}
+
+		displayFolder := filepath.Dir(c.CallbackValue)
+		if displayFolder == "." {
+			displayFolder = ""
+		} else if !strings.HasSuffix(displayFolder, "/") {
+			displayFolder = displayFolder + "/"
 		}
 
-		files, err := ioutil.ReadDir(expandedFolder)
+		actualFolder := filepath.Dir(abs)
+		files, err := ioutil.ReadDir(actualFolder)
 		if err != nil {
 			return ActionMessage(err.Error())
 		}
-		if folder == "." {
-			folder = ""
-		} else if !strings.HasSuffix(folder, "/") {
-			folder = folder + "/"
-		}
 
-		showHidden := c.CallbackValue != "" &&
-			!strings.HasSuffix(c.CallbackValue, "/") &&
-			strings.HasPrefix(filepath.Base(c.CallbackValue), ".")
+		showHidden := !strings.HasSuffix(abs, "/") && strings.HasPrefix(filepath.Base(abs), ".")
 
 		vals := make([]string, 0, len(files)*2)
 		for _, file := range files {
@@ -146,21 +154,21 @@ func actionPath(fileSuffixes []string, dirOnly bool) Action {
 			}
 
 			resolvedFile := file
-			if resolved, err := filepath.EvalSymlinks(folder + file.Name()); err == nil {
+			if resolved, err := filepath.EvalSymlinks(actualFolder + file.Name()); err == nil {
 				if stat, err := os.Stat(resolved); err == nil {
 					resolvedFile = stat
 				}
 			}
 
 			if resolvedFile.IsDir() {
-				vals = append(vals, folder+file.Name()+"/", style.ForPath(folder+file.Name()+"/"))
+				vals = append(vals, displayFolder+file.Name()+"/", style.ForPath(filepath.Clean(actualFolder+"/"+file.Name()+"/")))
 			} else if !dirOnly {
 				if len(fileSuffixes) == 0 {
 					fileSuffixes = []string{""}
 				}
 				for _, suffix := range fileSuffixes {
 					if strings.HasSuffix(file.Name(), suffix) {
-						vals = append(vals, folder+file.Name(), style.ForPath(folder+file.Name()))
+						vals = append(vals, displayFolder+file.Name(), style.ForPath(filepath.Clean(actualFolder+"/"+file.Name())))
 						break
 					}
 				}
diff --git a/storage.go b/storage.go
index da9650d..476f7a6 100644
--- a/storage.go
+++ b/storage.go
@@ -6,6 +6,7 @@ import (
 	"github.com/rsteube/carapace/internal/common"
 	"github.com/rsteube/carapace/internal/uid"
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 )
 
 // TODO storage needs better naming and structure
@@ -16,6 +17,7 @@ type entry struct {
 	positionalAny Action
 	dash          []Action
 	dashAny       Action
+	preinvoke     func(cmd *cobra.Command, flag *pflag.Flag, action Action) Action
 }
 
 type _storage map[*cobra.Command]*entry
@@ -32,24 +34,41 @@ func (s _storage) get(cmd *cobra.Command) (e *entry) {
 func (s _storage) getFlag(cmd *cobra.Command, name string) Action {
 	if flag := cmd.LocalFlags().Lookup(name); flag == nil && cmd.HasParent() {
 		return s.getFlag(cmd.Parent(), name)
+	} else {
+		return s.preinvoke(cmd, flag, s.get(cmd).flag[name])
 	}
-	// TODO nil check?
-	return s.get(cmd).flag[name]
+}
+
+func (s _storage) preinvoke(cmd *cobra.Command, flag *pflag.Flag, action Action) Action {
+	// TODO yuck - clean this up
+	entry := s.get(cmd)
+	a := action
+	if entry.preinvoke != nil {
+		a = ActionCallback(func(c Context) Action {
+			return entry.preinvoke(cmd, flag, action)
+		})
+	}
+	if cmd.HasParent() {
+		// TODO from cmd passed to preinvoke function
+		return s.preinvoke(cmd.Parent(), flag, a)
+	}
+	return a
 }
 
 func (s _storage) getPositional(cmd *cobra.Command, pos int) Action {
 	entry := s.get(cmd)
+
 	// TODO nil check?
 	if !common.IsDash(cmd) {
 		if len(entry.positional) > pos {
-			return entry.positional[pos]
+			return s.preinvoke(cmd, nil, entry.positional[pos])
 		}
-		return entry.positionalAny
+		return s.preinvoke(cmd, nil, entry.positionalAny)
 	} else {
 		if len(entry.dash) > pos {
-			return entry.dash[pos]
+			return s.preinvoke(cmd, nil, entry.dash[pos])
 		}
-		return entry.dashAny
+		return s.preinvoke(cmd, nil, entry.dashAny)
 	}
 }
 
diff --git a/third_party/github.com/acarl005/stripansi/LICENSE b/third_party/github.com/acarl005/stripansi/LICENSE
new file mode 100644
index 0000000..00abe0d
--- /dev/null
+++ b/third_party/github.com/acarl005/stripansi/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Andrew Carlson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third_party/github.com/acarl005/stripansi/stripansi.go b/third_party/github.com/acarl005/stripansi/stripansi.go
new file mode 100644
index 0000000..235732a
--- /dev/null
+++ b/third_party/github.com/acarl005/stripansi/stripansi.go
@@ -0,0 +1,13 @@
+package stripansi
+
+import (
+	"regexp"
+)
+
+const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
+
+var re = regexp.MustCompile(ansi)
+
+func Strip(str string) string {
+	return re.ReplaceAllString(str, "")
+}


Test patch:
diff --git a/action_test.go b/action_test.go
index f1fa3a8..02611c1 100644
--- a/action_test.go
+++ b/action_test.go
@@ -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)
 }
@@ -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: ""}),
 	)
 
diff --git a/carapace_test.go b/carapace_test.go
index ade01ff..ce03b6a 100644
--- a/carapace_test.go
+++ b/carapace_test.go
@@ -83,6 +83,7 @@ func TestContext(t *testing.T) {
 		Args:          []string{},
 		Parts:         []string{},
 		Env:           []string{},
+		Dir:           wd(""),
 	},
 		"")
 
@@ -91,6 +92,7 @@ func TestContext(t *testing.T) {
 		Args:          []string{"pos1"},
 		Parts:         []string{},
 		Env:           []string{},
+		Dir:           wd(""),
 	},
 		"pos1", "")
 
@@ -99,6 +101,7 @@ func TestContext(t *testing.T) {
 		Args:          []string{"pos1", "pos2"},
 		Parts:         []string{},
 		Env:           []string{},
+		Dir:           wd(""),
 	},
 		"pos1", "pos2", "po")
 
@@ -107,6 +110,7 @@ func TestContext(t *testing.T) {
 		Args:          []string{},
 		Parts:         []string{},
 		Env:           []string{},
+		Dir:           wd(""),
 	},
 		"--multiparts", "")
 
@@ -115,6 +119,7 @@ func TestContext(t *testing.T) {
 		Args:          []string{},
 		Parts:         []string{},
 		Env:           []string{},
+		Dir:           wd(""),
 	},
 		"--multiparts", "fir")
 
@@ -123,6 +128,7 @@ func TestContext(t *testing.T) {
 		Args:          []string{"pos1"},
 		Parts:         []string{"first"},
 		Env:           []string{},
+		Dir:           wd(""),
 	},
 		"pos1", "--multiparts", "first,seco")
 
@@ -131,6 +137,7 @@ func TestContext(t *testing.T) {
 		Args:          []string{},
 		Parts:         []string{},
 		Env:           []string{},
+		Dir:           wd(""),
 	},
 		"pos")
 
@@ -139,6 +146,7 @@ func TestContext(t *testing.T) {
 		Args:          []string{},
 		Parts:         []string{"first"},
 		Env:           []string{},
+		Dir:           wd(""),
 	},
 		"first:sec")
 
@@ -147,6 +155,7 @@ func TestContext(t *testing.T) {
 		Args:          []string{"first:second"},
 		Parts:         []string{},
 		Env:           []string{},
+		Dir:           wd(""),
 	},
 		"first:second", "thi")
 }
diff --git a/context_test.go b/context_test.go
new file mode 100644
index 0000000..7e4a5ba
--- /dev/null
+++ b/context_test.go
@@ -0,0 +1,63 @@
+package carapace
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func wd(s string) string {
+	if wd, _ := os.Getwd(); s != "" {
+		return wd + "/" + s
+	} else {
+		return wd
+	}
+}
+
+func home(s string) string {
+	if hd, _ := os.UserHomeDir(); s != "" {
+		return hd + "/" + s
+	} else {
+		return hd
+	}
+}
+
+func parent(s string) string {
+	if s != "" {
+		return filepath.Dir(wd("")) + "/" + s
+	}
+	return filepath.Dir(wd("")) + "/"
+}
+
+func TestContextAbs(t *testing.T) {
+	tests := append([]string{},
+		"/", "file", "/file",
+		"", "file", wd("file"),
+		"", "../", parent(""),
+		"", "../file", parent("file"),
+		"", "~/file", home("file"),
+		"/", "~/file", home("file"),
+		"/", "file", "/file",
+		"/dir", "file", "/dir/file",
+		"/dir", "./.file", "/dir/.file",
+		"", "/dir/", "/dir/",
+		"/dir/", "", "/dir/",
+		"~", "file", home("file"),
+		"", "/", "/",
+		"", ".hidden", wd(".hidden"),
+		"", "./", wd("")+"/",
+		"", "", wd("")+"/",
+		"", ".", wd("")+"/"+".",
+	)
+
+	for index := 0; index < len(tests); index += 3 {
+		actual, err := Context{Dir: tests[index]}.Abs(tests[index+1])
+		if err != nil {
+			t.Error(err.Error())
+		}
+		if expected := tests[index+2]; expected != actual {
+			t.Errorf("context: '%v' arg: '%v' expected: '%v' was: '%v'", tests[index], tests[index+1], expected, actual)
+		}
+	}
+
+}