├── go.mod ├── hclgrep ├── option.go ├── parse.go ├── usage.go ├── tokenize.go ├── cmd.go ├── match_test.go └── match.go ├── .github └── workflows │ └── go.yml ├── main.go ├── LICENSE ├── README.md └── go.sum /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/magodo/hclgrep 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/hashicorp/hcl/v2 v2.11.1 7 | github.com/zclconf/go-cty v1.8.0 8 | ) 9 | 10 | require ( 11 | github.com/agext/levenshtein v1.2.1 // indirect 12 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 13 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect 14 | golang.org/x/text v0.14.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /hclgrep/option.go: -------------------------------------------------------------------------------- 1 | package hclgrep 2 | 3 | import "io" 4 | 5 | type Option func(*Matcher) 6 | 7 | func OptionCmd(cmd Cmd) Option { 8 | return func(m *Matcher) { 9 | m.cmds = append(m.cmds, cmd) 10 | } 11 | } 12 | 13 | func OptionPrefixPosition(include bool) Option { 14 | return func(m *Matcher) { 15 | m.prefix = include 16 | } 17 | } 18 | 19 | func OptionOutput(o io.Writer) Option { 20 | return func(m *Matcher) { 21 | m.out = o 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "github.com/magodo/hclgrep/hclgrep" 8 | "os" 9 | ) 10 | 11 | func main() { 12 | opts, files, err := hclgrep.ParseArgs(os.Args[1:]) 13 | if err != nil { 14 | if errors.Is(err, flag.ErrHelp) { 15 | os.Exit(0) 16 | } 17 | fmt.Fprintln(os.Stderr, err) 18 | os.Exit(1) 19 | } 20 | m := hclgrep.NewMatcher(opts...) 21 | if err := m.Files(files); err != nil { 22 | fmt.Fprintln(os.Stderr, err) 23 | os.Exit(1) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /hclgrep/parse.go: -------------------------------------------------------------------------------- 1 | package hclgrep 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hashicorp/hcl/v2" 6 | "github.com/hashicorp/hcl/v2/hclsyntax" 7 | ) 8 | 9 | func compileExpr(expr string) (hclsyntax.Node, error) { 10 | toks, err := tokenize(expr) 11 | if err != nil { 12 | return nil, fmt.Errorf("cannot tokenize expr: %v", err) 13 | } 14 | 15 | p := toks.Bytes() 16 | node, diags := parse(p, "", hcl.InitialPos) 17 | if diags.HasErrors() { 18 | return nil, fmt.Errorf("cannot parse expr: %v", diags.Error()) 19 | } 20 | return node, nil 21 | } 22 | 23 | func parse(src []byte, filename string, start hcl.Pos) (hclsyntax.Node, hcl.Diagnostics) { 24 | // try as expr 25 | if expr, diags := hclsyntax.ParseExpression(src, filename, start); !diags.HasErrors() { 26 | return expr, nil 27 | } 28 | 29 | // try as file 30 | f, diags := hclsyntax.ParseConfig(src, filename, start) 31 | if diags.HasErrors() { 32 | return nil, diags 33 | } 34 | // This is critical for parsing the pattern, as here actually wants the specified attribute or block, 35 | // but not the whole file body, given it is parsed as a file. 36 | return bodyContent(f.Body.(*hclsyntax.Body)), nil 37 | } 38 | 39 | func bodyContent(body *hclsyntax.Body) hclsyntax.Node { 40 | if body == nil { 41 | return nil 42 | } 43 | if len(body.Blocks) == 0 && len(body.Attributes) == 1 { 44 | var k string 45 | for key := range body.Attributes { 46 | k = key 47 | break 48 | } 49 | return body.Attributes[k] 50 | } 51 | if len(body.Blocks) == 1 && len(body.Attributes) == 0 { 52 | return body.Blocks[0] 53 | } 54 | return body 55 | } 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, magodo 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /hclgrep/usage.go: -------------------------------------------------------------------------------- 1 | package hclgrep 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | var usage = func() { 9 | fmt.Fprintf(os.Stderr, `usage: hclgrep [options] commands [FILE...] 10 | 11 | hclgrep performs a query on the given HCL(v2) files. 12 | 13 | An option is one of the following: 14 | 15 | -H prefix the filename and byte offset of a match (defaults to "true" when reading from multiple files) 16 | 17 | A command is one of the following: 18 | 19 | -%s pattern find all nodes matching a pattern 20 | -%s pattern discard nodes not matching a pattern 21 | -%s pattern discard nodes matching a pattern 22 | -%s number navigate up a number of node parents 23 | -%s name="regexp" filter nodes by regexp against wildcard value of "name" 24 | -%s name print the wildcard node only (must be the last command) 25 | 26 | A pattern is a piece of HCL code which may include wildcards. It can be: 27 | 28 | - A body (zero or more attributes, and zero or more blocks) 29 | - An expression 30 | 31 | There are two types of wildcards can be used in a pattern, depending on the scope it resides in: 32 | 33 | - Attribute wildcard ("@"): represents an attribute, a block or an object element 34 | - Expression wildcard ("$"): represents an expression or a place that a string is accepted (i.e. as a block type, block label) 35 | 36 | The wildcards are followed by a name. Each wildcard with the same name must match the same node/string, excluding "_". Example: 37 | 38 | $x.$_ = $x # assignment of self to a field in self 39 | 40 | The wildcard name is only recorded for "-x" command or "-g" command (the first match in DFS). 41 | 42 | If "*" is before the name, it will match any number of nodes. Example: 43 | 44 | [$*_] # any number of elements in a tuple 45 | 46 | resource foo "name" { 47 | @*_ # any number of attributes/blocks inside the resource block body 48 | } 49 | `, CmdNameMatch, CmdNameFilterMatch, CmdNameFilterUnMatch, CmdNameParent, CmdNameRx, CmdNameWrite) 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hclgrep 2 | 3 | ![workflow](https://github.com/magodo/hclgrep/actions/workflows/go.yml/badge.svg) 4 | 5 | Search for HCL(v2) using syntax tree. 6 | 7 | The idea is heavily inspired by https://github.com/mvdan/gogrep. 8 | 9 | ## Install 10 | 11 | go install github.com/magodo/hclgrep@latest 12 | 13 | ## Usage 14 | 15 | usage: hclgrep [options] commands [FILE...] 16 | 17 | An option is one of the following: 18 | 19 | -H prefix the filename and byte offset of a match 20 | 21 | A command is one of the following: 22 | 23 | -x pattern find all nodes matching a pattern 24 | -g pattern discard nodes not matching a pattern 25 | -v pattern discard nodes matching a pattern 26 | -p number navigate up a number of node parents 27 | -rx name="regexp" filter nodes by regexp against wildcard value of "name" 28 | -w name print the wildcard node only (must be the last command) 29 | 30 | A pattern is a piece of HCL code which may include wildcards. It can be: 31 | 32 | - A [body](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#bodies) (zero or more attributes, and zero or more blocks) 33 | - An [expression](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#expressions) 34 | 35 | There are two types of wildcards can be used in a pattern, depending on the scope it resides in: 36 | 37 | - Attribute wildcard ("@"): represents an [attribute](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#attribute-definitions), a [block](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#blocks), or an [object element](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#collection-values) 38 | - Expression wildcard ("$"): represents an [expression](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#expressions) or a place that a string is accepted (i.e. as a block type, block label) 39 | 40 | The wildcards are followed by a name. Each wildcard with the same name must match the same node/string, excluding "\_". Example: 41 | 42 | $x.$_ = $x # assignment of self to a field in self 43 | 44 | The wildcard name is only recorded for "-x" command or "-g" command (the first match in DFS). 45 | 46 | If "\*" is before the name, it will match **any** number of nodes. Example: 47 | 48 | [$*_] # any number of elements in a tuple 49 | 50 | resource foo "name" { 51 | @*_ # any number of attributes/blocks inside the resource block body 52 | } 53 | 54 | ## Example 55 | 56 | - Grep dynamic blocks used in Terraform config 57 | 58 | $ hclgrep -x 'dynamic $_ {@*_}' main.tf 59 | 60 | - Grep potential mis-used "count" in Terraform config 61 | 62 | $ hclgrep -x 'var.$_[count.index]' main.tf 63 | 64 | - Grep module source addresses in Terraform config 65 | 66 | $ hclgrep -x 'module $_ {@*_}' \ 67 | -x 'source = $addr' \ 68 | -w addr main.tf 69 | 70 | - Grep AzureRM Terraform network security rule resource which allows 22 port for inbound traffic 71 | 72 | $ hclgrep -x 'resource azurerm_network_security_rule $_ {@*_}' \ 73 | -g 'direction = "Inbound"' \ 74 | -g 'access = "Allow"' \ 75 | -g 'destination_port_range = $port' \ 76 | -rx 'port="22|\*"' \ 77 | main.tf 78 | 79 | - Grep for the evaluated Terraform configurations, run following command in the root module (given there is no output variables defined) 80 | 81 | $ terraform show -no-color | sed --expression 's;(sensitive value);"";' | hclgrep -x '' 82 | 83 | ## Limitation 84 | 85 | - The **any** expression wildcard (`$*`) doesn't work inside a traversal. 86 | - The **any** wildcard doesn't remember the matched wildcard name. 87 | -------------------------------------------------------------------------------- /hclgrep/tokenize.go: -------------------------------------------------------------------------------- 1 | package hclgrep 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "github.com/hashicorp/hcl/v2" 8 | "github.com/hashicorp/hcl/v2/hclsyntax" 9 | ) 10 | 11 | // exprTokenType exists to add extra possible tokens on top of the ones 12 | // recognized by vanilla HCL2. 13 | type exprTokenType hclsyntax.TokenType 14 | 15 | const ( 16 | _ exprTokenType = -iota 17 | TokenWildcard 18 | TokenWildcardAny 19 | TokenAttrWildcard 20 | TokenAttrWildcardAny 21 | ) 22 | 23 | type fullToken struct { 24 | Type hclsyntax.TokenType 25 | Bytes []byte 26 | Range hcl.Range 27 | } 28 | 29 | type fullTokens []fullToken 30 | 31 | const ( 32 | wildcardLit = "$" 33 | attrWildcardLit = "@" 34 | ) 35 | 36 | // tokenize create fullTokens by substituting the wildcard token in the source. 37 | // Also it removes any leading newline. 38 | func tokenize(src string) (fullTokens, error) { 39 | tokens, _diags := hclsyntax.LexExpression([]byte(src), "", hcl.InitialPos) 40 | 41 | var diags hcl.Diagnostics 42 | for _, diag := range _diags { 43 | if tok := string(diag.Subject.SliceBytes([]byte(src))); diag.Summary == "Invalid character" && (tok == wildcardLit || tok == attrWildcardLit) { 44 | continue 45 | } 46 | diags = diags.Append(diag) 47 | } 48 | if diags.HasErrors() { 49 | return nil, errors.New(diags.Error()) 50 | } 51 | 52 | var start int 53 | for start = 0; start < len(tokens) && tokens[start].Type == hclsyntax.TokenNewline; start++ { 54 | } 55 | 56 | var remaining []fullToken 57 | for _, tok := range tokens[start:] { 58 | remaining = append(remaining, fullToken{tok.Type, tok.Bytes, tok.Range}) 59 | if tok.Type == hclsyntax.TokenEOF { 60 | break 61 | } 62 | } 63 | next := func() fullToken { 64 | t := remaining[0] 65 | remaining = remaining[1:] 66 | return t 67 | } 68 | 69 | var ( 70 | toks []fullToken 71 | wildcardTokenType = hclsyntax.TokenNil 72 | ) 73 | t := next() 74 | for { 75 | if t.Type == hclsyntax.TokenEOF { 76 | break 77 | } 78 | if !(t.Type == hclsyntax.TokenInvalid && 79 | (string(t.Bytes) == wildcardLit || string(t.Bytes) == attrWildcardLit)) { 80 | // regular HCL 81 | toks = append(toks, fullToken{ 82 | Type: t.Type, 83 | Range: t.Range, 84 | Bytes: t.Bytes, 85 | }) 86 | t = next() 87 | continue 88 | } 89 | switch string(t.Bytes) { 90 | case wildcardLit: 91 | wildcardTokenType = hclsyntax.TokenType(TokenWildcard) 92 | case attrWildcardLit: 93 | wildcardTokenType = hclsyntax.TokenType(TokenAttrWildcard) 94 | default: 95 | panic("never reach here") 96 | } 97 | t = next() 98 | if string(t.Bytes) == string(hclsyntax.TokenStar) { 99 | switch wildcardTokenType { 100 | case hclsyntax.TokenType(TokenWildcard): 101 | wildcardTokenType = hclsyntax.TokenType(TokenWildcardAny) 102 | case hclsyntax.TokenType(TokenAttrWildcard): 103 | wildcardTokenType = hclsyntax.TokenType(TokenAttrWildcardAny) 104 | } 105 | t = next() 106 | } 107 | if t.Type != hclsyntax.TokenIdent { 108 | return nil, fmt.Errorf("%v: wildcard must be followed by ident, got %v", 109 | t.Range, t.Type) 110 | } 111 | toks = append(toks, fullToken{ 112 | Type: wildcardTokenType, 113 | Bytes: t.Bytes, 114 | Range: t.Range, 115 | }) 116 | t = next() 117 | } 118 | 119 | return toks, nil 120 | } 121 | 122 | func (toks fullTokens) Bytes() []byte { 123 | var buf bytes.Buffer 124 | for i, t := range toks { 125 | var s string 126 | switch { 127 | case t.Type == hclsyntax.TokenType(TokenWildcard): 128 | s = wildName(string(t.Bytes), false) 129 | case t.Type == hclsyntax.TokenType(TokenWildcardAny): 130 | s = wildName(string(t.Bytes), true) 131 | case t.Type == hclsyntax.TokenType(TokenAttrWildcard): 132 | s = wildAttr(string(t.Bytes), false) 133 | case t.Type == hclsyntax.TokenType(TokenAttrWildcardAny): 134 | s = wildAttr(string(t.Bytes), true) 135 | default: 136 | s = string(t.Bytes) 137 | } 138 | buf.WriteString(s) 139 | 140 | if i+1 < len(toks) { 141 | peekTok := toks[i+1] 142 | if peekTok.Type == hclsyntax.TokenIdent || 143 | peekTok.Type == hclsyntax.TokenType(TokenWildcard) || 144 | peekTok.Type == hclsyntax.TokenType(TokenAttrWildcard) || 145 | peekTok.Type == hclsyntax.TokenType(TokenWildcardAny) || 146 | peekTok.Type == hclsyntax.TokenType(TokenAttrWildcardAny) { 147 | buf.WriteByte(' ') // for e.g. consecutive idents (e.g. ForExpr) 148 | } 149 | } 150 | } 151 | return buf.Bytes() 152 | } 153 | -------------------------------------------------------------------------------- /hclgrep/cmd.go: -------------------------------------------------------------------------------- 1 | package hclgrep 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/hashicorp/hcl/v2" 11 | "github.com/hashicorp/hcl/v2/hclsyntax" 12 | ) 13 | 14 | type CmdName string 15 | 16 | const ( 17 | CmdNameMatch CmdName = "x" 18 | CmdNameFilterMatch = "g" 19 | CmdNameFilterUnMatch = "v" 20 | CmdNameRx = "rx" 21 | CmdNameParent = "p" 22 | CmdNameWrite = "w" 23 | ) 24 | 25 | type Cmd struct { 26 | name CmdName 27 | src string 28 | value CmdValue 29 | } 30 | 31 | type CmdValue interface { 32 | Value() interface{} 33 | } 34 | 35 | type CmdValueRx struct { 36 | name string 37 | rx regexp.Regexp 38 | } 39 | 40 | func (v CmdValueRx) Value() interface{} { return v } 41 | 42 | type CmdValueNode struct { 43 | hclsyntax.Node 44 | } 45 | 46 | func (v CmdValueNode) Value() interface{} { return v.Node } 47 | 48 | type CmdValueLevel int 49 | 50 | func (v CmdValueLevel) Value() interface{} { return v } 51 | 52 | type CmdValueString string 53 | 54 | func (v CmdValueString) Value() interface{} { return v } 55 | 56 | type strCmdFlag struct { 57 | name CmdName 58 | cmds *[]Cmd 59 | } 60 | 61 | func (o *strCmdFlag) String() string { return "" } 62 | func (o *strCmdFlag) Set(val string) error { 63 | *o.cmds = append(*o.cmds, Cmd{name: o.name, src: val}) 64 | return nil 65 | } 66 | 67 | func ParseArgs(args []string) ([]Option, []string, error) { 68 | flagSet := flag.NewFlagSet("hclgrep", flag.ContinueOnError) 69 | flagSet.Usage = usage 70 | 71 | var prefix bool 72 | flagSet.BoolVar(&prefix, "H", false, "prefix filename and byte offset for a match") 73 | 74 | var cmds []Cmd 75 | flagSet.Var(&strCmdFlag{ 76 | name: CmdNameMatch, 77 | cmds: &cmds, 78 | }, string(CmdNameMatch), "") 79 | flagSet.Var(&strCmdFlag{ 80 | name: CmdNameFilterMatch, 81 | cmds: &cmds, 82 | }, string(CmdNameFilterMatch), "") 83 | flagSet.Var(&strCmdFlag{ 84 | name: CmdNameFilterUnMatch, 85 | cmds: &cmds, 86 | }, string(CmdNameFilterUnMatch), "") 87 | flagSet.Var(&strCmdFlag{ 88 | name: CmdNameParent, 89 | cmds: &cmds, 90 | }, string(CmdNameParent), "") 91 | flagSet.Var(&strCmdFlag{ 92 | name: CmdNameRx, 93 | cmds: &cmds, 94 | }, string(CmdNameRx), "") 95 | flagSet.Var(&strCmdFlag{ 96 | name: CmdNameWrite, 97 | cmds: &cmds, 98 | }, string(CmdNameWrite), "") 99 | 100 | if err := flagSet.Parse(args); err != nil { 101 | return nil, nil, err 102 | } 103 | 104 | if len(cmds) < 1 { 105 | return nil, nil, fmt.Errorf("need at least one command") 106 | } 107 | 108 | for i, cmd := range cmds { 109 | switch cmd.name { 110 | case CmdNameWrite: 111 | if i != len(cmds)-1 { 112 | return nil, nil, fmt.Errorf("`-%s` must be the last command", cmd.name) 113 | } 114 | cmds[i].value = CmdValueString(cmd.src) 115 | case CmdNameRx: 116 | name, rx, err := parseRegexpAttr(cmd.src) 117 | if err != nil { 118 | return nil, nil, err 119 | } 120 | cmds[i].value = CmdValueRx{name: name, rx: *rx} 121 | case CmdNameParent: 122 | n, err := strconv.Atoi(cmd.src) 123 | if err != nil { 124 | return nil, nil, err 125 | } 126 | if n < 0 { 127 | return nil, nil, fmt.Errorf("the number follows `-%s` must >=0, got %d", cmd.name, n) 128 | } 129 | cmds[i].value = CmdValueLevel(n) 130 | default: 131 | node, err := compileExpr(cmd.src) 132 | if err != nil { 133 | return nil, nil, err 134 | } 135 | cmds[i].value = CmdValueNode{node} 136 | } 137 | } 138 | 139 | opts := []Option{OptionPrefixPosition(prefix)} 140 | for _, cmd := range cmds { 141 | opts = append(opts, OptionCmd(cmd)) 142 | } 143 | return opts, flagSet.Args(), nil 144 | } 145 | 146 | func parseAttr(attr string) (string, string, error) { 147 | tokens, diags := hclsyntax.LexExpression([]byte(attr), "", hcl.InitialPos) 148 | if diags.HasErrors() { 149 | return "", "", fmt.Errorf(diags.Error()) 150 | } 151 | next := func() hclsyntax.Token { 152 | tok := tokens[0] 153 | tokens = tokens[1:] 154 | return tok 155 | } 156 | tok := next() 157 | if tok.Type != hclsyntax.TokenIdent { 158 | return "", "", fmt.Errorf("%v: attribute must starts with an ident, got %q", tok.Range, tok.Type) 159 | } 160 | name := string(tok.Bytes) 161 | if tok := next(); tok.Type != hclsyntax.TokenEqual { 162 | return "", "", fmt.Errorf(`%v: attribute name must be followed by "=", got %q`, tok.Range, tok.Type) 163 | } 164 | if tok := next(); tok.Type != hclsyntax.TokenOQuote { 165 | return "", "", fmt.Errorf("%v: attribute value must enclose within quotes", tok.Range) 166 | } 167 | tok = next() 168 | if tok.Type != hclsyntax.TokenQuotedLit { 169 | return "", "", fmt.Errorf("%v: attribute value must enclose within quotes", tok.Range) 170 | } 171 | value := string(tok.Bytes) 172 | if tok := next(); tok.Type != hclsyntax.TokenCQuote { 173 | return "", "", fmt.Errorf("%v: attribute value must enclose within quotes", tok.Range) 174 | } 175 | if tok := next(); tok.Type != hclsyntax.TokenEOF { 176 | return "", "", fmt.Errorf("%v: invalid content after attribute value", tok.Range) 177 | } 178 | return name, value, nil 179 | } 180 | 181 | func parseRegexpAttr(attr string) (string, *regexp.Regexp, error) { 182 | name, value, err := parseAttr(attr) 183 | if err != nil { 184 | return "", nil, fmt.Errorf("cannot parse attribute: %v", err) 185 | } 186 | if !strings.HasPrefix(value, "^") { 187 | value = "^" + value 188 | } 189 | if !strings.HasSuffix(value, "$") { 190 | value = value + "$" 191 | } 192 | rx, err := regexp.Compile(value) 193 | return name, rx, err 194 | } 195 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= 2 | github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 3 | github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= 4 | github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= 5 | github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 6 | github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= 7 | github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 11 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 12 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 15 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 16 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 17 | github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJbcc= 18 | github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= 19 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 20 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 21 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 22 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 23 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 24 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= 25 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 26 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= 27 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 29 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 30 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 31 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 32 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 33 | github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 34 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 35 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 36 | github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= 37 | github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= 38 | github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= 39 | github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= 40 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 41 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 42 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 43 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 44 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 45 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 46 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 47 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 48 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 49 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 50 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 51 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 52 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 53 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 56 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 58 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 66 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 67 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 68 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 69 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 70 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 71 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 72 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 73 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 74 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 75 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 76 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 77 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 78 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 79 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 80 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 82 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 83 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 84 | -------------------------------------------------------------------------------- /hclgrep/match_test.go: -------------------------------------------------------------------------------- 1 | package hclgrep 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "testing" 8 | 9 | "github.com/hashicorp/hcl/v2" 10 | "github.com/hashicorp/hcl/v2/hclsyntax" 11 | ) 12 | 13 | type wantErr string 14 | 15 | func tokErr(msg string) wantErr { 16 | return wantErr("cannot tokenize expr: " + msg) 17 | } 18 | 19 | func parseErr(msg string) wantErr { 20 | return wantErr("cannot parse expr: " + msg) 21 | } 22 | 23 | func attrErr(msg string) wantErr { 24 | return wantErr("cannot parse attribute: " + msg) 25 | } 26 | 27 | func otherErr(msg string) wantErr { 28 | return wantErr(msg) 29 | } 30 | 31 | func TestMatch(t *testing.T) { 32 | tests := []struct { 33 | args []string 34 | src string 35 | want interface{} 36 | }{ 37 | // literal expression 38 | {[]string{"-x", "1"}, "1", 1}, 39 | {[]string{"-x", "true"}, "false", 0}, 40 | 41 | // literal expression (wildcard) 42 | {[]string{"-x", "x = $_"}, "x = 1", 1}, 43 | {[]string{"-x", "x = $_"}, "x = false", 1}, 44 | {[]string{"-x", "x = $*_"}, "x = false", 1}, 45 | 46 | // tuple cons expression 47 | {[]string{"-x", "[1, 2]"}, "[1, 3]", 0}, 48 | {[]string{"-x", "[1, 2]"}, "[1, 2]", 1}, 49 | 50 | // tuple cons expression (wildcard) 51 | {[]string{"-x", "x = $_"}, "x = [1, 2, 3]", 1}, 52 | {[]string{"-x", "[1, $_, 3]"}, "[1, 2, 3]", 1}, 53 | {[]string{"-x", "[1, $_, 3]"}, "[1, 3]", 0}, 54 | {[]string{"-x", "[1, $x, $x]"}, "[1, 2, 2]", 1}, 55 | {[]string{"-x", "[1, $x, $x]"}, "[1, 2, 3]", 0}, 56 | { 57 | args: []string{"-x", ` 58 | [ 59 | $x, 60 | 1, 61 | $x, 62 | ]`}, 63 | src: ` 64 | [ 65 | 2, 66 | 1, 67 | 2, 68 | ]`, 69 | want: 1, 70 | }, 71 | { 72 | args: []string{"-x", ` 73 | [ 74 | $x, 75 | 1, 76 | $x, 77 | ]`}, 78 | src: `[2, 1, 2]`, 79 | want: 1, 80 | }, 81 | {[]string{"-x", "[1, $*_]"}, "[1, 2, 3]", 1}, 82 | {[]string{"-x", "[$*_, 1]"}, "[1, 2, 3]", 0}, 83 | {[]string{"-x", "[$*_]"}, "[]", 1}, 84 | {[]string{"-x", "[$*_, $x]"}, "[1, 2, 3]", 1}, 85 | 86 | // object const expression 87 | {[]string{"-x", "{a = b}"}, "{a = b}", 1}, 88 | {[]string{"-x", "{a = c}"}, "{a = b}", 0}, 89 | { 90 | args: []string{"-x", ` 91 | { 92 | a = b 93 | c = d 94 | }`}, 95 | src: ` 96 | { 97 | a = b 98 | c = d 99 | }`, 100 | want: 1, 101 | }, 102 | 103 | // object const expression (wildcard) 104 | {[]string{"-x", "x = $_"}, "x = {a = b}", 1}, 105 | {[]string{"-x", "{$x = $x}"}, "{a = a}", 1}, 106 | {[]string{"-x", "{$x = $x}"}, "{a = b}", 0}, 107 | { 108 | args: []string{"-x", ` 109 | { 110 | a = $x 111 | c = $x 112 | }`}, 113 | src: ` 114 | { 115 | a = b 116 | c = b 117 | }`, 118 | want: 1, 119 | }, 120 | { 121 | args: []string{"-x", ` 122 | { 123 | a = $x 124 | c = $x 125 | }`}, 126 | src: ` 127 | { 128 | a = b 129 | c = d 130 | }`, 131 | want: 0, 132 | }, 133 | { 134 | args: []string{"-x", ` 135 | { 136 | $_ = $_ 137 | $_ = $_ 138 | }`}, 139 | src: ` 140 | { 141 | a = b 142 | c = d 143 | }`, 144 | want: 1, 145 | }, 146 | { 147 | args: []string{"-x", ` 148 | { 149 | @_ 150 | @_ 151 | }`}, 152 | src: ` 153 | { 154 | a = b 155 | c = d 156 | }`, 157 | want: 1, 158 | }, 159 | { 160 | args: []string{"-x", ` 161 | { 162 | @*_ 163 | }`}, 164 | src: ` 165 | { 166 | a = b 167 | c = d 168 | }`, 169 | want: 1, 170 | }, 171 | { 172 | args: []string{"-x", ` 173 | { 174 | @*_ 175 | e = f 176 | }`}, 177 | src: ` 178 | { 179 | a = b 180 | c = d 181 | e = f 182 | }`, 183 | want: 1, 184 | }, 185 | 186 | // template expression 187 | {[]string{"-x", `"a"`}, `"a"`, 1}, 188 | {[]string{"-x", `"a"`}, `"b"`, 0}, 189 | { 190 | args: []string{"-x", `< v}"}, "{for k, v in map: k => upper(v)}", 0}, 243 | {[]string{"-x", "{for k, v in map: k => upper(v)}"}, "{for k, v in map: k => upper(v)}", 1}, 244 | 245 | // for expression (wildcard) 246 | {[]string{"-x", "x = $_"}, "x = {for k, v in map: k => upper(v)}", 1}, 247 | {[]string{"-x", "{for k, v in map: $k => upper($v)}"}, "{for k, v in map: k => upper(v)}", 1}, 248 | {[]string{"-x", "{for $k, $v in map: $k => upper($v)}"}, "{for k, v in map: k => upper(v)}", 1}, 249 | 250 | // index expression 251 | {[]string{"-x", "foo[a]"}, "foo[a]", 1}, 252 | {[]string{"-x", "foo[a]"}, "foo[b]", 0}, 253 | 254 | // index expression (wildcard) 255 | {[]string{"-x", "x = $_"}, "x = foo[a]", 1}, 256 | {[]string{"-x", "foo[$x]"}, "foo[a]", 1}, 257 | {[]string{"-x", "foo[$*x]"}, "foo[a]", 1}, 258 | {[]string{"-x", "a[$x]"}, "a[1]", 1}, 259 | {[]string{"-x", "foo()[$x]"}, "foo()[1]", 1}, 260 | {[]string{"-x", "[1,2,3][$x]"}, "[1,2,3][1]", 1}, 261 | {[]string{"-x", `"abc"[$x]`}, `"abc"[0]`, 1}, 262 | {[]string{"-x", `x[0][$x]`}, `x[0][0]`, 1}, 263 | {[]string{"-x", `x[$x][$x]`}, `x[0][0]`, 1}, 264 | {[]string{"-x", `x[$x][$x]`}, `x[0][1]`, 0}, 265 | 266 | // splat expression 267 | {[]string{"-x", "tuple.*.foo.bar[0]"}, "tuple.*.foo.bar[0]", 1}, 268 | {[]string{"-x", "tuple.*.foo.bar[0]"}, "tuple.*.bar.bar[0]", 0}, 269 | {[]string{"-x", "tuple[*].foo.bar[0]"}, "tuple[*].foo.bar[0]", 1}, 270 | {[]string{"-x", "tuple[*].foo.bar[0]"}, "tuple[*].bar.bar[0]", 0}, 271 | 272 | // splat expression (wildcard) 273 | {[]string{"-x", "x = $_"}, "x = tuple.*.foo.bar[0]", 1}, 274 | {[]string{"-x", "x = $_"}, "x = tuple[*].foo.bar[0]", 1}, 275 | {[]string{"-x", "x = $*_"}, "x = tuple[*].foo.bar[0]", 1}, 276 | 277 | // parenthese expression 278 | {[]string{"-x", "(a)"}, "(a)", 1}, 279 | {[]string{"-x", "(a)"}, "(b)", 0}, 280 | 281 | // parenthese expression (wildcard) 282 | {[]string{"-x", "x = $_"}, "x = (a)", 1}, 283 | {[]string{"-x", "($_)"}, "(b)", 1}, 284 | {[]string{"-x", "($*_)"}, "(b)", 1}, 285 | 286 | // unary operation expression 287 | {[]string{"-x", "-1"}, "-1", 1}, 288 | {[]string{"-x", "-1"}, "1", 0}, 289 | 290 | // unary operation expression (wildcard) 291 | {[]string{"-x", "x = $_"}, "x = -1", 1}, 292 | {[]string{"-x", "x = $_"}, "x = !true", 1}, 293 | {[]string{"-x", "x = $*_"}, "x = !true", 1}, 294 | 295 | // binary operation expression 296 | {[]string{"-x", "1+1"}, "1+1", 1}, 297 | {[]string{"-x", "1+1"}, "1-1", 0}, 298 | 299 | // binary operation expression (wildcard) 300 | {[]string{"-x", "x = $_"}, "x = 1+1", 1}, 301 | {[]string{"-x", "x = $*_"}, "x = 1+1", 1}, 302 | 303 | // conditional expression 304 | {[]string{"-x", "cond? 0:1"}, "cond? 0:1", 1}, 305 | {[]string{"-x", "cond? 0:1"}, "cond? 1:0", 0}, 306 | 307 | // conditional expression (wildcard) 308 | {[]string{"-x", "x = $_"}, "x = cond? 0:1", 1}, 309 | {[]string{"-x", "$_? 0:1"}, "cond? 0:1", 1}, 310 | {[]string{"-x", "cond? 0:$_"}, "cond? 0:1", 1}, 311 | {[]string{"-x", "cond? 0:$*_"}, "cond? 0:1", 1}, 312 | 313 | // scope traversal expression 314 | {[]string{"-x", "a"}, "a", 1}, 315 | {[]string{"-x", "a"}, "b", 0}, 316 | {[]string{"-x", "a.attr"}, "a.attr", 1}, 317 | {[]string{"-x", "a.attr"}, "a.attr2", 0}, 318 | {[]string{"-x", "a[0]"}, "a[0]", 1}, 319 | {[]string{"-x", "a[0]"}, "a[1]", 0}, 320 | {[]string{"-x", "a.0"}, "a.0", 1}, 321 | {[]string{"-x", "a.0"}, "a[0]", 1}, //index or legacy index are considered the same 322 | {[]string{"-x", "a.0"}, "a.1", 0}, 323 | 324 | // scope traversal expression (wildcard) 325 | {[]string{"-x", "x = $_"}, "x = a", 1}, 326 | {[]string{"-x", "x = $_"}, "x = a.attr", 1}, 327 | {[]string{"-x", "x = $_"}, "x = a[0]", 1}, 328 | {[]string{"-x", "x = $_"}, "x = a.0", 1}, 329 | {[]string{"-x", "x = $_"}, "x = a.x.y.x", 1}, 330 | {[]string{"-x", "$_.$_"}, "a.x.y.x", 0}, 331 | {[]string{"-x", "a.$_.$_.$_"}, "a.x.y.z", 1}, 332 | {[]string{"-x", "a.$x.$_.$x"}, "a.x.y.z", 0}, 333 | {[]string{"-x", "a.$x.$_.$x"}, "a.x.y.x", 1}, 334 | {[]string{"-x", "$_.$x.$_.$x"}, "a.x.y.x", 1}, 335 | {[]string{"-x", "a.$x.$*_.$x"}, "a.x.y.z", 0}, 336 | 337 | // relative traversal expression 338 | {[]string{"-x", "sort()[0]"}, "sort()[0]", 1}, 339 | {[]string{"-x", "sort()[0]"}, "sort()[1]", 0}, 340 | {[]string{"-x", "sort()[0]"}, "reverse()[0]", 0}, 341 | 342 | // relative traversal expression (wildcard) 343 | {[]string{"-x", "x = $_"}, "x = sort()[0]", 1}, 344 | {[]string{"-x", "$_()[0]"}, "sort()[0]", 1}, 345 | {[]string{"-x", "$_()[0]"}, "sort(arg)[0]", 0}, 346 | {[]string{"-x", "$*_()[0]"}, "sort(arg)[0]", 0}, 347 | 348 | // TODO: object cons key expression 349 | // TODO: template join expression 350 | // TODO: template wrap expression 351 | // TODO: anonym symbol expression 352 | 353 | // attribute 354 | {[]string{"-x", "a = a"}, "a = a", 1}, 355 | {[]string{"-x", "a = a"}, "a = b", 0}, 356 | 357 | // attribute (wildcard) 358 | {[]string{"-x", "$x = $x"}, "a = a", 1}, 359 | {[]string{"-x", "$x = $x"}, "a = b", 0}, 360 | {[]string{"-x", "$x = $*_"}, "a = b", 1}, 361 | 362 | // attributes 363 | { 364 | args: []string{"-x", ` 365 | a = b 366 | c = d 367 | `}, 368 | src: ` 369 | a = b 370 | c = d 371 | `, 372 | want: 1, 373 | }, 374 | { 375 | args: []string{"-x", ` 376 | a = b 377 | c = d 378 | `}, 379 | src: ` 380 | a = b 381 | `, 382 | want: 0, 383 | }, 384 | 385 | // attributes (wildcard) 386 | { 387 | args: []string{"-x", ` 388 | @x 389 | @y 390 | `}, 391 | src: ` 392 | a = b 393 | c = d 394 | `, 395 | want: 1, 396 | }, 397 | { 398 | args: []string{"-x", ` 399 | a = $x 400 | c = $x 401 | `}, 402 | src: ` 403 | a = b 404 | c = d 405 | `, 406 | want: 0, 407 | }, 408 | { 409 | args: []string{"-x", ` 410 | a = $x 411 | c = $x 412 | `}, 413 | src: ` 414 | a = b 415 | c = b 416 | `, 417 | want: 1, 418 | }, 419 | { 420 | args: []string{"-x", ` 421 | a = $x 422 | c = $x 423 | `}, 424 | src: ` 425 | a = b 426 | c = b 427 | `, 428 | want: 1, 429 | }, 430 | { 431 | args: []string{"-x", `@*_`}, 432 | src: ` 433 | a = b 434 | c = d 435 | `, 436 | want: 2, 437 | }, 438 | { 439 | args: []string{"-x", ` 440 | @*_ 441 | e = f 442 | `}, 443 | src: ` 444 | a = b 445 | c = d 446 | e = f 447 | `, 448 | want: 1, 449 | }, 450 | 451 | // block 452 | { 453 | args: []string{"-x", `blk { 454 | a = b 455 | }`}, 456 | src: `blk { 457 | a = b 458 | }`, 459 | want: 1, 460 | }, 461 | { 462 | args: []string{"-x", `blk { 463 | a = b 464 | c = d 465 | }`}, 466 | src: `blk { 467 | a = b 468 | }`, 469 | want: 0, 470 | }, 471 | 472 | // block (wildcard) 473 | { 474 | args: []string{"-x", `$_ { 475 | a = b 476 | }`}, 477 | src: `blk { 478 | a = b 479 | }`, 480 | want: 1, 481 | }, 482 | { 483 | args: []string{"-x", `blk { 484 | a = $x 485 | c = $x 486 | }`}, 487 | src: `blk { 488 | a = b 489 | c = d 490 | }`, 491 | want: 0, 492 | }, 493 | { 494 | args: []string{"-x", `blk { 495 | a = $x 496 | c = $x 497 | }`}, 498 | src: `blk { 499 | a = b 500 | c = b 501 | }`, 502 | want: 1, 503 | }, 504 | { 505 | args: []string{"-x", `$a { 506 | a = $x 507 | b = "" 508 | }`}, 509 | src: ` 510 | blk1 { 511 | blk2 { 512 | a = file("./a.txt") 513 | b = "" 514 | } 515 | } 516 | `, 517 | want: 1, 518 | }, 519 | { 520 | args: []string{"-x", `$*_ { 521 | a = b 522 | }`}, 523 | src: `type label1 label2 { 524 | a = b 525 | }`, 526 | want: 0, 527 | }, 528 | { 529 | args: []string{"-x", `type $*_ { 530 | a = b 531 | }`}, 532 | src: `type label1 label2 { 533 | a = b 534 | }`, 535 | want: 1, 536 | }, 537 | 538 | // blocks 539 | { 540 | args: []string{"-x", `blk1 { 541 | a = b 542 | } 543 | 544 | blk2 { 545 | c = d 546 | }`}, 547 | src: `blk1 { 548 | a = b 549 | } 550 | 551 | blk2 { 552 | c = d 553 | }`, 554 | want: 1, 555 | }, 556 | { 557 | args: []string{"-x", `blk1 { 558 | a = b 559 | } 560 | 561 | blk2 { 562 | c = d 563 | }`}, 564 | src: `blk1 { 565 | a = b 566 | }`, 567 | want: 0, 568 | }, 569 | 570 | // blocks (wildcard) 571 | { 572 | args: []string{"-x", ` 573 | $x { 574 | a = b 575 | } 576 | 577 | $x { 578 | c = d 579 | }`}, 580 | src: ` 581 | blk1 { 582 | a = b 583 | } 584 | 585 | blk2 { 586 | c = d 587 | }`, 588 | want: 0, 589 | }, 590 | { 591 | args: []string{"-x", ` 592 | $x { 593 | a = b 594 | } 595 | 596 | $x { 597 | c = d 598 | }`}, 599 | src: ` 600 | blk1 { 601 | a = b 602 | } 603 | 604 | blk1 { 605 | c = d 606 | }`, 607 | want: 1, 608 | }, 609 | { 610 | args: []string{"-x", ` 611 | @*_ 612 | 613 | $x { 614 | c = d 615 | }`}, 616 | src: ` 617 | blk1 {} 618 | blk1 {} 619 | 620 | blk1 { 621 | c = d 622 | }`, 623 | want: 1, 624 | }, 625 | { 626 | args: []string{"-x", `$_`}, 627 | src: ` 628 | blk1 {} 629 | blk1 {}`, 630 | want: 5, // 1 toplevel body + 2* (1 body + 1 block) 631 | }, 632 | 633 | // body 634 | { 635 | args: []string{"-x", ` 636 | a = 1 637 | block { 638 | b = 2 639 | } 640 | `}, 641 | src: ` 642 | a = 1 643 | block { 644 | b = 2 645 | } 646 | `, 647 | want: 1, 648 | }, 649 | { 650 | args: []string{"-x", ` 651 | a = 1 652 | block { 653 | b = 2 654 | } 655 | `}, 656 | src: ` 657 | a = 1 658 | `, 659 | want: 0, 660 | }, 661 | 662 | // body (wildcard) 663 | { 664 | args: []string{"-x", `blk { 665 | @_ 666 | @_ 667 | }`}, 668 | src: ` 669 | blk { 670 | a = 1 671 | block { 672 | b = 2 673 | } 674 | } 675 | `, 676 | want: 1, 677 | }, 678 | { 679 | args: []string{"-x", `@x`}, 680 | src: ` 681 | blk { 682 | a = 1 683 | block { 684 | b = 2 685 | } 686 | } 687 | `, 688 | want: 4, 689 | }, 690 | { 691 | args: []string{"-x", ` 692 | blk { 693 | $_ {} 694 | } 695 | `}, 696 | src: ` 697 | blk { 698 | blk1 {} 699 | } 700 | `, 701 | want: 1, 702 | }, 703 | { 704 | args: []string{"-x", ` 705 | @_ 706 | 707 | blk { 708 | @_ 709 | } 710 | `}, 711 | src: ` 712 | a = b 713 | 714 | blk { 715 | blk1 {} 716 | } 717 | `, 718 | want: 1, 719 | }, 720 | { 721 | args: []string{"-x", ` 722 | @x 723 | 724 | blk { 725 | @x 726 | } 727 | `}, 728 | src: ` 729 | a = b 730 | 731 | blk { 732 | a = b 733 | } 734 | `, 735 | want: 1, 736 | }, 737 | { 738 | args: []string{"-x", ` 739 | @x 740 | 741 | blk { 742 | @x 743 | } 744 | `}, 745 | src: ` 746 | a = b 747 | 748 | blk { 749 | a = c 750 | } 751 | `, 752 | want: 0, 753 | }, 754 | { 755 | args: []string{"-x", ` 756 | @x 757 | 758 | blk { 759 | @x 760 | } 761 | `}, 762 | src: ` 763 | blk1 {} 764 | 765 | blk { 766 | blk1 {} 767 | } 768 | `, 769 | want: 1, 770 | }, 771 | { 772 | args: []string{"-x", ` 773 | @x 774 | 775 | blk { 776 | @x 777 | } 778 | `}, 779 | src: ` 780 | a = b 781 | 782 | blk { 783 | blk1 {} 784 | } 785 | `, 786 | want: 0, 787 | }, 788 | { 789 | args: []string{"-x", ` 790 | @*_ 791 | 792 | blk { 793 | @x 794 | } 795 | `}, 796 | src: ` 797 | a = b 798 | blk1 {} 799 | 800 | blk { 801 | blk1 {} 802 | } 803 | `, 804 | want: 1, 805 | }, 806 | 807 | // expr tokenize errors 808 | {[]string{"-x", "$"}, "", tokErr(":1,2-2: wildcard must be followed by ident, got TokenEOF")}, 809 | 810 | // expr parse errors 811 | {[]string{"-x", "a = "}, "", parseErr(":1,3-3: Missing expression; Expected the start of an expression, but found the end of the file.")}, 812 | 813 | // no command 814 | {[]string{}, "", otherErr("need at least one command")}, 815 | 816 | // empty source 817 | {[]string{"-x", ""}, "", 1}, 818 | {[]string{"-x", "\t"}, "", 1}, 819 | {[]string{"-x", "a"}, "", 0}, 820 | 821 | // "-p" 822 | { 823 | args: []string{"-p", "0"}, 824 | src: ` 825 | blk { 826 | x = 1 827 | }`, 828 | want: `blk { 829 | x = 1 830 | }`, 831 | }, 832 | { 833 | args: []string{"-p", "1"}, 834 | src: ` 835 | blk { 836 | x = 1 837 | }`, 838 | want: 0, 839 | }, 840 | { 841 | args: []string{"-p", "-1"}, 842 | src: ` 843 | blk { 844 | x = 1 845 | }`, 846 | want: wantErr("the number follows `-p` must >=0, got -1"), 847 | }, 848 | { 849 | args: []string{"-x", "x = 1", "-p", "1"}, 850 | src: ` 851 | blk { 852 | x = 1 853 | }`, 854 | want: `{ 855 | x = 1 856 | }`, 857 | }, 858 | { 859 | args: []string{"-x", "x = 1", "-p", "2"}, 860 | src: ` 861 | blk { 862 | x = 1 863 | }`, 864 | want: `blk { 865 | x = 1 866 | }`, 867 | }, 868 | 869 | // "-rx" 870 | { 871 | args: []string{"-x", "x = $a", "-rx", `a="1"`}, 872 | src: `x = 1`, 873 | want: `x = 1`, 874 | }, 875 | { 876 | args: []string{"-x", "x = $a", "-rx", `a="f.."`}, 877 | src: `x = "foo"`, 878 | want: `x = "foo"`, 879 | }, 880 | { 881 | args: []string{"-x", "x = $a", "-rx", `a="true"`}, 882 | src: `x = true`, 883 | want: `x = true`, 884 | }, 885 | { 886 | args: []string{"-x", "x = $a", "-rx", `a="false"`}, 887 | src: `x = true`, 888 | want: 0, 889 | }, 890 | { 891 | args: []string{"-x", "x = $a", "-rx", `a="\*"`}, 892 | src: `x = "*"`, 893 | want: 1, 894 | }, 895 | { 896 | args: []string{"-x", "x = $a", "-rx", `a="\*"`}, 897 | src: `x = 123`, 898 | want: 0, 899 | }, 900 | { 901 | args: []string{"-x", "x = $a", "-rx", `nonexist="false"`}, 902 | src: `x = true`, 903 | want: 0, 904 | }, 905 | { 906 | args: []string{"-x", "x = $a", "-rx", ``}, 907 | src: ``, 908 | want: attrErr(":1,1-1: attribute must starts with an ident, got \"TokenEOF\""), 909 | }, 910 | { 911 | args: []string{"-x", "x = $a", "-rx", `a1`}, 912 | src: ``, 913 | want: attrErr(":1,3-3: attribute name must be followed by \"=\", got \"TokenEOF\""), 914 | }, 915 | { 916 | args: []string{"-x", "x = $a", "-rx", `a1=abc`}, 917 | src: ``, 918 | want: attrErr(":1,4-7: attribute value must enclose within quotes"), 919 | }, 920 | { 921 | args: []string{"-x", "x = $a", "-rx", `a1="abc`}, 922 | src: ``, 923 | want: attrErr(":1,8-8: attribute value must enclose within quotes"), 924 | }, 925 | { 926 | args: []string{"-x", "x = $a", "-rx", `a1="abc"tail`}, 927 | src: ``, 928 | want: attrErr(":1,9-13: invalid content after attribute value"), 929 | }, 930 | 931 | // "-v" 932 | { 933 | args: []string{"-x", "blk {@*_}", "-v", `a = $_`}, 934 | src: `blk { 935 | a = 1 936 | } 937 | 938 | blk { 939 | b = 1 940 | }`, 941 | want: `blk { 942 | b = 1 943 | }`, 944 | }, 945 | // `-v` pattern won't record wildcard name 946 | { 947 | args: []string{"-x", "blk {@*_}", "-v", `a = $x`, "-rx", `x="1"`}, 948 | src: `blk { 949 | a = 1 950 | } 951 | 952 | blk { 953 | b = 1 954 | }`, 955 | want: 0, 956 | }, 957 | 958 | // "-g" 959 | { 960 | args: []string{"-x", "blk {@*_}", "-g", `a = $_`}, 961 | src: `blk { 962 | a = 1 963 | } 964 | 965 | blk { 966 | b = 1 967 | }`, 968 | want: `blk { 969 | a = 1 970 | }`, 971 | }, 972 | // `-g` pattern records wildcard name 973 | { 974 | args: []string{"-x", "blk {@*_}", "-g", `a = $x`, "-rx", `x="1"`}, 975 | src: `blk { 976 | a = 1 977 | } 978 | 979 | blk { 980 | b = 1 981 | }`, 982 | want: `blk { 983 | a = 1 984 | }`, 985 | }, 986 | // short circut of -g pattern match, the recorded wildcard name is the first match (DFS) 987 | { 988 | args: []string{"-x", "blk {@*_}", "-g", `a = $x`, "-rx", `x="1"`}, 989 | src: `blk { 990 | a = 1 991 | nest { 992 | a = 2 993 | } 994 | }`, 995 | want: 1, 996 | }, 997 | { 998 | args: []string{"-x", "blk {@*_}", "-g", `a = $x`, "-rx", `x="2"`}, 999 | src: `blk { 1000 | a = 1 1001 | nest { 1002 | a = 2 1003 | } 1004 | }`, 1005 | want: 0, 1006 | }, 1007 | } 1008 | 1009 | for i, tc := range tests { 1010 | t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { 1011 | matchTest(t, tc.args, tc.src, tc.want) 1012 | }) 1013 | } 1014 | } 1015 | 1016 | func matchTest(t *testing.T, args []string, src string, anyWant interface{}) { 1017 | tfatalf := func(format string, a ...interface{}) { 1018 | t.Fatalf("%v | %s: %s", args, src, fmt.Sprintf(format, a...)) 1019 | } 1020 | opts, _, err := ParseArgs(args) 1021 | switch want := anyWant.(type) { 1022 | case wantErr: 1023 | if err == nil { 1024 | tfatalf("wanted error %q, got none", want) 1025 | } else if got := err.Error(); got != string(want) { 1026 | tfatalf("wanted error %q, got %q", want, got) 1027 | } 1028 | return 1029 | } 1030 | if err != nil { 1031 | tfatalf("unexpected error: %v", err) 1032 | } 1033 | 1034 | opts = append(opts, OptionOutput(io.Discard)) 1035 | m := NewMatcher(opts...) 1036 | matches := matchStrs(m, src) 1037 | switch want := anyWant.(type) { 1038 | case int: 1039 | if len(matches) != want { 1040 | tfatalf("wanted %d matches, got=%d", want, len(matches)) 1041 | } 1042 | case string: 1043 | if l := len(matches); l != 1 { 1044 | if l == 0 { 1045 | tfatalf("no match") 1046 | } else { 1047 | tfatalf("unexpected multiple matches %d", len(matches)) 1048 | } 1049 | } 1050 | m := matches[0] 1051 | got := string(m.Range().SliceBytes([]byte(src))) 1052 | if want != got { 1053 | tfatalf("wanted:\n%s\ngot:\n%s\n", want, got) 1054 | } 1055 | default: 1056 | panic(fmt.Sprintf("unexpected anyWant type: %T", anyWant)) 1057 | } 1058 | } 1059 | 1060 | func matchStrs(m Matcher, src string) []hclsyntax.Node { 1061 | srcNode, err := parse([]byte(src), "", hcl.InitialPos) 1062 | if err != nil { 1063 | panic(fmt.Sprintf("parsing source node: %v", err)) 1064 | } 1065 | return m.matches(srcNode) 1066 | } 1067 | 1068 | func TestFile(t *testing.T) { 1069 | tests := []struct { 1070 | args []string 1071 | src string 1072 | want interface{} 1073 | }{ 1074 | // reading from stdin without -H 1075 | {[]string{"-x", "foo = bar"}, "foo = bar", "foo = bar\n"}, 1076 | // reading from stdin with -H 1077 | {[]string{"-H", "-x", "foo = bar"}, "foo = bar", `:1,1-10: 1078 | foo = bar 1079 | `}, 1080 | // reading from one file without -H 1081 | {[]string{"-x", "foo = bar", "file"}, "foo = bar", "foo = bar\n"}, 1082 | // reading from one file with -H 1083 | {[]string{"-H", "-x", "foo = bar", "file"}, "foo = bar", `:1,1-10: 1084 | foo = bar 1085 | `}, 1086 | // -w only prints nothing 1087 | {[]string{"-w", "abc"}, "foo = bar", ""}, 1088 | // -w is not the last command 1089 | {[]string{"-x", "foo = $a", "-w", "a", "-x", "foo = $a"}, "foo = bar", otherErr("`-w` must be the last command")}, 1090 | // -w 1091 | {[]string{"-x", "foo = $a", "-w", "a"}, "foo = bar", "bar\n"}, 1092 | } 1093 | 1094 | for i, tc := range tests { 1095 | t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) { 1096 | fileTest(t, tc.args, tc.src, tc.want) 1097 | }) 1098 | } 1099 | } 1100 | 1101 | func fileTest(t *testing.T, args []string, src string, anyWant interface{}) { 1102 | tfatalf := func(format string, a ...interface{}) { 1103 | t.Fatalf("%v | %s: %s", args, src, fmt.Sprintf(format, a...)) 1104 | } 1105 | opts, _, err := ParseArgs(args) 1106 | switch want := anyWant.(type) { 1107 | case wantErr: 1108 | if err == nil { 1109 | tfatalf("wanted error %q, got none", want) 1110 | } else if got := err.Error(); got != string(want) { 1111 | tfatalf("wanted error %q, got %q", want, got) 1112 | } 1113 | return 1114 | } 1115 | if err != nil { 1116 | tfatalf("unexpected error: %v", err) 1117 | } 1118 | 1119 | buf := bytes.NewBufferString("") 1120 | opts = append(opts, OptionOutput(buf)) 1121 | m := NewMatcher(opts...) 1122 | if err := m.File("", bytes.NewBufferString(src)); err != nil { 1123 | tfatalf("m.file() error: %v", err) 1124 | } 1125 | switch want := anyWant.(type) { 1126 | case string: 1127 | got := buf.String() 1128 | if want != got { 1129 | tfatalf("wanted:\n%s\ngot:\n%s\n", want, got) 1130 | } 1131 | default: 1132 | panic(fmt.Sprintf("unexpected anyWant type: %T", anyWant)) 1133 | } 1134 | } 1135 | -------------------------------------------------------------------------------- /hclgrep/match.go: -------------------------------------------------------------------------------- 1 | package hclgrep 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/zclconf/go-cty/cty" 12 | 13 | "github.com/hashicorp/hcl/v2" 14 | "github.com/hashicorp/hcl/v2/hclsyntax" 15 | ) 16 | 17 | type Matcher struct { 18 | out io.Writer 19 | 20 | cmds []Cmd 21 | 22 | parents map[hclsyntax.Node]hclsyntax.Node 23 | b []byte 24 | 25 | // whether prefix the matches with filenname and byte offset 26 | prefix bool 27 | 28 | // node values recorded by name, excluding "_" (used only by the 29 | // actual matching phase) 30 | values map[string]substitution 31 | } 32 | 33 | func NewMatcher(opts ...Option) Matcher { 34 | m := Matcher{} 35 | for _, opt := range opts { 36 | opt(&m) 37 | } 38 | if m.out == nil { 39 | m.out = os.Stdout 40 | } 41 | return m 42 | } 43 | 44 | // Files matches multiple Files, output the final matches to matcher's out. In case the length of the files is 0, it matches the content from the stdin. 45 | func (m *Matcher) Files(files []string) error { 46 | if len(files) == 0 { 47 | if err := m.File("stdin", os.Stdin); err != nil { 48 | return err 49 | } 50 | } 51 | 52 | for _, file := range files { 53 | in, err := os.Open(file) 54 | if err != nil { 55 | return fmt.Errorf("openning %s: %w", file, err) 56 | } 57 | err = m.File(file, in) 58 | in.Close() 59 | if err != nil { 60 | return fmt.Errorf("processing %s: %w", file, err) 61 | } 62 | } 63 | return nil 64 | } 65 | 66 | // File matches one File, output the final matches to matcher's out. 67 | func (m *Matcher) File(fileName string, in io.Reader) error { 68 | m.parents = make(map[hclsyntax.Node]hclsyntax.Node) 69 | var err error 70 | m.b, err = io.ReadAll(in) 71 | if err != nil { 72 | return err 73 | } 74 | f, diags := hclsyntax.ParseConfig(m.b, fileName, hcl.InitialPos) 75 | if diags.HasErrors() { 76 | return fmt.Errorf("cannot parse source: %s", diags.Error()) 77 | } 78 | matches := m.matches(f.Body.(*hclsyntax.Body)) 79 | wd, _ := os.Getwd() 80 | 81 | if m.cmds[len(m.cmds)-1].name == CmdNameWrite { 82 | return nil 83 | } 84 | 85 | for _, n := range matches { 86 | rng := n.Range() 87 | output := string(rng.SliceBytes(m.b)) 88 | if m.prefix { 89 | if strings.HasPrefix(rng.Filename, wd) { 90 | rng.Filename = rng.Filename[len(wd)+1:] 91 | } 92 | output = fmt.Sprintf("%s:\n%s", rng, output) 93 | } 94 | 95 | fmt.Fprintf(m.out, "%s\n", output) 96 | } 97 | return nil 98 | } 99 | 100 | // matches matches one node. 101 | func (m *Matcher) matches(node hclsyntax.Node) []hclsyntax.Node { 102 | m.fillParents(node) 103 | initial := []submatch{{node: node, values: map[string]substitution{}}} 104 | final := m.submatches(m.cmds, initial) 105 | matches := make([]hclsyntax.Node, len(final)) 106 | for i := range matches { 107 | matches[i] = final[i].node 108 | } 109 | return matches 110 | } 111 | 112 | type parentsWalker struct { 113 | stack []hclsyntax.Node 114 | parents map[hclsyntax.Node]hclsyntax.Node 115 | } 116 | 117 | func (w *parentsWalker) Enter(node hclsyntax.Node) hcl.Diagnostics { 118 | switch node.(type) { 119 | case hclsyntax.Attributes, 120 | hclsyntax.Blocks, 121 | hclsyntax.ChildScope: 122 | return nil 123 | } 124 | w.parents[node] = w.stack[len(w.stack)-1] 125 | w.stack = append(w.stack, node) 126 | return nil 127 | } 128 | 129 | func (w *parentsWalker) Exit(node hclsyntax.Node) hcl.Diagnostics { 130 | switch node.(type) { 131 | case hclsyntax.Attributes, 132 | hclsyntax.Blocks, 133 | hclsyntax.ChildScope: 134 | return nil 135 | } 136 | w.stack = w.stack[:len(w.stack)-1] 137 | return nil 138 | } 139 | 140 | func (m *Matcher) fillParents(nodes ...hclsyntax.Node) { 141 | walker := &parentsWalker{ 142 | parents: map[hclsyntax.Node]hclsyntax.Node{}, 143 | stack: make([]hclsyntax.Node, 1, 32), 144 | } 145 | for _, node := range nodes { 146 | hclsyntax.Walk(node, walker) 147 | } 148 | m.parents = walker.parents 149 | } 150 | 151 | type submatch struct { 152 | node hclsyntax.Node 153 | values map[string]substitution 154 | } 155 | 156 | func (m *Matcher) submatches(cmds []Cmd, subs []submatch) []submatch { 157 | if len(cmds) == 0 { 158 | return subs 159 | } 160 | var fn func(Cmd, []submatch) []submatch 161 | cmd := cmds[0] 162 | switch cmd.name { 163 | case CmdNameMatch: 164 | fn = m.cmdMatch 165 | case CmdNameFilterMatch: 166 | fn = m.cmdFilter(true) 167 | case CmdNameFilterUnMatch: 168 | fn = m.cmdFilter(false) 169 | case CmdNameParent: 170 | fn = m.cmdParent 171 | case CmdNameRx: 172 | fn = m.cmdRx 173 | case CmdNameWrite: 174 | fn = m.cmdWrite 175 | default: 176 | panic(fmt.Sprintf("unknown command: %q", cmd.name)) 177 | } 178 | return m.submatches(cmds[1:], fn(cmd, subs)) 179 | } 180 | 181 | func (m *Matcher) cmdMatch(cmd Cmd, subs []submatch) []submatch { 182 | var matches []submatch 183 | for _, sub := range subs { 184 | hclsyntax.VisitAll(sub.node, func(node hclsyntax.Node) hcl.Diagnostics { 185 | m.values = valsCopy(sub.values) 186 | if m.node(cmd.value.Value().(hclsyntax.Node), node) { 187 | matches = append(matches, submatch{ 188 | node: node, 189 | values: m.values, 190 | }) 191 | } 192 | return nil 193 | }) 194 | } 195 | return matches 196 | } 197 | 198 | func (m *Matcher) cmdFilter(wantMatch bool) func(Cmd, []submatch) []submatch { 199 | return func(cmd Cmd, subs []submatch) []submatch { 200 | var matches []submatch 201 | var any bool 202 | for _, sub := range subs { 203 | any = false 204 | hclsyntax.VisitAll(sub.node, func(node hclsyntax.Node) hcl.Diagnostics { 205 | // return early if already match, so that the values are kept to be the state of the first match (DFS) 206 | if any { 207 | return nil 208 | } 209 | m.values = valsCopy(sub.values) 210 | if m.node(cmd.value.Value().(hclsyntax.Node), node) { 211 | any = true 212 | } 213 | return nil 214 | }) 215 | if any == wantMatch { 216 | // update the values of submatch for '-g' 217 | if wantMatch { 218 | sub.values = m.values 219 | } 220 | matches = append(matches, sub) 221 | } 222 | } 223 | return matches 224 | } 225 | } 226 | 227 | func (m *Matcher) cmdParent(cmd Cmd, subs []submatch) []submatch { 228 | var newsubs []submatch 229 | for _, sub := range subs { 230 | reps := int(cmd.value.Value().(CmdValueLevel)) 231 | for j := 0; j < reps; j++ { 232 | sub.node = m.parentOf(sub.node) 233 | } 234 | if sub.node != nil { 235 | newsubs = append(newsubs, sub) 236 | } 237 | } 238 | return newsubs 239 | } 240 | 241 | func (m *Matcher) cmdRx(cmd Cmd, subs []submatch) []submatch { 242 | var newsubs []submatch 243 | for _, sub := range subs { 244 | rx := cmd.value.Value().(CmdValueRx) 245 | val, ok := sub.values[rx.name] 246 | if !ok { 247 | continue 248 | } 249 | var valLit string 250 | switch { 251 | case val.String != nil: 252 | valLit = *val.String 253 | case val.Node != nil: 254 | var ok bool 255 | // check whether the node is a variable 256 | valLit, ok = variableExpr(val.Node) 257 | if !ok { 258 | switch node := val.Node.(type) { 259 | case *hclsyntax.TemplateExpr: 260 | if len(node.Parts) != 1 { 261 | continue 262 | } 263 | tmpl := node.Parts[0] 264 | lve, ok := tmpl.(*hclsyntax.LiteralValueExpr) 265 | if !ok { 266 | continue 267 | } 268 | value, _ := lve.Value(nil) 269 | valLit = value.AsString() 270 | case *hclsyntax.LiteralValueExpr: 271 | value, _ := node.Value(nil) 272 | switch value.Type() { 273 | case cty.String: 274 | valLit = value.AsString() 275 | case cty.Bool: 276 | valLit = "true" 277 | if value.False() { 278 | valLit = "false" 279 | } 280 | case cty.Number: 281 | // TODO: handle float? 282 | valLit = value.AsBigFloat().String() 283 | } 284 | } 285 | } 286 | case val.ObjectConsItem != nil: 287 | case val.Traverser != nil: 288 | switch trav := (*val.Traverser).(type) { 289 | case hcl.TraverseRoot: 290 | valLit = trav.Name 291 | case hcl.TraverseAttr: 292 | valLit = trav.Name 293 | default: 294 | continue 295 | } 296 | default: 297 | panic("never reach here") 298 | } 299 | 300 | if rx.rx.MatchString(valLit) { 301 | newsubs = append(newsubs, sub) 302 | } 303 | } 304 | return newsubs 305 | } 306 | 307 | func (m *Matcher) cmdWrite(cmd Cmd, subs []submatch) []submatch { 308 | for _, sub := range subs { 309 | name := string(cmd.value.Value().(CmdValueString)) 310 | val, ok := sub.values[name] 311 | if !ok { 312 | continue 313 | } 314 | switch { 315 | case val.String != nil: 316 | fmt.Fprintln(m.out, *val.String) 317 | case val.Node != nil: 318 | fmt.Fprintln(m.out, string(val.Node.Range().SliceBytes(m.b))) 319 | case val.ObjectConsItem != nil: 320 | case val.Traverser != nil: 321 | switch trav := (*val.Traverser).(type) { 322 | case hcl.TraverseRoot: 323 | fmt.Fprintln(m.out, trav.Name) 324 | case hcl.TraverseAttr: 325 | fmt.Fprintln(m.out, trav.Name) 326 | default: 327 | continue 328 | } 329 | default: 330 | panic("never reach here") 331 | } 332 | } 333 | 334 | return subs 335 | } 336 | 337 | func (m *Matcher) parentOf(node hclsyntax.Node) hclsyntax.Node { 338 | return m.parents[node] 339 | } 340 | 341 | func valsCopy(values map[string]substitution) map[string]substitution { 342 | v2 := make(map[string]substitution, len(values)) 343 | for k, v := range values { 344 | v2[k] = v 345 | } 346 | return v2 347 | } 348 | 349 | type substitution struct { 350 | String *string 351 | Node hclsyntax.Node 352 | ObjectConsItem *hclsyntax.ObjectConsItem 353 | Traverser *hcl.Traverser 354 | } 355 | 356 | func newStringSubstitution(s string) substitution { 357 | return substitution{String: &s} 358 | } 359 | 360 | func newNodeSubstitution(node hclsyntax.Node) substitution { 361 | return substitution{Node: node} 362 | } 363 | 364 | func newObjectConsItemSubstitution(item *hclsyntax.ObjectConsItem) substitution { 365 | return substitution{ObjectConsItem: item} 366 | } 367 | 368 | func newTraverserSubstitution(trav hcl.Traverser) substitution { 369 | return substitution{Traverser: &trav} 370 | } 371 | 372 | func (m *Matcher) node(pattern, node hclsyntax.Node) bool { 373 | if pattern == nil || node == nil { 374 | return pattern == node 375 | } 376 | 377 | switch x := pattern.(type) { 378 | // Expressions 379 | case *hclsyntax.LiteralValueExpr: 380 | y, ok := node.(*hclsyntax.LiteralValueExpr) 381 | return ok && x.Val.Equals(y.Val).True() 382 | case *hclsyntax.TupleConsExpr: 383 | y, ok := node.(*hclsyntax.TupleConsExpr) 384 | return ok && m.exprs(x.Exprs, y.Exprs) 385 | case *hclsyntax.ObjectConsExpr: 386 | y, ok := node.(*hclsyntax.ObjectConsExpr) 387 | return ok && m.objectConsItems(x.Items, y.Items) 388 | case *hclsyntax.TemplateExpr: 389 | y, ok := node.(*hclsyntax.TemplateExpr) 390 | return ok && m.exprs(x.Parts, y.Parts) 391 | case *hclsyntax.FunctionCallExpr: 392 | y, ok := node.(*hclsyntax.FunctionCallExpr) 393 | return ok && 394 | m.potentialWildcardIdentEqual(x.Name, y.Name) && 395 | m.exprs(x.Args, y.Args) && x.ExpandFinal == y.ExpandFinal 396 | case *hclsyntax.ForExpr: 397 | y, ok := node.(*hclsyntax.ForExpr) 398 | return ok && 399 | m.potentialWildcardIdentEqual(x.KeyVar, y.KeyVar) && 400 | m.potentialWildcardIdentEqual(x.ValVar, y.ValVar) && 401 | m.node(x.CollExpr, y.CollExpr) && m.node(x.KeyExpr, y.KeyExpr) && m.node(x.ValExpr, y.ValExpr) && m.node(x.CondExpr, y.CondExpr) && x.Group == y.Group 402 | case *hclsyntax.IndexExpr: 403 | // In case the index key of x is a wildcard, try to also match "y" even if it is not an IndexExpr 404 | xname, ok := variableExpr(x.Key) 405 | if ok && isWildName(xname) { 406 | switch y := node.(type) { 407 | case *hclsyntax.ScopeTraversalExpr: 408 | l := len(y.Traversal) 409 | ySourceTraversal := &hclsyntax.ScopeTraversalExpr{ 410 | Traversal: make(hcl.Traversal, l-1), 411 | } 412 | copy(ySourceTraversal.Traversal, y.Traversal[:l-1]) 413 | return m.node(x.Collection, ySourceTraversal) && m.wildcardMatchTraverse(xname, y.Traversal[l-1]) 414 | case *hclsyntax.IndexExpr: 415 | return m.node(x.Collection, y.Collection) && m.wildcardMatchNode(xname, y.Key) 416 | case *hclsyntax.RelativeTraversalExpr: 417 | return m.node(x.Collection, y.Source) && len(y.Traversal) == 1 && m.wildcardMatchTraverse(xname, y.Traversal[0]) 418 | default: 419 | return false 420 | } 421 | } 422 | 423 | // Otherwise, regular match against the same type 424 | y, ok := node.(*hclsyntax.IndexExpr) 425 | return ok && m.node(x.Collection, y.Collection) && m.node(x.Key, y.Key) 426 | case *hclsyntax.SplatExpr: 427 | y, ok := node.(*hclsyntax.SplatExpr) 428 | return ok && m.node(x.Source, y.Source) && m.node(x.Each, y.Each) && m.node(x.Item, y.Item) 429 | case *hclsyntax.ParenthesesExpr: 430 | y, ok := node.(*hclsyntax.ParenthesesExpr) 431 | return ok && m.node(x.Expression, y.Expression) 432 | case *hclsyntax.UnaryOpExpr: 433 | y, ok := node.(*hclsyntax.UnaryOpExpr) 434 | return ok && m.operation(x.Op, y.Op) && m.node(x.Val, y.Val) 435 | case *hclsyntax.BinaryOpExpr: 436 | y, ok := node.(*hclsyntax.BinaryOpExpr) 437 | return ok && m.operation(x.Op, y.Op) && m.node(x.LHS, y.LHS) && m.node(x.RHS, y.RHS) 438 | case *hclsyntax.ConditionalExpr: 439 | y, ok := node.(*hclsyntax.ConditionalExpr) 440 | return ok && m.node(x.Condition, y.Condition) && m.node(x.TrueResult, y.TrueResult) && m.node(x.FalseResult, y.FalseResult) 441 | case *hclsyntax.ScopeTraversalExpr: 442 | xname, ok := variableExpr(x) 443 | if ok && isWildName(xname) { 444 | name, _ := fromWildName(xname) 445 | return m.wildcardMatchNode(name, node) 446 | } 447 | y, ok := node.(*hclsyntax.ScopeTraversalExpr) 448 | return ok && m.traversal(x.Traversal, y.Traversal) 449 | case *hclsyntax.RelativeTraversalExpr: 450 | y, ok := node.(*hclsyntax.RelativeTraversalExpr) 451 | return ok && m.traversal(x.Traversal, y.Traversal) && m.node(x.Source, y.Source) 452 | case *hclsyntax.ObjectConsKeyExpr: 453 | y, ok := node.(*hclsyntax.ObjectConsKeyExpr) 454 | return ok && m.node(x.Wrapped, y.Wrapped) && x.ForceNonLiteral == y.ForceNonLiteral 455 | case *hclsyntax.TemplateJoinExpr: 456 | y, ok := node.(*hclsyntax.TemplateJoinExpr) 457 | return ok && m.node(x.Tuple, y.Tuple) 458 | case *hclsyntax.TemplateWrapExpr: 459 | y, ok := node.(*hclsyntax.TemplateWrapExpr) 460 | return ok && m.node(x.Wrapped, y.Wrapped) 461 | case *hclsyntax.AnonSymbolExpr: 462 | _, ok := node.(*hclsyntax.AnonSymbolExpr) 463 | // Only do type check 464 | return ok 465 | // Body 466 | case *hclsyntax.Body: 467 | y, ok := node.(*hclsyntax.Body) 468 | return ok && m.body(x, y) 469 | // Attribute 470 | case *hclsyntax.Attribute: 471 | return m.attribute(x, node) 472 | // Block 473 | case *hclsyntax.Block: 474 | y, ok := node.(*hclsyntax.Block) 475 | return ok && m.block(x, y) 476 | default: 477 | // Including: 478 | // - hclsyntax.ChildScope 479 | // - hclsyntax.Blocks 480 | // - hclsyntax.Attributes 481 | panic(fmt.Sprintf("unexpected node: %T", x)) 482 | } 483 | } 484 | 485 | type matchFunc func(*Matcher, interface{}, interface{}) bool 486 | type wildNameFunc func(interface{}) (string, bool) 487 | 488 | type iterable interface { 489 | at(i int) interface{} 490 | len() int 491 | } 492 | 493 | type stringIterable []string 494 | 495 | func (it stringIterable) at(i int) interface{} { 496 | return it[i] 497 | } 498 | func (it stringIterable) len() int { 499 | return len(it) 500 | } 501 | 502 | type nodeIterable []hclsyntax.Node 503 | 504 | func (it nodeIterable) at(i int) interface{} { 505 | return it[i] 506 | } 507 | 508 | func (it nodeIterable) len() int { 509 | return len(it) 510 | } 511 | 512 | type exprIterable []hclsyntax.Expression 513 | 514 | func (it exprIterable) at(i int) interface{} { 515 | return it[i] 516 | } 517 | 518 | func (it exprIterable) len() int { 519 | return len(it) 520 | } 521 | 522 | type objectConsItemIterable []hclsyntax.ObjectConsItem 523 | 524 | func (it objectConsItemIterable) at(i int) interface{} { 525 | return it[i] 526 | } 527 | 528 | func (it objectConsItemIterable) len() int { 529 | return len(it) 530 | } 531 | 532 | // iterableMatches matches two lists. It uses a common algorithm to match 533 | // wildcard patterns with any number of elements without recursion. 534 | func (m *Matcher) iterableMatches(ns1, ns2 iterable, nf wildNameFunc, mf matchFunc) bool { 535 | i1, i2 := 0, 0 536 | next1, next2 := 0, 0 537 | 538 | // We need to keep a copy of m.values so that we can restart 539 | // with a different "any of" match while discarding any matches 540 | // we found while trying it. 541 | var oldMatches map[string]substitution 542 | backupMatches := func() { 543 | oldMatches = make(map[string]substitution, len(m.values)) 544 | for k, v := range m.values { 545 | oldMatches[k] = v 546 | } 547 | } 548 | backupMatches() 549 | 550 | for i1 < ns1.len() || i2 < ns2.len() { 551 | if i1 < ns1.len() { 552 | n1 := ns1.at(i1) 553 | if _, any := nf(n1); any { 554 | // try to match zero or more at i2, 555 | // restarting at i2+1 if it fails 556 | next1 = i1 557 | next2 = i2 + 1 558 | i1++ 559 | backupMatches() 560 | continue 561 | } 562 | if i2 < ns2.len() && mf(m, n1, ns2.at(i2)) { 563 | // ordinary match 564 | i1++ 565 | i2++ 566 | continue 567 | } 568 | } 569 | // mismatch, try to restart 570 | if 0 < next2 && next2 <= ns2.len() { 571 | i1 = next1 572 | i2 = next2 573 | m.values = oldMatches 574 | continue 575 | } 576 | return false 577 | } 578 | return true 579 | } 580 | 581 | // Node comparisons 582 | 583 | func wildNameFromNode(in interface{}) (string, bool) { 584 | switch node := in.(type) { 585 | case *hclsyntax.ScopeTraversalExpr: 586 | name, ok := variableExpr(node) 587 | if !ok { 588 | return "", false 589 | } 590 | return fromWildName(name) 591 | case *hclsyntax.Attribute: 592 | return fromWildName(node.Name) 593 | default: 594 | return "", false 595 | } 596 | } 597 | 598 | func matchNode(m *Matcher, x, y interface{}) bool { 599 | nx, ny := x.(hclsyntax.Node), y.(hclsyntax.Node) 600 | return m.node(nx, ny) 601 | } 602 | 603 | func (m *Matcher) attribute(x *hclsyntax.Attribute, y hclsyntax.Node) bool { 604 | if x == nil || y == nil { 605 | return x == y 606 | } 607 | if isWildAttr(x.Name, x.Expr) { 608 | // The wildcard attribute can only match attribute or block 609 | switch y := y.(type) { 610 | case *hclsyntax.Attribute, 611 | *hclsyntax.Block: 612 | name, _ := fromWildName(x.Name) 613 | return m.wildcardMatchNode(name, y) 614 | default: 615 | return false 616 | } 617 | } 618 | attrY, ok := y.(*hclsyntax.Attribute) 619 | return ok && m.node(x.Expr, attrY.Expr) && 620 | m.potentialWildcardIdentEqual(x.Name, attrY.Name) 621 | } 622 | 623 | func (m *Matcher) block(x, y *hclsyntax.Block) bool { 624 | if x == nil || y == nil { 625 | return x == y 626 | } 627 | return m.potentialWildcardIdentEqual(x.Type, y.Type) && 628 | m.potentialWildcardIdentsEqual(x.Labels, y.Labels) && 629 | m.body(x.Body, y.Body) 630 | } 631 | 632 | func (m *Matcher) body(x, y *hclsyntax.Body) bool { 633 | if x == nil || y == nil { 634 | return x == y 635 | } 636 | 637 | // Sort the attributes/blocks to reserve the order in source 638 | bodyEltsX := sortBody(x) 639 | bodyEltsY := sortBody(y) 640 | return m.iterableMatches(nodeIterable(bodyEltsX), nodeIterable(bodyEltsY), wildNameFromNode, matchNode) 641 | } 642 | 643 | func (m *Matcher) exprs(exprs1, exprs2 []hclsyntax.Expression) bool { 644 | return m.iterableMatches(exprIterable(exprs1), exprIterable(exprs2), wildNameFromNode, matchNode) 645 | } 646 | 647 | // Operation comparisons 648 | 649 | func (m *Matcher) operation(op1, op2 *hclsyntax.Operation) bool { 650 | if op1 == nil || op2 == nil { 651 | return op1 == op2 652 | } 653 | return op1.Impl == op2.Impl && op1.Type.Equals(op2.Type) 654 | } 655 | 656 | // ObjectConsItems comparisons 657 | 658 | func wildNameFromObjectConsItem(in interface{}) (string, bool) { 659 | if node, ok := in.(hclsyntax.ObjectConsItem).KeyExpr.(*hclsyntax.ObjectConsKeyExpr); ok { 660 | name, ok := variableExpr(node.Wrapped) 661 | if !ok { 662 | return "", false 663 | } 664 | return fromWildName(name) 665 | } 666 | return "", false 667 | } 668 | 669 | func matchObjectConsItem(m *Matcher, x, y interface{}) bool { 670 | itemX, itemY := x.(hclsyntax.ObjectConsItem), y.(hclsyntax.ObjectConsItem) 671 | return m.objectConsItem(itemX, itemY) 672 | } 673 | 674 | func (m *Matcher) objectConsItem(item1, item2 hclsyntax.ObjectConsItem) bool { 675 | if key1, ok := item1.KeyExpr.(*hclsyntax.ObjectConsKeyExpr); ok { 676 | name, ok := variableExpr(key1.Wrapped) 677 | if ok && isWildAttr(name, item1.ValueExpr) { 678 | return m.wildcardMatchObjectConsItem(name, item2) 679 | } 680 | } 681 | return m.node(item1.KeyExpr, item2.KeyExpr) && m.node(item1.ValueExpr, item2.ValueExpr) 682 | } 683 | 684 | func (m *Matcher) objectConsItems(items1, items2 []hclsyntax.ObjectConsItem) bool { 685 | return m.iterableMatches(objectConsItemIterable(items1), objectConsItemIterable(items2), wildNameFromObjectConsItem, matchObjectConsItem) 686 | } 687 | 688 | // String comparisons 689 | 690 | func wildNameFromString(in interface{}) (string, bool) { 691 | return fromWildName(in.(string)) 692 | } 693 | 694 | func matchString(m *Matcher, x, y interface{}) bool { 695 | sx, sy := x.(string), y.(string) 696 | return m.potentialWildcardIdentEqual(sx, sy) 697 | } 698 | 699 | func (m *Matcher) potentialWildcardIdentEqual(identX, identY string) bool { 700 | if !isWildName(identX) { 701 | return identX == identY 702 | } 703 | name, _ := fromWildName(identX) 704 | return m.wildcardMatchString(name, identY) 705 | } 706 | 707 | func (m *Matcher) potentialWildcardIdentsEqual(identX, identY []string) bool { 708 | return m.iterableMatches(stringIterable(identX), stringIterable(identY), wildNameFromString, matchString) 709 | } 710 | 711 | // Traversal comparisons 712 | 713 | func (m *Matcher) traversal(traversal1, traversal2 hcl.Traversal) bool { 714 | if len(traversal1) != len(traversal2) { 715 | return false 716 | } 717 | for i, t1 := range traversal1 { 718 | if !m.traverser(t1, traversal2[i]) { 719 | return false 720 | } 721 | } 722 | return true 723 | } 724 | 725 | func (m *Matcher) traverser(t1, t2 hcl.Traverser) bool { 726 | switch t1 := t1.(type) { 727 | case hcl.TraverseRoot: 728 | t2, ok := t2.(hcl.TraverseRoot) 729 | return ok && m.potentialWildcardIdentEqual(t1.Name, t2.Name) 730 | case hcl.TraverseAttr: 731 | t2, ok := t2.(hcl.TraverseAttr) 732 | return ok && m.potentialWildcardIdentEqual(t1.Name, t2.Name) 733 | case hcl.TraverseIndex: 734 | t2, ok := t2.(hcl.TraverseIndex) 735 | return ok && t1.Key.Equals(t2.Key).True() 736 | case hcl.TraverseSplat: 737 | t2, ok := t2.(hcl.TraverseSplat) 738 | return ok && m.traversal(t1.Each, t2.Each) 739 | default: 740 | panic(fmt.Sprintf("unexpected node: %T", t1)) 741 | } 742 | } 743 | 744 | // Wildcard matchers 745 | 746 | func (m *Matcher) wildcardMatchNode(name string, node hclsyntax.Node) bool { 747 | // Wildcard never matches multiple attributes/blocks. 748 | // On one hand, it is because we have any wildcard, which already meets this requirement. 749 | // One the other hand, Go panics to use the attributes/blocks slice as map key. 750 | switch node.(type) { 751 | case hclsyntax.Attributes, 752 | hclsyntax.Blocks: 753 | return false 754 | } 755 | 756 | if name == "_" { 757 | // values are discarded, matches anything 758 | return true 759 | } 760 | prev, ok := m.values[name] 761 | if !ok { 762 | m.values[name] = newNodeSubstitution(node) 763 | return true 764 | } 765 | switch { 766 | case prev.String != nil: 767 | nodeVar, ok := variableExpr(node) 768 | return ok && nodeVar == *prev.String 769 | case prev.Node != nil: 770 | return m.node(prev.Node, node) 771 | case prev.ObjectConsItem != nil: 772 | return false 773 | default: 774 | panic("never reach here") 775 | } 776 | } 777 | 778 | func (m *Matcher) wildcardMatchString(name, target string) bool { 779 | if name == "_" { 780 | // values are discarded, matches anything 781 | return true 782 | } 783 | prev, ok := m.values[name] 784 | if !ok { 785 | m.values[name] = newStringSubstitution(target) 786 | return true 787 | } 788 | 789 | switch { 790 | case prev.String != nil: 791 | return *prev.String == target 792 | case prev.Node != nil: 793 | prevName, ok := variableExpr(prev.Node) 794 | return ok && prevName == target 795 | case prev.ObjectConsItem != nil: 796 | return false 797 | case prev.Traverser != nil: 798 | switch trav := (*prev.Traverser).(type) { 799 | case hcl.TraverseRoot: 800 | return trav.Name == target 801 | case hcl.TraverseAttr: 802 | return trav.Name == target 803 | default: 804 | return false 805 | } 806 | default: 807 | panic("never reach here") 808 | } 809 | } 810 | 811 | func (m *Matcher) wildcardMatchObjectConsItem(name string, item hclsyntax.ObjectConsItem) bool { 812 | if name == "_" { 813 | // values are discarded, matches anything 814 | return true 815 | } 816 | prev, ok := m.values[name] 817 | if !ok { 818 | m.values[name] = newObjectConsItemSubstitution(&item) 819 | return true 820 | } 821 | switch { 822 | case prev.String != nil: 823 | return false 824 | case prev.Node != nil: 825 | return false 826 | case prev.ObjectConsItem != nil: 827 | return m.objectConsItem(*prev.ObjectConsItem, item) 828 | case prev.Traverser != nil: 829 | return false 830 | default: 831 | panic("never reach here") 832 | } 833 | } 834 | 835 | func (m *Matcher) wildcardMatchTraverse(name string, trav hcl.Traverser) bool { 836 | if name == "_" { 837 | // values are discarded, matches anything 838 | return true 839 | } 840 | prev, ok := m.values[name] 841 | if !ok { 842 | m.values[name] = newTraverserSubstitution(trav) 843 | return true 844 | } 845 | switch { 846 | case prev.String != nil: 847 | switch trav := trav.(type) { 848 | case hcl.TraverseRoot: 849 | return trav.Name == *prev.String 850 | case hcl.TraverseAttr: 851 | return trav.Name == *prev.String 852 | default: 853 | return false 854 | } 855 | case prev.Node != nil: 856 | return false 857 | case prev.ObjectConsItem != nil: 858 | return false 859 | case prev.Traverser != nil: 860 | return m.traverser(trav, *prev.Traverser) 861 | default: 862 | panic("never reach here") 863 | } 864 | } 865 | 866 | // Two wildcard: expression wildcard ($) and attribute wildcard (@) 867 | // - expression wildcard: $ => hclgrep_ 868 | // - expression wildcard (any): $ => hclgrep_any_ 869 | // - attribute wildcard : @ => hclgrep-_ = hclgrepattr 870 | // - attribute wildcard (any) : @ => hclgrep_any-_ = hclgrepattr 871 | const ( 872 | wildPrefix = "hclgrep_" 873 | wildExtraAny = "any_" 874 | wildAttrValue = "hclgrepattr" 875 | ) 876 | 877 | var wildattrCounters = map[string]int{} 878 | 879 | func wildName(name string, any bool) string { 880 | prefix := wildPrefix 881 | if any { 882 | prefix += wildExtraAny 883 | } 884 | return prefix + name 885 | } 886 | 887 | func wildAttr(name string, any bool) string { 888 | attr := wildName(name, any) + "-" + strconv.Itoa(wildattrCounters[name]) + "=" + wildAttrValue 889 | wildattrCounters[name] += 1 890 | return attr 891 | } 892 | 893 | func isWildName(name string) bool { 894 | return strings.HasPrefix(name, wildPrefix) 895 | } 896 | 897 | func isWildAttr(key string, value hclsyntax.Expression) bool { 898 | v, ok := variableExpr(value) 899 | return ok && v == wildAttrValue && isWildName(key) 900 | } 901 | 902 | func fromWildName(name string) (ident string, any bool) { 903 | ident = strings.TrimPrefix(strings.Split(name, "-")[0], wildPrefix) 904 | return strings.TrimPrefix(ident, wildExtraAny), strings.HasPrefix(ident, wildExtraAny) 905 | } 906 | 907 | func variableExpr(node hclsyntax.Node) (string, bool) { 908 | vexp, ok := node.(*hclsyntax.ScopeTraversalExpr) 909 | if !(ok && len(vexp.Traversal) == 1 && !vexp.Traversal.IsRelative()) { 910 | return "", false 911 | } 912 | return vexp.Traversal.RootName(), true 913 | } 914 | 915 | func sortBody(body *hclsyntax.Body) []hclsyntax.Node { 916 | l := len(body.Blocks) + len(body.Attributes) 917 | m := make(map[int]hclsyntax.Node, l) 918 | offsets := make([]int, 0, l) 919 | for _, blk := range body.Blocks { 920 | offset := blk.Range().Start.Byte 921 | m[offset] = blk 922 | offsets = append(offsets, offset) 923 | } 924 | for _, attr := range body.Attributes { 925 | offset := attr.Range().Start.Byte 926 | m[offset] = attr 927 | offsets = append(offsets, offset) 928 | } 929 | sort.Ints(offsets) 930 | out := make([]hclsyntax.Node, 0, l) 931 | for _, offset := range offsets { 932 | out = append(out, m[offset]) 933 | } 934 | return out 935 | } 936 | --------------------------------------------------------------------------------