Skip to content

Commit

Permalink
Add flags to customise the behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
pjcdawkins committed Jan 3, 2025
1 parent 787a157 commit 3eb70c1
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 75 deletions.
177 changes: 122 additions & 55 deletions commands/config_install.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package commands

import (
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
Expand All @@ -11,20 +13,43 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/symfony-cli/terminal"
"gopkg.in/yaml.v3"

"github.com/platformsh/cli/internal/config"
"github.com/platformsh/cli/internal/config/alt"
)

var configInstallCommand = &cobra.Command{
Use: "config:install [flags] [url]",
Short: "Installs an alternative CLI, downloading new configuration from a URL",
Args: cobra.ExactArgs(1),
RunE: runConfigInstall,
func newConfigInstallCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "config:install [flags] [url]",
Short: "Installs an alternative CLI, downloading new configuration from a URL",
Args: cobra.ExactArgs(1),
RunE: runConfigInstall,
}
cmd.Flags().String("bin-dir", "", "Install the executable in the given directory")
cmd.Flags().String("config-dir", "", "Install the configuration in the given directory")
cmd.Flags().Bool("absolute", false,
"Use the absolute path to the current executable, instead of the configured name")
cmd.Flags().BoolP("force", "f", false, "Force installation even if a duplicate executable exists")
return cmd
}

