Skip to content

Commit 1f2b0fe

Browse files
rsleedbxmikefarah
authored andcommitted
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.
1 parent 1228bcf commit 1f2b0fe

File tree

6 files changed

+125
-8
lines changed

6 files changed

+125
-8
lines changed

cmd/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ yq -P -oy sample.json
168168
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "properties-separator", yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "separator to use between keys and values")
169169
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "properties-array-brackets", yqlib.ConfiguredPropertiesPreferences.UseArrayBrackets, "use [x] in array paths (e.g. for SpringBoot)")
170170

171+
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredShellVariablesPreferences.KeySeparator, "shell-key-separator", yqlib.ConfiguredShellVariablesPreferences.KeySeparator, "separator for shell variable key paths")
172+
if err = rootCmd.RegisterFlagCompletionFunc("shell-key-separator", cobra.NoFileCompletions); err != nil {
173+
panic(err)
174+
}
175+
171176
rootCmd.PersistentFlags().BoolVar(&yqlib.StringInterpolationEnabled, "string-interpolation", yqlib.StringInterpolationEnabled, "Toggles strings interpolation of \\(exp)")
172177

173178
rootCmd.PersistentFlags().BoolVarP(&nullInput, "null-input", "n", false, "Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.")

pkg/yqlib/doc/usage/shellvariables.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,23 @@ will output
8484
name='Miles O'"'"'Brien'
8585
```
8686

87+
## Encode shell variables: custom separator
88+
Use --shell-key-separator to specify a custom separator between keys. This is useful when the original keys contain underscores.
89+
90+
Given a sample.yml file of:
91+
```yaml
92+
my_app:
93+
db_config:
94+
host: localhost
95+
port: 5432
96+
```
97+
then
98+
```bash
99+
yq -o=shell --shell-key-separator="__" sample.yml
100+
```
101+
will output
102+
```sh
103+
my_app__db_config__host=localhost
104+
my_app__db_config__port=5432
105+
```
106+

pkg/yqlib/encoder_shellvariables.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import (
1212
)
1313

1414
type shellVariablesEncoder struct {
15+
prefs ShellVariablesPreferences
1516
}
1617

1718
func NewShellVariablesEncoder() Encoder {
18-
return &shellVariablesEncoder{}
19+
return &shellVariablesEncoder{
20+
prefs: ConfiguredShellVariablesPreferences,
21+
}
1922
}
2023

2124
func (pe *shellVariablesEncoder) CanHandleAliases() bool {
@@ -58,7 +61,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat
5861
return err
5962
case SequenceNode:
6063
for index, child := range node.Content {
61-
err := pe.doEncode(w, child, appendPath(path, index))
64+
err := pe.doEncode(w, child, pe.appendPath(path, index))
6265
if err != nil {
6366
return err
6467
}
@@ -68,7 +71,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat
6871
for index := 0; index < len(node.Content); index = index + 2 {
6972
key := node.Content[index]
7073
value := node.Content[index+1]
71-
err := pe.doEncode(w, value, appendPath(path, key.Value))
74+
err := pe.doEncode(w, value, pe.appendPath(path, key.Value))
7275
if err != nil {
7376
return err
7477
}
@@ -81,7 +84,7 @@ func (pe *shellVariablesEncoder) doEncode(w *io.Writer, node *CandidateNode, pat
8184
}
8285
}
8386

84-
func appendPath(cookedPath string, rawKey interface{}) string {
87+
func (pe *shellVariablesEncoder) appendPath(cookedPath string, rawKey interface{}) string {
8588

8689
// Shell variable names must match
8790
// [a-zA-Z_]+[a-zA-Z0-9_]*
@@ -126,7 +129,7 @@ func appendPath(cookedPath string, rawKey interface{}) string {
126129
}
127130
return key
128131
}
129-
return cookedPath + "_" + key
132+
return cookedPath + pe.prefs.KeySeparator + key
130133
}
131134

132135
func quoteValue(value string) string {

pkg/yqlib/encoder_shellvariables_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,47 @@ func TestShellVariablesEncoderEmptyMap(t *testing.T) {
9191
func TestShellVariablesEncoderScalarNode(t *testing.T) {
9292
assertEncodesTo(t, "some string", "value='some string'")
9393
}
94+
95+
func assertEncodesToWithSeparator(t *testing.T, yaml string, shellvars string, separator string) {
96+
var output bytes.Buffer
97+
writer := bufio.NewWriter(&output)
98+
99+
// Save the original separator
100+
originalSeparator := ConfiguredShellVariablesPreferences.KeySeparator
101+
defer func() {
102+
ConfiguredShellVariablesPreferences.KeySeparator = originalSeparator
103+
}()
104+
105+
// Set the custom separator
106+
ConfiguredShellVariablesPreferences.KeySeparator = separator
107+
108+
var encoder = NewShellVariablesEncoder()
109+
inputs, err := readDocuments(strings.NewReader(yaml), "test.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
110+
if err != nil {
111+
panic(err)
112+
}
113+
node := inputs.Front().Value.(*CandidateNode)
114+
err = encoder.Encode(writer, node)
115+
if err != nil {
116+
panic(err)
117+
}
118+
writer.Flush()
119+
120+
test.AssertResult(t, shellvars, strings.TrimSuffix(output.String(), "\n"))
121+
}
122+
123+
func TestShellVariablesEncoderCustomSeparator(t *testing.T) {
124+
assertEncodesToWithSeparator(t, "a:\n b: Lewis\n c: Carroll", "a__b=Lewis\na__c=Carroll", "__")
125+
}
126+
127+
func TestShellVariablesEncoderCustomSeparatorNested(t *testing.T) {
128+
assertEncodesToWithSeparator(t, "my_app:\n db_config:\n host: localhost", "my_app__db_config__host=localhost", "__")
129+
}
130+
131+
func TestShellVariablesEncoderCustomSeparatorArray(t *testing.T) {
132+
assertEncodesToWithSeparator(t, "a: [{n: Alice}, {n: Bob}]", "a__0__n=Alice\na__1__n=Bob", "__")
133+
}
134+
135+
func TestShellVariablesEncoderCustomSeparatorSingleChar(t *testing.T) {
136+
assertEncodesToWithSeparator(t, "a:\n b: value", "aXb=value", "X")
137+
}

pkg/yqlib/shellvariables.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package yqlib
2+
3+
type ShellVariablesPreferences struct {
4+
KeySeparator string
5+
}
6+
7+
func NewDefaultShellVariablesPreferences() ShellVariablesPreferences {
8+
return ShellVariablesPreferences{
9+
KeySeparator: "_",
10+
}
11+
}
12+
13+
var ConfiguredShellVariablesPreferences = NewDefaultShellVariablesPreferences()
14+

pkg/yqlib/shellvariables_test.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,33 @@ var shellVariablesScenarios = []formatScenario{
5454
input: "name: Miles O'Brien",
5555
expected: `name='Miles O'"'"'Brien'` + "\n",
5656
},
57+
{
58+
description: "Encode shell variables: custom separator",
59+
subdescription: "Use --shell-key-separator to specify a custom separator between keys. This is useful when the original keys contain underscores.",
60+
input: "" +
61+
"my_app:" + "\n" +
62+
" db_config:" + "\n" +
63+
" host: localhost" + "\n" +
64+
" port: 5432",
65+
expected: "" +
66+
"my_app__db_config__host=localhost" + "\n" +
67+
"my_app__db_config__port=5432" + "\n",
68+
scenarioType: "shell-separator",
69+
},
5770
}
5871

5972
func TestShellVariableScenarios(t *testing.T) {
6073
for _, s := range shellVariablesScenarios {
6174
//fmt.Printf("\t<%s> <%s>\n", s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()))
62-
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()), s.description)
75+
if s.scenarioType == "shell-separator" {
76+
// Save and restore the original separator
77+
originalSeparator := ConfiguredShellVariablesPreferences.KeySeparator
78+
ConfiguredShellVariablesPreferences.KeySeparator = "__"
79+
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()), s.description)
80+
ConfiguredShellVariablesPreferences.KeySeparator = originalSeparator
81+
} else {
82+
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder()), s.description)
83+
}
6384
}
6485
genericScenarios := make([]interface{}, len(shellVariablesScenarios))
6586
for i, s := range shellVariablesScenarios {
@@ -87,12 +108,22 @@ func documentShellVariableScenario(_ *testing.T, w *bufio.Writer, i interface{})
87108

88109
expression := s.expression
89110

90-
if expression != "" {
111+
if s.scenarioType == "shell-separator" {
112+
writeOrPanic(w, "```bash\nyq -o=shell --shell-key-separator=\"__\" sample.yml\n```\n")
113+
} else if expression != "" {
91114
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=shell '%v' sample.yml\n```\n", expression))
92115
} else {
93116
writeOrPanic(w, "```bash\nyq -o=shell sample.yml\n```\n")
94117
}
95118
writeOrPanic(w, "will output\n")
96119

97-
writeOrPanic(w, fmt.Sprintf("```sh\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder())))
120+
if s.scenarioType == "shell-separator" {
121+
// Save and restore the original separator
122+
originalSeparator := ConfiguredShellVariablesPreferences.KeySeparator
123+
ConfiguredShellVariablesPreferences.KeySeparator = "__"
124+
writeOrPanic(w, fmt.Sprintf("```sh\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder())))
125+
ConfiguredShellVariablesPreferences.KeySeparator = originalSeparator
126+
} else {
127+
writeOrPanic(w, fmt.Sprintf("```sh\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewShellVariablesEncoder())))
128+
}
98129
}

0 commit comments

Comments
 (0)