Use a lazy-quoting @sh encoder (#1548)

* Use a lazy-quoting @sh encoder

* Add internal quoting style switch to @sh

* Add test for stray empty quotes in @sh
This commit is contained in:
Vít Zikmund 2023-02-09 08:15:07 +01:00 committed by GitHub
parent f64f73a0ce
commit 93b7c999be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 54 additions and 9 deletions

View File

@ -478,7 +478,7 @@ yq '.coolData | @sh' sample.yml
```
will output
```yaml
'strings with spaces and a \'quote\''
strings' with spaces and a '\'quote\'
```
## Decode a base64 encoded string

View File

@ -9,13 +9,14 @@ import (
yaml "gopkg.in/yaml.v3"
)
var pattern = regexp.MustCompile(`[^\w@%+=:,./-]`)
var unsafeChars = regexp.MustCompile(`[^\w@%+=:,./-]`)
type shEncoder struct {
quoteAll bool
}
func NewShEncoder() Encoder {
return &shEncoder{}
return &shEncoder{false}
}
func (e *shEncoder) CanHandleAliases() bool {
@ -36,9 +37,43 @@ func (e *shEncoder) Encode(writer io.Writer, originalNode *yaml.Node) error {
return fmt.Errorf("cannot encode %v as URI, can only operate on strings. Please first pipe through another encoding operator to convert the value to a string", node.Tag)
}
value := originalNode.Value
if pattern.MatchString(value) {
value = "'" + strings.ReplaceAll(value, "'", "\\'") + "'"
}
return writeString(writer, value)
return writeString(writer, e.encode(originalNode.Value))
}
// put any (shell-unsafe) characters into a single-quoted block, close the block lazily
func (e *shEncoder) encode(input string) string {
const quote = '\''
var inQuoteBlock = false
var encoded strings.Builder
encoded.Grow(len(input))
for _, ir := range input {
// open or close a single-quote block
if ir == quote {
if inQuoteBlock {
// get out of a quote block for an input quote
encoded.WriteRune(quote)
inQuoteBlock = !inQuoteBlock
}
// escape the quote with a backslash
encoded.WriteRune('\\')
} else {
if e.shouldQuote(ir) && !inQuoteBlock {
// start a quote block for any (unsafe) characters
encoded.WriteRune(quote)
inQuoteBlock = !inQuoteBlock
}
}
// pass on the input character
encoded.WriteRune(ir)
}
// close any pending quote block
if inQuoteBlock {
encoded.WriteRune(quote)
}
return encoded.String()
}
func (e *shEncoder) shouldQuote(ir rune) bool {
return e.quoteAll || unsafeChars.MatchString(string(ir))
}

View File

@ -263,9 +263,19 @@ var encoderDecoderOperatorScenarios = []expressionScenario{
document: "coolData: strings with spaces and a 'quote'",
expression: ".coolData | @sh",
expected: []string{
"D0, P[coolData], (!!str)::'strings with spaces and a \\'quote\\''\n",
"D0, P[coolData], (!!str)::strings' with spaces and a '\\'quote\\'\n",
},
},
{
description: "Encode a string to sh",
subdescription: "Watch out for stray '' (empty strings)",
document: "coolData: \"'starts, contains more '' and ends with a quote'\"",
expression: ".coolData | @sh",
expected: []string{
"D0, P[coolData], (!!str)::\\'starts,' contains more '\\'\\'' and ends with a quote'\\'\n",
},
skipDoc: true,
},
{
description: "Decode a base64 encoded string",
subdescription: "Decoded data is assumed to be a string.",