Skip to content
Merged
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
31 changes: 29 additions & 2 deletions packages/cmd/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
FormatCSV string = "csv"
FormatYaml string = "yaml"
FormatDotEnvExport string = "dotenv-export"
FormatDotEnvEval string = "dotenv-eval"
)

// exportCmd represents the export command
Expand Down Expand Up @@ -237,6 +238,8 @@ func getDefaultFilename(format string) string {
return "secrets.yaml"
case FormatDotEnvExport:
return ".env"
case FormatDotEnvEval:
return ".env"
case FormatDotenv:
return ".env"
default:
Expand All @@ -255,6 +258,8 @@ func getDefaultExtension(format string) string {
return ".yaml"
case FormatDotEnvExport:
return ".env"
case FormatDotEnvEval:
return ".env"
case FormatDotenv:
return ".env"
default:
Expand All @@ -266,7 +271,7 @@ func init() {
RootCmd.AddCommand(exportCmd)
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, json, csv)")
exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, dotenv-export, dotenv-eval, json, csv, yaml)")
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 @@ -284,14 +289,16 @@ func formatEnvs(envs []models.SingleEnvironmentVariable, format string) (string,
return formatAsDotEnv(envs), nil
case FormatDotEnvExport:
return formatAsDotEnvExport(envs), nil
case FormatDotEnvEval:
return formatAsDotEnvEval(envs), nil
case FormatJson:
return formatAsJson(envs), nil
case FormatCSV:
return formatAsCSV(envs), nil
case FormatYaml:
return formatAsYaml(envs)
default:
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport})
return "", fmt.Errorf("invalid format type: %s. Available format types are [%s]", format, []string{FormatDotenv, FormatJson, FormatCSV, FormatYaml, FormatDotEnvExport, FormatDotEnvEval})
}
}

Expand Down Expand Up @@ -325,6 +332,26 @@ func formatAsDotEnvExport(envs []models.SingleEnvironmentVariable) string {
return dotenv
}

// 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
// contents (newlines, single quotes, $, ", \, etc.).
func formatAsDotEnvEval(envs []models.SingleEnvironmentVariable) string {
var dotenv string
for _, env := range envs {
dotenv += fmt.Sprintf("export %s=%s\n", env.Key, posixShellQuote(env.Value))
}
return dotenv
Comment thread
varonix0 marked this conversation as resolved.
}

// posixShellQuote wraps a value in single quotes and escapes any embedded
// single quotes using the standard `'\”` sequence. Single-quoted POSIX
// strings preserve every other character verbatim (including newlines,
// backslashes, $, and "), so this is sufficient for eval/source.
Comment thread
varonix0 marked this conversation as resolved.
func posixShellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
}

func formatAsYaml(envs []models.SingleEnvironmentVariable) (string, error) {
m := make(map[string]string)
for _, env := range envs {
Expand Down
76 changes: 76 additions & 0 deletions packages/cmd/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,79 @@ func TestFormatAsYaml(t *testing.T) {
})
}
}

func TestFormatAsDotEnvEval(t *testing.T) {
tests := []struct {
name string
input []models.SingleEnvironmentVariable
expected string
}{
{
name: "Empty input",
input: []models.SingleEnvironmentVariable{},
expected: "",
},
{
name: "Simple value",
input: []models.SingleEnvironmentVariable{
{Key: "KEY1", Value: "simple"},
},
expected: "export KEY1='simple'\n",
},
{
name: "Value containing single quote",
input: []models.SingleEnvironmentVariable{
{Key: "KEY1", Value: "it's a value"},
},
expected: "export KEY1='it'\\''s a value'\n",
},
{
name: "Multiline value is preserved verbatim",
input: []models.SingleEnvironmentVariable{
{Key: "KEY1", Value: "line1\nline2"},
},
expected: "export KEY1='line1\nline2'\n",
},
{
name: "Multiline value with skipMultilineEncoding set still emits real newlines",
input: []models.SingleEnvironmentVariable{
{Key: "KEY1", Value: "line1\nline2", SkipMultilineEncoding: true},
},
expected: "export KEY1='line1\nline2'\n",
},
{
name: "Shell metacharacters are preserved literally inside single quotes",
input: []models.SingleEnvironmentVariable{
{Key: "KEY1", Value: `$(rm -rf /) "quotes" \backslash`},
},
expected: "export KEY1='$(rm -rf /) \"quotes\" \\backslash'\n",
},
}

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

func TestPosixShellQuote(t *testing.T) {
tests := []struct {
input string
expected string
}{
{input: "", expected: "''"},
{input: "plain", expected: "'plain'"},
{input: "it's", expected: `'it'\''s'`},
{input: "'leading", expected: `''\''leading'`},
{input: "trailing'", expected: `'trailing'\'''`},
{input: "a'b'c", expected: `'a'\''b'\''c'`},
{input: "with\nnewline", expected: "'with\nnewline'"},
}

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