Merge pull request #70 from asdf-vm/tb/asdf-help-command

feat(golang-rewrite): implement `asdf help` command
This commit is contained in:
Trevor Brown 2024-09-26 13:50:20 -04:00 committed by Trevor Brown
commit ec8985af8f
9 changed files with 396 additions and 6 deletions

View File

@ -13,6 +13,7 @@ import (
"asdf/internal/config"
"asdf/internal/exec"
"asdf/internal/help"
"asdf/internal/info"
"asdf/internal/installs"
"asdf/internal/plugins"
@ -65,6 +66,14 @@ func Execute(version string) {
return execCommand(logger, command, args)
},
},
{
Name: "help",
Action: func(cCtx *cli.Context) error {
toolName := cCtx.Args().Get(0)
toolVersion := cCtx.Args().Get(1)
return helpCommand(logger, version, toolName, toolVersion)
},
},
{
Name: "info",
Action: func(_ *cli.Context) error {
@ -408,6 +417,36 @@ func infoCommand(conf config.Config, version string) error {
return info.Print(conf, version)
}
func helpCommand(logger *log.Logger, asdfVersion, tool, version string) error {
conf, err := config.LoadConfig()
if err != nil {
logger.Printf("error loading config: %s", err)
return err
}
if tool != "" {
if version != "" {
err := help.PrintToolVersion(conf, tool, version)
if err != nil {
os.Exit(1)
}
return err
}
err := help.PrintTool(conf, tool)
if err != nil {
os.Exit(1)
}
return err
}
err = help.Print(asdfVersion)
if err != nil {
os.Exit(1)
}
return err
}
func pluginUpdateCommand(cCtx *cli.Context, logger *log.Logger, pluginName, ref string) error {
updateAll := cCtx.Bool("all")
if !updateAll && pluginName == "" {

120
internal/help/help.go Normal file
View File

@ -0,0 +1,120 @@
// Package help contains functions responsible for generating help output for
// asdf and asdf plugins.
package help
import (
_ "embed"
"fmt"
"io"
"os"
"asdf/internal/config"
"asdf/internal/plugins"
"asdf/internal/versions"
)
//go:embed help.txt
var helpText string
const quote = "\"Late but latest\"\n-- Rajinikanth"
// Print help output to STDOUT
func Print(asdfVersion string) error {
return Write(asdfVersion, os.Stdout)
}
// PrintTool write tool help output to STDOUT
func PrintTool(conf config.Config, toolName string) error {
return WriteToolHelp(conf, toolName, os.Stdout, os.Stderr)
}
// PrintToolVersion write help for specific tool version to STDOUT
func PrintToolVersion(conf config.Config, toolName, toolVersion string) error {
return WriteToolVersionHelp(conf, toolName, toolVersion, os.Stdout, os.Stderr)
}
// Write help output to an io.Writer
func Write(asdfVersion string, writer io.Writer) error {
_, err := writer.Write([]byte(fmt.Sprintf("version: %s", asdfVersion)))
if err != nil {
return err
}
_, err = writer.Write([]byte(helpText))
if err != nil {
return err
}
_, err = writer.Write([]byte("\n"))
if err != nil {
return err
}
_, err = writer.Write([]byte(quote))
if err != nil {
return err
}
_, err = writer.Write([]byte("\n"))
if err != nil {
return err
}
return nil
}
// WriteToolHelp output to an io.Writer
func WriteToolHelp(conf config.Config, toolName string, writer io.Writer, errWriter io.Writer) error {
return writePluginHelp(conf, toolName, "", writer, errWriter)
}
// WriteToolVersionHelp output to an io.Writer
func WriteToolVersionHelp(conf config.Config, toolName, toolVersion string, writer io.Writer, errWriter io.Writer) error {
return writePluginHelp(conf, toolName, toolVersion, writer, errWriter)
}
func writePluginHelp(conf config.Config, toolName, toolVersion string, writer io.Writer, errWriter io.Writer) error {
plugin := plugins.New(conf, toolName)
env := map[string]string{
"ASDF_INSTALL_PATH": plugin.Dir,
}
if toolVersion != "" {
versionType, version := versions.ParseString(toolVersion)
env["ASDF_INSTALL_VERSION"] = version
env["ASDF_INSTALL_TYPE"] = versionType
}
if err := plugin.Exists(); err != nil {
errWriter.Write([]byte(fmt.Sprintf("No plugin named %s\n", plugin.Name)))
return err
}
err := plugin.RunCallback("help.overview", []string{}, env, writer, errWriter)
if _, ok := err.(plugins.NoCallbackError); ok {
// No such callback, print err msg
errWriter.Write([]byte(fmt.Sprintf("No documentation for plugin %s\n", plugin.Name)))
return err
}
if err != nil {
return err
}
err = plugin.RunCallback("help.deps", []string{}, env, writer, errWriter)
if _, ok := err.(plugins.NoCallbackError); !ok {
return err
}
err = plugin.RunCallback("help.config", []string{}, env, writer, errWriter)
if _, ok := err.(plugins.NoCallbackError); !ok {
return err
}
err = plugin.RunCallback("help.links", []string{}, env, writer, errWriter)
if _, ok := err.(plugins.NoCallbackError); !ok {
return err
}
return nil
}

66
internal/help/help.txt Normal file
View File

@ -0,0 +1,66 @@
MANAGE PLUGINS
asdf plugin add <name> [<git-url>] Add a plugin from the plugin repo OR,
add a Git repo as a plugin by
specifying the name and repo url
asdf plugin list [--urls] [--refs] List installed plugins. Optionally show
git urls and git-ref
asdf plugin list all List plugins registered on asdf-plugins
repository with URLs
asdf plugin remove <name> Remove plugin and package versions
asdf plugin update <name> [<git-ref>] Update a plugin to latest commit on
default branch or a particular git-ref
asdf plugin update --all Update all plugins to latest commit on
default branch
MANAGE TOOLS
asdf current Display current version set or being
used for all packages
asdf current <name> Display current version set or being
used for package
asdf global <name> <version> Set the package global version
asdf global <name> latest[:<version>] Set the package global version to the
latest provided version
asdf help <name> [<version>] Output documentation for plugin and tool
asdf install Install all the package versions listed
in the .tool-versions file
asdf install <name> Install one tool at the version
specified in the .tool-versions file
asdf install <name> <version> Install a specific version of a package
asdf install <name> latest[:<version>] Install the latest stable version of a
package, or with optional version,
install the latest stable version that
begins with the given string
asdf latest <name> [<version>] Show latest stable version of a package
asdf latest --all Show latest stable version of all the
packages and if they are installed
asdf list <name> [version] List installed versions of a package and
optionally filter the versions
asdf list all <name> [<version>] List all versions of a package and
optionally filter the returned versions
asdf local <name> <version> Set the package local version
asdf local <name> latest[:<version>] Set the package local version to the
latest provided version
asdf shell <name> <version> Set the package version to
`ASDF_${LANG}_VERSION` in the current shell
asdf uninstall <name> <version> Remove a specific version of a package
asdf where <name> [<version>] Display install path for an installed
or current version
asdf which <command> Display the path to an executable
UTILS
asdf exec <command> [args...] Executes the command shim for current version
asdf env <command> [util] Runs util (default: `env`) inside the
environment used for command shim execution.
asdf info Print OS, Shell and ASDF debug information.
asdf version Print the currently installed version of ASDF
asdf reshim <name> <version> Recreate shims for version of a package
asdf shim-versions <command> List the plugins and versions that
provide a command
asdf update Update asdf to the latest stable release
asdf update --head Update asdf to the latest on the master branch
RESOURCES
GitHub: https://github.com/asdf-vm/asdf
Docs: https://asdf-vm.com

133
internal/help/help_test.go Normal file
View File

@ -0,0 +1,133 @@
package help
import (
"os"
"path/filepath"
"strings"
"testing"
"asdf/internal/config"
"asdf/internal/plugins"
"asdf/repotest"
"github.com/stretchr/testify/assert"
)
const (
version = "0.15.0"
testPluginName = "lua"
)
func TestWrite(t *testing.T) {
testDataDir := t.TempDir()
err := os.MkdirAll(filepath.Join(testDataDir, "plugins"), 0o777)
assert.Nil(t, err)
var stdout strings.Builder
err = Write(version, &stdout)
assert.Nil(t, err)
output := stdout.String()
// Simple format assertions
assert.Contains(t, output, "version: ")
assert.Contains(t, output, "MANAGE PLUGINS\n")
assert.Contains(t, output, "MANAGE TOOLS\n")
assert.Contains(t, output, "UTILS\n")
assert.Contains(t, output, "RESOURCES\n")
}
func TestWriteToolHelp(t *testing.T) {
conf, plugin := generateConfig(t)
t.Run("when plugin implements all help callbacks", func(t *testing.T) {
var stdout strings.Builder
var stderr strings.Builder
err := WriteToolHelp(conf, plugin.Name, &stdout, &stderr)
assert.Nil(t, err)
assert.Empty(t, stderr.String())
expected := "Dummy plugin documentation\n\nDummy plugin is a plugin only used for unit tests\n"
assert.Equal(t, stdout.String(), expected)
})
t.Run("when plugin does not have help.overview callback", func(t *testing.T) {
var stdout strings.Builder
var stderr strings.Builder
plugin := installPlugin(t, conf, "dummy_legacy_plugin", "legacy-plugin")
err := WriteToolHelp(conf, plugin.Name, &stdout, &stderr)
assert.EqualError(t, err, "Plugin named legacy-plugin does not have a callback named help.overview")
assert.Empty(t, stdout.String())
assert.Equal(t, stderr.String(), "No documentation for plugin legacy-plugin\n")
})
t.Run("when plugin does not exist", func(t *testing.T) {
var stdout strings.Builder
var stderr strings.Builder
err := WriteToolHelp(conf, "non-existent", &stdout, &stderr)
assert.EqualError(t, err, "Plugin named non-existent not installed")
assert.Empty(t, stdout.String())
assert.Equal(t, stderr.String(), "No plugin named non-existent\n")
})
}
func TestWriteToolVersionHelp(t *testing.T) {
conf, plugin := generateConfig(t)
t.Run("when plugin implements all help callbacks", func(t *testing.T) {
var stdout strings.Builder
var stderr strings.Builder
err := WriteToolVersionHelp(conf, plugin.Name, "1.2.3", &stdout, &stderr)
assert.Nil(t, err)
assert.Empty(t, stderr.String())
expected := "Dummy plugin documentation\n\nDummy plugin is a plugin only used for unit tests\n\nDetails specific for version 1.2.3\n"
assert.Equal(t, stdout.String(), expected)
})
t.Run("when plugin does not have help.overview callback", func(t *testing.T) {
var stdout strings.Builder
var stderr strings.Builder
plugin := installPlugin(t, conf, "dummy_legacy_plugin", "legacy-plugin")
err := WriteToolVersionHelp(conf, plugin.Name, "1.2.3", &stdout, &stderr)
assert.EqualError(t, err, "Plugin named legacy-plugin does not have a callback named help.overview")
assert.Empty(t, stdout.String())
assert.Equal(t, stderr.String(), "No documentation for plugin legacy-plugin\n")
})
t.Run("when plugin does not exist", func(t *testing.T) {
var stdout strings.Builder
var stderr strings.Builder
err := WriteToolVersionHelp(conf, "non-existent", "1.2.3", &stdout, &stderr)
assert.EqualError(t, err, "Plugin named non-existent not installed")
assert.Empty(t, stdout.String())
assert.Equal(t, stderr.String(), "No plugin named non-existent\n")
})
}
func generateConfig(t *testing.T) (config.Config, plugins.Plugin) {
t.Helper()
testDataDir := t.TempDir()
conf, err := config.LoadConfig()
assert.Nil(t, err)
conf.DataDir = testDataDir
return conf, installPlugin(t, conf, "dummy_plugin", testPluginName)
}
func installPlugin(t *testing.T, conf config.Config, fixture, pluginName string) plugins.Plugin {
_, err := repotest.InstallPlugin(fixture, conf.DataDir, pluginName)
assert.Nil(t, err)
return plugins.New(conf, pluginName)
}

View File

@ -4,6 +4,7 @@
package resolve
import (
"fmt"
"os"
"path"
"strings"
@ -72,7 +73,7 @@ func findVersionsInDir(conf config.Config, plugin plugins.Plugin, directory stri
// findVersionsInEnv returns the version from the environment if present
func findVersionsInEnv(pluginName string) ([]string, string, bool) {
envVariableName := "ASDF_" + strings.ToUpper(pluginName) + "_VERSION"
envVariableName := variableVersionName(pluginName)
versionString := os.Getenv(envVariableName)
if versionString == "" {
return []string{}, envVariableName, false
@ -118,3 +119,7 @@ func parseVersion(rawVersions string) []string {
}
return versions
}
func variableVersionName(toolName string) string {
return fmt.Sprintf("ASDF_%s_VERSION", strings.ToUpper(toolName))
}

View File

@ -1,6 +1,7 @@
package resolve
import (
"fmt"
"os"
"path/filepath"
"testing"
@ -206,3 +207,29 @@ func TestFindVersionsInEnv(t *testing.T) {
os.Unsetenv("ASDF_LUA_VERSION")
})
}
func TestVariableVersionName(t *testing.T) {
tests := []struct {
input string
output string
}{
{
input: "ruby",
output: "ASDF_RUBY_VERSION",
},
{
input: "lua",
output: "ASDF_LUA_VERSION",
},
{
input: "foo-bar",
output: "ASDF_FOO-BAR_VERSION",
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("input: %s, output: %s", tt.input, tt.output), func(t *testing.T) {
assert.Equal(t, tt.output, variableVersionName(tt.input))
})
}
}

View File

@ -376,7 +376,7 @@ func installPlugin(t *testing.T, conf config.Config, fixture, pluginName string)
_, err := repotest.InstallPlugin(fixture, conf.DataDir, pluginName)
assert.Nil(t, err)
return plugins.New(conf, testPluginName)
return plugins.New(conf, pluginName)
}
func installVersion(t *testing.T, conf config.Config, plugin plugins.Plugin, version string) {

View File

@ -23,9 +23,9 @@ func TestBatsTests(t *testing.T) {
runBatsFile(t, dir, "current_command.bats")
})
//t.Run("help_command", func(t *testing.T) {
// runBatsFile(t, dir, "help_command.bats")
//})
t.Run("help_command", func(t *testing.T) {
runBatsFile(t, dir, "help_command.bats")
})
t.Run("info_command", func(t *testing.T) {
runBatsFile(t, dir, "info_command.bats")

View File

@ -75,7 +75,7 @@ EOF
[ "$status" -eq 0 ]
[[ $output == 'version: v'* ]]
[[ $output == *$'MANAGE PLUGINS\n'* ]]
[[ $output == *$'MANAGE PACKAGES\n'* ]]
[[ $output == *$'MANAGE TOOLS\n'* ]]
[[ $output == *$'UTILS\n'* ]]
[[ $output == *$'"Late but latest"\n-- Rajinikanth' ]]
}