11package env
22
33import (
4- "fmt"
5- "os"
64 "slices"
7- "sort"
85 "strings"
96
107 "github.com/spf13/cobra"
@@ -14,29 +11,26 @@ import (
1411 errUtils "github.com/cloudposse/atmos/errors"
1512 cfg "github.com/cloudposse/atmos/pkg/config"
1613 "github.com/cloudposse/atmos/pkg/data"
14+ envfmt "github.com/cloudposse/atmos/pkg/env"
1715 "github.com/cloudposse/atmos/pkg/flags"
1816 "github.com/cloudposse/atmos/pkg/flags/compat"
17+ ghactions "github.com/cloudposse/atmos/pkg/github/actions"
1918 "github.com/cloudposse/atmos/pkg/schema"
2019 u "github.com/cloudposse/atmos/pkg/utils"
2120)
2221
23- const (
24- // DefaultFileMode is the file mode for output files.
25- defaultFileMode = 0o644
26- )
27-
2822// SupportedFormats lists all supported output formats.
2923var SupportedFormats = []string {"bash" , "json" , "dotenv" , "github" }
3024
3125// envParser handles flag parsing with Viper precedence.
3226var envParser * flags.StandardParser
3327
3428// envCmd outputs environment variables from atmos.yaml.
29+ // Args validator is auto-applied by the command registry for commands without PositionalArgsBuilder.
3530var envCmd = & cobra.Command {
3631 Use : "env" ,
3732 Short : "Output environment variables configured in atmos.yaml" ,
3833 Long : `Outputs environment variables from the 'env' section of atmos.yaml in various formats suitable for shell evaluation, .env files, JSON consumption, or GitHub Actions workflows.` ,
39- Args : cobra .NoArgs ,
4034 RunE : func (cmd * cobra.Command , args []string ) error {
4135 // Parse flags using Viper (respects precedence: flags > env > config > defaults).
4236 v := viper .GetViper ()
@@ -45,16 +39,17 @@ var envCmd = &cobra.Command{
4539 }
4640
4741 // Get output format.
48- format := v .GetString ("format" )
49- if ! slices .Contains (SupportedFormats , format ) {
42+ formatStr := v .GetString ("format" )
43+ if ! slices .Contains (SupportedFormats , formatStr ) {
5044 return errUtils .Build (errUtils .ErrInvalidArgumentError ).
51- WithExplanationf ("Invalid --format value %q." , format ).
45+ WithExplanationf ("Invalid --format value %q." , formatStr ).
5246 WithHintf ("Supported formats: %s." , strings .Join (SupportedFormats , ", " )).
5347 Err ()
5448 }
5549
56- // Get output file path.
50+ // Get output file path and export flag .
5751 output := v .GetString ("output" )
52+ exportPrefix := v .GetBool ("export" )
5853
5954 // Build ConfigAndStacksInfo with CLI overrides (--config, --config-path, --base-path).
6055 // These are persistent flags inherited from the root command.
@@ -85,50 +80,60 @@ var envCmd = &cobra.Command{
8580 envVars = make (map [string ]string )
8681 }
8782
88- // Handle GitHub format special case.
89- if format == "github" {
90- if output == "" {
91- // GITHUB_ENV is an external CI environment variable set by GitHub Actions,
92- // not an Atmos configuration variable, so os.Getenv is appropriate here.
93- //nolint:forbidigo // GITHUB_ENV is an external CI env var, not Atmos config
94- output = os .Getenv ("GITHUB_ENV" )
95- if output == "" {
83+ // Handle JSON format separately (not supported by pkg/env).
84+ if formatStr == "json" {
85+ if output != "" {
86+ return u .WriteToFileAsJSON (output , envVars , 0o644 )
87+ }
88+ return outputEnvAsJSON (& atmosConfig , envVars )
89+ }
90+
91+ // Handle GitHub format special case (requires output path).
92+ if formatStr == "github" {
93+ path := output
94+ if path == "" {
95+ path = ghactions .GetEnvPath ()
96+ if path == "" {
9697 return errUtils .Build (errUtils .ErrRequiredFlagNotProvided ).
9798 WithExplanation ("--format=github requires GITHUB_ENV environment variable to be set, or use --output to specify a file path." ).
9899 Err ()
99100 }
100101 }
101- return writeEnvToFile (envVars , output , formatGitHub )
102+ dataMap := convertToAnyMap (envVars )
103+ formatted , err := envfmt .FormatData (dataMap , envfmt .FormatGitHub )
104+ if err != nil {
105+ return errUtils .Build (errUtils .ErrInvalidArgumentError ).
106+ WithCause (err ).
107+ WithExplanation ("Failed to format environment variables for GitHub output." ).
108+ Err ()
109+ }
110+ return envfmt .WriteToFile (path , formatted )
102111 }
103112
104- // Handle file output for other formats.
105- if output != "" {
106- var formatter func (map [string ]string ) string
107- switch format {
108- case "bash" :
109- formatter = formatBash
110- case "dotenv" :
111- formatter = formatDotenv
112- case "json" :
113- // For JSON file output, use the utility function.
114- return u .WriteToFileAsJSON (output , envVars , defaultFileMode )
115- default :
116- formatter = formatBash
117- }
118- return writeEnvToFile (envVars , output , formatter )
113+ // Parse format string to Format type.
114+ format , err := envfmt .ParseFormat (formatStr )
115+ if err != nil {
116+ return errUtils .Build (errUtils .ErrInvalidArgumentError ).
117+ WithCause (err ).
118+ WithExplanationf ("Invalid --format value %q." , formatStr ).
119+ Err ()
119120 }
120121
121- // Output to stdout.
122- switch format {
123- case "json" :
124- return outputEnvAsJSON (& atmosConfig , envVars )
125- case "bash" :
126- return outputEnvAsBash (envVars )
127- case "dotenv" :
128- return outputEnvAsDotenv (envVars )
129- default :
130- return outputEnvAsBash (envVars )
122+ // Format the environment variables with export option.
123+ dataMap := convertToAnyMap (envVars )
124+ formatted , err := envfmt .FormatData (dataMap , format , envfmt .WithExport (exportPrefix ))
125+ if err != nil {
126+ return errUtils .Build (errUtils .ErrInvalidArgumentError ).
127+ WithCause (err ).
128+ WithExplanation ("Failed to format environment variables." ).
129+ Err ()
130+ }
131+
132+ // Output to file or stdout.
133+ if output != "" {
134+ return envfmt .WriteToFile (output , formatted )
131135 }
136+ return data .Write (formatted )
132137 },
133138}
134139
@@ -137,93 +142,24 @@ func outputEnvAsJSON(atmosConfig *schema.AtmosConfiguration, envVars map[string]
137142 return u .PrintAsJSON (atmosConfig , envVars )
138143}
139144
140- // outputEnvAsBash outputs environment variables as shell export statements.
141- func outputEnvAsBash (envVars map [string ]string ) error {
142- return data .Write (formatBash (envVars ))
143- }
144-
145- // outputEnvAsDotenv outputs environment variables in .env format.
146- func outputEnvAsDotenv (envVars map [string ]string ) error {
147- return data .Write (formatDotenv (envVars ))
148- }
149-
150- // formatBash formats environment variables as shell export statements.
151- func formatBash (envVars map [string ]string ) string {
152- keys := sortedKeys (envVars )
153- var sb strings.Builder
154- for _ , key := range keys {
155- value := envVars [key ]
156- // Escape single quotes for safe single-quoted shell literals: ' -> '\''.
157- safe := strings .ReplaceAll (value , "'" , "'\\ ''" )
158- sb .WriteString (fmt .Sprintf ("export %s='%s'\n " , key , safe ))
159- }
160- return sb .String ()
161- }
162-
163- // formatDotenv formats environment variables in .env format.
164- func formatDotenv (envVars map [string ]string ) string {
165- keys := sortedKeys (envVars )
166- var sb strings.Builder
167- for _ , key := range keys {
168- value := envVars [key ]
169- // Use the same safe single-quoted escaping as bash output.
170- safe := strings .ReplaceAll (value , "'" , "'\\ ''" )
171- sb .WriteString (fmt .Sprintf ("%s='%s'\n " , key , safe ))
172- }
173- return sb .String ()
174- }
175-
176- // formatGitHub formats environment variables for GitHub Actions $GITHUB_ENV file.
177- // Uses KEY=value format without quoting. For multiline values, GitHub uses heredoc syntax.
178- func formatGitHub (envVars map [string ]string ) string {
179- keys := sortedKeys (envVars )
180- var sb strings.Builder
181- for _ , key := range keys {
182- value := envVars [key ]
183- // Check if value contains newlines - use heredoc syntax.
184- // Use ATMOS_EOF_ prefix to avoid collision with values containing "EOF".
185- if strings .Contains (value , "\n " ) {
186- sb .WriteString (fmt .Sprintf ("%s<<ATMOS_EOF_%s\n %s\n ATMOS_EOF_%s\n " , key , key , value , key ))
187- } else {
188- sb .WriteString (fmt .Sprintf ("%s=%s\n " , key , value ))
189- }
190- }
191- return sb .String ()
192- }
193-
194- // writeEnvToFile writes formatted environment variables to a file (append mode).
195- func writeEnvToFile (envVars map [string ]string , filePath string , formatter func (map [string ]string ) string ) error {
196- // Open file in append mode, create if doesn't exist.
197- f , err := os .OpenFile (filePath , os .O_APPEND | os .O_CREATE | os .O_WRONLY , defaultFileMode )
198- if err != nil {
199- return fmt .Errorf ("failed to open file '%s': %w" , filePath , err )
200- }
201- defer f .Close ()
202-
203- content := formatter (envVars )
204- if _ , err := f .WriteString (content ); err != nil {
205- return fmt .Errorf ("failed to write to file '%s': %w" , filePath , err )
206- }
207- return nil
208- }
209-
210- // sortedKeys returns the keys of a map sorted alphabetically.
211- func sortedKeys (m map [string ]string ) []string {
212- keys := make ([]string , 0 , len (m ))
213- for k := range m {
214- keys = append (keys , k )
145+ // convertToAnyMap converts a map[string]string to map[string]any for use with env formatters.
146+ func convertToAnyMap (envVars map [string ]string ) map [string ]any {
147+ result := make (map [string ]any , len (envVars ))
148+ for k , v := range envVars {
149+ result [k ] = v
215150 }
216- sort .Strings (keys )
217- return keys
151+ return result
218152}
219153
220154func init () {
221155 // Create parser with env-specific flags using functional options.
222156 envParser = flags .NewStandardParser (
223157 flags .WithStringFlag ("format" , "f" , "bash" , "Output format: bash, json, dotenv, github" ),
224158 flags .WithStringFlag ("output" , "o" , "" , "Output file path (default: stdout, or $GITHUB_ENV for github format)" ),
159+ flags .WithBoolFlag ("export" , "" , true , "Include 'export' prefix in bash format (default: true)" ),
225160 flags .WithEnvVars ("format" , "ATMOS_ENV_FORMAT" ),
226161 flags .WithEnvVars ("output" , "ATMOS_ENV_OUTPUT" ),
162+ flags .WithEnvVars ("export" , "ATMOS_ENV_EXPORT" ),
227163 )
228164
229165 // Register flags using the standard RegisterFlags method.
0 commit comments