Add --shell-key-separator flag for customizable shell output format

- Add ShellVariablesPreferences struct with KeySeparator field (default: '_')
- Update shellVariablesEncoder to use configurable separator
- Add --shell-key-separator CLI flag
- Add comprehensive tests for custom separator functionality
- Update documentation with example usage for custom separator

This feature allows users to specify a custom separator (e.g. '__') when
outputting shell variables, which helps disambiguate nested keys from
keys that contain underscores in their names.

Example:
  yq -o=shell --shell-key-separator='__' file.yaml

Fixes ambiguity when original YAML keys contain underscores.
This commit is contained in:
Robert Lee 2025-10-13 10:05:12 -04:00 committed by Mike Farah
parent 1228bcfa75
commit 1f2b0fe76b
6 changed files with 125 additions and 8 deletions

View File

@ -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.")

View File

@ -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
```

View File

@ -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 {

View File

@ -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")
}

View File

@ -0,0 +1,14 @@
package yqlib
type ShellVariablesPreferences struct {
KeySeparator string
}
func NewDefaultShellVariablesPreferences() ShellVariablesPreferences {
return ShellVariablesPreferences{
KeySeparator: "_",
}
}
var ConfiguredShellVariablesPreferences = NewDefaultShellVariablesPreferences()

View File

@ -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())))
}
}