Add --properties-separator option (#1951)

This commit adds the --properties-separator option, which lets users
specify the separator used between keys and values in the properties
output format. This is done by adjusting the value of
github.com/magiconair/properties#Properties.WriteSeparator at encode
time.

Some refactoring of the properties encoder unit tests was done to make
it easier to write unit tests that include different separator values.

Fixes: #1864

Signed-off-by: Ryan Drew <ryan.drew@isovalent.com>
This commit is contained in:
Ryan Drew 2024-02-19 16:57:44 -07:00 committed by GitHub
parent 9a8151d316
commit 2865022cf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 208 additions and 76 deletions

View File

@ -120,6 +120,8 @@ yq -P -oy sample.json
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.UnquotedKeys, "lua-unquoted", yqlib.ConfiguredLuaPreferences.UnquotedKeys, "output unquoted string keys (e.g. {foo=\"bar\"})") rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.UnquotedKeys, "lua-unquoted", yqlib.ConfiguredLuaPreferences.UnquotedKeys, "output unquoted string keys (e.g. {foo=\"bar\"})")
rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.Globals, "lua-globals", yqlib.ConfiguredLuaPreferences.Globals, "output keys as top-level global variables") rootCmd.PersistentFlags().BoolVar(&yqlib.ConfiguredLuaPreferences.Globals, "lua-globals", yqlib.ConfiguredLuaPreferences.Globals, "output keys as top-level global variables")
rootCmd.PersistentFlags().StringVar(&yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "properties-separator", yqlib.ConfiguredPropertiesPreferences.KeyValueSeparator, "separator to use between keys and values")
rootCmd.PersistentFlags().BoolVarP(&nullInput, "null-input", "n", false, "Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.") rootCmd.PersistentFlags().BoolVarP(&nullInput, "null-input", "n", false, "Don't read input, simply evaluate the expression given. Useful for creating docs from scratch.")
rootCmd.PersistentFlags().BoolVarP(&noDocSeparators, "no-doc", "N", false, "Don't print document separators (---)") rootCmd.PersistentFlags().BoolVarP(&noDocSeparators, "no-doc", "N", false, "Don't print document separators (---)")

View File

