diff options
| -rw-r--r-- | docs-src/content/functions/regexp.yml | 158 | ||||
| -rw-r--r-- | docs/content/functions/regexp.md | 205 | ||||
| -rw-r--r-- | funcs/regexp.go | 65 | ||||
| -rw-r--r-- | funcs/regexp_test.go | 97 | ||||
| -rw-r--r-- | regexp/regexp.go | 44 | ||||
| -rw-r--r-- | regexp/regexp_test.go | 98 |
6 files changed, 630 insertions, 37 deletions
diff --git a/docs-src/content/functions/regexp.yml b/docs-src/content/functions/regexp.yml new file mode 100644 index 00000000..db46f797 --- /dev/null +++ b/docs-src/content/functions/regexp.yml @@ -0,0 +1,158 @@ +ns: regexp +preamble: | + These functions allow user you to search and modify text with regular expressions. + + The syntax of the regular expressions accepted is [Go's `regexp` syntax](https://golang.org/pkg/regexp/syntax/#hdr-Syntax), + and is the same general syntax used by Perl, Python, and other languages. +funcs: + - name: regexp.Find + description: | + Returns a string holding the text of the leftmost match in `input` + of the regular expression `expression`. + + This function provides the same behaviour as Go's + [`regexp.FindString`](https://golang.org/pkg/regexp/#Regexp.FindString) function. + pipeline: true + arguments: + - name: expression + required: true + description: The regular expression + - name: input + required: true + description: The input to search + examples: + - | + $ gomplate -i '{{ regexp.Find "[a-z]{3}" "foobar"}}' + foo + - | + $ gomplate -i 'no {{ "will not match" | regexp.Find "[0-9]" }}numbers' + no numbers + - name: regexp.FindAll + description: | + Returns a list of all successive matches of the regular expression. + + This can be called with 2 or 3 arguments. When called with 2 arguments, the + `n` argument (number of matches) will be set to `-1`, causing all matches + to be returned. + + This function provides the same behaviour as Go's + [`regexp.FindAllString`](https://golang.org/pkg/regexp/#Regexp.FindAllString) function. + pipeline: true + arguments: + - name: expression + required: true + description: The regular expression + - name: n + required: false + description: The number of matches to return + - name: input + required: true + description: The input to search + examples: + - | + $ gomplate -i '{{ regexp.FindAll "[a-z]{3}" "foobar" | toJSON}}' + ["foo", "bar"] + - | + $ gomplate -i '{{ "foo bar baz qux" | regexp.FindAll "[a-z]{3}" 3 | toJSON}}' + ["foo", "bar", "baz"] + - name: regexp.Match + description: | + Returns `true` if a given regular expression matches a given input. + + This returns a boolean which can be used in an `if` condition, for example. + pipeline: true + arguments: + - name: expression + required: true + description: The regular expression + - name: input + required: true + description: The input to test + examples: + - | + $ gomplate -i '{{ if (.Env.USER | regexp.Match `^h`) }}username ({{.Env.USER}}) starts with h!{{end}}' + username (hairyhenderson) starts with h! + - name: regexp.Replace + description: | + Replaces matches of a regular expression with the replacement string. + + The replacement is substituted after expanding variables beginning with `$`. + + This function provides the same behaviour as Go's + [`regexp.ReplaceAllString`](https://golang.org/pkg/regexp/#Regexp.ReplaceAllString) function. + pipeline: true + arguments: + - name: expression + required: true + description: The regular expression string + - name: replacement + required: true + description: The replacement string + - name: input + required: true + description: The input string to operate on + examples: + - | + $ gomplate -i '{{ regexp.Replace "(foo)bar" "$1" "foobar"}}' + foo + - | + $ gomplate -i '{{ regexp.Replace "(?P<first>[a-zA-Z]+) (?P<last>[a-zA-Z]+)" "${last}, ${first}" "Alan Turing"}}' + Turing, Alan + - name: regexp.ReplaceLiteral + description: | + Replaces matches of a regular expression with the replacement string. + + The replacement is substituted directly, without expanding variables + beginning with `$`. + + This function provides the same behaviour as Go's + [`regexp.ReplaceAllLiteralString`](https://golang.org/pkg/regexp/#Regexp.ReplaceAllLiteralString) function. + pipeline: true + arguments: + - name: expression + required: true + description: The regular expression string + - name: replacement + required: true + description: The replacement string + - name: input + required: true + description: The input string to operate on + examples: + - | + $ gomplate -i '{{ regexp.ReplaceLiteral "(foo)bar" "$1" "foobar"}}' + $1 + - | + $ gomplate -i '{{ `foo.bar,baz` | regexp.ReplaceLiteral `\W` `$` }}' + foo$bar$baz + - name: regexp.Split + description: | + Splits `input` into sub-strings, separated by the expression. + + This can be called with 2 or 3 arguments. When called with 2 arguments, the + `n` argument (number of matches) will be set to `-1`, causing all sub-strings + to be returned. + + This is equivalent to [`strings.SplitN`](../strings/#strings-splitn), + except that regular expressions are supported. + + This function provides the same behaviour as Go's + [`regexp.Split`](https://golang.org/pkg/regexp/#Regexp.Split) function. + pipeline: true + arguments: + - name: expression + required: true + description: The regular expression + - name: n + required: false + description: The number of matches to return + - name: input + required: true + description: The input to search + examples: + - | + $ gomplate -i '{{ regexp.Split `[\s,.]` "foo bar,baz.qux" | toJSON}}' + ["foo","bar","baz","qux"] + - | + $ gomplate -i '{{ "foo bar.baz,qux" | regexp.Split `[\s,.]` 3 | toJSON}}' + ["foo","bar","baz"] diff --git a/docs/content/functions/regexp.md b/docs/content/functions/regexp.md index c97a81a8..f7622438 100644 --- a/docs/content/functions/regexp.md +++ b/docs/content/functions/regexp.md @@ -5,62 +5,106 @@ menu: parent: functions --- -## `regexp.Replace` +These functions allow user you to search and modify text with regular expressions. -Replaces matches of a regular expression with the replacement string. The syntax -of the regular expressions accepted is [Go's `regexp` syntax](https://golang.org/pkg/regexp/syntax/#hdr-Syntax), +The syntax of the regular expressions accepted is [Go's `regexp` syntax](https://golang.org/pkg/regexp/syntax/#hdr-Syntax), and is the same general syntax used by Perl, Python, and other languages. -### Usage +## `regexp.Find` + +Returns a string holding the text of the leftmost match in `input` +of the regular expression `expression`. + +This function provides the same behaviour as Go's +[`regexp.FindString`](https://golang.org/pkg/regexp/#Regexp.FindString) function. +### Usage ```go -regexp.Replace expression replacement input +regexp.Find expression input ``` + ```go -input | regexp.Replace expression replacement +input | regexp.Find expression ``` ### Arguments -| name | description | -|--------|-------| -| `expression` | The regular expression string | -| `replacement` | The replacement string | -| `input` | the input string to operate on | +| name | description | +|------|-------------| +| `expression` | _(required)_ The regular expression | +| `input` | _(required)_ The input to search | ### Examples ```console -$ gomplate -i '{{ regexp.Replace "(foo)bar" "$1" "foobar"}}' +$ gomplate -i '{{ regexp.Find "[a-z]{3}" "foobar"}}' foo ``` +```console +$ gomplate -i 'no {{ "will not match" | regexp.Find "[0-9]" }}numbers' +no numbers +``` + +## `regexp.FindAll` + +Returns a list of all successive matches of the regular expression. + +This can be called with 2 or 3 arguments. When called with 2 arguments, the +`n` argument (number of matches) will be set to `-1`, causing all matches +to be returned. + +This function provides the same behaviour as Go's +[`regexp.FindAllString`](https://golang.org/pkg/regexp/#Regexp.FindAllString) function. + +### Usage +```go +regexp.FindAll expression [false] input +``` + +```go +input | regexp.FindAll expression [false] +``` + +### Arguments + +| name | description | +|------|-------------| +| `expression` | _(required)_ The regular expression | +| `false` | _(optional)_ The number of matches to return | +| `input` | _(required)_ The input to search | + +### Examples ```console -$ gomplate -i '{{ regexp.Replace "(?P<first>[a-zA-Z]+) (?P<last>[a-zA-Z]+)" "${last}, ${first}" "Alan Turing"}}' -Turing, Alan +$ gomplate -i '{{ regexp.FindAll "[a-z]{3}" "foobar" | toJSON}}' +["foo", "bar"] +``` +```console +$ gomplate -i '{{ "foo bar baz qux" | regexp.FindAll "[a-z]{3}" 3 | toJSON}}' +["foo", "bar", "baz"] ``` ## `regexp.Match` -Returns `true` if a given regular expression matches a given input string. +Returns `true` if a given regular expression matches a given input. This returns a boolean which can be used in an `if` condition, for example. ### Usage - ```go -regexp.Match expression input +regexp.Match expression input ``` + ```go -input | regexp.Match expression +input | regexp.Match expression ``` ### Arguments -| name | description | -|--------|-------| -| `expression` | the regular expression to match | -| `input` | the input string to test | +| name | description | +|------|-------------| +| `expression` | _(required)_ The regular expression | +| `input` | _(required)_ The input to test | ### Examples @@ -68,3 +112,120 @@ input | regexp.Match expression $ gomplate -i '{{ if (.Env.USER | regexp.Match `^h`) }}username ({{.Env.USER}}) starts with h!{{end}}' username (hairyhenderson) starts with h! ``` + +## `regexp.Replace` + +Replaces matches of a regular expression with the replacement string. + +The replacement is substituted after expanding variables beginning with `$`. + +This function provides the same behaviour as Go's +[`regexp.ReplaceAllString`](https://golang.org/pkg/regexp/#Regexp.ReplaceAllString) function. + +### Usage +```go +regexp.Replace expression replacement input +``` + +```go +input | regexp.Replace expression replacement +``` + +### Arguments + +| name | description | +|------|-------------| +| `expression` | _(required)_ The regular expression string | +| `replacement` | _(required)_ The replacement string | +| `input` | _(required)_ The input string to operate on | + +### Examples + +```console +$ gomplate -i '{{ regexp.Replace "(foo)bar" "$1" "foobar"}}' +foo +``` +```console +$ gomplate -i '{{ regexp.Replace "(?P<first>[a-zA-Z]+) (?P<last>[a-zA-Z]+)" "${last}, ${first}" "Alan Turing"}}' +Turing, Alan +``` + +## `regexp.ReplaceLiteral` + +Replaces matches of a regular expression with the replacement string. + +The replacement is substituted directly, without expanding variables +beginning with `$`. + +This function provides the same behaviour as Go's +[`regexp.ReplaceAllLiteralString`](https://golang.org/pkg/regexp/#Regexp.ReplaceAllLiteralString) function. + +### Usage +```go +regexp.ReplaceLiteral expression replacement input +``` + +```go +input | regexp.ReplaceLiteral expression replacement +``` + +### Arguments + +| name | description | +|------|-------------| +| `expression` | _(required)_ The regular expression string | +| `replacement` | _(required)_ The replacement string | +| `input` | _(required)_ The input string to operate on | + +### Examples + +```console +$ gomplate -i '{{ regexp.ReplaceLiteral "(foo)bar" "$1" "foobar"}}' +$1 +``` +```console +$ gomplate -i '{{ `foo.bar,baz` | regexp.ReplaceLiteral `\W` `$` }}' +foo$bar$baz +``` + +## `regexp.Split` + +Splits `input` into sub-strings, separated by the expression. + +This can be called with 2 or 3 arguments. When called with 2 arguments, the +`n` argument (number of matches) will be set to `-1`, causing all sub-strings +to be returned. + +This is equivalent to [`strings.SplitN`](../strings/#strings-splitn), +except that regular expressions are supported. + +This function provides the same behaviour as Go's +[`regexp.Split`](https://golang.org/pkg/regexp/#Regexp.Split) function. + +### Usage +```go +regexp.Split expression [false] input +``` + +```go +input | regexp.Split expression [false] +``` + +### Arguments + +| name | description | +|------|-------------| +| `expression` | _(required)_ The regular expression | +| `false` | _(optional)_ The number of matches to return | +| `input` | _(required)_ The input to search | + +### Examples + +```console +$ gomplate -i '{{ regexp.Split `[\s,.]` "foo bar,baz.qux" | toJSON}}' +["foo","bar","baz","qux"] +``` +```console +$ gomplate -i '{{ "foo bar.baz,qux" | regexp.Split `[\s,.]` 3 | toJSON}}' +["foo","bar","baz"] +``` diff --git a/funcs/regexp.go b/funcs/regexp.go index ad5578fb..344b5cf8 100644 --- a/funcs/regexp.go +++ b/funcs/regexp.go @@ -3,6 +3,8 @@ package funcs import ( "sync" + "github.com/pkg/errors" + "github.com/hairyhenderson/gomplate/conv" "github.com/hairyhenderson/gomplate/regexp" ) @@ -26,12 +28,65 @@ func AddReFuncs(f map[string]interface{}) { // ReFuncs - type ReFuncs struct{} -// Replace - -func (f *ReFuncs) Replace(re, replacement string, input interface{}) string { - return regexp.Replace(re, replacement, conv.ToString(input)) +// Find - +func (f *ReFuncs) Find(re, input interface{}) (string, error) { + return regexp.Find(conv.ToString(re), conv.ToString(input)) +} + +// FindAll - +func (f *ReFuncs) FindAll(args ...interface{}) ([]string, error) { + re := "" + n := 0 + input := "" + switch len(args) { + case 2: + n = -1 + re = conv.ToString(args[0]) + input = conv.ToString(args[1]) + case 3: + re = conv.ToString(args[0]) + n = conv.ToInt(args[1]) + input = conv.ToString(args[2]) + default: + return nil, errors.Errorf("wrong number of args: want 2 or 3, got %d", len(args)) + } + return regexp.FindAll(re, n, input) } // Match - -func (f *ReFuncs) Match(re string, input interface{}) bool { - return regexp.Match(re, conv.ToString(input)) +func (f *ReFuncs) Match(re, input interface{}) bool { + return regexp.Match(conv.ToString(re), conv.ToString(input)) +} + +// Replace - +func (f *ReFuncs) Replace(re, replacement, input interface{}) string { + return regexp.Replace(conv.ToString(re), + conv.ToString(replacement), + conv.ToString(input)) +} + +// ReplaceLiteral - +func (f *ReFuncs) ReplaceLiteral(re, replacement, input interface{}) (string, error) { + return regexp.ReplaceLiteral(conv.ToString(re), + conv.ToString(replacement), + conv.ToString(input)) +} + +// Split - +func (f *ReFuncs) Split(args ...interface{}) ([]string, error) { + re := "" + n := -1 + input := "" + switch len(args) { + case 2: + re = conv.ToString(args[0]) + input = conv.ToString(args[1]) + case 3: + re = conv.ToString(args[0]) + n = conv.ToInt(args[1]) + input = conv.ToString(args[2]) + default: + return nil, errors.Errorf("wrong number of args: want 2 or 3, got %d", len(args)) + } + return regexp.Split(re, n, input) } diff --git a/funcs/regexp_test.go b/funcs/regexp_test.go index 31671da0..905c4146 100644 --- a/funcs/regexp_test.go +++ b/funcs/regexp_test.go @@ -15,3 +15,100 @@ func TestMatch(t *testing.T) { re := &ReFuncs{} assert.True(t, re.Match(`i\ `, "hi world")) } + +func TestFind(t *testing.T) { + re := &ReFuncs{} + f, err := re.Find(`[a-z]+`, `foo bar baz`) + assert.NoError(t, err) + assert.Equal(t, "foo", f) + + _, err = re.Find(`[a-`, "") + assert.Error(t, err) + + f, err = re.Find("4", 42) + assert.NoError(t, err) + assert.Equal(t, "4", f) + + f, err = re.Find(false, 42) + assert.NoError(t, err) + assert.Equal(t, "", f) +} + +func TestFindAll(t *testing.T) { + re := &ReFuncs{} + f, err := re.FindAll(`[a-z]+`, `foo bar baz`) + assert.NoError(t, err) + assert.EqualValues(t, []string{"foo", "bar", "baz"}, f) + + f, err = re.FindAll(`[a-z]+`, -1, `foo bar baz`) + assert.NoError(t, err) + assert.EqualValues(t, []string{"foo", "bar", "baz"}, f) + + _, err = re.FindAll(`[a-`, "") + assert.Error(t, err) + + _, err = re.FindAll("") + assert.Error(t, err) + + _, err = re.FindAll("", "", "", "") + assert.Error(t, err) + + f, err = re.FindAll(`[a-z]+`, 0, `foo bar baz`) + assert.NoError(t, err) + assert.Nil(t, f) + + f, err = re.FindAll(`[a-z]+`, 2, `foo bar baz`) + assert.NoError(t, err) + assert.EqualValues(t, []string{"foo", "bar"}, f) + + f, err = re.FindAll(`[a-z]+`, 14, `foo bar baz`) + assert.NoError(t, err) + assert.EqualValues(t, []string{"foo", "bar", "baz"}, f) + + f, err = re.FindAll(`qux`, `foo bar baz`) + assert.NoError(t, err) + assert.Nil(t, f) +} + +func TestSplit(t *testing.T) { + re := &ReFuncs{} + f, err := re.Split(` `, `foo bar baz`) + assert.NoError(t, err) + assert.EqualValues(t, []string{"foo", "bar", "baz"}, f) + + f, err = re.Split(`\s+`, -1, `foo bar baz`) + assert.NoError(t, err) + assert.EqualValues(t, []string{"foo", "bar", "baz"}, f) + + _, err = re.Split(`[a-`, "") + assert.Error(t, err) + + _, err = re.Split("") + assert.Error(t, err) + + _, err = re.Split("", "", "", "") + assert.Error(t, err) + + f, err = re.Split(` `, 0, `foo bar baz`) + assert.NoError(t, err) + assert.Nil(t, f) + + f, err = re.Split(`\s+`, 2, `foo bar baz`) + assert.NoError(t, err) + assert.EqualValues(t, []string{"foo", "bar baz"}, f) + + f, err = re.Split(`\s`, 14, `foo bar baz`) + assert.NoError(t, err) + assert.EqualValues(t, []string{"foo", "", "bar", "baz"}, f) + + f, err = re.Split(`[\s,.]`, 14, `foo bar.baz,qux`) + assert.NoError(t, err) + assert.EqualValues(t, []string{"foo", "bar", "baz", "qux"}, f) +} + +func TestReplaceLiteral(t *testing.T) { + re := &ReFuncs{} + r, err := re.ReplaceLiteral("i", "ello$1", "hi world") + assert.NoError(t, err) + assert.Equal(t, "hello$1 world", r) +} diff --git a/regexp/regexp.go b/regexp/regexp.go index 8aeaa43c..a6cadc74 100644 --- a/regexp/regexp.go +++ b/regexp/regexp.go @@ -2,10 +2,22 @@ package regexp import stdre "regexp" -// Replace - -func Replace(expression, replacement, input string) string { - re := stdre.MustCompile(expression) - return re.ReplaceAllString(input, replacement) +// Find - +func Find(expression, input string) (string, error) { + re, err := stdre.Compile(expression) + if err != nil { + return "", err + } + return re.FindString(input), nil +} + +// FindAll - +func FindAll(expression string, n int, input string) ([]string, error) { + re, err := stdre.Compile(expression) + if err != nil { + return nil, err + } + return re.FindAllString(input, n), nil } // Match - @@ -13,3 +25,27 @@ func Match(expression, input string) bool { re := stdre.MustCompile(expression) return re.MatchString(input) } + +// Replace - +func Replace(expression, replacement, input string) string { + re := stdre.MustCompile(expression) + return re.ReplaceAllString(input, replacement) +} + +// ReplaceLiteral - +func ReplaceLiteral(expression, replacement, input string) (string, error) { + re, err := stdre.Compile(expression) + if err != nil { + return "", err + } + return re.ReplaceAllLiteralString(input, replacement), nil +} + +// Split - +func Split(expression string, n int, input string) ([]string, error) { + re, err := stdre.Compile(expression) + if err != nil { + return nil, err + } + return re.Split(input, n), nil +} diff --git a/regexp/regexp_test.go b/regexp/regexp_test.go index 15adccd4..89f6cb86 100644 --- a/regexp/regexp_test.go +++ b/regexp/regexp_test.go @@ -6,18 +6,104 @@ import ( "github.com/stretchr/testify/assert" ) -func TestReplace(t *testing.T) { - assert.Equal(t, "-T-T-", Replace("a(x*)b", "T", "-ab-axxb-")) - assert.Equal(t, "--xx-", Replace("a(x*)b", "$1", "-ab-axxb-")) - assert.Equal(t, "---", Replace("a(x*)b", "$1W", "-ab-axxb-")) - assert.Equal(t, "-W-xxW-", Replace("a(x*)b", "${1}W", "-ab-axxb-")) +func TestFind(t *testing.T) { + f, err := Find(`[a-z]+`, `foo bar baz`) + assert.NoError(t, err) + assert.Equal(t, "foo", f) - assert.Equal(t, "Turing, Alan", Replace("(?P<first>[a-zA-Z]+) (?P<last>[a-zA-Z]+)", "${last}, ${first}", "Alan Turing")) + _, err = Find(`[a-`, "") + assert.Error(t, err) } +func TestFindAll(t *testing.T) { + _, err := FindAll(`[a-`, 42, "") + assert.Error(t, err) + + testdata := []struct { + re string + n int + in string + expected []string + }{ + {`[a-z]+`, -1, `foo bar baz`, []string{"foo", "bar", "baz"}}, + {`[a-z]+`, 0, `foo bar baz`, nil}, + {`[a-z]+`, 2, `foo bar baz`, []string{"foo", "bar"}}, + {`[a-z]+`, 14, `foo bar baz`, []string{"foo", "bar", "baz"}}, + } + + for _, d := range testdata { + f, err := FindAll(d.re, d.n, d.in) + assert.NoError(t, err) + assert.EqualValues(t, d.expected, f) + } +} func TestMatch(t *testing.T) { assert.True(t, Match(`^[a-z]+\[[0-9]+\]$`, "adam[23]")) assert.True(t, Match(`^[a-z]+\[[0-9]+\]$`, "eve[7]")) assert.False(t, Match(`^[a-z]+\[[0-9]+\]$`, "Job[48]")) assert.False(t, Match(`^[a-z]+\[[0-9]+\]$`, "snakey")) } + +func TestReplace(t *testing.T) { + testdata := []struct { + expected string + expression string + replacement string + input string + }{ + {"-T-T-", "a(x*)b", "T", "-ab-axxb-"}, + {"--xx-", "a(x*)b", "$1", "-ab-axxb-"}, + {"---", "a(x*)b", "$1W", "-ab-axxb-"}, + {"-W-xxW-", "a(x*)b", "${1}W", "-ab-axxb-"}, + {"Turing, Alan", "(?P<first>[a-zA-Z]+) (?P<last>[a-zA-Z]+)", "${last}, ${first}", "Alan Turing"}, + } + for _, d := range testdata { + assert.Equal(t, d.expected, Replace(d.expression, d.replacement, d.input)) + } +} + +func TestReplaceLiteral(t *testing.T) { + _, err := ReplaceLiteral(`[a-`, "", "") + assert.Error(t, err) + + testdata := []struct { + expected string + expression string + replacement string + input string + }{ + {"-T-T-", "a(x*)b", "T", "-ab-axxb-"}, + {"-$1-$1-", "a(x*)b", "$1", "-ab-axxb-"}, + {"-$1W-$1W-", "a(x*)b", "$1W", "-ab-axxb-"}, + {"-${1}W-${1}W-", "a(x*)b", "${1}W", "-ab-axxb-"}, + {"${last}, ${first}", "(?P<first>[a-zA-Z]+) (?P<last>[a-zA-Z]+)", "${last}, ${first}", "Alan Turing"}, + } + for _, d := range testdata { + r, err := ReplaceLiteral(d.expression, d.replacement, d.input) + assert.NoError(t, err) + assert.Equal(t, d.expected, r) + } +} + +func TestSplit(t *testing.T) { + _, err := Split(`[a-`, 42, "") + assert.Error(t, err) + + testdata := []struct { + re string + n int + in string + expected []string + }{ + {`\s+`, -1, "foo bar baz\tqux", []string{"foo", "bar", "baz", "qux"}}, + {`,`, 0, `foo bar baz`, nil}, + {` `, 2, `foo bar baz`, []string{"foo", "bar baz"}}, + {`[\s,.]`, 14, `foo bar.baz,qux`, []string{"foo", "bar", "baz", "qux"}}, + } + + for _, d := range testdata { + f, err := Split(d.re, d.n, d.in) + assert.NoError(t, err) + assert.EqualValues(t, d.expected, f) + } +} |
