diff --git a/doc/stacker_yaml.md b/doc/stacker_yaml.md index 450170d4..62ba9103 100644 --- a/doc/stacker_yaml.md +++ b/doc/stacker_yaml.md @@ -352,6 +352,9 @@ substitution file. An error will be printed that explains how to rewrite it: error "A=B" was provided as a substitution and unsupported placeholder "${A}" was found. Replace "${A}" with "${{A}}" to use the substitution. +Shell variables whose names only start with a provided key, such as `$FOO_BAR` +when `FOO` is provided, are not placeholders for that key and are left alone. + Substitutions can also be specified in a yaml file given with the argument `--substitute-file`, with any number of key: value pairs: diff --git a/pkg/types/stackerfile.go b/pkg/types/stackerfile.go index f19de39e..a42bb0d1 100644 --- a/pkg/types/stackerfile.go +++ b/pkg/types/stackerfile.go @@ -77,14 +77,28 @@ func substitute(content string, substitutions []string) (string, error) { nobracket := fmt.Sprintf("$%s", membs[0]) onebracket := fmt.Sprintf("${%s}", membs[0]) bad_content := content - for _, unsupp_placeholder := range []string{nobracket, onebracket} { - if strings.Contains(content, unsupp_placeholder) { - msg := fmt.Sprintf("%q was provided as a substitution "+ - "and unsupported placeholder %q was found. "+ - "Replace %q with \"${{%s}}\" to use the substitution.\n", subst, - unsupp_placeholder, unsupp_placeholder, membs[0]) - unsupported_messages = append(unsupported_messages, msg) - } + + // $FOO is only a placeholder for FOO if it isn't followed by + // another character of a variable name, e.g. $FOO_BAR is not. + nobracket_re, err := regexp.Compile(regexp.QuoteMeta(nobracket) + `([^A-Za-z0-9_]|$)`) + if err != nil { + return "", err + } + + unsupp_placeholders := []string{} + if nobracket_re.MatchString(content) { + unsupp_placeholders = append(unsupp_placeholders, nobracket) + } + if strings.Contains(content, onebracket) { + unsupp_placeholders = append(unsupp_placeholders, onebracket) + } + + for _, unsupp_placeholder := range unsupp_placeholders { + msg := fmt.Sprintf("%q was provided as a substitution "+ + "and unsupported placeholder %q was found. "+ + "Replace %q with \"${{%s}}\" to use the substitution.\n", subst, + unsupp_placeholder, unsupp_placeholder, membs[0]) + unsupported_messages = append(unsupported_messages, msg) } // the ${FOO:bar} syntax never worked! but since we documented it diff --git a/pkg/types/types_test.go b/pkg/types/types_test.go index c741150f..45c5d879 100644 --- a/pkg/types/types_test.go +++ b/pkg/types/types_test.go @@ -108,6 +108,27 @@ func TestSubstitute(t *testing.T) { t.Fatalf("substitution with unsupported placeholders should have failed but didn't") } + // variables which merely start with the name of a provided + // substitution, e.g. $VERSION_ID for VERSION, are not placeholders + s = "$VERSION_ID $VERSIONS ${VERSION_ID} ${VERSIONS:-1} ${{VERSION}}" + result, err = substitute(s, []string{"VERSION=1.0"}) + if err != nil { + t.Fatalf("failed substitution: %s", err) + } + + expected = "$VERSION_ID $VERSIONS ${VERSION_ID} ${VERSIONS:-1} 1.0" + if result != expected { + t.Fatalf("bad substitution result, expected '%s' got '%s'", expected, result) + } + + // but $VERSION followed by a non-name character still is + for _, s := range []string{"$VERSION", "$VERSION/bin", "$VERSION-rc1", "\"$VERSION\""} { + _, err = substitute(s, []string{"VERSION=1.0"}) + if err == nil { + t.Fatalf("substitution of %q with unsupported placeholder should have failed but didn't", s) + } + } + // a double bracket placeholder with no default value must be provided s = "${{NOT_SET}}" _, err = substitute(s, []string{})