diff --git a/cli/command.go b/cli/command.go index 96773833..07e1c5c0 100644 --- a/cli/command.go +++ b/cli/command.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "os" + "path/filepath" "sort" "strings" "unicode" @@ -91,6 +92,15 @@ type Command interface { // command, and returns them. This is most useful for testing where callers // want to simulate inputs or assert certain command outputs. Pipe() (stdin, stdout, stderr *bytes.Buffer) + + // WorkingDir returns the absolute path of current working directory from + // where the command was started. All symlinks are resolved to their real + // paths. + WorkingDir() (string, error) + + // ExecutablePath returns the absolute path of the CLI executable binary. All + // symlinks are resolved to their real values. + ExecutablePath() (string, error) } // ArgPredictor is an optional interface that [Command] can implement to declare @@ -367,6 +377,42 @@ func (c *BaseCommand) Pipe() (stdin, stdout, stderr *bytes.Buffer) { return } +// WorkingDir returns the working directory. See [Command] for more information. +func (c *BaseCommand) WorkingDir() (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get current working directory: %w", err) + } + + symCwd, err := filepath.EvalSymlinks(cwd) + if err != nil { + return "", fmt.Errorf("failed to resolve symlinks for current working directory: %w", err) + } + + abs, err := filepath.Abs(symCwd) + if err != nil { + return "", fmt.Errorf("failed to compute absolute path for current working directory: %w", err) + } + + return abs, nil +} + +// ExecutablePath returns the executable's path. See [Command] for more +// information. +func (c *BaseCommand) ExecutablePath() (string, error) { + pth, err := os.Executable() + if err != nil { + return "", fmt.Errorf("failed to get executable path: %w", err) + } + + sym, err := filepath.EvalSymlinks(pth) + if err != nil { + return "", fmt.Errorf("failed to evaluate executable path symlink: %w", err) + } + + return sym, nil +} + // buildCompleteCommands maps a [Command] to its flag and argument completion. If // the given command is a [RootCommand], it recursively builds the entire // complete tree. diff --git a/cli/command_test.go b/cli/command_test.go index 97d2d5d7..5d1a975e 100644 --- a/cli/command_test.go +++ b/cli/command_test.go @@ -388,6 +388,32 @@ func TestBaseCommand_Prompt_Cancels(t *testing.T) { } } +func TestBaseCommand_WorkingDir(t *testing.T) { + t.Parallel() + + var cmd RootCommand + dir, err := cmd.WorkingDir() + if err != nil { + t.Fatal(err) + } + if dir == "" { + t.Errorf("expected working dir to be defined") + } +} + +func TestBaseCommand_ExecutablePath(t *testing.T) { + t.Parallel() + + var cmd RootCommand + pth, err := cmd.ExecutablePath() + if err != nil { + t.Fatal(err) + } + if pth == "" { + t.Errorf("expected executable path to be defined") + } +} + type TestCommand struct { BaseCommand