@ -187,7 +187,7 @@ func createEncoder(format yqlib.PrinterOutputFormat) (yqlib.Encoder, error) {
case yqlib.JSONOutputFormat: case yqlib.JSONOutputFormat:
return yqlib.NewJSONEncoder(indent, colorsEnabled, unwrapScalar), nil return yqlib.NewJSONEncoder(indent, colorsEnabled, unwrapScalar), nil
case yqlib.PropsOutputFormat: case yqlib.PropsOutputFormat:
return yqlib.NewPropertiesEncoder(unwrapScalar), nil return yqlib.NewPropertiesEncoder(unwrapScalar, yqlib.ConfiguredPropertiesPreferences), nil
case yqlib.CSVOutputFormat: case yqlib.CSVOutputFormat:
return yqlib.NewCsvEncoder(yqlib.ConfiguredCsvPreferences), nil return yqlib.NewCsvEncoder(yqlib.ConfiguredCsvPreferences), nil
case yqlib.TSVOutputFormat: case yqlib.TSVOutputFormat:

View File

@ -120,6 +120,36 @@ emptyArray =
emptyMap = emptyMap =
``` ```
## Encode properties: use custom separator
Provide a custom key-value separator using the `--properties-separator` flag.
Given a sample.yml file of:
```yaml
# block comments come through
person: # neither do comments on maps
name: Mike Wazowski # comments on values appear
pets:
- cat # comments on array values appear
food: [pizza] # comments on arrays do not
emptyArray: []
emptyMap: []
```
then
```bash
yq -o props --properties-separator=";" sample.yml
```
will output
```properties
# block comments come through
# comments on values appear
person.name;Mike Wazowski
# comments on array values appear
person.pets.0;cat
person.food.0;pizza
```
## Decode properties ## Decode properties
Given a sample.properties file of: Given a sample.properties file of:
```properties ```properties

View File

@ -12,11 +12,13 @@ import (
type propertiesEncoder struct { type propertiesEncoder struct {
unwrapScalar bool unwrapScalar bool
prefs PropertiesPreferences
} }
func NewPropertiesEncoder(unwrapScalar bool) Encoder { func NewPropertiesEncoder(unwrapScalar bool, prefs PropertiesPreferences) Encoder {
return &propertiesEncoder{ return &propertiesEncoder{
unwrapScalar: unwrapScalar, unwrapScalar: unwrapScalar,
prefs: prefs,
} }
} }
@ -69,6 +71,7 @@ func (pe *propertiesEncoder) Encode(writer io.Writer, node *CandidateNode) error
mapKeysToStrings(node) mapKeysToStrings(node)
p := properties.NewProperties() p := properties.NewProperties()
p.WriteSeparator = pe.prefs.KeyValueSeparator
err := pe.doEncode(p, node, "", nil) err := pe.doEncode(p, node, "", nil)
if err != nil { if err != nil {
return err return err

View File

@ -3,17 +3,60 @@ package yqlib
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"fmt"
"strings" "strings"
"testing" "testing"
"github.com/mikefarah/yq/v4/test" "github.com/mikefarah/yq/v4/test"
) )
func yamlToProps(sampleYaml string, unwrapScalar bool) string { type keyValuePair struct {
key string
value string
comment string
}
func (kv *keyValuePair) String(unwrap bool, sep string) string {
builder := strings.Builder{}
if kv.comment != "" {
builder.WriteString(kv.comment)
builder.WriteString("\n")
}
builder.WriteString(kv.key)
builder.WriteString(sep)
if unwrap {
builder.WriteString(kv.value)
} else {
builder.WriteString("\"")
builder.WriteString(kv.value)
builder.WriteString("\"")
}
return builder.String()
}
type testProperties struct {
pairs []keyValuePair
}
func (tp *testProperties) String(unwrap bool, sep string) string {
kvs := []string{}
for _, kv := range tp.pairs {
kvs = append(kvs, kv.String(unwrap, sep))
}
return strings.Join(kvs, "\n")
}
func yamlToProps(sampleYaml string, unwrapScalar bool, separator string) string {
var output bytes.Buffer var output bytes.Buffer
writer := bufio.NewWriter(&output) writer := bufio.NewWriter(&output)
var propsEncoder = NewPropertiesEncoder(unwrapScalar) var propsEncoder = NewPropertiesEncoder(unwrapScalar, PropertiesPreferences{KeyValueSeparator: separator})
inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences)) inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
if err != nil { if err != nil {
panic(err) panic(err)
@ -28,80 +71,97 @@ func yamlToProps(sampleYaml string, unwrapScalar bool) string {
return strings.TrimSuffix(output.String(), "\n") return strings.TrimSuffix(output.String(), "\n")
} }
func TestPropertiesEncoderSimple_Unwrapped(t *testing.T) { func doTest(t *testing.T, sampleYaml string, props testProperties, testUnwrapped, testWrapped bool) {
wraps := []bool{}
if testUnwrapped {
wraps = append(wraps, true)
}
if testWrapped {
wraps = append(wraps, false)
}
for _, unwrap := range wraps {
fmt.Println(props)
fmt.Println(unwrap)
for _, sep := range []string{" = ", ";", "=", " "} {
var actualProps = yamlToProps(sampleYaml, unwrap, sep)
test.AssertResult(t, props.String(unwrap, sep), actualProps)
}
}
}
func TestPropertiesEncoderSimple(t *testing.T) {
var sampleYaml = `a: 'bob cool'` var sampleYaml = `a: 'bob cool'`
var expectedProps = `a = bob cool` doTest(
var actualProps = yamlToProps(sampleYaml, true) t, sampleYaml,
test.AssertResult(t, expectedProps, actualProps) testProperties{
pairs: []keyValuePair{
{
key: "a",
value: "bob cool",
},
},
},
true, true,
)
} }
func TestPropertiesEncoderSimple_Wrapped(t *testing.T) { func TestPropertiesEncoderSimpleWithComments(t *testing.T) {
var sampleYaml = `a: 'bob cool'`
var expectedProps = `a = "bob cool"`
var actualProps = yamlToProps(sampleYaml, false)
test.AssertResult(t, expectedProps, actualProps)
}
func TestPropertiesEncoderSimpleWithComments_Unwrapped(t *testing.T) {
var sampleYaml = `a: 'bob cool' # line` var sampleYaml = `a: 'bob cool' # line`
var expectedProps = `# line doTest(
a = bob cool` t, sampleYaml,
var actualProps = yamlToProps(sampleYaml, true) testProperties{
test.AssertResult(t, expectedProps, actualProps) pairs: []keyValuePair{
{
key: "a",
value: "bob cool",
comment: "# line",
},
},
},
true, true,
)
} }
func TestPropertiesEncoderSimpleWithComments_Wrapped(t *testing.T) { func TestPropertiesEncoderDeep(t *testing.T) {
var sampleYaml = `a: 'bob cool' # line`
var expectedProps = `# line
a = "bob cool"`
var actualProps = yamlToProps(sampleYaml, false)
test.AssertResult(t, expectedProps, actualProps)
}
func TestPropertiesEncoderDeep_Unwrapped(t *testing.T) {
var sampleYaml = `a: var sampleYaml = `a:
b: "bob cool" b: "bob cool"
` `
var expectedProps = `a.b = bob cool` doTest(
var actualProps = yamlToProps(sampleYaml, true) t, sampleYaml,
test.AssertResult(t, expectedProps, actualProps) testProperties{
pairs: []keyValuePair{
{
key: "a.b",
value: "bob cool",
},
},
},
true, true,
)
} }
func TestPropertiesEncoderDeep_Wrapped(t *testing.T) { func TestPropertiesEncoderDeepWithComments(t *testing.T) {
var sampleYaml = `a:
b: "bob cool"
`
var expectedProps = `a.b = "bob cool"`
var actualProps = yamlToProps(sampleYaml, false)
test.AssertResult(t, expectedProps, actualProps)
}
func TestPropertiesEncoderDeepWithComments_Unwrapped(t *testing.T) {
var sampleYaml = `a: # a thing var sampleYaml = `a: # a thing
b: "bob cool" # b thing b: "bob cool" # b thing
` `
var expectedProps = `# b thing doTest(
a.b = bob cool` t, sampleYaml,
var actualProps = yamlToProps(sampleYaml, true) testProperties{
test.AssertResult(t, expectedProps, actualProps) pairs: []keyValuePair{
} {
key: "a.b",
func TestPropertiesEncoderDeepWithComments_Wrapped(t *testing.T) { value: "bob cool",
var sampleYaml = `a: # a thing comment: "# b thing",
b: "bob cool" # b thing },
` },
},
var expectedProps = `# b thing true, true,
a.b = "bob cool"` )
var actualProps = yamlToProps(sampleYaml, false)
test.AssertResult(t, expectedProps, actualProps)
} }
func TestPropertiesEncoderArray_Unwrapped(t *testing.T) { func TestPropertiesEncoderArray_Unwrapped(t *testing.T) {
@ -109,19 +169,43 @@ func TestPropertiesEncoderArray_Unwrapped(t *testing.T) {
b: [{c: dog}, {c: cat}] b: [{c: dog}, {c: cat}]
` `
var expectedProps = `a.b.0.c = dog doTest(
a.b.1.c = cat` t, sampleYaml,
var actualProps = yamlToProps(sampleYaml, true) testProperties{
test.AssertResult(t, expectedProps, actualProps) pairs: []keyValuePair{
{
key: "a.b.0.c",
value: "dog",
},
{
key: "a.b.1.c",
value: "cat",
},
},
},
true, false,
)
} }
func TestPropertiesEncoderArray_Wrapped(t *testing.T) { func TestPropertiesEncoderArray_Wrapped(t *testing.T) {
var sampleYaml = `a: var sampleYaml = `a:
b: [{c: dog named jim}, {c: cat named jam}] b: [{c: dog named jim}, {c: cat named jim}]
` `
var expectedProps = `a.b.0.c = "dog named jim" doTest(
a.b.1.c = "cat named jam"` t, sampleYaml,
var actualProps = yamlToProps(sampleYaml, false) testProperties{
test.AssertResult(t, expectedProps, actualProps) pairs: []keyValuePair{
{
key: "a.b.0.c",
value: "dog named jim",
},
{
key: "a.b.1.c",
value: "cat named jim",
},
},
},
false, true,
)
} }

View File

@ -14,7 +14,7 @@ func configureEncoder(format PrinterOutputFormat, indent int) Encoder {
case JSONOutputFormat: case JSONOutputFormat:
return NewJSONEncoder(indent, false, false) return NewJSONEncoder(indent, false, false)
case PropsOutputFormat: case PropsOutputFormat:
return NewPropertiesEncoder(true) return NewPropertiesEncoder(true, ConfiguredPropertiesPreferences)
case CSVOutputFormat: case CSVOutputFormat:
return NewCsvEncoder(ConfiguredCsvPreferences) return NewCsvEncoder(ConfiguredCsvPreferences)
case TSVOutputFormat: case TSVOutputFormat:

13
pkg/yqlib/properties.go Normal file
View File

@ -0,0 +1,13 @@
package yqlib
type PropertiesPreferences struct {
KeyValueSeparator string
}
func NewDefaultPropertiesPreferences() PropertiesPreferences {
return PropertiesPreferences{
KeyValueSeparator: " = ",
}
}
var ConfiguredPropertiesPreferences = NewDefaultPropertiesPreferences()

View File

@ -272,7 +272,7 @@ func documentUnwrappedEncodePropertyScenario(w *bufio.Writer, s formatScenario)
} }
writeOrPanic(w, "will output\n") writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(true)))) writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(true, ConfiguredPropertiesPreferences))))
} }
func documentWrappedEncodePropertyScenario(w *bufio.Writer, s formatScenario) { func documentWrappedEncodePropertyScenario(w *bufio.Writer, s formatScenario) {
@ -297,7 +297,7 @@ func documentWrappedEncodePropertyScenario(w *bufio.Writer, s formatScenario) {
} }
writeOrPanic(w, "will output\n") writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(false)))) writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(false, ConfiguredPropertiesPreferences))))
} }
func documentDecodePropertyScenario(w *bufio.Writer, s formatScenario) { func documentDecodePropertyScenario(w *bufio.Writer, s formatScenario) {
@ -347,7 +347,7 @@ func documentRoundTripPropertyScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, "will output\n") writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", mustProcessFormatScenario(s, NewPropertiesDecoder(), NewPropertiesEncoder(true)))) writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", mustProcessFormatScenario(s, NewPropertiesDecoder(), NewPropertiesEncoder(true, ConfiguredPropertiesPreferences))))
} }
func documentPropertyScenario(_ *testing.T, w *bufio.Writer, i interface{}) { func documentPropertyScenario(_ *testing.T, w *bufio.Writer, i interface{}) {
@ -374,13 +374,13 @@ func TestPropertyScenarios(t *testing.T) {
for _, s := range propertyScenarios { for _, s := range propertyScenarios {
switch s.scenarioType { switch s.scenarioType {
case "": case "":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(true)), s.description) test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(true, ConfiguredPropertiesPreferences)), s.description)
case "decode": case "decode":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(2, false, ConfiguredYamlPreferences)), s.description) test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewPropertiesDecoder(), NewYamlEncoder(2, false, ConfiguredYamlPreferences)), s.description)
case "encode-wrapped": case "encode-wrapped":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(false)), s.description) test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewPropertiesEncoder(false, ConfiguredPropertiesPreferences)), s.description)
case "roundtrip": case "roundtrip":
test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewPropertiesDecoder(), NewPropertiesEncoder(true)), s.description) test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewPropertiesDecoder(), NewPropertiesEncoder(true, ConfiguredPropertiesPreferences)), s.description)
default: default:
panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType)) panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType))