diff --git a/cmd/root.go b/cmd/root.go index 268ff169dc..7a669dade0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -168,6 +168,11 @@ yq -P -oy sample.json rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "properties-separator", yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "separator to use between keys and values") rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "properties-array-brackets", yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "use [x] in array paths (e.g. for SpringBoot)") + rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredShellVariablesPreferences.KeySeparator, "shell-key-separator", yqlib.ConfiguredShellVariablesPreferences.KeySeparator, "separator for shell variable key paths") + if err = rootCmd.RegisterFlagCompletionFunc("shell-key-separator", cobra.NoFileCompletions); err != nil { + panic(err) + } + rootCmd.PersistentFlags().BoolVar(&yqlib.StringInterpolationEnabled, "string-interpolation", yqlib.StringInterpolationEnabled, "Toggles strings interpolation of \\(exp)") rootCmd.PersistentFlags().BoolVarP(&nullInput, "null-input", "n", false, "Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.") diff --git a/pkg/yqlib/doc/usage/shellvariables.md b/pkg/yqlib/doc/usage/shellvariables.md index ba50315a77..455e669bb1 100644 --- a/pkg/yqlib/doc/usage/shellvariables.md +++ b/pkg/yqlib/doc/usage/shellvariables.md @@ -84,3 +84,23 @@ will output name='Miles O'"'"'Brien' ``` +## Encode shell variables: custom separator +Use --shell-key-separator to specify a custom separator between keys. This is useful when the original keys contain underscores. + +Given a sample.yml file of: +```yaml +my_app: + db_config: + host: localhost + port: 5432 +``` +then +```bash +yq -o=shell --shell-key-separator="__" sample.yml +``` +will output +```sh +my_app__db_config__host=localhost +my_app__db_config__port=5432 +``` + diff --git a/pkg/yqlib/encoder_shellvariables.go b/pkg/yqlib/encoder_shellvariables.go index be54efc66c..3923670347 100644 --- a/pkg/yqlib/encoder_shellvariables.go +++ b/pkg/yqlib/encoder_shellvariables.go @@ -12,10 +12,13 @@ import ( ) type shellVariablesEncoder struct { + prefs ShellVariablesPreferences } func NewShellVariablesEncoder() Encoder { - return &shellVariablesEncoder{} + return &shellVariablesEncoder{ + prefs: ConfiguredShellVariablesPreferences, + } } func (pe *shellVariablesEncoder) CanHandleAliases() bool { @@ -58,7 +61,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat return err case SequenceNode: for index, child := range node.Content { - err := pe.doEncode(w, child, appendPath(path, index)) + err := pe.doEncode(w, child, pe.appendPath(path, index)) if err != nil { return err } @@ -68,7 +71,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat for index := 0; index < len(node.Content); index = index + 2 { key := node.Content[index] value := node.Content[index+1] - err := pe.doEncode(w, value, appendPath(path, key.Value)) + err := pe.doEncode(w, value, pe.appendPath(path, key.Value)) if err != nil { return err } @@ -81,7 +84,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat } } -func appendPath(cookedPath string, rawKey interface{}) string { +func (pe *shellVariablesEncoder) appendPath(cookedPath string, rawKey interface{}) string { // Shell variable names must match // [a-zA-Z_]+[a-zA-Z0-9_]* @@ -126,7 +129,7 @@ func appendPath(cookedPath string, rawKey interface{}) string { } return key } - return cookedPath + "_" + key + return cookedPath + pe.prefs.KeySeparator + key } func quoteValue(value string) string { diff --git a/pkg/yqlib/encoder_shellvariables_test.go b/pkg/yqlib/encoder_shellvariables_test.go index 3d3d905f07..d41ad663bb 100644 --- a/pkg/yqlib/encoder_shellvariables_test.go +++ b/pkg/yqlib/encoder_shellvariables_test.go @@ -91,3 +91,47 @@ func TestShellVariablesEncoderEmptyMap(t *testing.T) { func TestShellVariablesEncoderScalarNode(t *testing.T) { assertEncodesTo(t, "some string", "value='some string'") } + +func assertEncodesToWithSeparator(t *testing.T, yaml string, shellvars string, separator string) { + var output bytes.Buffer + writer := bufio.NewWriter(&output) + + // Save the original separator + originalSeparator := ConfiguredShellVariablesPreferences.KeySeparator + defer func() { + ConfiguredShellVariablesPreferences.KeySeparator = originalSeparator + }() + + // Set the custom separator + ConfiguredShellVariablesPreferences.KeySeparator = separator + + var encoder = NewShellVariablesEncoder() + inputs, err := readDocuments(strings.NewReader(yaml), "test.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences)) + if err != nil { + panic(err) + } + node := inputs.Front().Value.(*CandidateNode) + err = encoder.Encode(writer, node) + if err != nil { + panic(err) + } + writer.Flush() + + test.AssertResult(t, shellvars, strings.TrimSuffix(output.String(), "\n")) +} + +func TestShellVariablesEncoderCustomSeparator(t *testing.T) { + assertEncodesToWithSeparator(t, "a:\n b: Lewis\n c: Carroll", "a__b=Lewis\na__c=Carroll", "__") +} + +func TestShellVariablesEncoderCustomSeparatorNested(t *testing.T) { + assertEncodesToWithSeparator(t, "my_app:\n db_config:\n host: localhost", "my_app__db_config__host=localhost", "__") +} + +func TestShellVariablesEncoderCustomSeparatorArray(t *testing.T) { + assertEncodesToWithSeparator(t, "a: [{n: Alice}, {n: Bob}]", "a__0__n=Alice\na__1__n=Bob", "__") +} + +func TestShellVariablesEncoderCustomSeparatorSingleChar(t *testing.T) { + assertEncodesToWithSeparator(t, "a:\n b: value", "aXb=value", "X") +} diff --git a/pkg/yqlib/shellvariables.go b/pkg/yqlib/shellvariables.go new file mode 100644 index 0000000000..634fc6fa49 --- /dev/null +++ b/pkg/yqlib/shellvariables.go @@ -0,0 +1,14 @@ +package yqlib + +type ShellVariablesPreferences struct { + KeySeparator string +} + +func NewDefaultShellVariablesPreferences() ShellVariablesPreferences { + return ShellVariablesPreferences{ + KeySeparator: "_", + } +} + +var ConfiguredShellVariablesPreferences = NewDefaultShellVariablesPreferences() + diff --git a/pkg/yqlib/shellvariables_test.go b/pkg/yqlib/shellvariables_test.go index 5a7f56f44e..d123f4bde3 100644 --- a/pkg/yqlib/shellvariables_test.go +++ b/pkg/yqlib/shellvariables_test.go @@ -54,12 +54,33 @@ var shellVariablesScenarios = []formatScenario{ input: "name: Miles O'Brien", expected: `name='Miles O'"'"'Brien'` + "\n", }, + { + description: "Encode shell variables: custom separator", + subdescription: "Use --shell-key-separator to specify a custom separator between keys. This is useful when the original keys contain underscores.", + input: "" + + "my_app:" + "\n" + + " db_config:" + "\n" + + " host: localhost" + "\n" + + " port: 5432", + expected: "" + + "my_app__db_config__host=localhost" + "\n" + + "my_app__db_config__port=5432" + "\n", + scenarioType: "shell-separator", + }, } func TestShellVariableScenarios(t *testing.T) { for _, s := range shellVariablesScenarios { //fmt.Printf("\t<%s> <%s>\n", s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder())) - test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()), s.description) + if s.scenarioType == "shell-separator" { + // Save and restore the original separator + originalSeparator := ConfiguredShellVariablesPreferences.KeySeparator + ConfiguredShellVariablesPreferences.KeySeparator = "__" + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()), s.description) + ConfiguredShellVariablesPreferences.KeySeparator = originalSeparator + } else { + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()), s.description) + } } genericScenarios := make([]interface{}, len(shellVariablesScenarios)) for i, s := range shellVariablesScenarios { @@ -87,12 +108,22 @@ func documentShellVariableScenario(_ *testing.T, w *bufio.Writer, i interface{}) expression := s.expression - if expression != "" { + if s.scenarioType == "shell-separator" { + writeOrPanic(w, "```bash\nyq -o=shell --shell-key-separator=\"__\" sample.yml\n```\n") + } else if expression != "" { writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=shell '%v' sample.yml\n```\n", expression)) } else { writeOrPanic(w, "```bash\nyq -o=shell sample.yml\n```\n") } writeOrPanic(w, "will output\n") - writeOrPanic(w, fmt.Sprintf("```sh\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()))) + if s.scenarioType == "shell-separator" { + // Save and restore the original separator + originalSeparator := ConfiguredShellVariablesPreferences.KeySeparator + ConfiguredShellVariablesPreferences.KeySeparator = "__" + writeOrPanic(w, fmt.Sprintf("```sh\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()))) + ConfiguredShellVariablesPreferences.KeySeparator = originalSeparator + } else { + writeOrPanic(w, fmt.Sprintf("```sh\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()))) + } }