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 will output
```yaml ```yaml
'strings with spaces and a \'quote\'' strings' with spaces and a '\'quote\'
``` ```
## Decode a base64 encoded string ## Decode a base64 encoded string

View File

@ -9,13 +9,14 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
var pattern = regexp.MustCompile(`[^\w@%+=:,./-]`) var unsafeChars = regexp.MustCompile(`[^\w@%+=:,./-]`)
type shEncoder struct { type shEncoder struct {
quoteAll bool
} }
func NewShEncoder() Encoder { func NewShEncoder() Encoder {
return &shEncoder{} return &shEncoder{false}
} }
func (e *shEncoder) CanHandleAliases() bool { 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) 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 return writeString(writer, e.encode(originalNode.Value))
if pattern.MatchString(value) { }
value = "'" + strings.ReplaceAll(value, "'", "\\'") + "'"
} // put any (shell-unsafe) characters into a single-quoted block, close the block lazily
return writeString(writer, value) 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'", document: "coolData: strings with spaces and a 'quote'",
expression: ".coolData | @sh", expression: ".coolData | @sh",
expected: []string{ 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", description: "Decode a base64 encoded string",
subdescription: "Decoded data is assumed to be a string.", subdescription: "Decoded data is assumed to be a string.",