Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions packages/cmd/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ const (
FormatDotEnvEval string = "dotenv-eval"
)

const (
QuoteSingle string = "single"
QuoteDouble string = "double"
QuoteNone string = "none"
)

// exportCmd represents the export command
var exportCmd = &cobra.Command{
Use: "export",
Expand Down Expand Up @@ -68,6 +74,16 @@ var exportCmd = &cobra.Command{
util.HandleError(err)
}

quoteFlag, err := cmd.Flags().GetString("quote")
if err != nil {
util.HandleError(err)
}

quote, err := quoteCharacter(quoteFlag)
if err != nil {
util.HandleError(err)
}
Comment on lines +77 to +85

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 --quote silently ignored for non-dotenv formats

quoteCharacter is resolved unconditionally before the format is known, so passing --quote=double --format=json (or yaml, csv, dotenv-eval) produces no output change and no warning. A user who sets this flag expecting it to apply may be left wondering why output is unchanged. Additionally, an invalid quote value (e.g. --quote=backtick --format=json) raises an error even though the flag has no effect on json/csv/yaml/dotenv-eval output — the error message is misleading in that context. Moving the quoteCharacter call inside formatEnvs (or gating it after format validation) would prevent both surprises.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


templatePath, err := cmd.Flags().GetString("template")
if err != nil {
util.HandleError(err)
Expand Down Expand Up @@ -142,7 +158,7 @@ var exportCmd = &cobra.Command{
secrets = util.FilterSecretsByTag(secrets, tagSlugs)
secrets = util.SortSecretsByKeys(secrets)

output, err = formatEnvs(secrets, format)
output, err = formatEnvs(secrets, format, quote)
if err != nil {
util.HandleError(err)
}
Expand Down Expand Up @@ -267,6 +283,7 @@ func init() {
exportCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
exportCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, dotenv-export, dotenv-eval, json, csv, yaml)")
exportCmd.Flags().String("quote", "single", "Set the quote character wrapping values for the dotenv and dotenv-export formats (single, double, none)")
exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
exportCmd.Flags().Bool("include-imports", true, "Imported linked secrets")
exportCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
Expand All @@ -278,12 +295,12 @@ func init() {
}

// Format according to the format flag
func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string, error) {
func formatEnvs(envs []models.SingleEnvironmentVariable, format string, quote string) (string, error) {
switch strings.ToLower(format) {
case FormatDotenv:
return formatAsDotEnv(envs), nil
return formatAsDotEnv(envs, quote), nil
case FormatDotEnvExport:
return formatAsDotEnvExport(envs), nil
return formatAsDotEnvExport(envs, quote), nil
case FormatDotEnvEval:
return formatAsDotEnvEval(envs), nil
case FormatJson:
Expand All @@ -309,24 +326,42 @@ func formatAsCSV(envs []models.SingleEnvironmentVariable) string {
return csvString.String()
}

// Format environment variables as a dotenv file
func formatAsDotEnv(envs []models.SingleEnvironmentVariable) string {
// Format environment variables as a dotenv file. quote is the character used to
// wrap each value ("'", "\"", or "" for none); resolve it via quoteCharacter.
func formatAsDotEnv(envs []models.SingleEnvironmentVariable, quote string) string {
var dotenv string
for _, env := range envs {
dotenv += fmt.Sprintf("%s='%s'\n", env.Key, escapeNewLinesIfRequired(env))
dotenv += fmt.Sprintf("%s=%s%s%s\n", env.Key, quote, escapeNewLinesIfRequired(env), quote)
}
return dotenv
}

// Format environment variables as a dotenv file with export at the beginning
func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string {
// Format environment variables as a dotenv file with export at the beginning.
// quote behaves as in formatAsDotEnv.
func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable, quote string) string {
var dotenv string
for _, env := range envs {
dotenv += fmt.Sprintf("export %s='%s'\n", env.Key, escapeNewLinesIfRequired(env))
dotenv += fmt.Sprintf("export %s=%s%s%s\n", env.Key, quote, escapeNewLinesIfRequired(env), quote)
}
return dotenv
}

// quoteCharacter maps the --quote flag value to the character used to wrap
// dotenv values. An empty value defaults to single quotes so existing exports
// are unaffected.
func quoteCharacter(quote string) (string, error) {
switch strings.ToLower(quote) {
case QuoteSingle, "":
return "'", nil
case QuoteDouble:
return "\"", nil
case QuoteNone:
return "", nil
default:
return "", fmt.Errorf("invalid quote type: %s. Available quote types are [%s]", quote, strings.Join([]string{QuoteSingle, QuoteDouble, QuoteNone}, ", "))
}
}

// Format environment variables for shell eval/source. Values are wrapped in
// single quotes with POSIX escaping so the output is safe to evaluate via
// `eval "$(infisical export --format=dotenv-eval)"` regardless of value
Expand Down
102 changes: 102 additions & 0 deletions packages/cmd/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,105 @@ func TestPosixShellQuote(t *testing.T) {
})
}
}

func TestQuoteCharacter(t *testing.T) {
tests := []struct {
name string
input string
expected string
expectErr bool
}{
{name: "single", input: "single", expected: "'"},
{name: "double", input: "double", expected: "\""},
{name: "none", input: "none", expected: ""},
{name: "empty defaults to single", input: "", expected: "'"},
{name: "case insensitive", input: "DOUBLE", expected: "\""},
{name: "invalid value returns error", input: "backtick", expectErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := quoteCharacter(tt.input)
if tt.expectErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, got)
})
}
}

func TestFormatAsDotEnv(t *testing.T) {
tests := []struct {
name string
input []models.SingleEnvironmentVariable
quote string
expected string
}{
{
name: "single quote keeps the existing default behavior",
input: []models.SingleEnvironmentVariable{{Key: "KEY1", Value: "VALUE1"}, {Key: "KEY2", Value: "VALUE2"}},
quote: "'",
expected: "KEY1='VALUE1'\nKEY2='VALUE2'\n",
},
{
name: "double quote wraps values in double quotes",
input: []models.SingleEnvironmentVariable{{Key: "KEY1", Value: "VALUE1"}},
quote: "\"",
expected: "KEY1=\"VALUE1\"\n",
},
{
name: "none emits bare values for docker --env-file",
input: []models.SingleEnvironmentVariable{{Key: "KEY1", Value: "VALUE1"}},
quote: "",
expected: "KEY1=VALUE1\n",
},
{
name: "double quote with multiline-encoded value emits escaped newlines for dotenv expansion",
input: []models.SingleEnvironmentVariable{{Key: "PRIVATE_KEY", Value: "line1\nline2", SkipMultilineEncoding: true}},
quote: "\"",
expected: "PRIVATE_KEY=\"line1\\nline2\"\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, formatAsDotEnv(tt.input, tt.quote))
})
}
}

func TestFormatAsDotEnvExport(t *testing.T) {
tests := []struct {
name string
input []models.SingleEnvironmentVariable
quote string
expected string
}{
{
name: "single quote keeps the existing default behavior",
input: []models.SingleEnvironmentVariable{{Key: "KEY1", Value: "VALUE1"}},
quote: "'",
expected: "export KEY1='VALUE1'\n",
},
{
name: "double quote wraps values in double quotes",
input: []models.SingleEnvironmentVariable{{Key: "KEY1", Value: "VALUE1"}},
quote: "\"",
expected: "export KEY1=\"VALUE1\"\n",
},
{
name: "none emits bare values",
input: []models.SingleEnvironmentVariable{{Key: "KEY1", Value: "VALUE1"}},
quote: "",
expected: "export KEY1=VALUE1\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, formatAsDotEnvExport(tt.input, tt.quote))
})
}
}