diff --git a/cmd/cmd.go b/cmd/cmd.go index 9f110609..401e3eee 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -10,6 +10,7 @@ import ( "strings" "asdf/internal/config" + "asdf/internal/exec" "asdf/internal/info" "asdf/internal/installs" "asdf/internal/plugins" @@ -44,6 +45,15 @@ func Execute(version string) { Usage: "The multiple runtime version manager", UsageText: usageText, Commands: []*cli.Command{ + { + Name: "exec", + Action: func(cCtx *cli.Context) error { + command := cCtx.Args().Get(0) + args := cCtx.Args().Slice() + + return execCommand(logger, command, args) + }, + }, { Name: "info", Action: func(_ *cli.Context) error { @@ -159,6 +169,43 @@ func Execute(version string) { } } +func execCommand(logger *log.Logger, command string, args []string) error { + if command == "" { + logger.Printf("no command specified") + return fmt.Errorf("no command specified") + } + + conf, err := config.LoadConfig() + if err != nil { + logger.Printf("error loading config: %s", err) + return err + } + + currentDir, err := os.Getwd() + if err != nil { + logger.Printf("unable to get current directory: %s", err) + return err + } + + executable, found, err := shims.FindExecutable(conf, command, currentDir) + if err != nil { + logger.Printf("executable not found due to reason: %s", err.Error()) + return err + } + + if !found { + logger.Print("executable not found") + return fmt.Errorf("executable not found") + } + if len(args) > 1 { + args = args[1:] + } else { + args = []string{} + } + + return exec.Exec(executable, args, os.Environ()) +} + func pluginAddCommand(_ *cli.Context, conf config.Config, logger *log.Logger, pluginName, pluginRepo string) error { if pluginName == "" { // Invalid arguments diff --git a/go.mod b/go.mod index 5604846b..56b60633 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-git/go-git/v5 v5.11.0 github.com/mitchellh/go-homedir v1.1.0 github.com/otiai10/copy v1.14.0 + github.com/rogpeppe/go-internal v1.11.0 github.com/sethvargo/go-envconfig v1.0.0 github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.27.1 diff --git a/internal/exec/exec.go b/internal/exec/exec.go new file mode 100644 index 00000000..68f92482 --- /dev/null +++ b/internal/exec/exec.go @@ -0,0 +1,12 @@ +// Package exec handles replacing the asdf go process with +package exec + +import ( + "syscall" +) + +// Exec invokes syscall.Exec to exec an executable. Requires an absolute path to +// executable. +func Exec(executablePath string, args []string, env []string) error { + return syscall.Exec(executablePath, append([]string{executablePath}, args...), env) +} diff --git a/internal/exec/exec_test.go b/internal/exec/exec_test.go new file mode 100644 index 00000000..31b063ea --- /dev/null +++ b/internal/exec/exec_test.go @@ -0,0 +1,34 @@ +package exec + +import ( + "fmt" + "os" + "os/exec" + "testing" + + "github.com/rogpeppe/go-internal/testscript" +) + +func execit() int { + // Exec only works with absolute path + cmdPath, _ := exec.LookPath(os.Args[1]) + err := Exec(cmdPath, os.Args[2:], os.Environ()) + + if err != nil { + fmt.Printf("Err: %#+v\n", err.Error()) + } + + return 0 +} + +func TestMain(m *testing.M) { + os.Exit(testscript.RunMain(m, map[string]func() int{ + "execit": execit, + })) +} + +func TestExec(t *testing.T) { + testscript.Run(t, testscript.Params{ + Dir: "testdata/script", + }) +} diff --git a/internal/exec/testdata/script/exec-env.txtar b/internal/exec/testdata/script/exec-env.txtar new file mode 100644 index 00000000..3dc4223f --- /dev/null +++ b/internal/exec/testdata/script/exec-env.txtar @@ -0,0 +1,3 @@ +env ENV=foo +execit echo this is a $ENV +stdout 'this is a foo\n' diff --git a/internal/exec/testdata/script/exec-simple.txtar b/internal/exec/testdata/script/exec-simple.txtar new file mode 100644 index 00000000..7c7183cd --- /dev/null +++ b/internal/exec/testdata/script/exec-simple.txtar @@ -0,0 +1,2 @@ +execit echo this is a test +stdout 'this is a test\n'