func runConfigInstall(cmd *cobra.Command, args []string) error {
cnf := config.FromContext(cmd.Context())

// Validate input.
executableDir, err := getExecutableDir(cmd)
if err != nil {
return err
}
configDir, err := getConfigDir(cmd)
if err != nil {
return err
}
target, err := getExecutableTarget(cmd, cnf)
if err != nil {
return err
}

cmd.PrintErrln("Downloading and validating new CLI configuration...")
cmd.PrintErrln()

Expand All @@ -36,29 +61,37 @@ func runConfigInstall(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
cnf := config.FromContext(cmd.Context())
newExecutable := newCnfStruct.Application.Executable
if newExecutable == cnf.Application.Executable {
return fmt.Errorf("cannot install config for same executable name as this program: %s", newExecutable)
}

// Find the directory and file paths.
executableDir, err := alt.FindBinDir()
if err != nil {
return err
}
configDir, err := alt.FindConfigDir()
if err != nil {
return err
configFilePath := filepath.Join(configDir, newExecutable) + ".yaml"
executableFilePath := filepath.Join(executableDir, newExecutable) + alt.GetExecutableFileExtension()

pathVariableName := "PATH"
if runtime.GOOS == "windows" {
pathVariableName = "Path"
}
configFilePath := filepath.Join(configDir, newExecutable+".yaml")
executableFilePath := filepath.Join(executableDir, newExecutable)

if path, err := exec.LookPath(newExecutable); err == nil && path != executableFilePath {
return fmt.Errorf(
"cannot install config: an executable with the same name already exists at another location: %s",
path,
)
// Check for duplicates.
{
force, err := cmd.Flags().GetBool("force")
if err != nil {
return err
}
if !force {
if path, err := exec.LookPath(newExecutable); err == nil && path != executableFilePath {
cmd.PrintErrln("An executable with the same name already exists at another location.")
cmd.PrintErrf(
"Use %s to ignore this check. "+
"You would need to verify the %s precedence manually.\n",
color.RedString("--force"),
pathVariableName,
)
return fmt.Errorf("install failed to duplicate executable with the name %s at: %s", newExecutable, path)
}
}
}

// Make a formatter to replace the home directory with ~ in filenames.
Expand All @@ -73,42 +106,27 @@ func runConfigInstall(cmd *cobra.Command, args []string) error {
}()

cmd.PrintErrln("The following files will be created or overwritten:")
cmd.PrintErrf(" Config file: %s\n", color.CyanString(replaceHomeDir(configFilePath)))
cmd.PrintErrf(" Configuration file: %s\n", color.CyanString(replaceHomeDir(configFilePath)))
cmd.PrintErrf(" Executable: %s\n", color.CyanString(replaceHomeDir(executableFilePath)))
cmd.PrintErrf("The executable runs %s with the new configuration.\n",
color.CyanString(replaceHomeDir(target)))
cmd.PrintErrln()
if terminal.Stdin.IsInteractive() {
if !terminal.AskConfirmation("Are you sure you want to continue?", true) {
os.Exit(1)
}
cmd.PrintErrln()
}
cmd.PrintErrln()

// Generate the file content.
executableContent := fmt.Sprintf(
"#!/bin/sh\n"+
"# This file is automatically generated by the %s.\n"+
"export CLI_CONFIG_FILE=%s\n"+
`[ "$#" -gt 1 ] && shift # Skip first argument`+"\n"+
`%s "$@"`+"\n",
cnf.Application.Name,
// Create the files.
a := alt.New(
executableFilePath,
fmt.Sprint("Automatically generated by the", cnf.Application.Name),
target,
configFilePath,
cnf.Application.Executable,
newCnfNode,
)
configContent, err := yaml.Marshal(newCnfNode)
if err != nil {
return fmt.Errorf("failed to marshal new config: %w", err)
}

// Start writing the files.
if err := os.MkdirAll(configDir, 0o755); err != nil {
return err
}
if err := os.MkdirAll(executableDir, 0o755); err != nil {
return err
}
if err := os.WriteFile(configFilePath, configContent, 0o600); err != nil {
return err
}
if err := os.WriteFile(executableFilePath, []byte(executableContent), 0o700); err != nil { //nolint:gosec
if err := a.GenerateAndSave(); err != nil {
return err
}

Expand All @@ -120,17 +138,12 @@ func runConfigInstall(cmd *cobra.Command, args []string) error {
return fmt.Errorf("could not determine if the executable directory is in the PATH: %w", err)
}

envVarName := "PATH"
if runtime.GOOS == "windows" {
envVarName = "Path"
}

if isInPath {
cmd.PrintErrln("Run the new CLI with:", color.GreenString(filepath.Base(executableFilePath)))
} else {
cmd.PrintErrf(
"Add the following directory to your %s: %s\n",
envVarName,
pathVariableName,
color.YellowString(replaceHomeDir(executableDir)),
)
cmd.PrintErrln()
Expand All @@ -142,3 +155,57 @@ func runConfigInstall(cmd *cobra.Command, args []string) error {

return nil
}

func getExecutableTarget(cmd *cobra.Command, cnf *config.Config) (string, error) {
abs, err := cmd.Flags().GetBool("absolute")
if err != nil {
return "", err
}
if abs {
return os.Executable()
}
return cnf.Application.Executable, nil
}

func getConfigDir(cmd *cobra.Command) (string, error) {
configDirOpt, err := cmd.Flags().GetString("config-dir")
if err != nil {
return "", err
}
if configDirOpt != "" {
return validateUserProvidedDir(configDirOpt)
}
return alt.FindConfigDir()
}

func getExecutableDir(cmd *cobra.Command) (string, error) {
binDirOpt, err := cmd.Flags().GetString("bin-dir")
if err != nil {
return "", err
}
if binDirOpt != "" {
return validateUserProvidedDir(binDirOpt)
}
return alt.FindBinDir()
}

func validateUserProvidedDir(path string) (string, error) {
abs, err := filepath.Abs(path)
if err != nil {
return "", err
}
path = abs

lstat, err := os.Lstat(path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return "", fmt.Errorf("directory not found: %s", path)
}
return "", err
}
if !lstat.IsDir() {
return "", fmt.Errorf("%s is not a directory", path)
}

return path, nil
}
44 changes: 26 additions & 18 deletions commands/config_install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,20 @@ import (
"gopkg.in/yaml.v3"

"github.com/platformsh/cli/internal/config"
"github.com/platformsh/cli/internal/config/alt"
)

func TestConfigInstallCmd(t *testing.T) {
tempDir := t.TempDir()
tempBinDir := filepath.Join(tempDir, "bin")
require.NoError(t, os.Mkdir(tempBinDir, 0o755))
_ = os.Setenv("HOME", tempDir)
_ = os.Setenv("XDG_CONFIG_HOME", "")

// Ensure filesystem functions looking for UserHomeDir or UserConfigDir return the test directory.
homeEnv := os.Getenv("HOME")
require.NoError(t, os.Setenv("HOME", tempDir))
require.NoError(t, os.Unsetenv("XDG_CONFIG_HOME"))
require.NoError(t, os.Unsetenv("TEST_HOME"))
t.Cleanup(func() {
_ = os.Setenv("HOME", homeEnv)
})
remoteConfig := testConfig()

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/test-config.yaml" {
cnf := testConfig()
_ = yaml.NewEncoder(w).Encode(cnf)
_ = yaml.NewEncoder(w).Encode(remoteConfig)
}
}))
defer server.Close()
Expand All @@ -44,9 +39,11 @@ func TestConfigInstallCmd(t *testing.T) {
defer cancel()
ctx = config.ToContext(ctx, cnf)

cmd := configInstallCommand
cmd := newConfigInstallCommand()
cmd.SetContext(ctx)
cmd.SetOut(io.Discard)
_ = cmd.Flags().Set("config-dir", tempDir)
_ = cmd.Flags().Set("bin-dir", tempBinDir)

args := []string{testConfigURL}

Expand All @@ -58,15 +55,26 @@ func TestConfigInstallCmd(t *testing.T) {
cnf.Application.Executable = "test-cli-executable-host"
err = cmd.RunE(cmd, args)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(tempDir, alt.HomeSubDir, "test-cli-executable.yaml"))
assert.FileExists(t, filepath.Join(tempDir, alt.HomeSubDir, "bin", "test-cli-executable"))
assert.Contains(t, stdErrBuf.String(), filepath.Join("~", alt.HomeSubDir, "test-cli-executable.yaml"))
assert.Contains(t, stdErrBuf.String(), filepath.Join("~", alt.HomeSubDir, "bin", "test-cli-executable"))
assert.FileExists(t, filepath.Join(tempDir, "test-cli-executable.yaml"))
assert.FileExists(t, filepath.Join(tempBinDir, "test-cli-executable"))
assert.Contains(t, stdErrBuf.String(), "~/test-cli-executable.yaml")
assert.Contains(t, stdErrBuf.String(), "~/bin/test-cli-executable")
assert.Contains(t, stdErrBuf.String(), "Add the following directory to your PATH")

b, err := os.ReadFile(filepath.Join(tempDir, alt.HomeSubDir, "bin", "test-cli-executable"))
b, err := os.ReadFile(filepath.Join(tempBinDir, "test-cli-executable"))
require.NoError(t, err)
assert.Contains(t, string(b), filepath.Join(tempDir, alt.HomeSubDir, "test-cli-executable.yaml"))
assert.Contains(t, string(b), filepath.Join(tempDir, "test-cli-executable.yaml"))
assert.Contains(t, string(b), `test-cli-executable-host "$@"`)

_ = os.Setenv("PATH", tempBinDir+":"+os.Getenv("PATH"))
remoteConfig.Application.Executable = "test-cli-executable2"
err = cmd.RunE(cmd, args)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(tempDir, "test-cli-executable2.yaml"))
assert.FileExists(t, filepath.Join(tempBinDir, "test-cli-executable2"))
assert.Contains(t, stdErrBuf.String(), "~/test-cli-executable2.yaml")
assert.Contains(t, stdErrBuf.String(), "~/bin/test-cli-executable2")
assert.Contains(t, stdErrBuf.String(), "Run the new CLI with: test-cli-executable2")
}

func testConfig() *config.Config {
Expand Down
4 changes: 2 additions & 2 deletions commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob
DisableFlagParsing: false,
FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
SilenceUsage: true,
SilenceErrors: true,
SilenceErrors: false,
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
if viper.GetBool("quiet") && !viper.GetBool("debug") && !viper.GetBool("verbose") {
viper.Set("no-interaction", true)
Expand Down Expand Up @@ -153,7 +153,7 @@ func newRootCommand(cnf *config.Config, assets *vendorization.VendorAssets) *cob

// Add subcommands.
cmd.AddCommand(
configInstallCommand,
newConfigInstallCommand(),
newCompletionCommand(cnf),
newHelpCommand(cnf),
newListCommand(cnf),
Expand Down
Loading

0 comments on commit 3eb70c1

Please sign in to comment.