[
  {
    "path": ".drone.yml",
    "content": "kind: pipeline\nname: default\n\nsteps:\n- name: build\n  image: golang:1.11\n  commands:\n  - go test -v ./...\n"
  },
  {
    "path": ".gitignore",
    "content": "/envsubst\ncoverage.out\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 drone.io\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "cmd/envsubst/main.go",
    "content": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/drone/envsubst/v2\"\n)\n\nfunc main() {\n\tstdin := bufio.NewScanner(os.Stdin)\n\tstdout := bufio.NewWriter(os.Stdout)\n\n\tfor stdin.Scan() {\n\t\tline, err := envsubst.EvalEnv(stdin.Text())\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Error while envsubst: %v\", err)\n\t\t}\n\t\t_, err = fmt.Fprintln(stdout, line)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Error while writing to stdout: %v\", err)\n\t\t}\n\t\tstdout.Flush()\n\t}\n}\n\n"
  },
  {
    "path": "eval.go",
    "content": "package envsubst\n\nimport \"os\"\n\n// Eval replaces ${var} in the string based on the mapping function.\nfunc Eval(s string, mapping func(string) string) (string, error) {\n\tt, err := Parse(s)\n\tif err != nil {\n\t\treturn s, err\n\t}\n\treturn t.Execute(mapping)\n}\n\n// EvalEnv replaces ${var} in the string according to the values of the\n// current environment variables. References to undefined variables are\n// replaced by the empty string.\nfunc EvalEnv(s string) (string, error) {\n\treturn Eval(s, os.Getenv)\n}\n"
  },
  {
    "path": "eval_test.go",
    "content": "package envsubst\n\nimport \"testing\"\n\n// test cases sourced from tldp.org\n// http://www.tldp.org/LDP/abs/html/parameter-substitution.html\n\nfunc TestExpand(t *testing.T) {\n\tvar expressions = []struct {\n\t\tparams map[string]string\n\t\tinput  string\n\t\toutput string\n\t}{\n\t\t// text-only\n\t\t{\n\t\t\tparams: map[string]string{},\n\t\t\tinput:  \"abcdEFGH28ij\",\n\t\t\toutput: \"abcdEFGH28ij\",\n\t\t},\n\t\t// length\n\t\t{\n\t\t\tparams: map[string]string{\"var01\": \"abcdEFGH28ij\"},\n\t\t\tinput:  \"${#var01}\",\n\t\t\toutput: \"12\",\n\t\t},\n\t\t// uppercase first\n\t\t{\n\t\t\tparams: map[string]string{\"var01\": \"abcdEFGH28ij\"},\n\t\t\tinput:  \"${var01^}\",\n\t\t\toutput: \"AbcdEFGH28ij\",\n\t\t},\n\t\t// uppercase\n\t\t{\n\t\t\tparams: map[string]string{\"var01\": \"abcdEFGH28ij\"},\n\t\t\tinput:  \"${var01^^}\",\n\t\t\toutput: \"ABCDEFGH28IJ\",\n\t\t},\n\t\t// lowercase first\n\t\t{\n\t\t\tparams: map[string]string{\"var01\": \"ABCDEFGH28IJ\"},\n\t\t\tinput:  \"${var01,}\",\n\t\t\toutput: \"aBCDEFGH28IJ\",\n\t\t},\n\t\t// lowercase\n\t\t{\n\t\t\tparams: map[string]string{\"var01\": \"ABCDEFGH28IJ\"},\n\t\t\tinput:  \"${var01,,}\",\n\t\t\toutput: \"abcdefgh28ij\",\n\t\t},\n\t\t// substring with position\n\t\t{\n\t\t\tparams: map[string]string{\"path_name\": \"/home/bozo/ideas/thoughts.for.today\"},\n\t\t\tinput:  \"${path_name:11}\",\n\t\t\toutput: \"ideas/thoughts.for.today\",\n\t\t},\n\t\t// substring with position and length\n\t\t{\n\t\t\tparams: map[string]string{\"path_name\": \"/home/bozo/ideas/thoughts.for.today\"},\n\t\t\tinput:  \"${path_name:11:5}\",\n\t\t\toutput: \"ideas\",\n\t\t},\n\t\t// default not used\n\t\t{\n\t\t\tparams: map[string]string{\"var\": \"abc\"},\n\t\t\tinput:  \"${var=xyz}\",\n\t\t\toutput: \"abc\",\n\t\t},\n\t\t// default used\n\t\t{\n\t\t\tparams: map[string]string{},\n\t\t\tinput:  \"${var=xyz}\",\n\t\t\toutput: \"xyz\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{\"default_var\": \"foo\"},\n\t\t\tinput:  \"something ${var=${default_var}}\",\n\t\t\toutput: \"something foo\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{\"default_var\": \"foo1\"},\n\t\t\tinput:  `foo: ${var=${default_var}-suffix}`,\n\t\t\toutput: \"foo: foo1-suffix\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{\"default_var\": \"foo1\"},\n\t\t\tinput:  `foo: ${var=prefix${default_var}-suffix}`,\n\t\t\toutput: \"foo: prefixfoo1-suffix\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{},\n\t\t\tinput:  \"${var:=xyz}\",\n\t\t\toutput: \"xyz\",\n\t\t},\n\t\t// replace suffix\n\t\t{\n\t\t\tparams: map[string]string{\"stringZ\": \"abcABC123ABCabc\"},\n\t\t\tinput:  \"${stringZ/%abc/XYZ}\",\n\t\t\toutput: \"abcABC123ABCXYZ\",\n\t\t},\n\t\t// replace prefix\n\t\t{\n\t\t\tparams: map[string]string{\"stringZ\": \"abcABC123ABCabc\"},\n\t\t\tinput:  \"${stringZ/#abc/XYZ}\",\n\t\t\toutput: \"XYZABC123ABCabc\",\n\t\t},\n\t\t// replace all\n\t\t{\n\t\t\tparams: map[string]string{\"stringZ\": \"abcABC123ABCabc\"},\n\t\t\tinput:  \"${stringZ//abc/xyz}\",\n\t\t\toutput: \"xyzABC123ABCxyz\",\n\t\t},\n\t\t// replace first\n\t\t{\n\t\t\tparams: map[string]string{\"stringZ\": \"abcABC123ABCabc\"},\n\t\t\tinput:  \"${stringZ/abc/xyz}\",\n\t\t\toutput: \"xyzABC123ABCabc\",\n\t\t},\n\t\t// delete shortest match prefix\n\t\t{\n\t\t\tparams: map[string]string{\"filename\": \"bash.string.txt\"},\n\t\t\tinput:  \"${filename#*.}\",\n\t\t\toutput: \"string.txt\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{\"filename\": \"path/to/file\"},\n\t\t\tinput:  \"${filename#*/}\",\n\t\t\toutput: \"to/file\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{\"filename\": \"/path/to/file\"},\n\t\t\tinput:  \"${filename#*/}\",\n\t\t\toutput: \"path/to/file\",\n\t\t},\n\t\t// delete longest match prefix\n\t\t{\n\t\t\tparams: map[string]string{\"filename\": \"bash.string.txt\"},\n\t\t\tinput:  \"${filename##*.}\",\n\t\t\toutput: \"txt\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{\"filename\": \"path/to/file\"},\n\t\t\tinput:  \"${filename##*/}\",\n\t\t\toutput: \"file\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{\"filename\": \"/path/to/file\"},\n\t\t\tinput:  \"${filename##*/}\",\n\t\t\toutput: \"file\",\n\t\t},\n\t\t// delete shortest match suffix\n\t\t{\n\t\t\tparams: map[string]string{\"filename\": \"bash.string.txt\"},\n\t\t\tinput:  \"${filename%.*}\",\n\t\t\toutput: \"bash.string\",\n\t\t},\n\t\t// delete longest match suffix\n\t\t{\n\t\t\tparams: map[string]string{\"filename\": \"bash.string.txt\"},\n\t\t\tinput:  \"${filename%%.*}\",\n\t\t\toutput: \"bash\",\n\t\t},\n\n\t\t// nested parameters\n\t\t{\n\t\t\tparams: map[string]string{\"var01\": \"abcdEFGH28ij\"},\n\t\t\tinput:  \"${var=${var01^^}}\",\n\t\t\toutput: \"ABCDEFGH28IJ\",\n\t\t},\n\t\t// escaped\n\t\t{\n\t\t\tparams: map[string]string{\"var01\": \"abcdEFGH28ij\"},\n\t\t\tinput:  \"$${var01}\",\n\t\t\toutput: \"${var01}\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{\"var01\": \"abcdEFGH28ij\"},\n\t\t\tinput:  \"some text ${var01}$${var$${var01}$var01${var01}\",\n\t\t\toutput: \"some text abcdEFGH28ij${var${var01}$var01abcdEFGH28ij\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{\"default_var\": \"foo\"},\n\t\t\tinput:  \"something $${var=${default_var}}\",\n\t\t\toutput: \"something ${var=foo}\",\n\t\t},\n\t\t// some common escaping use cases\n\t\t{\n\t\t\tparams: map[string]string{\"stringZ\": \"foo/bar\"},\n\t\t\tinput:  `${stringZ/\\//-}`,\n\t\t\toutput: \"foo-bar\",\n\t\t},\n\t\t{\n\t\t\tparams: map[string]string{\"stringZ\": \"foo/bar/baz\"},\n\t\t\tinput:  `${stringZ//\\//-}`,\n\t\t\toutput: \"foo-bar-baz\",\n\t\t},\n\t\t// escape outside of expansion shouldn't be processed\n\t\t{\n\t\t\tparams: map[string]string{\"default_var\": \"foo\"},\n\t\t\tinput:  \"\\\\\\\\something ${var=${default_var}}\",\n\t\t\toutput: \"\\\\\\\\something foo\",\n\t\t},\n\t\t// substitute with a blank string\n\t\t{\n\t\t\tparams: map[string]string{\"stringZ\": \"foo.bar\"},\n\t\t\tinput:  `${stringZ/./}`,\n\t\t\toutput: \"foobar\",\n\t\t},\n\t}\n\n\tfor _, expr := range expressions {\n\t\tt.Run(expr.input, func(t *testing.T) {\n\t\t\tt.Logf(expr.input)\n\t\t\toutput, err := Eval(expr.input, func(s string) string {\n\t\t\t\treturn expr.params[s]\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Want %q expanded but got error %q\", expr.input, err)\n\t\t\t}\n\n\t\t\tif output != expr.output {\n\t\t\t\tt.Errorf(\"Want %q expanded to %q, got %q\",\n\t\t\t\t\texpr.input,\n\t\t\t\t\texpr.output,\n\t\t\t\t\toutput)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "funcs.go",
    "content": "package envsubst\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\t\"unicode/utf8\"\n\n\t\"github.com/drone/envsubst/v2/path\"\n)\n\n// defines a parameter substitution function.\ntype substituteFunc func(string, ...string) string\n\n// toLen returns the length of string s.\nfunc toLen(s string, args ...string) string {\n\treturn strconv.Itoa(len(s))\n}\n\n// toLower returns a copy of the string s with all characters\n// mapped to their lower case.\nfunc toLower(s string, args ...string) string {\n\treturn strings.ToLower(s)\n}\n\n// toUpper returns a copy of the string s with all characters\n// mapped to their upper case.\nfunc toUpper(s string, args ...string) string {\n\treturn strings.ToUpper(s)\n}\n\n// toLowerFirst returns a copy of the string s with the first\n// character mapped to its lower case.\nfunc toLowerFirst(s string, args ...string) string {\n\tif s == \"\" {\n\t\treturn s\n\t}\n\tr, n := utf8.DecodeRuneInString(s)\n\treturn string(unicode.ToLower(r)) + s[n:]\n}\n\n// toUpperFirst returns a copy of the string s with the first\n// character mapped to its upper case.\nfunc toUpperFirst(s string, args ...string) string {\n\tif s == \"\" {\n\t\treturn s\n\t}\n\tr, n := utf8.DecodeRuneInString(s)\n\treturn string(unicode.ToUpper(r)) + s[n:]\n}\n\n// toDefault returns a copy of the string s if not empty, else\n// returns a concatenation of the args without a separator.\nfunc toDefault(s string, args ...string) string {\n\tif len(s) == 0 && len(args) > 0 {\n\t\t// don't use any separator\n\t\ts = strings.Join(args, \"\")\n\t}\n\treturn s\n}\n\n// toSubstr returns a slice of the string s at the specified\n// length and position.\nfunc toSubstr(s string, args ...string) string {\n\tif len(args) == 0 {\n\t\treturn s // should never happen\n\t}\n\n\tpos, err := strconv.Atoi(args[0])\n\tif err != nil {\n\t\t// bash returns the string if the position\n\t\t// cannot be parsed.\n\t\treturn s\n\t}\n\n\tif pos < 0 {\n\t\t// if pos is negative (counts from the end) add it\n\t\t// to length to get first character offset\n\t\tpos = len(s) + pos\n\n\t\t// if negative offset exceeds the length of the string\n\t\t// start from 0\n\t\tif pos < 0 {\n\t\t\tpos = 0\n\t\t}\n\t}\n\n\tif len(args) == 1 {\n\t\tif pos < len(s) {\n\t\t\treturn s[pos:]\n\t\t}\n\t\t// if the position exceeds the length of the\n\t\t// string an empty string is returned\n\t\treturn \"\"\n\t}\n\n\tlength, err := strconv.Atoi(args[1])\n\tif err != nil {\n\t\t// bash returns the string if the length\n\t\t// cannot be parsed.\n\t\treturn s\n\t}\n\n\tif pos+length >= len(s) {\n\t\tif pos < len(s) {\n\t\t\t// if the position exceeds the length of the\n\t\t\t// string just return the rest of it like bash\n\t\t\treturn s[pos:]\n\t\t}\n\t\t// if the position exceeds the length of the\n\t\t// string an empty string is returned\n\t\treturn \"\"\n\t}\n\n\treturn s[pos : pos+length]\n}\n\n// replaceAll returns a copy of the string s with all instances\n// of the substring replaced with the replacement string.\nfunc replaceAll(s string, args ...string) string {\n\tswitch len(args) {\n\tcase 0:\n\t\treturn s\n\tcase 1:\n\t\treturn strings.Replace(s, args[0], \"\", -1)\n\tdefault:\n\t\treturn strings.Replace(s, args[0], args[1], -1)\n\t}\n}\n\n// replaceFirst returns a copy of the string s with the first\n// instance of the substring replaced with the replacement string.\nfunc replaceFirst(s string, args ...string) string {\n\tswitch len(args) {\n\tcase 0:\n\t\treturn s\n\tcase 1:\n\t\treturn strings.Replace(s, args[0], \"\", 1)\n\tdefault:\n\t\treturn strings.Replace(s, args[0], args[1], 1)\n\t}\n}\n\n// replacePrefix returns a copy of the string s with the matching\n// prefix replaced with the replacement string.\nfunc replacePrefix(s string, args ...string) string {\n\tif len(args) != 2 {\n\t\treturn s\n\t}\n\tif strings.HasPrefix(s, args[0]) {\n\t\treturn strings.Replace(s, args[0], args[1], 1)\n\t}\n\treturn s\n}\n\n// replaceSuffix returns a copy of the string s with the matching\n// suffix replaced with the replacement string.\nfunc replaceSuffix(s string, args ...string) string {\n\tif len(args) != 2 {\n\t\treturn s\n\t}\n\tif strings.HasSuffix(s, args[0]) {\n\t\ts = strings.TrimSuffix(s, args[0])\n\t\ts = s + args[1]\n\t}\n\treturn s\n}\n\n// TODO\n\nfunc trimShortestPrefix(s string, args ...string) string {\n\tif len(args) != 0 {\n\t\ts = trimShortest(s, args[0])\n\t}\n\treturn s\n}\n\nfunc trimShortestSuffix(s string, args ...string) string {\n\tif len(args) != 0 {\n\t\tr := reverse(s)\n\t\trarg := reverse(args[0])\n\t\ts = reverse(trimShortest(r, rarg))\n\t}\n\treturn s\n}\n\nfunc trimLongestPrefix(s string, args ...string) string {\n\tif len(args) != 0 {\n\t\ts = trimLongest(s, args[0])\n\t}\n\treturn s\n}\n\nfunc trimLongestSuffix(s string, args ...string) string {\n\tif len(args) != 0 {\n\t\tr := reverse(s)\n\t\trarg := reverse(args[0])\n\t\ts = reverse(trimLongest(r, rarg))\n\t}\n\treturn s\n}\n\nfunc trimShortest(s, arg string) string {\n\tvar shortestMatch string\n\tfor i := 0; i < len(s); i++ {\n\t\tmatch, err := path.Match(arg, s[0:len(s)-i])\n\n\t\tif err != nil {\n\t\t\treturn s\n\t\t}\n\n\t\tif match {\n\t\t\tshortestMatch = s[0 : len(s)-i]\n\t\t}\n\t}\n\n\tif shortestMatch != \"\" {\n\t\treturn strings.TrimPrefix(s, shortestMatch)\n\t}\n\n\treturn s\n}\n\nfunc trimLongest(s, arg string) string {\n\tfor i := 0; i < len(s); i++ {\n\t\tmatch, err := path.Match(arg, s[0:len(s)-i])\n\n\t\tif err != nil {\n\t\t\treturn s\n\t\t}\n\n\t\tif match {\n\t\t\treturn strings.TrimPrefix(s, s[0:len(s)-i])\n\t\t}\n\t}\n\n\treturn s\n}\n\nfunc reverse(s string) string {\n\tr := []rune(s)\n\tfor i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {\n\t\tr[i], r[j] = r[j], r[i]\n\t}\n\treturn string(r)\n}\n"
  },
  {
    "path": "funcs_test.go",
    "content": "package envsubst\n\nimport \"testing\"\n\nfunc Test_len(t *testing.T) {\n\tgot, want := toLen(\"Hello World\"), \"11\"\n\tif got != want {\n\t\tt.Errorf(\"Expect len function to return %s, got %s\", want, got)\n\t}\n}\n\nfunc Test_lower(t *testing.T) {\n\tgot, want := toLower(\"Hello World\"), \"hello world\"\n\tif got != want {\n\t\tt.Errorf(\"Expect lower function to return %s, got %s\", want, got)\n\t}\n}\n\nfunc Test_lowerFirst(t *testing.T) {\n\tgot, want := toLowerFirst(\"HELLO WORLD\"), \"hELLO WORLD\"\n\tif got != want {\n\t\tt.Errorf(\"Expect lowerFirst function to return %s, got %s\", want, got)\n\t}\n\tdefer func() {\n\t\tif recover() != nil {\n\t\t\tt.Errorf(\"Expect empty string does not panic lowerFirst\")\n\t\t}\n\t}()\n\ttoLowerFirst(\"\")\n}\n\nfunc Test_upper(t *testing.T) {\n\tgot, want := toUpper(\"Hello World\"), \"HELLO WORLD\"\n\tif got != want {\n\t\tt.Errorf(\"Expect upper function to return %s, got %s\", want, got)\n\t}\n}\n\nfunc Test_upperFirst(t *testing.T) {\n\tgot, want := toUpperFirst(\"hello world\"), \"Hello world\"\n\tif got != want {\n\t\tt.Errorf(\"Expect upperFirst function to return %s, got %s\", want, got)\n\t}\n\tdefer func() {\n\t\tif recover() != nil {\n\t\t\tt.Errorf(\"Expect empty string does not panic upperFirst\")\n\t\t}\n\t}()\n\ttoUpperFirst(\"\")\n}\n\nfunc Test_default(t *testing.T) {\n\tgot, want := toDefault(\"Hello World\", \"Hola Mundo\"), \"Hello World\"\n\tif got != want {\n\t\tt.Errorf(\"Expect default function uses variable value\")\n\t}\n\n\tgot, want = toDefault(\"\", \"Hola Mundo\"), \"Hola Mundo\"\n\tif got != want {\n\t\tt.Errorf(\"Expect default function uses default value, when variable empty. Got %s, Want %s\", got, want)\n\t}\n\n\tgot, want = toDefault(\"\", \"Hola Mundo\", \"-Bonjour le monde\", \"-Halló heimur\"), \"Hola Mundo-Bonjour le monde-Halló heimur\"\n\tif got != want {\n\t\tt.Errorf(\"Expect default function to use concatenated args when variable empty. Got %s, Want %s\", got, want)\n\t}\n}\n\nfunc Test_substr(t *testing.T) {\n\tgot, want := toSubstr(\"123456789123456789\", \"0\", \"8\"), \"12345678\"\n\tif got != want {\n\t\tt.Errorf(\"Expect substr function to cut from beginning to length\")\n\t}\n\n\tgot, want = toSubstr(\"123456789123456789\", \"1\", \"8\"), \"23456789\"\n\tif got != want {\n\t\tt.Errorf(\"Expect substr function to cut from offset to length\")\n\t}\n\n\tgot, want = toSubstr(\"123456789123456789\", \"9\"), \"123456789\"\n\tif got != want {\n\t\tt.Errorf(\"Expect substr function to cut beginnging with offset\")\n\t}\n\n\tgot, want = toSubstr(\"123456789123456789\", \"9\", \"50\"), \"123456789\"\n\tif got != want {\n\t\tt.Errorf(\"Expect substr function to ignore length if out of bound\")\n\t}\n\n\tgot, want = toSubstr(\"123456789123456789\", \"-3\", \"2\"), \"78\"\n\tif got != want {\n\t\tt.Errorf(\"Expect substr function to count negative offsets from the end\")\n\t}\n\n\tgot, want = toSubstr(\"123456789123456789\", \"-300\", \"3\"), \"123\"\n\tif got != want {\n\t\tt.Errorf(\"Expect substr function to cut from the beginning to length for negative offsets exceeding string length\")\n\t}\n\t\n\tgot, want = toSubstr(\"12345678\", \"9\", \"1\"), \"\"\n\tif got != want {\n\t\tt.Errorf(\"Expect substr function to cut entire string if pos is itself out of bound\")\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/drone/envsubst/v2\n\nrequire github.com/google/go-cmp v0.2.0\n\ngo 1.13\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\n"
  },
  {
    "path": "parse/node.go",
    "content": "package parse\n\n// Node is an element in the parse tree.\ntype Node interface {\n\tnode()\n}\n\n// empty string node\nvar empty = new(TextNode)\n\n// a template is represented by a tree consisting of one\n// or more of the following nodes.\ntype (\n\t// TextNode represents a string of text.\n\tTextNode struct {\n\t\tValue string\n\t}\n\n\t// FuncNode represents a string function.\n\tFuncNode struct {\n\t\tParam string\n\t\tName  string\n\t\tArgs  []Node\n\t}\n\n\t// ListNode represents a list of nodes.\n\tListNode struct {\n\t\tNodes []Node\n\t}\n\n\t// ParamNode struct{\n\t// \tName string\n\t// }\n\t//\n\t// CaseNode struct {\n\t// \tName string\n\t// \tFirst bool\n\t// }\n\t//\n\t// LowerNode struct {\n\t// \tName string\n\t// \tFirst bool\n\t// }\n\t//\n\t// SubstrNode struct {\n\t// \tName string\n\t// \tPos Node\n\t// \tLen Node\n\t// }\n\t//\n\t// ReplaceNode struct {\n\t// \tName string\n\t// \tSubstring Node\n\t// \tReplacement Node\n\t// }\n\t//\n\t// TrimNode struct{\n\t//\n\t// }\n\t//\n\t// DefaultNode struct {\n\t// \tName string\n\t// \tDefault Node\n\t// }\n)\n\n// newTextNode returns a new TextNode.\nfunc newTextNode(text string) *TextNode {\n\treturn &TextNode{Value: text}\n}\n\n// newListNode returns a new ListNode.\nfunc newListNode(nodes ...Node) *ListNode {\n\treturn &ListNode{Nodes: nodes}\n}\n\n// newFuncNode returns a new FuncNode.\nfunc newFuncNode(name string) *FuncNode {\n\treturn &FuncNode{Param: name}\n}\n\n// node() defines the node in a parse tree\n\nfunc (*TextNode) node() {}\nfunc (*ListNode) node() {}\nfunc (*FuncNode) node() {}\n"
  },
  {
    "path": "parse/parse.go",
    "content": "package parse\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\t// ErrBadSubstitution represents a substitution parsing error.\n\tErrBadSubstitution = errors.New(\"bad substitution\")\n\n\t// ErrMissingClosingBrace represents a missing closing brace \"}\" error.\n\tErrMissingClosingBrace = errors.New(\"missing closing brace\")\n\n\t// ErrParseVariableName represents the error when unable to parse a\n\t// variable name within a substitution.\n\tErrParseVariableName = errors.New(\"unable to parse variable name\")\n\n\t// ErrParseFuncSubstitution represents the error when unable to parse the\n\t// substitution within a function parameter.\n\tErrParseFuncSubstitution = errors.New(\"unable to parse substitution within function\")\n\n\t// ErrParseDefaultFunction represent the error when unable to parse a\n\t// default function.\n\tErrParseDefaultFunction = errors.New(\"unable to parse default function\")\n)\n\n// Tree is the representation of a single parsed SQL statement.\ntype Tree struct {\n\tRoot Node\n\n\t// Parsing only; cleared after parse.\n\tscanner *scanner\n}\n\n// Parse parses the string and returns a Tree.\nfunc Parse(buf string) (*Tree, error) {\n\tt := new(Tree)\n\tt.scanner = new(scanner)\n\treturn t.Parse(buf)\n}\n\n// Parse parses the string buffer to construct an ast\n// representation for expansion.\nfunc (t *Tree) Parse(buf string) (tree *Tree, err error) {\n\tt.scanner.init(buf)\n\tt.Root, err = t.parseAny()\n\treturn t, err\n}\n\nfunc (t *Tree) parseAny() (Node, error) {\n\tt.scanner.accept = acceptRune\n\tt.scanner.mode = scanIdent | scanLbrack | scanEscape\n\tt.scanner.escapeChars = dollar\n\n\tswitch t.scanner.scan() {\n\tcase tokenIdent:\n\t\tleft := newTextNode(\n\t\t\tt.scanner.string(),\n\t\t)\n\t\tright, err := t.parseAny()\n\t\tswitch {\n\t\tcase err != nil:\n\t\t\treturn nil, err\n\t\tcase right == empty:\n\t\t\treturn left, nil\n\t\t}\n\t\treturn newListNode(left, right), nil\n\tcase tokenEOF:\n\t\treturn empty, nil\n\tcase tokenLbrack:\n\t\tleft, err := t.parseFunc()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tright, err := t.parseAny()\n\t\tswitch {\n\t\tcase err != nil:\n\t\t\treturn nil, err\n\t\tcase right == empty:\n\t\t\treturn left, nil\n\t\t}\n\t\treturn newListNode(left, right), nil\n\t}\n\n\treturn nil, ErrBadSubstitution\n}\n\nfunc (t *Tree) parseFunc() (Node, error) {\n\t// Turn on all escape characters\n\tt.scanner.escapeChars = escapeAll\n\tswitch t.scanner.peek() {\n\tcase '#':\n\t\treturn t.parseLenFunc()\n\t}\n\n\tvar name string\n\tt.scanner.accept = acceptIdent\n\tt.scanner.mode = scanIdent\n\n\tswitch t.scanner.scan() {\n\tcase tokenIdent:\n\t\tname = t.scanner.string()\n\tdefault:\n\t\treturn nil, ErrParseVariableName\n\t}\n\n\tswitch t.scanner.peek() {\n\tcase ':':\n\t\treturn t.parseDefaultOrSubstr(name)\n\tcase '=':\n\t\treturn t.parseDefaultFunc(name)\n\tcase ',', '^':\n\t\treturn t.parseCasingFunc(name)\n\tcase '/':\n\t\treturn t.parseReplaceFunc(name)\n\tcase '#':\n\t\treturn t.parseRemoveFunc(name, acceptHashFunc)\n\tcase '%':\n\t\treturn t.parseRemoveFunc(name, acceptPercentFunc)\n\t}\n\n\tt.scanner.accept = acceptIdent\n\tt.scanner.mode = scanRbrack\n\tswitch t.scanner.scan() {\n\tcase tokenRbrack:\n\t\treturn newFuncNode(name), nil\n\tdefault:\n\t\treturn nil, ErrMissingClosingBrace\n\t}\n}\n\n// parse a substitution function parameter.\nfunc (t *Tree) parseParam(accept acceptFunc, mode byte) (Node, error) {\n\tt.scanner.accept = accept\n\tt.scanner.mode = mode | scanLbrack\n\tswitch t.scanner.scan() {\n\tcase tokenLbrack:\n\t\treturn t.parseFunc()\n\tcase tokenIdent:\n\t\treturn newTextNode(\n\t\t\tt.scanner.string(),\n\t\t), nil\n\tcase tokenRbrack:\n\t\treturn newTextNode(\n\t\t\tt.scanner.string(),\n\t\t), nil\n\tdefault:\n\t\treturn nil, ErrParseFuncSubstitution\n\t}\n}\n\n// parse either a default or substring substitution function.\nfunc (t *Tree) parseDefaultOrSubstr(name string) (Node, error) {\n\tt.scanner.read()\n\tr := t.scanner.peek()\n\tt.scanner.unread()\n\tswitch r {\n\tcase '=', '-', '?', '+':\n\t\treturn t.parseDefaultFunc(name)\n\tdefault:\n\t\treturn t.parseSubstrFunc(name)\n\t}\n}\n\n// parses the ${param:offset} string function\n// parses the ${param:offset:length} string function\nfunc (t *Tree) parseSubstrFunc(name string) (Node, error) {\n\tnode := new(FuncNode)\n\tnode.Param = name\n\n\tt.scanner.accept = acceptOneColon\n\tt.scanner.mode = scanIdent\n\tswitch t.scanner.scan() {\n\tcase tokenIdent:\n\t\tnode.Name = t.scanner.string()\n\tdefault:\n\t\treturn nil, ErrBadSubstitution\n\t}\n\n\t// scan arg[1]\n\t{\n\t\tparam, err := t.parseParam(rejectColonClose, scanIdent)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// param.Value = t.scanner.string()\n\t\tnode.Args = append(node.Args, param)\n\t}\n\n\t// expect delimiter or close\n\tt.scanner.accept = acceptColon\n\tt.scanner.mode = scanIdent | scanRbrack\n\tswitch t.scanner.scan() {\n\tcase tokenRbrack:\n\t\treturn node, nil\n\tcase tokenIdent:\n\t\t// no-op\n\tdefault:\n\t\treturn nil, ErrBadSubstitution\n\t}\n\n\t// scan arg[2]\n\t{\n\t\tparam, err := t.parseParam(acceptNotClosing, scanIdent)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnode.Args = append(node.Args, param)\n\t}\n\n\treturn node, t.consumeRbrack()\n}\n\n// parses the ${param%word} string function\n// parses the ${param%%word} string function\n// parses the ${param#word} string function\n// parses the ${param##word} string function\nfunc (t *Tree) parseRemoveFunc(name string, accept acceptFunc) (Node, error) {\n\tnode := new(FuncNode)\n\tnode.Param = name\n\n\tt.scanner.accept = accept\n\tt.scanner.mode = scanIdent\n\tswitch t.scanner.scan() {\n\tcase tokenIdent:\n\t\tnode.Name = t.scanner.string()\n\tdefault:\n\t\treturn nil, ErrBadSubstitution\n\t}\n\n\t// scan arg[1]\n\t{\n\t\tparam, err := t.parseParam(acceptNotClosing, scanIdent)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// param.Value = t.scanner.string()\n\t\tnode.Args = append(node.Args, param)\n\t}\n\n\treturn node, t.consumeRbrack()\n}\n\n// parses the ${param/pattern/string} string function\n// parses the ${param//pattern/string} string function\n// parses the ${param/#pattern/string} string function\n// parses the ${param/%pattern/string} string function\nfunc (t *Tree) parseReplaceFunc(name string) (Node, error) {\n\tnode := new(FuncNode)\n\tnode.Param = name\n\n\tt.scanner.accept = acceptReplaceFunc\n\tt.scanner.mode = scanIdent\n\tswitch t.scanner.scan() {\n\tcase tokenIdent:\n\t\tnode.Name = t.scanner.string()\n\tdefault:\n\t\treturn nil, ErrBadSubstitution\n\t}\n\n\t// scan arg[1]\n\t{\n\t\tparam, err := t.parseParam(acceptNotSlash, scanIdent|scanEscape)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnode.Args = append(node.Args, param)\n\t}\n\n\t// expect delimiter\n\tt.scanner.accept = acceptSlash\n\tt.scanner.mode = scanIdent\n\tswitch t.scanner.scan() {\n\tcase tokenIdent:\n\t\t// no-op\n\tdefault:\n\t\treturn nil, ErrBadSubstitution\n\t}\n\n\t// check for blank string\n\tswitch t.scanner.peek() {\n\tcase '}':\n\t\treturn node, t.consumeRbrack()\n\t}\n\n\t// scan arg[2]\n\t{\n\t\tparam, err := t.parseParam(acceptNotClosing, scanIdent|scanEscape)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnode.Args = append(node.Args, param)\n\t}\n\n\treturn node, t.consumeRbrack()\n}\n\n// parses the ${parameter=word} string function\n// parses the ${parameter:=word} string function\n// parses the ${parameter:-word} string function\n// parses the ${parameter:?word} string function\n// parses the ${parameter:+word} string function\nfunc (t *Tree) parseDefaultFunc(name string) (Node, error) {\n\tnode := new(FuncNode)\n\tnode.Param = name\n\n\tt.scanner.accept = acceptDefaultFunc\n\tif t.scanner.peek() == '=' {\n\t\tt.scanner.accept = acceptOneEqual\n\t}\n\tt.scanner.mode = scanIdent\n\tswitch t.scanner.scan() {\n\tcase tokenIdent:\n\t\tnode.Name = t.scanner.string()\n\tdefault:\n\t\treturn nil, ErrParseDefaultFunction\n\t}\n\n\t// loop through all possible runes in default param\n\tfor {\n\t\t// this acts as the break condition. Peek to see if we reached the end\n\t\tswitch t.scanner.peek() {\n\t\tcase '}':\n\t\t\treturn node, t.consumeRbrack()\n\t\t}\n\t\tparam, err := t.parseParam(acceptNotClosing, scanIdent)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnode.Args = append(node.Args, param)\n\t}\n}\n\n// parses the ${param,} string function\n// parses the ${param,,} string function\n// parses the ${param^} string function\n// parses the ${param^^} string function\nfunc (t *Tree) parseCasingFunc(name string) (Node, error) {\n\tnode := new(FuncNode)\n\tnode.Param = name\n\n\tt.scanner.accept = acceptCasingFunc\n\tt.scanner.mode = scanIdent\n\tswitch t.scanner.scan() {\n\tcase tokenIdent:\n\t\tnode.Name = t.scanner.string()\n\tdefault:\n\t\treturn nil, ErrBadSubstitution\n\t}\n\n\treturn node, t.consumeRbrack()\n}\n\n// parses the ${#param} string function\nfunc (t *Tree) parseLenFunc() (Node, error) {\n\tnode := new(FuncNode)\n\n\tt.scanner.accept = acceptOneHash\n\tt.scanner.mode = scanIdent\n\tswitch t.scanner.scan() {\n\tcase tokenIdent:\n\t\tnode.Name = t.scanner.string()\n\tdefault:\n\t\treturn nil, ErrBadSubstitution\n\t}\n\n\tt.scanner.accept = acceptIdent\n\tt.scanner.mode = scanIdent\n\tswitch t.scanner.scan() {\n\tcase tokenIdent:\n\t\tnode.Param = t.scanner.string()\n\tdefault:\n\t\treturn nil, ErrBadSubstitution\n\t}\n\n\treturn node, t.consumeRbrack()\n}\n\n// consumeRbrack consumes a right closing bracket. If a closing\n// bracket token is not consumed an ErrBadSubstitution is returned.\nfunc (t *Tree) consumeRbrack() error {\n\tt.scanner.mode = scanRbrack\n\tif t.scanner.scan() != tokenRbrack {\n\t\treturn ErrBadSubstitution\n\t}\n\treturn nil\n}\n\n// consumeDelimiter consumes a function argument delimiter. If a\n// delimiter is not consumed an ErrBadSubstitution is returned.\n// func (t *Tree) consumeDelimiter(accept acceptFunc, mode uint) error {\n// \tt.scanner.accept = accept\n// \tt.scanner.mode = mode\n// \tif t.scanner.scan() != tokenRbrack {\n// \t\treturn ErrBadSubstitution\n// \t}\n// \treturn nil\n// }\n"
  },
  {
    "path": "parse/parse_test.go",
    "content": "package parse\n\nimport (\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n)\n\nvar tests = []struct {\n\tText string\n\tNode Node\n}{\n\n\t//\n\t// text only\n\t//\n\t{\n\t\tText: \"text\",\n\t\tNode: &TextNode{Value: \"text\"},\n\t},\n\t{\n\t\tText: \"}text\",\n\t\tNode: &TextNode{Value: \"}text\"},\n\t},\n\t{\n\t\tText: \"http://github.com\",\n\t\tNode: &TextNode{Value: \"http://github.com\"}, // should not escape double slash\n\t},\n\t{\n\t\tText: \"$${string}\",\n\t\tNode: &TextNode{Value: \"${string}\"}, // should not escape double dollar\n\t},\n\t{\n\t\tText: \"$$string\",\n\t\tNode: &TextNode{Value: \"$string\"}, // should not escape double dollar\n\t},\n\t{\n\t\tText: `\\\\.\\pipe\\pipename`,\n\t\tNode: &TextNode{Value: `\\\\.\\pipe\\pipename`},\n\t},\n\n\t//\n\t// variable only\n\t//\n\t{\n\t\tText: \"${string}\",\n\t\tNode: &FuncNode{Param: \"string\"},\n\t},\n\n\t//\n\t// text transform functions\n\t//\n\t{\n\t\tText: \"${string,}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \",\",\n\t\t\tArgs:  nil,\n\t\t},\n\t},\n\t{\n\t\tText: \"${string,,}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \",,\",\n\t\t\tArgs:  nil,\n\t\t},\n\t},\n\t{\n\t\tText: \"${string^}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"^\",\n\t\t\tArgs:  nil,\n\t\t},\n\t},\n\t{\n\t\tText: \"${string^^}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"^^\",\n\t\t\tArgs:  nil,\n\t\t},\n\t},\n\n\t//\n\t// substring functions\n\t//\n\t{\n\t\tText: \"${string:position}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \":\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"position\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string:position:length}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \":\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"position\"},\n\t\t\t\t&TextNode{Value: \"length\"},\n\t\t\t},\n\t\t},\n\t},\n\n\t//\n\t// string removal functions\n\t//\n\t{\n\t\tText: \"${string#substring}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"#\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"substring\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string##substring}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"##\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"substring\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string%substring}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"%\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"substring\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string%%substring}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"%%\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"substring\"},\n\t\t\t},\n\t\t},\n\t},\n\n\t//\n\t// string replace functions\n\t//\n\t{\n\t\tText: \"${string/substring/replacement}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"/\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"substring\"},\n\t\t\t\t&TextNode{Value: \"replacement\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string//substring/replacement}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"//\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"substring\"},\n\t\t\t\t&TextNode{Value: \"replacement\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string/#substring/replacement}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"/#\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"substring\"},\n\t\t\t\t&TextNode{Value: \"replacement\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string/%substring/replacement}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"/%\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"substring\"},\n\t\t\t\t&TextNode{Value: \"replacement\"},\n\t\t\t},\n\t\t},\n\t},\n\n\t//\n\t// default value functions\n\t//\n\t{\n\t\tText: \"${string=default}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"=\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"default\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string:=default}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \":=\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"default\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string:-default}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \":-\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"default\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string:?default}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \":?\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"default\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string:+default}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \":+\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"default\"},\n\t\t\t},\n\t\t},\n\t},\n\n\t//\n\t// length function\n\t//\n\t{\n\t\tText: \"${#string}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"#\",\n\t\t},\n\t},\n\n\t//\n\t// special characters in argument\n\t//\n\t{\n\t\tText: \"${string#$%:*{}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"#\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"$%:*{\"},\n\t\t\t},\n\t\t},\n\t},\n\n\t// text before and after function\n\t{\n\t\tText: \"hello ${#string} world\",\n\t\tNode: &ListNode{\n\t\t\tNodes: []Node{\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"hello \",\n\t\t\t\t},\n\t\t\t\t&ListNode{\n\t\t\t\t\tNodes: []Node{\n\t\t\t\t\t\t&FuncNode{\n\t\t\t\t\t\t\tParam: \"string\",\n\t\t\t\t\t\t\tName:  \"#\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&TextNode{\n\t\t\t\t\t\t\tValue: \" world\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t// text before and after function with \\\\ outside of function\n\t{\n\t\tText: `\\\\ hello ${#string} world \\\\`,\n\t\tNode: &ListNode{\n\t\t\tNodes: []Node{\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: `\\\\ hello `,\n\t\t\t\t},\n\t\t\t\t&ListNode{\n\t\t\t\t\tNodes: []Node{\n\t\t\t\t\t\t&FuncNode{\n\t\t\t\t\t\t\tParam: \"string\",\n\t\t\t\t\t\t\tName:  \"#\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&TextNode{\n\t\t\t\t\t\t\tValue: ` world \\\\`,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\n\t// escaped function arguments\n\t{\n\t\tText: `${string/\\/position/length}`,\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"/\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"/position\",\n\t\t\t\t},\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"length\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: `${string/\\/position\\\\/length}`,\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"/\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"/position\\\\\",\n\t\t\t\t},\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"length\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: `${string/position/\\/length}`,\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"/\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"position\",\n\t\t\t\t},\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"/length\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: `${string/position/\\/length\\\\}`,\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"/\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"position\",\n\t\t\t\t},\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"/length\\\\\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: `${string/position/\\/leng\\\\th}`,\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"/\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"position\",\n\t\t\t\t},\n\t\t\t\t&TextNode{\n\t\t\t\t\tValue: \"/leng\\\\th\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\n\t// functions in functions\n\t{\n\t\tText: \"${string:${position}}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \":\",\n\t\t\tArgs: []Node{\n\t\t\t\t&FuncNode{\n\t\t\t\t\tParam: \"position\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string:${stringy:position:length}:${stringz,,}}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \":\",\n\t\t\tArgs: []Node{\n\t\t\t\t&FuncNode{\n\t\t\t\t\tParam: \"stringy\",\n\t\t\t\t\tName:  \":\",\n\t\t\t\t\tArgs: []Node{\n\t\t\t\t\t\t&TextNode{Value: \"position\"},\n\t\t\t\t\t\t&TextNode{Value: \"length\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t&FuncNode{\n\t\t\t\t\tParam: \"stringz\",\n\t\t\t\t\tName:  \",,\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string#${stringz}}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"#\",\n\t\t\tArgs: []Node{\n\t\t\t\t&FuncNode{Param: \"stringz\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string=${stringz}}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"=\",\n\t\t\tArgs: []Node{\n\t\t\t\t&FuncNode{Param: \"stringz\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string=prefix-${var}}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"=\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"prefix-\"},\n\t\t\t\t&FuncNode{Param: \"var\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string=${var}-suffix}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"=\",\n\t\t\tArgs: []Node{\n\t\t\t\t&FuncNode{Param: \"var\"},\n\t\t\t\t&TextNode{Value: \"-suffix\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string=prefix-${var}-suffix}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"=\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"prefix-\"},\n\t\t\t\t&FuncNode{Param: \"var\"},\n\t\t\t\t&TextNode{Value: \"-suffix\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string=prefix${var} suffix}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"=\",\n\t\t\tArgs: []Node{\n\t\t\t\t&TextNode{Value: \"prefix\"},\n\t\t\t\t&FuncNode{Param: \"var\"},\n\t\t\t\t&TextNode{Value: \" suffix\"},\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tText: \"${string//${stringy}/${stringz}}\",\n\t\tNode: &FuncNode{\n\t\t\tParam: \"string\",\n\t\t\tName:  \"//\",\n\t\t\tArgs: []Node{\n\t\t\t\t&FuncNode{Param: \"stringy\"},\n\t\t\t\t&FuncNode{Param: \"stringz\"},\n\t\t\t},\n\t\t},\n\t},\n}\n\nfunc TestParse(t *testing.T) {\n\tfor _, test := range tests {\n\t\tt.Log(test.Text)\n\t\tt.Run(test.Text, func(t *testing.T) {\n\t\t\tgot, err := Parse(test.Text)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tif diff := cmp.Diff(test.Node, got.Root); diff != \"\" {\n\t\t\t\tt.Errorf(diff)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "parse/scan.go",
    "content": "package parse\n\nimport (\n\t\"unicode\"\n\t\"unicode/utf8\"\n)\n\n// eof rune sent when end of file is reached\nvar eof = rune(0)\n\n// token is a lexical token.\ntype token uint\n\n// list of lexical tokens.\nconst (\n\t// special tokens\n\ttokenIllegal token = iota\n\ttokenEOF\n\n\t// identifiers and literals\n\ttokenIdent\n\n\t// operators and delimiters\n\ttokenLbrack\n\ttokenRbrack\n\ttokenQuote\n)\n\n// predefined mode bits to control recognition of tokens.\nconst (\n\tscanIdent byte = 1 << iota\n\tscanLbrack\n\tscanRbrack\n\tscanEscape\n)\n\n// predefined mode bits to control escape tokens.\nconst (\n\tdollar byte = 1 << iota\n\tbackslash\n\tescapeAll = dollar | backslash\n)\n\n// returns true if rune is accepted.\ntype acceptFunc func(r rune, i int) bool\n\n// scanner implements a lexical scanner that reads unicode\n// characters and tokens from a string buffer.\ntype scanner struct {\n\tbuf         string\n\tpos         int\n\tstart       int\n\twidth       int\n\tmode        byte\n\tescapeChars byte\n\n\taccept acceptFunc\n}\n\n// init initializes a scanner with a new buffer.\nfunc (s *scanner) init(buf string) {\n\ts.buf = buf\n\ts.pos = 0\n\ts.start = 0\n\ts.width = 0\n\ts.accept = nil\n}\n\n// read returns the next unicode character. It returns eof at\n// the end of the string buffer.\nfunc (s *scanner) read() rune {\n\tif s.pos >= len(s.buf) {\n\t\ts.width = 0\n\t\treturn eof\n\t}\n\tr, w := utf8.DecodeRuneInString(s.buf[s.pos:])\n\ts.width = w\n\ts.pos += s.width\n\treturn r\n}\n\nfunc (s *scanner) unread() {\n\ts.pos -= s.width\n}\n\n// skip skips over the curring unicode character in the buffer\n// by slicing and removing from the buffer.\nfunc (s *scanner) skip() {\n\tl := s.buf[:s.pos-1]\n\tr := s.buf[s.pos:]\n\ts.buf = l + r\n}\n\n// peek returns the next unicode character in the buffer without\n// advancing the scanner. It returns eof if the scanner's position\n// is at the last character of the source.\nfunc (s *scanner) peek() rune {\n\tr := s.read()\n\ts.unread()\n\treturn r\n}\n\n// string returns the string corresponding to the most recently\n// scanned token. Valid after calling scan().\nfunc (s *scanner) string() string {\n\treturn s.buf[s.start:s.pos]\n}\n\n// tests if the bit exists for a given character bit\nfunc (s *scanner) shouldEscape(character byte) bool {\n\treturn s.escapeChars&character != 0\n}\n\n// scan reads the next token or Unicode character from source and\n// returns it. It returns EOF at the end of the source.\nfunc (s *scanner) scan() token {\n\ts.start = s.pos\n\tr := s.read()\n\tswitch {\n\tcase r == eof:\n\t\treturn tokenEOF\n\tcase s.scanLbrack(r):\n\t\treturn tokenLbrack\n\tcase s.scanRbrack(r):\n\t\treturn tokenRbrack\n\tcase s.scanIdent(r):\n\t\treturn tokenIdent\n\t}\n\treturn tokenIllegal\n}\n\n// scanIdent reads the next token or Unicode character from source\n// and returns true if the Ident character is accepted.\nfunc (s *scanner) scanIdent(r rune) bool {\n\tif s.mode&scanIdent == 0 {\n\t\treturn false\n\t}\n\tif s.scanEscaped(r) {\n\t\ts.skip()\n\t} else if !s.accept(r, s.pos-s.start) {\n\t\treturn false\n\t}\nloop:\n\tfor {\n\t\tr := s.read()\n\t\tswitch {\n\t\tcase r == eof:\n\t\t\ts.unread()\n\t\t\tbreak loop\n\t\tcase s.scanLbrack(r):\n\t\t\ts.unread()\n\t\t\ts.unread()\n\t\t\tbreak loop\n\t\t}\n\t\tif s.scanEscaped(r) {\n\t\t\ts.skip()\n\t\t\tcontinue\n\t\t}\n\t\tif !s.accept(r, s.pos-s.start) {\n\t\t\ts.unread()\n\t\t\tbreak loop\n\t\t}\n\t}\n\treturn true\n}\n\n// scanLbrack reads the next token or Unicode character from source\n// and returns true if the open bracket is encountered.\nfunc (s *scanner) scanLbrack(r rune) bool {\n\tif s.mode&scanLbrack == 0 {\n\t\treturn false\n\t}\n\tif r == '$' {\n\t\tif s.read() == '{' {\n\t\t\treturn true\n\t\t}\n\t\ts.unread()\n\t}\n\treturn false\n}\n\n// scanRbrack reads the next token or Unicode character from source\n// and returns true if the closing bracket is encountered.\nfunc (s *scanner) scanRbrack(r rune) bool {\n\tif s.mode&scanRbrack == 0 {\n\t\treturn false\n\t}\n\treturn r == '}'\n}\n\n// scanEscaped reads the next token or Unicode character from source\n// and returns true if it being escaped and should be skipped.\nfunc (s *scanner) scanEscaped(r rune) bool {\n\tif s.mode&scanEscape == 0 {\n\t\treturn false\n\t}\n\tif r == '$' && s.shouldEscape(dollar) {\n\t\tif s.peek() == '$' {\n\t\t\treturn true\n\t\t}\n\t}\n\tif r == '\\\\' && s.shouldEscape(backslash) {\n\t\tswitch s.peek() {\n\t\tcase '/', '\\\\':\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn false\n}\n\n//\n// scanner functions accept or reject runes.\n//\n\nfunc acceptRune(r rune, i int) bool {\n\treturn true\n}\n\nfunc acceptIdent(r rune, i int) bool {\n\treturn unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'\n}\n\nfunc acceptColon(r rune, i int) bool {\n\treturn r == ':'\n}\n\nfunc acceptOneHash(r rune, i int) bool {\n\treturn r == '#' && i == 1\n}\n\nfunc acceptNone(r rune, i int) bool {\n\treturn false\n}\n\nfunc acceptNotClosing(r rune, i int) bool {\n\treturn r != '}'\n}\n\nfunc acceptHashFunc(r rune, i int) bool {\n\treturn r == '#' && i < 3\n}\n\nfunc acceptPercentFunc(r rune, i int) bool {\n\treturn r == '%' && i < 3\n}\n\nfunc acceptDefaultFunc(r rune, i int) bool {\n\tswitch {\n\tcase i == 1 && r == ':':\n\t\treturn true\n\tcase i == 2 && (r == '=' || r == '-' || r == '?' || r == '+'):\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc acceptReplaceFunc(r rune, i int) bool {\n\tswitch {\n\tcase i == 1 && r == '/':\n\t\treturn true\n\tcase i == 2 && (r == '/' || r == '#' || r == '%'):\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc acceptOneEqual(r rune, i int) bool {\n\treturn i == 1 && r == '='\n}\n\nfunc acceptOneColon(r rune, i int) bool {\n\treturn i == 1 && r == ':'\n}\n\nfunc rejectColonClose(r rune, i int) bool {\n\treturn r != ':' && r != '}'\n}\n\nfunc acceptSlash(r rune, i int) bool {\n\treturn r == '/'\n}\n\nfunc acceptNotSlash(r rune, i int) bool {\n\treturn r != '/'\n}\n\nfunc acceptCasingFunc(r rune, i int) bool {\n\treturn (r == ',' || r == '^') && i < 3\n}\n"
  },
  {
    "path": "parse/scan_test.go",
    "content": "package parse\n"
  },
  {
    "path": "path/match.go",
    "content": "// Copyright 2010 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage path\n\nimport (\n\t\"errors\"\n\t\"unicode/utf8\"\n)\n\n// ErrBadPattern indicates a globbing pattern was malformed.\nvar ErrBadPattern = errors.New(\"syntax error in pattern\")\n\n// Match reports whether name matches the shell file name pattern.\n// The pattern syntax is:\n//\n//\tpattern:\n//\t\t{ term }\n//\tterm:\n//\t\t'*'         matches any sequence of non-/ characters\n//\t\t'?'         matches any single non-/ character\n//\t\t'[' [ '^' ] { character-range } ']'\n//\t\t            character class (must be non-empty)\n//\t\tc           matches character c (c != '*', '?', '\\\\', '[')\n//\t\t'\\\\' c      matches character c\n//\n//\tcharacter-range:\n//\t\tc           matches character c (c != '\\\\', '-', ']')\n//\t\t'\\\\' c      matches character c\n//\t\tlo '-' hi   matches character c for lo <= c <= hi\n//\n// Match requires pattern to match all of name, not just a substring.\n// The only possible returned error is ErrBadPattern, when pattern\n// is malformed.\n//\nfunc Match(pattern, name string) (matched bool, err error) {\nPattern:\n\tfor len(pattern) > 0 {\n\t\tvar star bool\n\t\tvar chunk string\n\t\tstar, chunk, pattern = scanChunk(pattern)\n\t\tif star && chunk == \"\" {\n\t\t\t// Trailing * matches rest of string unless it has a /.\n\t\t\t// return !strings.Contains(name, \"/\"), nil\n\n\t\t\t// Return rest of string\n\t\t\treturn true, nil\n\t\t}\n\t\t// Look for match at current position.\n\t\tt, ok, err := matchChunk(chunk, name)\n\t\t// if we're the last chunk, make sure we've exhausted the name\n\t\t// otherwise we'll give a false result even if we could still match\n\t\t// using the star\n\t\tif ok && (len(t) == 0 || len(pattern) > 0) {\n\t\t\tname = t\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif star {\n\t\t\t// Look for match skipping i+1 bytes.\n\t\t\tfor i := 0; i < len(name); i++ {\n\t\t\t\tt, ok, err := matchChunk(chunk, name[i+1:])\n\t\t\t\tif ok {\n\t\t\t\t\t// if we're the last chunk, make sure we exhausted the name\n\t\t\t\t\tif len(pattern) == 0 && len(t) > 0 {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tname = t\n\t\t\t\t\tcontinue Pattern\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn false, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false, nil\n\t}\n\treturn len(name) == 0, nil\n}\n\n// scanChunk gets the next segment of pattern, which is a non-star string\n// possibly preceded by a star.\nfunc scanChunk(pattern string) (star bool, chunk, rest string) {\n\tfor len(pattern) > 0 && pattern[0] == '*' {\n\t\tpattern = pattern[1:]\n\t\tstar = true\n\t}\n\tinrange := false\n\tvar i int\nScan:\n\tfor i = 0; i < len(pattern); i++ {\n\t\tswitch pattern[i] {\n\t\tcase '\\\\':\n\t\t\t// error check handled in matchChunk: bad pattern.\n\t\t\tif i+1 < len(pattern) {\n\t\t\t\ti++\n\t\t\t}\n\t\tcase '[':\n\t\t\tinrange = true\n\t\tcase ']':\n\t\t\tinrange = false\n\t\tcase '*':\n\t\t\tif !inrange {\n\t\t\t\tbreak Scan\n\t\t\t}\n\t\t}\n\t}\n\treturn star, pattern[0:i], pattern[i:]\n}\n\n// matchChunk checks whether chunk matches the beginning of s.\n// If so, it returns the remainder of s (after the match).\n// Chunk is all single-character operators: literals, char classes, and ?.\nfunc matchChunk(chunk, s string) (rest string, ok bool, err error) {\n\tfor len(chunk) > 0 {\n\t\tif len(s) == 0 {\n\t\t\treturn\n\t\t}\n\t\tswitch chunk[0] {\n\t\tcase '[':\n\t\t\t// character class\n\t\t\tr, n := utf8.DecodeRuneInString(s)\n\t\t\ts = s[n:]\n\t\t\tchunk = chunk[1:]\n\t\t\t// possibly negated\n\t\t\tnotNegated := true\n\t\t\tif len(chunk) > 0 && chunk[0] == '^' {\n\t\t\t\tnotNegated = false\n\t\t\t\tchunk = chunk[1:]\n\t\t\t}\n\t\t\t// parse all ranges\n\t\t\tmatch := false\n\t\t\tnrange := 0\n\t\t\tfor {\n\t\t\t\tif len(chunk) > 0 && chunk[0] == ']' && nrange > 0 {\n\t\t\t\t\tchunk = chunk[1:]\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tvar lo, hi rune\n\t\t\t\tif lo, chunk, err = getEsc(chunk); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\thi = lo\n\t\t\t\tif chunk[0] == '-' {\n\t\t\t\t\tif hi, chunk, err = getEsc(chunk[1:]); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif lo <= r && r <= hi {\n\t\t\t\t\tmatch = true\n\t\t\t\t}\n\t\t\t\tnrange++\n\t\t\t}\n\t\t\tif match != notNegated {\n\t\t\t\treturn\n\t\t\t}\n\n\t\tcase '?':\n\t\t\t_, n := utf8.DecodeRuneInString(s)\n\t\t\ts = s[n:]\n\t\t\tchunk = chunk[1:]\n\n\t\tcase '\\\\':\n\t\t\tchunk = chunk[1:]\n\t\t\tif len(chunk) == 0 {\n\t\t\t\terr = ErrBadPattern\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfallthrough\n\n\t\tdefault:\n\t\t\tif chunk[0] != s[0] {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ts = s[1:]\n\t\t\tchunk = chunk[1:]\n\t\t}\n\t}\n\treturn s, true, nil\n}\n\n// getEsc gets a possibly-escaped character from chunk, for a character class.\nfunc getEsc(chunk string) (r rune, nchunk string, err error) {\n\tif len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {\n\t\terr = ErrBadPattern\n\t\treturn\n\t}\n\tif chunk[0] == '\\\\' {\n\t\tchunk = chunk[1:]\n\t\tif len(chunk) == 0 {\n\t\t\terr = ErrBadPattern\n\t\t\treturn\n\t\t}\n\t}\n\tr, n := utf8.DecodeRuneInString(chunk)\n\tif r == utf8.RuneError && n == 1 {\n\t\terr = ErrBadPattern\n\t}\n\tnchunk = chunk[n:]\n\tif len(nchunk) == 0 {\n\t\terr = ErrBadPattern\n\t}\n\treturn\n}\n"
  },
  {
    "path": "readme.md",
    "content": "# envsubst\n\n`envsubst` is a Go package for expanding variables in a string using `${var}` syntax.\nIncludes support for bash string replacement functions.\n\n## Documentation\n\n[Documentation can be found on GoDoc][doc].\n\n## Supported Functions\n\n| __Expression__                | __Meaning__                                                     |\n| -----------------             | --------------                                                  |\n| `${var}`                      | Value of `$var`\n| `${#var}`                     | String length of `$var`\n| `${var^}`                     | Uppercase first character of `$var`\n| `${var^^}`                    | Uppercase all characters in `$var`\n| `${var,}`                     | Lowercase first character of `$var`\n| `${var,,}`                    | Lowercase all characters in `$var`\n| `${var:n}`                    | Offset `$var` `n` characters from start\n| `${var:n:len}`                | Offset `$var` `n` characters with max length of `len`\n| `${var#pattern}`              | Strip shortest `pattern` match from start\n| `${var##pattern}`             | Strip longest `pattern` match from start\n| `${var%pattern}`              | Strip shortest `pattern` match from end\n| `${var%%pattern}`             | Strip longest `pattern` match from end\n| `${var-default`               | If `$var` is not set, evaluate expression as `$default`\n| `${var:-default`              | If `$var` is not set or is empty, evaluate expression as `$default`\n| `${var=default`               | If `$var` is not set, evaluate expression as `$default`\n| `${var:=default`              | If `$var` is not set or is empty, evaluate expression as `$default`\n| `${var/pattern/replacement}`  | Replace as few `pattern` matches as possible with `replacement`\n| `${var//pattern/replacement}` | Replace as many `pattern` matches as possible with `replacement`\n| `${var/#pattern/replacement}` | Replace `pattern` match with `replacement` from `$var` start\n| `${var/%pattern/replacement}` | Replace `pattern` match with `replacement` from `$var` end\n\nFor a deeper reference, see [bash-hackers](https://wiki.bash-hackers.org/syntax/pe#case_modification) or [gnu pattern matching](https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html).\n\n## Unsupported Functions\n\n* `${var-default}`\n* `${var+default}`\n* `${var:?default}`\n* `${var:+default}`\n\n[doc]: http://godoc.org/github.com/drone/envsubst\n"
  },
  {
    "path": "template.go",
    "content": "package envsubst\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"io/ioutil\"\n\n\t\"github.com/drone/envsubst/v2/parse\"\n)\n\n// state represents the state of template execution. It is not part of the\n// template so that multiple executions can run in parallel.\ntype state struct {\n\ttemplate *Template\n\twriter   io.Writer\n\tnode     parse.Node // current node\n\n\t// maps variable names to values\n\tmapper func(string) string\n}\n\n// Template is the representation of a parsed shell format string.\ntype Template struct {\n\ttree *parse.Tree\n}\n\n// Parse creates a new shell format template and parses the template\n// definition from string s.\nfunc Parse(s string) (t *Template, err error) {\n\tt = new(Template)\n\tt.tree, err = parse.Parse(s)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn t, nil\n}\n\n// ParseFile creates a new shell format template and parses the template\n// definition from the named file.\nfunc ParseFile(path string) (*Template, error) {\n\tb, err := ioutil.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn Parse(string(b))\n}\n\n// Execute applies a parsed template to the specified data mapping.\nfunc (t *Template) Execute(mapping func(string) string) (str string, err error) {\n\tb := new(bytes.Buffer)\n\ts := new(state)\n\ts.node = t.tree.Root\n\ts.mapper = mapping\n\ts.writer = b\n\terr = t.eval(s)\n\tif err != nil {\n\t\treturn\n\t}\n\treturn b.String(), nil\n}\n\nfunc (t *Template) eval(s *state) (err error) {\n\tswitch node := s.node.(type) {\n\tcase *parse.TextNode:\n\t\terr = t.evalText(s, node)\n\tcase *parse.FuncNode:\n\t\terr = t.evalFunc(s, node)\n\tcase *parse.ListNode:\n\t\terr = t.evalList(s, node)\n\t}\n\treturn err\n}\n\nfunc (t *Template) evalText(s *state, node *parse.TextNode) error {\n\t_, err := io.WriteString(s.writer, node.Value)\n\treturn err\n}\n\nfunc (t *Template) evalList(s *state, node *parse.ListNode) (err error) {\n\tfor _, n := range node.Nodes {\n\t\ts.node = n\n\t\terr = t.eval(s)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (t *Template) evalFunc(s *state, node *parse.FuncNode) error {\n\tvar w = s.writer\n\tvar buf bytes.Buffer\n\tvar args []string\n\tfor _, n := range node.Args {\n\t\tbuf.Reset()\n\t\ts.writer = &buf\n\t\ts.node = n\n\t\terr := t.eval(s)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\targs = append(args, buf.String())\n\t}\n\n\t// restore the origin writer\n\ts.writer = w\n\ts.node = node\n\n\tv := s.mapper(node.Param)\n\n\tfn := lookupFunc(node.Name, len(args))\n\n\t_, err := io.WriteString(s.writer, fn(v, args...))\n\treturn err\n}\n\n// lookupFunc returns the parameters substitution function by name. If the\n// named function does not exists, a default function is returned.\nfunc lookupFunc(name string, args int) substituteFunc {\n\tswitch name {\n\tcase \",\":\n\t\treturn toLowerFirst\n\tcase \",,\":\n\t\treturn toLower\n\tcase \"^\":\n\t\treturn toUpperFirst\n\tcase \"^^\":\n\t\treturn toUpper\n\tcase \"#\":\n\t\tif args == 0 {\n\t\t\treturn toLen\n\t\t}\n\t\treturn trimShortestPrefix\n\tcase \"##\":\n\t\treturn trimLongestPrefix\n\tcase \"%\":\n\t\treturn trimShortestSuffix\n\tcase \"%%\":\n\t\treturn trimLongestSuffix\n\tcase \":\":\n\t\treturn toSubstr\n\tcase \"/#\":\n\t\treturn replacePrefix\n\tcase \"/%\":\n\t\treturn replaceSuffix\n\tcase \"/\":\n\t\treturn replaceFirst\n\tcase \"//\":\n\t\treturn replaceAll\n\tcase \"=\", \":=\", \":-\":\n\t\treturn toDefault\n\tcase \":?\", \":+\", \"-\", \"+\":\n\t\treturn toDefault\n\tdefault:\n\t\treturn toDefault\n\t}\n}\n"
  }
]