diff --git a/cmd/env.go b/cmd/env.go new file mode 100644 index 000000000..9d9ed5fe0 --- /dev/null +++ b/cmd/env.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "fmt" + "io" + + "github.com/a8m/envsubst" + "github.com/spf13/cobra" +) + +func newEnvCmd() *cobra.Command { + var envCmd = &cobra.Command{ + Use: "env", + Short: "Manage environment variables", + } + var substCmd = &cobra.Command{ + Use: "subst", + Short: "Substitute environment variables in a string", + RunE: substRunE, + } + envCmd.AddCommand(substCmd) + return envCmd +} + +// Accepts a string as an argument, or reads from stdin if no argument is provided. +func substRunE(cmd *cobra.Command, args []string) error { + var input string + if len(args) > 0 { + input = args[0] + } else { + // Read from stdin + b, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return err + } + input = string(b) + } + if input == "" { + return nil + } + output, err := envsubst.String(input) + if err != nil { + return err + } + _, err = fmt.Fprint(cmd.OutOrStdout(), output) + return err +} diff --git a/cmd/env_test.go b/cmd/env_test.go new file mode 100644 index 000000000..a8ea0e27e --- /dev/null +++ b/cmd/env_test.go @@ -0,0 +1,97 @@ +package cmd + +import ( + "bytes" + "os" + "testing" + + "gotest.tools/v3/assert" +) + +func TestSubstRunE(t *testing.T) { + // Set environment variables for testing + err := os.Setenv("ENV_NAME", "world") + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + name string + input string + output string + }{ + { + name: "substitute variables", + input: "Hello $ENV_NAME!", + output: "Hello world!", + }, + { + name: "no variables to substitute", + input: "Hello, world!", + output: "Hello, world!", + }, + { + name: "empty input", + input: "", + output: "", + }, + { + name: "no variables JSON", + input: `{"foo": "bar"}`, + output: `{"foo": "bar"}`, + }, + { + name: "substitute variables JSON", + input: `{"foo": "$ENV_NAME"}`, + output: `{"foo": "world"}`, + }, + { + name: "no variables key=value", + input: `foo=bar`, + output: `foo=bar`, + }, + } + + // Run tests for each test case as argument + for _, tc := range testCases { + t.Run("arg: "+tc.name, func(t *testing.T) { + // Set up test command + cmd := newEnvCmd() + + // Capture output + outputBuf := bytes.Buffer{} + cmd.SetOut(&outputBuf) + + // Run command + cmd.SetArgs([]string{"subst", tc.input}) + err := cmd.Execute() + + // Check output and error + assert.NilError(t, err) + assert.Equal(t, tc.output, outputBuf.String()) + }) + } + // Run tests for each test case as stdin + for _, tc := range testCases { + t.Run("stdin: "+tc.name, func(t *testing.T) { + // Set up test command + cmd := newEnvCmd() + + // Set up input + inputBuf := bytes.NewBufferString(tc.input) + cmd.SetIn(inputBuf) + + // Capture output + outputBuf := bytes.Buffer{} + cmd.SetOut(&outputBuf) + + // Run command + cmd.SetArgs([]string{"subst"}) + err = cmd.Execute() + + // Check output and error + assert.NilError(t, err) + assert.Equal(t, tc.output, outputBuf.String()) + }) + } +} diff --git a/cmd/root.go b/cmd/root.go index a6257771f..942278c69 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -174,6 +174,7 @@ func MakeCommands() *cobra.Command { rootCmd.AddCommand(newSwitchCommand(rootOptions)) rootCmd.AddCommand(newAdminCommand(rootOptions)) rootCmd.AddCommand(newCompletionCommand()) + rootCmd.AddCommand(newEnvCmd()) flags := rootCmd.PersistentFlags() diff --git a/cmd/root_test.go b/cmd/root_test.go index 6ea4c5667..def65ae10 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -16,7 +16,7 @@ var _ = Describe("Root", func() { Describe("subcommands", func() { It("can create commands", func() { commands := cmd.MakeCommands() - Expect(len(commands.Commands())).To(Equal(23)) + Expect(len(commands.Commands())).To(Equal(24)) }) }) diff --git a/go.mod b/go.mod index c73f2c9a2..b20bd23c5 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( require ( github.com/OneOfOne/xxhash v1.2.8 // indirect + github.com/a8m/envsubst v1.4.2 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/charmbracelet/bubbles v0.11.0 // indirect diff --git a/go.sum b/go.sum index 8591b37e6..3b83fe785 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nB github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= +github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=