Repository: score-spec/score-compose
Pull request #37 (2023-07-23 15:56:31)

Fix: Do not substitute strings without brackets
Fix to pass through strings without brackets, e.g. `$abc` -> `$abc`.

#### Types of changes
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] New chore (expected functionality to be implemented)

#### Checklist:
- [ ] My change requires a change to the documentation.
  - [ ] I have updated the documentation accordingly.
- [x] I've signed off with an email address that matches the commit author.



Patch:
diff --git a/internal/compose/templates.go b/internal/compose/templates.go
index 7f222bd..66e1173 100644
--- a/internal/compose/templates.go
+++ b/internal/compose/templates.go
@@ -10,7 +10,7 @@ package compose
 import (
 	"fmt"
 	"log"
-	"os"
+	"regexp"
 	"strings"
 
 	"github.com/mitchellh/mapstructure"
@@ -18,6 +18,10 @@ import (
 	score "github.com/score-spec/score-go/types"
 )
 
+var (
+	placeholderRegEx = regexp.MustCompile(`\$(\$|{([a-zA-Z0-9.\-_\[\]"'#]+)})`)
+)
+
 // templatesContext ia an utility type that provides a context for '${...}' templates substitution
 type templatesContext struct {
 	meta      map[string]interface{}
@@ -49,22 +53,30 @@ func buildContext(metadata score.WorkloadMeta, resources score.ResourcesSpecs) (
 
 // Substitute replaces all matching '${...}' templates in a source string
 func (ctx *templatesContext) Substitute(src string) string {
-	return os.Expand(src, ctx.mapVar)
+	return placeholderRegEx.ReplaceAllStringFunc(src, func(str string) string {
+		// WORKAROUND: ReplaceAllStringFunc(..) does not provide match details
+		//             https://github.com/golang/go/issues/5690
+		var matches = placeholderRegEx.FindStringSubmatch(str)
+
+		// SANITY CHECK
+		if len(matches) != 3 {
+			log.Printf("Error: could not find a proper match in previously captured string fragment")
+			return src
+		}
+
+		// EDGE CASE: Captures "$$" sequences and empty templates "${}"
+		if matches[2] == "" {
+			return matches[1]
+		}
+
+		return ctx.mapVar(matches[2])
+	})
 }
 
 // MapVar replaces objects and properties references with corresponding values
 // Returns an empty string if the reference can't be resolved
 func (ctx *templatesContext) mapVar(ref string) string {
-	if ref == "" {
-		return ""
-	}
-
-	// NOTE: os.Expand(..) would invoke a callback function with "$" as an argument for escaped sequences.
-	//       "$${abc}" is treated as "$$" pattern and "{abc}" static text.
-	//       The first segment (pattern) would trigger a callback function call.
-	//       By returning "$" value we would ensure that escaped sequences would remain in the source text.
-	//       For example "$${abc}" would result in "${abc}" after os.Expand(..) call.
-	if ref == "$" {
+	if ref == "" || ref == "$" {
 		return ref
 	}
 


Test patch:
diff --git a/internal/compose/templates_test.go b/internal/compose/templates_test.go
index 84ab2c0..03a0caf 100644
--- a/internal/compose/templates_test.go
+++ b/internal/compose/templates_test.go
@@ -71,6 +71,7 @@ func TestSubstitute(t *testing.T) {
 
 	assert.Equal(t, "", ctx.Substitute(""))
 	assert.Equal(t, "abc", ctx.Substitute("abc"))
+	assert.Equal(t, "$abc", ctx.Substitute("$abc"))
 	assert.Equal(t, "abc $ abc", ctx.Substitute("abc $$ abc"))
 	assert.Equal(t, "${abc}", ctx.Substitute("$${abc}"))