├── hcl ├── doc.go ├── spectests │ ├── .gitignore │ └── spec_test.go ├── hclsyntax │ ├── fuzz │ │ ├── config │ │ │ ├── corpus │ │ │ │ ├── empty.hcl │ │ │ │ ├── attr-literal.hcl │ │ │ │ ├── block-empty.hcl │ │ │ │ ├── utf8.hcl │ │ │ │ ├── attr-expr.hcl │ │ │ │ ├── block-attrs.hcl │ │ │ │ └── block-nested.hcl │ │ │ └── fuzz.go │ │ ├── expr │ │ │ ├── corpus │ │ │ │ ├── empty.hcle │ │ │ │ ├── int.hcle │ │ │ │ ├── literal.hcle │ │ │ │ ├── var.hcle │ │ │ │ ├── utf8.hcle │ │ │ │ ├── escape-dollar.hcle │ │ │ │ ├── escape-newline.hcle │ │ │ │ ├── function-call.hcle │ │ │ │ ├── splat-attr.hcle │ │ │ │ └── splat-full.hcle │ │ │ └── fuzz.go │ │ ├── .gitignore │ │ ├── template │ │ │ ├── corpus │ │ │ │ ├── empty.tmpl │ │ │ │ ├── int.tmpl │ │ │ │ ├── literal.tmpl │ │ │ │ ├── just-interp.tmpl │ │ │ │ ├── utf8.tmpl │ │ │ │ ├── escape-dollar.tmpl │ │ │ │ ├── escape-newline.tmpl │ │ │ │ └── function-call.tmpl │ │ │ └── fuzz.go │ │ ├── traversal │ │ │ ├── corpus │ │ │ │ ├── root.hclt │ │ │ │ ├── attr.hclt │ │ │ │ ├── index.hclt │ │ │ │ └── complex.hclt │ │ │ └── fuzz.go │ │ ├── Makefile │ │ └── README.md │ ├── doc.go │ ├── file.go │ ├── generate.go │ ├── keywords.go │ ├── node.go │ ├── diagnostics.go │ ├── didyoumean.go │ ├── didyoumean_test.go │ ├── public_test.go │ ├── navigation.go │ ├── walk.go │ ├── expression_vars.go │ ├── variables.go │ ├── expression_vars_gen.go │ ├── peeker_test.go │ ├── scan_string_lit.rl │ └── navigation_test.go ├── json │ ├── fuzz │ │ ├── .gitignore │ │ ├── config │ │ │ ├── corpus │ │ │ │ ├── empty.hcl.json │ │ │ │ ├── block-empty.json │ │ │ │ ├── list-empty.json │ │ │ │ ├── list-nested.json │ │ │ │ ├── attr-literal.hcl.json │ │ │ │ ├── number-int.hcl.json │ │ │ │ ├── number-big.hcl.json │ │ │ │ ├── utf8.hcl.json │ │ │ │ ├── attr-expr.hcl.json │ │ │ │ ├── block-attrs.hcl.json │ │ │ │ ├── list-values.json │ │ │ │ └── block-nested.hcl.json │ │ │ └── fuzz.go │ │ ├── Makefile │ │ └── README.md │ ├── doc.go │ ├── peeker.go │ ├── tokentype_string.go │ ├── didyoumean_test.go │ ├── navigation_test.go │ ├── didyoumean.go │ ├── navigation.go │ ├── ast.go │ ├── public_test.go │ └── public.go ├── integrationtest │ └── doc.go ├── schema.go ├── eval_context.go ├── didyoumean.go ├── static_expr.go ├── expr_list.go ├── expr_map.go ├── expr_call.go └── expr_unwrap.go ├── specsuite ├── tests │ ├── empty.hcl │ ├── empty.hcl.json │ ├── comments │ │ ├── hash_comment.hcl │ │ ├── slash_comment.hcl │ │ ├── hash_comment.hcldec │ │ ├── multiline_comment.hcl │ │ ├── multiline_comment.hcldec │ │ ├── slash_comment.hcldec │ │ ├── hash_comment.t │ │ ├── slash_comment.t │ │ └── multiline_comment.t │ ├── structure │ │ ├── blocks │ │ │ ├── single_unclosed.hcl │ │ │ ├── single_empty_oneline.hcl │ │ │ ├── single_expected.hcl │ │ │ ├── single_oneline.hcl │ │ │ ├── single_expected.t │ │ │ ├── single_empty_oneline.t │ │ │ ├── single_expected.hcldec │ │ │ ├── single_unclosed.hcldec │ │ │ ├── single_empty_oneline.hcldec │ │ │ ├── single_oneline.t │ │ │ ├── single_oneline.hcldec │ │ │ ├── single_oneline_invalid.hcl │ │ │ ├── single_unclosed.t │ │ │ ├── single_oneline_invalid.hcldec │ │ │ └── single_oneline_invalid.t │ │ └── attributes │ │ │ ├── singleline_bad.hcl │ │ │ ├── singleline_bad.hcldec │ │ │ ├── expected.hcl │ │ │ ├── unexpected.hcl │ │ │ ├── expected.hcldec │ │ │ ├── expected.t │ │ │ ├── unexpected.hcldec │ │ │ ├── unexpected.t │ │ │ └── singleline_bad.t │ ├── empty.hcldec │ ├── empty.t │ └── expressions │ │ ├── heredoc.hcldec │ │ ├── heredoc.t │ │ └── heredoc.hcl └── README.md ├── guide ├── .gitignore ├── requirements.txt ├── Makefile ├── make.bat ├── go.rst ├── index.rst └── go_parsing.rst ├── cmd ├── hclspecsuite │ ├── README.md │ ├── log.go │ ├── main.go │ └── diagnostics.go └── hcldec │ ├── examples │ ├── sh-config-file │ │ ├── example.conf │ │ ├── spec.hcldec │ │ └── example.sh │ └── npm-package │ │ ├── example.npmhcl │ │ └── spec.hcldec │ ├── spec_funcs.go │ ├── vars.go │ ├── diags_json.go │ └── README.md ├── hcled ├── doc.go └── navigation.go ├── .travis.yml ├── hcltest └── doc.go ├── .travis.sh ├── ext ├── transform │ ├── doc.go │ ├── transformer.go │ ├── transform_test.go │ └── transform.go ├── typeexpr │ ├── doc.go │ ├── type_string_test.go │ └── README.md ├── include │ ├── doc.go │ ├── map_resolver.go │ ├── resolver.go │ ├── file_resolver.go │ ├── transformer_test.go │ └── transformer.go ├── README.md ├── userfunc │ ├── doc.go │ ├── README.md │ └── public.go └── dynblock │ ├── schema.go │ ├── expr_wrap.go │ ├── public.go │ ├── iteration.go │ ├── variables_hcldec.go │ └── unknown_body.go ├── gohcl ├── types.go ├── encode_test.go └── doc.go ├── hcldec ├── block_labels.go ├── doc.go ├── gob.go ├── decode.go ├── schema.go ├── variables.go └── public.go ├── hclwrite ├── native_node_sorter.go ├── doc.go ├── ast_attribute.go ├── ast_test.go ├── public.go ├── ast_block.go ├── examples_test.go └── ast.go ├── hclpack ├── exprsourcetype_string.go ├── positions_packed_test.go ├── doc.go ├── didyoumean.go ├── json_marshal_test.go ├── vlq.go ├── expression_test.go ├── pack_native.go └── example_test.go ├── extras └── grammar │ ├── HCL.yaml-tmLanguage │ ├── HCLTemplate.yaml-tmLanguage │ ├── HCL.json-tmLanguage │ ├── HCLTemplate.json-tmLanguage │ ├── build.go │ └── HCLExpression.yaml-tmLanguage └── go.mod /hcl/doc.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | -------------------------------------------------------------------------------- /specsuite/tests/empty.hcl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hcl/spectests/.gitignore: -------------------------------------------------------------------------------- 1 | tmp_* 2 | -------------------------------------------------------------------------------- /specsuite/tests/empty.hcl.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /guide/.gitignore: -------------------------------------------------------------------------------- 1 | env/* 2 | _build/* 3 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/config/corpus/empty.hcl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/corpus/empty.hcle: -------------------------------------------------------------------------------- 1 | "" -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/corpus/int.hcle: -------------------------------------------------------------------------------- 1 | 42 -------------------------------------------------------------------------------- /hcl/json/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | fuzz*-fuzz.zip 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | fuzz*-fuzz.zip 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/corpus/literal.hcle: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/corpus/var.hcle: -------------------------------------------------------------------------------- 1 | var.bar -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/template/corpus/empty.tmpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/traversal/corpus/root.hclt: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/template/corpus/int.tmpl: -------------------------------------------------------------------------------- 1 | foo ${42} -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/template/corpus/literal.tmpl: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/traversal/corpus/attr.hclt: -------------------------------------------------------------------------------- 1 | foo.bar -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/traversal/corpus/index.hclt: -------------------------------------------------------------------------------- 1 | foo[1] -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/empty.hcl.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/corpus/utf8.hcle: -------------------------------------------------------------------------------- 1 | föo("föo") + föo -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/template/corpus/just-interp.tmpl: -------------------------------------------------------------------------------- 1 | ${var.bar} -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/template/corpus/utf8.tmpl: -------------------------------------------------------------------------------- 1 | föo ${föo("föo")} -------------------------------------------------------------------------------- /specsuite/tests/comments/hash_comment.hcl: -------------------------------------------------------------------------------- 1 | # Hash comment 2 | -------------------------------------------------------------------------------- /specsuite/tests/comments/slash_comment.hcl: -------------------------------------------------------------------------------- 1 | // Slash comment 2 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_unclosed.hcl: -------------------------------------------------------------------------------- 1 | a { 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/config/corpus/attr-literal.hcl: -------------------------------------------------------------------------------- 1 | foo = "bar" 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/config/corpus/block-empty.hcl: -------------------------------------------------------------------------------- 1 | block { 2 | } 3 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/corpus/escape-dollar.hcle: -------------------------------------------------------------------------------- 1 | "hi $${var.foo}" -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/corpus/escape-newline.hcle: -------------------------------------------------------------------------------- 1 | "bar\nbaz" 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/corpus/function-call.hcle: -------------------------------------------------------------------------------- 1 | title(var.name) -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/corpus/splat-attr.hcle: -------------------------------------------------------------------------------- 1 | foo.bar.*.baz 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/corpus/splat-full.hcle: -------------------------------------------------------------------------------- 1 | foo.bar[*].baz 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/template/corpus/escape-dollar.tmpl: -------------------------------------------------------------------------------- 1 | hi $${var.foo} -------------------------------------------------------------------------------- /specsuite/tests/empty.hcldec: -------------------------------------------------------------------------------- 1 | literal { 2 | value = "ok" 3 | } 4 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_empty_oneline.hcl: -------------------------------------------------------------------------------- 1 | a {} 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/config/corpus/utf8.hcl: -------------------------------------------------------------------------------- 1 | foo = "föo ${föo("föo")}" 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/template/corpus/escape-newline.tmpl: -------------------------------------------------------------------------------- 1 | foo ${"bar\nbaz"} -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_expected.hcl: -------------------------------------------------------------------------------- 1 | a { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_oneline.hcl: -------------------------------------------------------------------------------- 1 | a { b = "foo" } 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/config/corpus/attr-expr.hcl: -------------------------------------------------------------------------------- 1 | foo = upper(bar + baz[1]) 2 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/template/corpus/function-call.tmpl: -------------------------------------------------------------------------------- 1 | hi ${title(var.name)} -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/traversal/corpus/complex.hclt: -------------------------------------------------------------------------------- 1 | foo.bar[1].baz["foo"].pizza -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/block-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "block": {} 3 | } 4 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/list-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": [] 3 | } 4 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/list-nested.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": [[]] 3 | } 4 | -------------------------------------------------------------------------------- /guide/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinxcontrib-golangdomain 3 | sphinx-autoapi 4 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/config/corpus/block-attrs.hcl: -------------------------------------------------------------------------------- 1 | block { 2 | foo = true 3 | } 4 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/attr-literal.hcl.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } 4 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/number-int.hcl.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": 1024 3 | } 4 | -------------------------------------------------------------------------------- /specsuite/tests/comments/hash_comment.hcldec: -------------------------------------------------------------------------------- 1 | literal { 2 | value = "ok" 3 | } 4 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_expected.t: -------------------------------------------------------------------------------- 1 | result_type = object({}) 2 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/number-big.hcl.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": 1.234234e30 3 | } 4 | -------------------------------------------------------------------------------- /specsuite/tests/comments/multiline_comment.hcl: -------------------------------------------------------------------------------- 1 | /* 2 | Multi-line comment 3 | */ 4 | -------------------------------------------------------------------------------- /specsuite/tests/comments/multiline_comment.hcldec: -------------------------------------------------------------------------------- 1 | literal { 2 | value = "ok" 3 | } 4 | -------------------------------------------------------------------------------- /specsuite/tests/comments/slash_comment.hcldec: -------------------------------------------------------------------------------- 1 | literal { 2 | value = "ok" 3 | } 4 | -------------------------------------------------------------------------------- /specsuite/tests/structure/attributes/singleline_bad.hcl: -------------------------------------------------------------------------------- 1 | a = "a value", b = "b value" 2 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_empty_oneline.t: -------------------------------------------------------------------------------- 1 | result_type = object({}) 2 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/utf8.hcl.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "föo ${föo(\"föo\")}" 3 | } 4 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/attr-expr.hcl.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "${upper(bar + baz[1])}" 3 | } 4 | -------------------------------------------------------------------------------- /specsuite/tests/structure/attributes/singleline_bad.hcldec: -------------------------------------------------------------------------------- 1 | literal { 2 | value = null 3 | } 4 | -------------------------------------------------------------------------------- /specsuite/tests/structure/attributes/expected.hcl: -------------------------------------------------------------------------------- 1 | a = "a value" 2 | b = "b value" 3 | c = "c value" 4 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/block-attrs.hcl.json: -------------------------------------------------------------------------------- 1 | { 2 | "block": { 3 | "foo": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_expected.hcldec: -------------------------------------------------------------------------------- 1 | block { 2 | block_type = "a" 3 | object {} 4 | } 5 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_unclosed.hcldec: -------------------------------------------------------------------------------- 1 | block { 2 | block_type = "a" 3 | object {} 4 | } 5 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/config/corpus/block-nested.hcl: -------------------------------------------------------------------------------- 1 | block { 2 | another_block { 3 | foo = bar 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_empty_oneline.hcldec: -------------------------------------------------------------------------------- 1 | block { 2 | block_type = "a" 3 | object {} 4 | } 5 | -------------------------------------------------------------------------------- /specsuite/tests/structure/attributes/unexpected.hcl: -------------------------------------------------------------------------------- 1 | a = "a value" 2 | b = "b value" 3 | c = "c value" 4 | d = "d value" 5 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_oneline.t: -------------------------------------------------------------------------------- 1 | result_type = object({ 2 | b = string 3 | }) 4 | result = { 5 | b = "foo" 6 | } 7 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/list-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": [ 3 | "hello", 4 | true, 5 | 1.2 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/corpus/block-nested.hcl.json: -------------------------------------------------------------------------------- 1 | { 2 | "block": { 3 | "another_block": { 4 | "foo": "bar" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cmd/hclspecsuite/README.md: -------------------------------------------------------------------------------- 1 | # `hclspecsuite` 2 | 3 | `hclspecsuite` is the test harness for 4 | [the HCL specification test suite](../../specsuite/README.md). 5 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_oneline.hcldec: -------------------------------------------------------------------------------- 1 | block { 2 | block_type = "a" 3 | object { 4 | attr "b" { 5 | type = string 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /cmd/hcldec/examples/sh-config-file/example.conf: -------------------------------------------------------------------------------- 1 | name = "Juan" 2 | friend { 3 | name = "John" 4 | } 5 | friend { 6 | name = "Yann" 7 | } 8 | friend { 9 | name = "Ermintrude" 10 | } 11 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_oneline_invalid.hcl: -------------------------------------------------------------------------------- 1 | a { b = "foo", c = "bar" } 2 | a { b = "foo" 3 | } 4 | a { b = "foo" 5 | c = "bar" } 6 | a { b = "foo" 7 | c = "bar" 8 | } 9 | a { d {} } 10 | -------------------------------------------------------------------------------- /specsuite/tests/comments/hash_comment.t: -------------------------------------------------------------------------------- 1 | # This test parses a file containing only a comment. It is a parsing-only test, 2 | # so the hcldec spec for this test is just a literal value given below. 3 | 4 | result = "ok" 5 | -------------------------------------------------------------------------------- /specsuite/tests/comments/slash_comment.t: -------------------------------------------------------------------------------- 1 | # This test parses a file containing only a comment. It is a parsing-only test, 2 | # so the hcldec spec for this test is just a literal value given below. 3 | 4 | result = "ok" 5 | -------------------------------------------------------------------------------- /specsuite/tests/comments/multiline_comment.t: -------------------------------------------------------------------------------- 1 | # This test parses a file containing only a comment. It is a parsing-only test, 2 | # so the hcldec spec for this test is just a literal value given below. 3 | 4 | result = "ok" 5 | -------------------------------------------------------------------------------- /specsuite/tests/structure/attributes/expected.hcldec: -------------------------------------------------------------------------------- 1 | object { 2 | attr "a" { 3 | type = string 4 | } 5 | attr "b" { 6 | type = string 7 | } 8 | attr "c" { 9 | type = string 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /specsuite/tests/structure/attributes/expected.t: -------------------------------------------------------------------------------- 1 | result_type = object({ 2 | a = string 3 | b = string 4 | c = string 5 | }) 6 | result = { 7 | a = "a value" 8 | b = "b value" 9 | c = "c value" 10 | } 11 | -------------------------------------------------------------------------------- /specsuite/tests/structure/attributes/unexpected.hcldec: -------------------------------------------------------------------------------- 1 | object { 2 | attr "a" { 3 | type = string 4 | } 5 | attr "b" { 6 | type = string 7 | } 8 | attr "d" { 9 | type = string 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /hcled/doc.go: -------------------------------------------------------------------------------- 1 | // Package hcled provides functionality intended to help an application 2 | // that embeds HCL to deliver relevant information to a text editor or IDE 3 | // for navigating around and analyzing configuration files. 4 | package hcled 5 | -------------------------------------------------------------------------------- /cmd/hclspecsuite/log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | type LogBeginCallback func(testName string, testFile *TestFile) 8 | type LogProblemsCallback func(testName string, testFile *TestFile, diags hcl.Diagnostics) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.11.x 5 | - 1.12.x 6 | 7 | env: 8 | - GO111MODULE=on 9 | 10 | before_install: 11 | - go get -v ./... 12 | 13 | script: 14 | - ./.travis.sh 15 | 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | -------------------------------------------------------------------------------- /cmd/hcldec/examples/npm-package/example.npmhcl: -------------------------------------------------------------------------------- 1 | name = "hello-world" 2 | version = "v0.0.1" 3 | 4 | author { 5 | name = "Иван Петрович Сидоров" 6 | } 7 | 8 | contributor { 9 | name = "Juan Pérez" 10 | } 11 | 12 | dependencies = { 13 | left-pad = "1.2.0" 14 | } 15 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_unclosed.t: -------------------------------------------------------------------------------- 1 | diagnostics { 2 | error { 3 | from { 4 | line = 2 5 | column = 1 6 | byte = 4 7 | } 8 | to { 9 | line = 2 10 | column = 1 11 | byte = 4 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /hcl/json/fuzz/config/fuzz.go: -------------------------------------------------------------------------------- 1 | package fuzzconfig 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl/json" 5 | ) 6 | 7 | func Fuzz(data []byte) int { 8 | _, diags := json.Parse(data, "") 9 | 10 | if diags.HasErrors() { 11 | return 0 12 | } 13 | 14 | return 1 15 | } 16 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_oneline_invalid.hcldec: -------------------------------------------------------------------------------- 1 | block_list { 2 | block_type = "a" 3 | object { 4 | attr "b" { 5 | type = string 6 | } 7 | attr "c" { 8 | type = string 9 | } 10 | block_list "d" { 11 | object {} 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /hcltest/doc.go: -------------------------------------------------------------------------------- 1 | // Package hcltest contains utilities that aim to make it more convenient 2 | // to write tests for code that interacts with the HCL API. 3 | // 4 | // This package is intended for use only in test code. It is optimized for 5 | // convenience of use over all other concerns. 6 | package hcltest 7 | -------------------------------------------------------------------------------- /specsuite/tests/empty.t: -------------------------------------------------------------------------------- 1 | # This test ensures that we can successfully parse an empty file. 2 | # Since an empty file has no content, the hcldec spec for this test is 3 | # just a literal value, which we test below. 4 | 5 | result = "ok" 6 | 7 | traversals { 8 | # Explicitly no traversals 9 | } 10 | -------------------------------------------------------------------------------- /specsuite/tests/expressions/heredoc.hcldec: -------------------------------------------------------------------------------- 1 | variables { 2 | bar = "Bar" 3 | space_bar = " Bar" 4 | words = ["Foo", "Bar", "Baz"] 5 | } 6 | 7 | object { 8 | attr "normal" { 9 | type = map(string) 10 | } 11 | attr "flush" { 12 | type = map(string) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | for d in $(go list ./... | grep -v vendor); do 7 | go test -coverprofile=profile.out -covermode=atomic $d 8 | if [ -f profile.out ]; then 9 | cat profile.out >> coverage.txt 10 | rm profile.out 11 | fi 12 | done 13 | -------------------------------------------------------------------------------- /specsuite/tests/structure/attributes/unexpected.t: -------------------------------------------------------------------------------- 1 | diagnostics { 2 | error { 3 | # An argument named "c" is not expected here. 4 | from { 5 | line = 3 6 | column = 1 7 | byte = 28 8 | } 9 | to { 10 | line = 3 11 | column = 2 12 | byte = 29 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /hcl/integrationtest/doc.go: -------------------------------------------------------------------------------- 1 | // Package integrationtest is an internal package that contains some 2 | // tests that attempt to exercise many HCL features together in realistic 3 | // scenarios. This is in addition to -- but not a substitute for -- unit tests 4 | // that verify the behavior of each feature separately. 5 | package integrationtest 6 | -------------------------------------------------------------------------------- /hcl/hclsyntax/doc.go: -------------------------------------------------------------------------------- 1 | // Package hclsyntax contains the parser, AST, etc for HCL's native language, 2 | // as opposed to the JSON variant. 3 | // 4 | // In normal use applications should rarely depend on this package directly, 5 | // instead preferring the higher-level interface of the main hcl package and 6 | // its companion package hclparse. 7 | package hclsyntax 8 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/config/fuzz.go: -------------------------------------------------------------------------------- 1 | package fuzzconfig 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/hashicorp/hcl2/hcl/hclsyntax" 6 | ) 7 | 8 | func Fuzz(data []byte) int { 9 | _, diags := hclsyntax.ParseConfig(data, "", hcl.Pos{Line: 1, Column: 1}) 10 | 11 | if diags.HasErrors() { 12 | return 0 13 | } 14 | 15 | return 1 16 | } 17 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/expr/fuzz.go: -------------------------------------------------------------------------------- 1 | package fuzzexpr 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/hashicorp/hcl2/hcl/hclsyntax" 6 | ) 7 | 8 | func Fuzz(data []byte) int { 9 | _, diags := hclsyntax.ParseExpression(data, "", hcl.Pos{Line: 1, Column: 1}) 10 | 11 | if diags.HasErrors() { 12 | return 0 13 | } 14 | 15 | return 1 16 | } 17 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/template/fuzz.go: -------------------------------------------------------------------------------- 1 | package fuzztemplate 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/hashicorp/hcl2/hcl/hclsyntax" 6 | ) 7 | 8 | func Fuzz(data []byte) int { 9 | _, diags := hclsyntax.ParseTemplate(data, "", hcl.Pos{Line: 1, Column: 1}) 10 | 11 | if diags.HasErrors() { 12 | return 0 13 | } 14 | 15 | return 1 16 | } 17 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/traversal/fuzz.go: -------------------------------------------------------------------------------- 1 | package fuzztraversal 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/hashicorp/hcl2/hcl/hclsyntax" 6 | ) 7 | 8 | func Fuzz(data []byte) int { 9 | _, diags := hclsyntax.ParseTraversalAbs(data, "", hcl.Pos{Line: 1, Column: 1}) 10 | 11 | if diags.HasErrors() { 12 | return 0 13 | } 14 | 15 | return 1 16 | } 17 | -------------------------------------------------------------------------------- /ext/transform/doc.go: -------------------------------------------------------------------------------- 1 | // Package transform is a helper package for writing extensions that work 2 | // by applying transforms to bodies. 3 | // 4 | // It defines a type for body transformers, and then provides utilities in 5 | // terms of that type for working with transformers, including recursively 6 | // applying such transforms as heirarchical block structures are extracted. 7 | package transform 8 | -------------------------------------------------------------------------------- /hcl/json/doc.go: -------------------------------------------------------------------------------- 1 | // Package json is the JSON parser for HCL. It parses JSON files and returns 2 | // implementations of the core HCL structural interfaces in terms of the 3 | // JSON data inside. 4 | // 5 | // This is not a generic JSON parser. Instead, it deals with the mapping from 6 | // the JSON information model to the HCL information model, using a number 7 | // of hard-coded structural conventions. 8 | package json 9 | -------------------------------------------------------------------------------- /ext/typeexpr/doc.go: -------------------------------------------------------------------------------- 1 | // Package typeexpr extends HCL with a convention for describing HCL types 2 | // within configuration files. 3 | // 4 | // The type syntax is processed statically from a hcl.Expression, so it cannot 5 | // use any of the usual language operators. This is similar to type expressions 6 | // in statically-typed programming languages. 7 | // 8 | // variable "example" { 9 | // type = list(string) 10 | // } 11 | package typeexpr 12 | -------------------------------------------------------------------------------- /gohcl/types.go: -------------------------------------------------------------------------------- 1 | package gohcl 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/hashicorp/hcl2/hcl" 7 | ) 8 | 9 | var victimExpr hcl.Expression 10 | var victimBody hcl.Body 11 | 12 | var exprType = reflect.TypeOf(&victimExpr).Elem() 13 | var bodyType = reflect.TypeOf(&victimBody).Elem() 14 | var blockType = reflect.TypeOf((*hcl.Block)(nil)) 15 | var attrType = reflect.TypeOf((*hcl.Attribute)(nil)) 16 | var attrsType = reflect.TypeOf(hcl.Attributes(nil)) 17 | -------------------------------------------------------------------------------- /hcl/hclsyntax/file.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | // File is the top-level object resulting from parsing a configuration file. 8 | type File struct { 9 | Body *Body 10 | Bytes []byte 11 | } 12 | 13 | func (f *File) AsHCLFile() *hcl.File { 14 | return &hcl.File{ 15 | Body: f.Body, 16 | Bytes: f.Bytes, 17 | 18 | // TODO: The Nav object, once we have an implementation of it 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /hcl/json/peeker.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | type peeker struct { 4 | tokens []token 5 | pos int 6 | } 7 | 8 | func newPeeker(tokens []token) *peeker { 9 | return &peeker{ 10 | tokens: tokens, 11 | pos: 0, 12 | } 13 | } 14 | 15 | func (p *peeker) Peek() token { 16 | return p.tokens[p.pos] 17 | } 18 | 19 | func (p *peeker) Read() token { 20 | ret := p.tokens[p.pos] 21 | if ret.Type != tokenEOF { 22 | p.pos++ 23 | } 24 | return ret 25 | } 26 | -------------------------------------------------------------------------------- /hcldec/block_labels.go: -------------------------------------------------------------------------------- 1 | package hcldec 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | type blockLabel struct { 8 | Value string 9 | Range hcl.Range 10 | } 11 | 12 | func labelsForBlock(block *hcl.Block) []blockLabel { 13 | ret := make([]blockLabel, len(block.Labels)) 14 | for i := range block.Labels { 15 | ret[i] = blockLabel{ 16 | Value: block.Labels[i], 17 | Range: block.LabelRanges[i], 18 | } 19 | } 20 | return ret 21 | } 22 | -------------------------------------------------------------------------------- /ext/include/doc.go: -------------------------------------------------------------------------------- 1 | // Package include implements a HCL extension that allows inclusion of 2 | // one HCL body into another using blocks of type "include", with the following 3 | // structure: 4 | // 5 | // include { 6 | // path = "./foo.hcl" 7 | // } 8 | // 9 | // The processing of the given path is delegated to the calling application, 10 | // allowing it to decide how to interpret the path and which syntaxes to 11 | // support for referenced files. 12 | package include 13 | -------------------------------------------------------------------------------- /cmd/hcldec/examples/sh-config-file/spec.hcldec: -------------------------------------------------------------------------------- 1 | object { 2 | attr "name" { 3 | type = string 4 | required = true 5 | } 6 | default "greeting" { 7 | attr { 8 | name = "greeting" 9 | type = string 10 | } 11 | literal { 12 | value = "Hello" 13 | } 14 | } 15 | block_list "friends" { 16 | block_type = "friend" 17 | attr { 18 | name = "name" 19 | type = string 20 | required = true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ext/README.md: -------------------------------------------------------------------------------- 1 | # HCL Extensions 2 | 3 | This directory contains some packages implementing some extensions to HCL 4 | that add features by building on the core API in the main `hcl` package. 5 | 6 | These serve as optional language extensions for use-cases that are limited only 7 | to specific callers. Generally these make the language more expressive at 8 | the expense of increased dynamic behavior that may be undesirable for 9 | applications that need to impose more rigid structure on configuration. 10 | -------------------------------------------------------------------------------- /hcl/hclsyntax/generate.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | //go:generate go run expression_vars_gen.go 4 | //go:generate ruby unicode2ragel.rb --url=http://www.unicode.org/Public/9.0.0/ucd/DerivedCoreProperties.txt -m UnicodeDerived -p ID_Start,ID_Continue -o unicode_derived.rl 5 | //go:generate ragel -Z scan_tokens.rl 6 | //go:generate gofmt -w scan_tokens.go 7 | //go:generate ragel -Z scan_string_lit.rl 8 | //go:generate gofmt -w scan_string_lit.go 9 | //go:generate stringer -type TokenType -output token_type_string.go 10 | -------------------------------------------------------------------------------- /specsuite/tests/structure/attributes/singleline_bad.t: -------------------------------------------------------------------------------- 1 | # This test verifies that comma-separated attributes on the same line are 2 | # reported as an error, rather than being parsed like an object constructor 3 | # expression. 4 | 5 | diagnostics { 6 | error { 7 | # Message like "missing newline after argument" or "each argument must be on its own line" 8 | from { 9 | line = 1 10 | column = 14 11 | byte = 13 12 | } 13 | to { 14 | line = 1 15 | column = 15 16 | byte = 14 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /hclwrite/native_node_sorter.go: -------------------------------------------------------------------------------- 1 | package hclwrite 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl/hclsyntax" 5 | ) 6 | 7 | type nativeNodeSorter struct { 8 | Nodes []hclsyntax.Node 9 | } 10 | 11 | func (s nativeNodeSorter) Len() int { 12 | return len(s.Nodes) 13 | } 14 | 15 | func (s nativeNodeSorter) Less(i, j int) bool { 16 | rangeI := s.Nodes[i].Range() 17 | rangeJ := s.Nodes[j].Range() 18 | return rangeI.Start.Byte < rangeJ.Start.Byte 19 | } 20 | 21 | func (s nativeNodeSorter) Swap(i, j int) { 22 | s.Nodes[i], s.Nodes[j] = s.Nodes[j], s.Nodes[i] 23 | } 24 | -------------------------------------------------------------------------------- /hclwrite/doc.go: -------------------------------------------------------------------------------- 1 | // Package hclwrite deals with the problem of generating HCL configuration 2 | // and of making specific surgical changes to existing HCL configurations. 3 | // 4 | // It operates at a different level of abstraction than the main HCL parser 5 | // and AST, since details such as the placement of comments and newlines 6 | // are preserved when unchanged. 7 | // 8 | // The hclwrite API follows a similar principle to XML/HTML DOM, allowing nodes 9 | // to be read out, created and inserted, etc. Nodes represent syntax constructs 10 | // rather than semantic concepts. 11 | package hclwrite 12 | -------------------------------------------------------------------------------- /hcl/hclsyntax/keywords.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type Keyword []byte 8 | 9 | var forKeyword = Keyword([]byte{'f', 'o', 'r'}) 10 | var inKeyword = Keyword([]byte{'i', 'n'}) 11 | var ifKeyword = Keyword([]byte{'i', 'f'}) 12 | var elseKeyword = Keyword([]byte{'e', 'l', 's', 'e'}) 13 | var endifKeyword = Keyword([]byte{'e', 'n', 'd', 'i', 'f'}) 14 | var endforKeyword = Keyword([]byte{'e', 'n', 'd', 'f', 'o', 'r'}) 15 | 16 | func (kw Keyword) TokenMatches(token Token) bool { 17 | if token.Type != TokenIdent { 18 | return false 19 | } 20 | return bytes.Equal([]byte(kw), token.Bytes) 21 | } 22 | -------------------------------------------------------------------------------- /hcl/schema.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | 3 | // BlockHeaderSchema represents the shape of a block header, and is 4 | // used for matching blocks within bodies. 5 | type BlockHeaderSchema struct { 6 | Type string 7 | LabelNames []string 8 | } 9 | 10 | // AttributeSchema represents the requirements for an attribute, and is used 11 | // for matching attributes within bodies. 12 | type AttributeSchema struct { 13 | Name string 14 | Required bool 15 | } 16 | 17 | // BodySchema represents the desired shallow structure of a body. 18 | type BodySchema struct { 19 | Attributes []AttributeSchema 20 | Blocks []BlockHeaderSchema 21 | } 22 | -------------------------------------------------------------------------------- /hclpack/exprsourcetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ExprSourceType"; DO NOT EDIT. 2 | 3 | package hclpack 4 | 5 | import "strconv" 6 | 7 | const ( 8 | _ExprSourceType_name_0 = "ExprLiteralJSON" 9 | _ExprSourceType_name_1 = "ExprNative" 10 | _ExprSourceType_name_2 = "ExprTemplate" 11 | ) 12 | 13 | func (i ExprSourceType) String() string { 14 | switch { 15 | case i == 76: 16 | return _ExprSourceType_name_0 17 | case i == 78: 18 | return _ExprSourceType_name_1 19 | case i == 84: 20 | return _ExprSourceType_name_2 21 | default: 22 | return "ExprSourceType(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /hcldec/doc.go: -------------------------------------------------------------------------------- 1 | // Package hcldec provides a higher-level API for unpacking the content of 2 | // HCL bodies, implemented in terms of the low-level "Content" API exposed 3 | // by the bodies themselves. 4 | // 5 | // It allows decoding an entire nested configuration in a single operation 6 | // by providing a description of the intended structure. 7 | // 8 | // For some applications it may be more convenient to use the "gohcl" 9 | // package, which has a similar purpose but decodes directly into native 10 | // Go data types. hcldec instead targets the cty type system, and thus allows 11 | // a cty-driven application to remain within that type system. 12 | package hcldec 13 | -------------------------------------------------------------------------------- /guide/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = HCL 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /hcl/hclsyntax/node.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | // Node is the abstract type that every AST node implements. 8 | // 9 | // This is a closed interface, so it cannot be implemented from outside of 10 | // this package. 11 | type Node interface { 12 | // This is the mechanism by which the public-facing walk functions 13 | // are implemented. Implementations should call the given function 14 | // for each child node and then replace that node with its return value. 15 | // The return value might just be the same node, for non-transforming 16 | // walks. 17 | walkChildNodes(w internalWalkFunc) 18 | 19 | Range() hcl.Range 20 | } 21 | 22 | type internalWalkFunc func(Node) 23 | -------------------------------------------------------------------------------- /hcl/json/fuzz/Makefile: -------------------------------------------------------------------------------- 1 | 2 | ifndef FUZZ_WORK_DIR 3 | $(error FUZZ_WORK_DIR is not set) 4 | endif 5 | 6 | default: 7 | @echo "See README.md for usage instructions" 8 | 9 | fuzz-config: fuzz-exec-config 10 | 11 | fuzz-exec-%: fuzz%-fuzz.zip 12 | go-fuzz -bin=./fuzz$*-fuzz.zip -workdir=$(FUZZ_WORK_DIR) 13 | 14 | fuzz%-fuzz.zip: %/fuzz.go 15 | go-fuzz-build github.com/hashicorp/hcl2/hcl/json/fuzz/$* 16 | 17 | tools: 18 | go get -u github.com/dvyukov/go-fuzz/go-fuzz 19 | go get -u github.com/dvyukov/go-fuzz/go-fuzz-build 20 | 21 | clean: 22 | rm fuzz*-fuzz.zip 23 | 24 | .PHONY: tools clean fuzz-config fuzz-expr fuzz-template fuzz-traversal 25 | .PRECIOUS: fuzzconfig-fuzz.zip fuzzexpr-fuzz.zip fuzztemplate-fuzz.zip fuzztraversal-fuzz.zip 26 | -------------------------------------------------------------------------------- /hcl/eval_context.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | "github.com/zclconf/go-cty/cty/function" 6 | ) 7 | 8 | // An EvalContext provides the variables and functions that should be used 9 | // to evaluate an expression. 10 | type EvalContext struct { 11 | Variables map[string]cty.Value 12 | Functions map[string]function.Function 13 | parent *EvalContext 14 | } 15 | 16 | // NewChild returns a new EvalContext that is a child of the receiver. 17 | func (ctx *EvalContext) NewChild() *EvalContext { 18 | return &EvalContext{parent: ctx} 19 | } 20 | 21 | // Parent returns the parent of the receiver, or nil if the receiver has 22 | // no parent. 23 | func (ctx *EvalContext) Parent() *EvalContext { 24 | return ctx.parent 25 | } 26 | -------------------------------------------------------------------------------- /hcldec/gob.go: -------------------------------------------------------------------------------- 1 | package hcldec 2 | 3 | import ( 4 | "encoding/gob" 5 | ) 6 | 7 | func init() { 8 | // Every Spec implementation should be registered with gob, so that 9 | // specs can be sent over gob channels, such as using 10 | // github.com/hashicorp/go-plugin with plugins that need to describe 11 | // what shape of configuration they are expecting. 12 | gob.Register(ObjectSpec(nil)) 13 | gob.Register(TupleSpec(nil)) 14 | gob.Register((*AttrSpec)(nil)) 15 | gob.Register((*LiteralSpec)(nil)) 16 | gob.Register((*ExprSpec)(nil)) 17 | gob.Register((*BlockSpec)(nil)) 18 | gob.Register((*BlockListSpec)(nil)) 19 | gob.Register((*BlockSetSpec)(nil)) 20 | gob.Register((*BlockMapSpec)(nil)) 21 | gob.Register((*BlockLabelSpec)(nil)) 22 | gob.Register((*DefaultSpec)(nil)) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/hcldec/spec_funcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty/function" 5 | "github.com/zclconf/go-cty/cty/function/stdlib" 6 | ) 7 | 8 | var specFuncs = map[string]function.Function{ 9 | "abs": stdlib.AbsoluteFunc, 10 | "coalesce": stdlib.CoalesceFunc, 11 | "concat": stdlib.ConcatFunc, 12 | "hasindex": stdlib.HasIndexFunc, 13 | "int": stdlib.IntFunc, 14 | "jsondecode": stdlib.JSONDecodeFunc, 15 | "jsonencode": stdlib.JSONEncodeFunc, 16 | "length": stdlib.LengthFunc, 17 | "lower": stdlib.LowerFunc, 18 | "max": stdlib.MaxFunc, 19 | "min": stdlib.MinFunc, 20 | "reverse": stdlib.ReverseFunc, 21 | "strlen": stdlib.StrlenFunc, 22 | "substr": stdlib.SubstrFunc, 23 | "upper": stdlib.UpperFunc, 24 | } 25 | -------------------------------------------------------------------------------- /hclpack/positions_packed_test.go: -------------------------------------------------------------------------------- 1 | package hclpack 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestBinaryRoundTrip(t *testing.T) { 10 | startPacked := positionsPacked{ 11 | {FileIdx: 0, LineDelta: 1, ColumnDelta: 2, ByteDelta: 3}, 12 | {FileIdx: 1, LineDelta: 2, ColumnDelta: 3, ByteDelta: 4}, 13 | {FileIdx: 2, LineDelta: 3, ColumnDelta: 4, ByteDelta: 5}, 14 | } 15 | 16 | b, err := startPacked.MarshalBinary() 17 | if err != nil { 18 | t.Fatalf("Failed to marshal: %s", err) 19 | } 20 | 21 | var endPacked positionsPacked 22 | err = endPacked.UnmarshalBinary(b) 23 | if err != nil { 24 | t.Fatalf("Failed to unmarshal: %s", err) 25 | } 26 | 27 | if !cmp.Equal(startPacked, endPacked) { 28 | t.Errorf("Incorrect result\n%s", cmp.Diff(startPacked, endPacked)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/Makefile: -------------------------------------------------------------------------------- 1 | 2 | ifndef FUZZ_WORK_DIR 3 | $(error FUZZ_WORK_DIR is not set) 4 | endif 5 | 6 | default: 7 | @echo "See README.md for usage instructions" 8 | 9 | fuzz-config: fuzz-exec-config 10 | fuzz-expr: fuzz-exec-expr 11 | fuzz-template: fuzz-exec-template 12 | fuzz-traversal: fuzz-exec-traversal 13 | 14 | fuzz-exec-%: fuzz%-fuzz.zip 15 | go-fuzz -bin=./fuzz$*-fuzz.zip -workdir=$(FUZZ_WORK_DIR) 16 | 17 | fuzz%-fuzz.zip: %/fuzz.go 18 | go-fuzz-build github.com/hashicorp/hcl2/hcl/hclsyntax/fuzz/$* 19 | 20 | tools: 21 | go get -u github.com/dvyukov/go-fuzz/go-fuzz 22 | go get -u github.com/dvyukov/go-fuzz/go-fuzz-build 23 | 24 | clean: 25 | rm fuzz*-fuzz.zip 26 | 27 | .PHONY: tools clean fuzz-config fuzz-expr fuzz-template fuzz-traversal 28 | .PRECIOUS: fuzzconfig-fuzz.zip fuzzexpr-fuzz.zip fuzztemplate-fuzz.zip fuzztraversal-fuzz.zip 29 | -------------------------------------------------------------------------------- /ext/include/map_resolver.go: -------------------------------------------------------------------------------- 1 | package include 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl2/hcl" 7 | ) 8 | 9 | // MapResolver returns a Resolver that consults the given map for preloaded 10 | // bodies (the values) associated with static include paths (the keys). 11 | // 12 | // An error diagnostic is returned if a path is requested that does not appear 13 | // as a key in the given map. 14 | func MapResolver(m map[string]hcl.Body) Resolver { 15 | return ResolverFunc(func(path string, refRange hcl.Range) (hcl.Body, hcl.Diagnostics) { 16 | if body, ok := m[path]; ok { 17 | return body, nil 18 | } 19 | 20 | return nil, hcl.Diagnostics{ 21 | { 22 | Severity: hcl.DiagError, 23 | Summary: "Invalid include path", 24 | Detail: fmt.Sprintf("The include path %q is not recognized.", path), 25 | Subject: &refRange, 26 | }, 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /hclpack/doc.go: -------------------------------------------------------------------------------- 1 | // Package hclpack provides a straightforward representation of HCL block/body 2 | // structure that can be easily serialized and deserialized for compact 3 | // transmission (e.g. over a network) without transmitting the full source code. 4 | // 5 | // Expressions are retained in native syntax source form so that their 6 | // evaluation can be delayed until a package structure is decoded by some 7 | // other system that has enough information to populate the evaluation context. 8 | // 9 | // Packed structures retain source location information but do not retain 10 | // actual source code. To make sense of source locations returned in diagnostics 11 | // and via other APIs the caller must somehow gain access to the original source 12 | // code that the packed representation was built from, which is a problem that 13 | // must be solved somehow by the calling application. 14 | package hclpack 15 | -------------------------------------------------------------------------------- /hcl/didyoumean.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | 3 | import ( 4 | "github.com/agext/levenshtein" 5 | ) 6 | 7 | // nameSuggestion tries to find a name from the given slice of suggested names 8 | // that is close to the given name and returns it if found. If no suggestion 9 | // is close enough, returns the empty string. 10 | // 11 | // The suggestions are tried in order, so earlier suggestions take precedence 12 | // if the given string is similar to two or more suggestions. 13 | // 14 | // This function is intended to be used with a relatively-small number of 15 | // suggestions. It's not optimized for hundreds or thousands of them. 16 | func nameSuggestion(given string, suggestions []string) string { 17 | for _, suggestion := range suggestions { 18 | dist := levenshtein.Distance(given, suggestion, nil) 19 | if dist < 3 { // threshold determined experimentally 20 | return suggestion 21 | } 22 | } 23 | return "" 24 | } 25 | -------------------------------------------------------------------------------- /hcl/hclsyntax/diagnostics.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | // setDiagEvalContext is an internal helper that will impose a particular 8 | // EvalContext on a set of diagnostics in-place, for any diagnostic that 9 | // does not already have an EvalContext set. 10 | // 11 | // We generally expect diagnostics to be immutable, but this is safe to use 12 | // on any Diagnostics where none of the contained Diagnostic objects have yet 13 | // been seen by a caller. Its purpose is to apply additional context to a 14 | // set of diagnostics produced by a "deeper" component as the stack unwinds 15 | // during expression evaluation. 16 | func setDiagEvalContext(diags hcl.Diagnostics, expr hcl.Expression, ctx *hcl.EvalContext) { 17 | for _, diag := range diags { 18 | if diag.Expression == nil { 19 | diag.Expression = expr 20 | diag.EvalContext = ctx 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /hclpack/didyoumean.go: -------------------------------------------------------------------------------- 1 | package hclpack 2 | 3 | import ( 4 | "github.com/agext/levenshtein" 5 | ) 6 | 7 | // nameSuggestion tries to find a name from the given slice of suggested names 8 | // that is close to the given name and returns it if found. If no suggestion 9 | // is close enough, returns the empty string. 10 | // 11 | // The suggestions are tried in order, so earlier suggestions take precedence 12 | // if the given string is similar to two or more suggestions. 13 | // 14 | // This function is intended to be used with a relatively-small number of 15 | // suggestions. It's not optimized for hundreds or thousands of them. 16 | func nameSuggestion(given string, suggestions []string) string { 17 | for _, suggestion := range suggestions { 18 | dist := levenshtein.Distance(given, suggestion, nil) 19 | if dist < 3 { // threshold determined experimentally 20 | return suggestion 21 | } 22 | } 23 | return "" 24 | } 25 | -------------------------------------------------------------------------------- /hcl/hclsyntax/didyoumean.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "github.com/agext/levenshtein" 5 | ) 6 | 7 | // nameSuggestion tries to find a name from the given slice of suggested names 8 | // that is close to the given name and returns it if found. If no suggestion 9 | // is close enough, returns the empty string. 10 | // 11 | // The suggestions are tried in order, so earlier suggestions take precedence 12 | // if the given string is similar to two or more suggestions. 13 | // 14 | // This function is intended to be used with a relatively-small number of 15 | // suggestions. It's not optimized for hundreds or thousands of them. 16 | func nameSuggestion(given string, suggestions []string) string { 17 | for _, suggestion := range suggestions { 18 | dist := levenshtein.Distance(given, suggestion, nil) 19 | if dist < 3 { // threshold determined experimentally 20 | return suggestion 21 | } 22 | } 23 | return "" 24 | } 25 | -------------------------------------------------------------------------------- /ext/userfunc/doc.go: -------------------------------------------------------------------------------- 1 | // Package userfunc implements a HCL extension that allows user-defined 2 | // functions in HCL configuration. 3 | // 4 | // Using this extension requires some integration effort on the part of the 5 | // calling application, to pass any declared functions into a HCL evaluation 6 | // context after processing. 7 | // 8 | // The function declaration syntax looks like this: 9 | // 10 | // function "foo" { 11 | // params = ["name"] 12 | // result = "Hello, ${name}!" 13 | // } 14 | // 15 | // When a user-defined function is called, the expression given for the "result" 16 | // attribute is evaluated in an isolated evaluation context that defines variables 17 | // named after the given parameter names. 18 | // 19 | // The block name "function" may be overridden by the calling application, if 20 | // that default name conflicts with an existing block or attribute name in 21 | // the application. 22 | package userfunc 23 | -------------------------------------------------------------------------------- /guide/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=HCL 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /hcl/json/tokentype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type tokenType scanner.go"; DO NOT EDIT. 2 | 3 | package json 4 | 5 | import "strconv" 6 | 7 | const _tokenType_name = "tokenInvalidtokenCommatokenColontokenEqualstokenKeywordtokenNumbertokenStringtokenBrackOtokenBrackCtokenBraceOtokenBraceCtokenEOF" 8 | 9 | var _tokenType_map = map[tokenType]string{ 10 | 0: _tokenType_name[0:12], 11 | 44: _tokenType_name[12:22], 12 | 58: _tokenType_name[22:32], 13 | 61: _tokenType_name[32:43], 14 | 75: _tokenType_name[43:55], 15 | 78: _tokenType_name[55:66], 16 | 83: _tokenType_name[66:77], 17 | 91: _tokenType_name[77:88], 18 | 93: _tokenType_name[88:99], 19 | 123: _tokenType_name[99:110], 20 | 125: _tokenType_name[110:121], 21 | 9220: _tokenType_name[121:129], 22 | } 23 | 24 | func (i tokenType) String() string { 25 | if str, ok := _tokenType_map[i]; ok { 26 | return str 27 | } 28 | return "tokenType(" + strconv.FormatInt(int64(i), 10) + ")" 29 | } 30 | -------------------------------------------------------------------------------- /ext/userfunc/README.md: -------------------------------------------------------------------------------- 1 | # HCL User Functions Extension 2 | 3 | This HCL extension allows a calling application to support user-defined 4 | functions. 5 | 6 | Functions are defined via a specific block type, like this: 7 | 8 | ```hcl 9 | function "add" { 10 | params = [a, b] 11 | result = a + b 12 | } 13 | 14 | function "list" { 15 | params = [] 16 | variadic_param = items 17 | result = items 18 | } 19 | ``` 20 | 21 | The extension is implemented as a pre-processor for `cty.Body` objects. Given 22 | a body that may contain functions, the `DecodeUserFunctions` function searches 23 | for blocks that define functions and returns a functions map suitable for 24 | inclusion in a `hcl.EvalContext`. It also returns a new `cty.Body` that 25 | contains the remainder of the content from the given body, allowing for 26 | further processing of remaining content. 27 | 28 | For more information, see [the godoc reference](http://godoc.org/github.com/hashicorp/hcl2/ext/userfunc). 29 | -------------------------------------------------------------------------------- /cmd/hcldec/examples/sh-config-file/example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # All paths from this point on are relative to the directory containing this 6 | # script, for simplicity's sake. 7 | cd "$( dirname "${BASH_SOURCE[0]}" )" 8 | 9 | # Read the config file using hcldec and then use jq to extract values in a 10 | # shell-friendly form. jq will ensure that the values are properly quoted and 11 | # escaped for consumption by the shell. 12 | CONFIG_VARS="$(hcldec --spec=spec.hcldec example.conf | jq -r '@sh "NAME=\(.name) GREETING=\(.greeting) FRIENDS=(\(.friends))"')" 13 | if [ $? != 0 ]; then 14 | # If hcldec or jq failed then it has already printed out some error messages 15 | # and so we can bail out. 16 | exit $? 17 | fi 18 | 19 | # Import our settings into our environment 20 | eval "$CONFIG_VARS" 21 | 22 | # ...and now, some contrived usage of the settings we loaded: 23 | echo "$GREETING $NAME!" 24 | for name in ${FRIENDS[@]}; do 25 | echo "$GREETING $name, too!" 26 | done 27 | -------------------------------------------------------------------------------- /hcldec/decode.go: -------------------------------------------------------------------------------- 1 | package hcldec 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | func decode(body hcl.Body, blockLabels []blockLabel, ctx *hcl.EvalContext, spec Spec, partial bool) (cty.Value, hcl.Body, hcl.Diagnostics) { 9 | schema := ImpliedSchema(spec) 10 | 11 | var content *hcl.BodyContent 12 | var diags hcl.Diagnostics 13 | var leftovers hcl.Body 14 | 15 | if partial { 16 | content, leftovers, diags = body.PartialContent(schema) 17 | } else { 18 | content, diags = body.Content(schema) 19 | } 20 | 21 | val, valDiags := spec.decode(content, blockLabels, ctx) 22 | diags = append(diags, valDiags...) 23 | 24 | return val, leftovers, diags 25 | } 26 | 27 | func impliedType(spec Spec) cty.Type { 28 | return spec.impliedType() 29 | } 30 | 31 | func sourceRange(body hcl.Body, blockLabels []blockLabel, spec Spec) hcl.Range { 32 | schema := ImpliedSchema(spec) 33 | content, _, _ := body.PartialContent(schema) 34 | 35 | return spec.sourceRange(content, blockLabels) 36 | } 37 | -------------------------------------------------------------------------------- /hcled/navigation.go: -------------------------------------------------------------------------------- 1 | package hcled 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | type contextStringer interface { 8 | ContextString(offset int) string 9 | } 10 | 11 | // ContextString returns a string describing the context of the given byte 12 | // offset, if available. An empty string is returned if no such information 13 | // is available, or otherwise the returned string is in a form that depends 14 | // on the language used to write the referenced file. 15 | func ContextString(file *hcl.File, offset int) string { 16 | if cser, ok := file.Nav.(contextStringer); ok { 17 | return cser.ContextString(offset) 18 | } 19 | return "" 20 | } 21 | 22 | type contextDefRanger interface { 23 | ContextDefRange(offset int) hcl.Range 24 | } 25 | 26 | func ContextDefRange(file *hcl.File, offset int) hcl.Range { 27 | if cser, ok := file.Nav.(contextDefRanger); ok { 28 | defRange := cser.ContextDefRange(offset) 29 | if !defRange.Empty() { 30 | return defRange 31 | } 32 | } 33 | return file.Body.MissingItemRange() 34 | } 35 | -------------------------------------------------------------------------------- /ext/dynblock/schema.go: -------------------------------------------------------------------------------- 1 | package dynblock 2 | 3 | import "github.com/hashicorp/hcl2/hcl" 4 | 5 | var dynamicBlockHeaderSchema = hcl.BlockHeaderSchema{ 6 | Type: "dynamic", 7 | LabelNames: []string{"type"}, 8 | } 9 | 10 | var dynamicBlockBodySchemaLabels = &hcl.BodySchema{ 11 | Attributes: []hcl.AttributeSchema{ 12 | { 13 | Name: "for_each", 14 | Required: true, 15 | }, 16 | { 17 | Name: "iterator", 18 | Required: false, 19 | }, 20 | { 21 | Name: "labels", 22 | Required: true, 23 | }, 24 | }, 25 | Blocks: []hcl.BlockHeaderSchema{ 26 | { 27 | Type: "content", 28 | LabelNames: nil, 29 | }, 30 | }, 31 | } 32 | 33 | var dynamicBlockBodySchemaNoLabels = &hcl.BodySchema{ 34 | Attributes: []hcl.AttributeSchema{ 35 | { 36 | Name: "for_each", 37 | Required: true, 38 | }, 39 | { 40 | Name: "iterator", 41 | Required: false, 42 | }, 43 | }, 44 | Blocks: []hcl.BlockHeaderSchema{ 45 | { 46 | Type: "content", 47 | LabelNames: nil, 48 | }, 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /hclpack/json_marshal_test.go: -------------------------------------------------------------------------------- 1 | package hclpack 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | 8 | "github.com/hashicorp/hcl2/hcl" 9 | ) 10 | 11 | func TestJSONRoundTrip(t *testing.T) { 12 | src := ` 13 | service "example" { 14 | priority = 2 15 | platform { 16 | os = "linux" 17 | arch = "amd64" 18 | } 19 | process "web" { 20 | exec = ["./webapp"] 21 | } 22 | process "worker" { 23 | exec = ["./worker"] 24 | } 25 | } 26 | ` 27 | 28 | startBody, diags := PackNativeFile([]byte(src), "example.svc", hcl.Pos{Line: 1, Column: 1}) 29 | if diags.HasErrors() { 30 | t.Fatalf("Failed to parse: %s", diags.Error()) 31 | } 32 | 33 | jb, err := startBody.MarshalJSON() 34 | if err != nil { 35 | t.Fatalf("Failed to marshal: %s", err) 36 | } 37 | 38 | endBody := &Body{} 39 | err = endBody.UnmarshalJSON(jb) 40 | if err != nil { 41 | t.Fatalf("Failed to unmarshal: %s", err) 42 | } 43 | 44 | if !cmp.Equal(startBody, endBody) { 45 | t.Errorf("incorrect result\n%s", cmp.Diff(startBody, endBody)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /hcldec/schema.go: -------------------------------------------------------------------------------- 1 | package hcldec 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | // ImpliedSchema returns the *hcl.BodySchema implied by the given specification. 8 | // This is the schema that the Decode function will use internally to 9 | // access the content of a given body. 10 | func ImpliedSchema(spec Spec) *hcl.BodySchema { 11 | var attrs []hcl.AttributeSchema 12 | var blocks []hcl.BlockHeaderSchema 13 | 14 | // visitSameBodyChildren walks through the spec structure, calling 15 | // the given callback for each descendent spec encountered. We are 16 | // interested in the specs that reference attributes and blocks. 17 | var visit visitFunc 18 | visit = func(s Spec) { 19 | if as, ok := s.(attrSpec); ok { 20 | attrs = append(attrs, as.attrSchemata()...) 21 | } 22 | 23 | if bs, ok := s.(blockSpec); ok { 24 | blocks = append(blocks, bs.blockHeaderSchemata()...) 25 | } 26 | 27 | s.visitSameBodyChildren(visit) 28 | } 29 | 30 | visit(spec) 31 | 32 | return &hcl.BodySchema{ 33 | Attributes: attrs, 34 | Blocks: blocks, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /hcl/json/didyoumean_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import "testing" 4 | 5 | func TestKeywordSuggestion(t *testing.T) { 6 | tests := []struct { 7 | Input, Want string 8 | }{ 9 | {"true", "true"}, 10 | {"false", "false"}, 11 | {"null", "null"}, 12 | {"bananas", ""}, 13 | {"NaN", ""}, 14 | {"Inf", ""}, 15 | {"Infinity", ""}, 16 | {"void", ""}, 17 | {"undefined", ""}, 18 | 19 | {"ture", "true"}, 20 | {"tru", "true"}, 21 | {"tre", "true"}, 22 | {"treu", "true"}, 23 | {"rtue", "true"}, 24 | 25 | {"flase", "false"}, 26 | {"fales", "false"}, 27 | {"flse", "false"}, 28 | {"fasle", "false"}, 29 | {"fasel", "false"}, 30 | {"flue", "false"}, 31 | 32 | {"nil", "null"}, 33 | {"nul", "null"}, 34 | {"unll", "null"}, 35 | {"nll", "null"}, 36 | } 37 | 38 | for _, test := range tests { 39 | t.Run(test.Input, func(t *testing.T) { 40 | got := keywordSuggestion(test.Input) 41 | if got != test.Want { 42 | t.Errorf( 43 | "wrong result\ninput: %q\ngot: %q\nwant: %q", 44 | test.Input, got, test.Want, 45 | ) 46 | } 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /guide/go.rst: -------------------------------------------------------------------------------- 1 | Using HCL in a Go application 2 | ============================= 3 | 4 | HCL is itself written in Go_ and currently it is primarily intended for use as 5 | a library within other Go programs. 6 | 7 | This section describes a number of different ways HCL can be used to define 8 | and process a configuration language within a Go program. For simple situations, 9 | HCL can decode directly into Go ``struct`` values in a similar way as encoding 10 | packages such as ``encoding/json`` and ``encoding/xml``. 11 | 12 | The HCL Go API also offers some alternative approaches however, for processing 13 | languages that may be more complex or that include portions whose expected 14 | structure cannot be determined until runtime. 15 | 16 | The following sections give an overview of different ways HCL can be used in 17 | a Go program. 18 | 19 | .. toctree:: 20 | :maxdepth: 1 21 | :caption: Sub-sections: 22 | 23 | go_parsing 24 | go_diagnostics 25 | go_decoding_gohcl 26 | go_decoding_hcldec 27 | go_expression_eval 28 | go_decoding_lowlevel 29 | go_patterns 30 | 31 | .. _Go: https://golang.org/ 32 | -------------------------------------------------------------------------------- /ext/include/resolver.go: -------------------------------------------------------------------------------- 1 | package include 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | // A Resolver maps an include path (an arbitrary string, but usually something 8 | // filepath-like) to a hcl.Body. 9 | // 10 | // The parameter "refRange" is the source range of the expression in the calling 11 | // body that provided the given path, for use in generating "invalid path"-type 12 | // diagnostics. 13 | // 14 | // If the returned body is nil, it will be ignored. 15 | // 16 | // Any returned diagnostics will be emitted when content is requested from the 17 | // final composed body (after all includes have been dealt with). 18 | type Resolver interface { 19 | ResolveBodyPath(path string, refRange hcl.Range) (hcl.Body, hcl.Diagnostics) 20 | } 21 | 22 | // ResolverFunc is a function type that implements Resolver. 23 | type ResolverFunc func(path string, refRange hcl.Range) (hcl.Body, hcl.Diagnostics) 24 | 25 | // ResolveBodyPath is an implementation of Resolver.ResolveBodyPath. 26 | func (f ResolverFunc) ResolveBodyPath(path string, refRange hcl.Range) (hcl.Body, hcl.Diagnostics) { 27 | return f(path, refRange) 28 | } 29 | -------------------------------------------------------------------------------- /hcl/static_expr.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | 3 | import ( 4 | "github.com/zclconf/go-cty/cty" 5 | ) 6 | 7 | type staticExpr struct { 8 | val cty.Value 9 | rng Range 10 | } 11 | 12 | // StaticExpr returns an Expression that always evaluates to the given value. 13 | // 14 | // This is useful to substitute default values for expressions that are 15 | // not explicitly given in configuration and thus would otherwise have no 16 | // Expression to return. 17 | // 18 | // Since expressions are expected to have a source range, the caller must 19 | // provide one. Ideally this should be a real source range, but it can 20 | // be a synthetic one (with an empty-string filename) if no suitable range 21 | // is available. 22 | func StaticExpr(val cty.Value, rng Range) Expression { 23 | return staticExpr{val, rng} 24 | } 25 | 26 | func (e staticExpr) Value(ctx *EvalContext) (cty.Value, Diagnostics) { 27 | return e.val, nil 28 | } 29 | 30 | func (e staticExpr) Variables() []Traversal { 31 | return nil 32 | } 33 | 34 | func (e staticExpr) Range() Range { 35 | return e.rng 36 | } 37 | 38 | func (e staticExpr) StartRange() Range { 39 | return e.rng 40 | } 41 | -------------------------------------------------------------------------------- /hclpack/vlq.go: -------------------------------------------------------------------------------- 1 | package hclpack 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/bsm/go-vlq" 7 | ) 8 | 9 | type vlqBuf []byte 10 | 11 | var vlqSpace [vlq.MaxLen64]byte 12 | 13 | func newVLQBuf(byteCap int) vlqBuf { 14 | return make(vlqBuf, 0, byteCap) 15 | } 16 | 17 | func (b vlqBuf) AppendInt(i int) vlqBuf { 18 | spc := cap(b) - len(b) 19 | if spc < len(vlqSpace) { 20 | b = append(b, vlqSpace[:]...) 21 | b = b[:len(b)-len(vlqSpace)] 22 | } 23 | into := b[len(b):cap(b)] 24 | l := vlq.PutInt(into, int64(i)) 25 | b = b[:len(b)+l] 26 | return b 27 | } 28 | 29 | func (b vlqBuf) ReadInt() (int, vlqBuf, error) { 30 | v, adv := vlq.Int([]byte(b)) 31 | if adv <= 0 { 32 | if adv == 0 { 33 | return 0, b, errors.New("missing expected VLQ value") 34 | } else { 35 | return 0, b, errors.New("invalid VLQ value") 36 | } 37 | } 38 | if int64(int(v)) != v { 39 | return 0, b, errors.New("VLQ value too big for integer on this platform") 40 | } 41 | return int(v), b[adv:], nil 42 | } 43 | 44 | func (b vlqBuf) AppendRawByte(by byte) vlqBuf { 45 | return append(b, by) 46 | } 47 | 48 | func (b vlqBuf) Bytes() []byte { 49 | return []byte(b) 50 | } 51 | -------------------------------------------------------------------------------- /hcl/hclsyntax/didyoumean_test.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import "testing" 4 | 5 | func TestNameSuggestion(t *testing.T) { 6 | var keywords = []string{"false", "true", "null"} 7 | 8 | tests := []struct { 9 | Input, Want string 10 | }{ 11 | {"true", "true"}, 12 | {"false", "false"}, 13 | {"null", "null"}, 14 | {"bananas", ""}, 15 | {"NaN", ""}, 16 | {"Inf", ""}, 17 | {"Infinity", ""}, 18 | {"void", ""}, 19 | {"undefined", ""}, 20 | 21 | {"ture", "true"}, 22 | {"tru", "true"}, 23 | {"tre", "true"}, 24 | {"treu", "true"}, 25 | {"rtue", "true"}, 26 | 27 | {"flase", "false"}, 28 | {"fales", "false"}, 29 | {"flse", "false"}, 30 | {"fasle", "false"}, 31 | {"fasel", "false"}, 32 | {"flue", "false"}, 33 | 34 | {"nil", "null"}, 35 | {"nul", "null"}, 36 | {"unll", "null"}, 37 | {"nll", "null"}, 38 | } 39 | 40 | for _, test := range tests { 41 | t.Run(test.Input, func(t *testing.T) { 42 | got := nameSuggestion(test.Input, keywords) 43 | if got != test.Want { 44 | t.Errorf( 45 | "wrong result\ninput: %q\ngot: %q\nwant: %q", 46 | test.Input, got, test.Want, 47 | ) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /hclwrite/ast_attribute.go: -------------------------------------------------------------------------------- 1 | package hclwrite 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl/hclsyntax" 5 | ) 6 | 7 | type Attribute struct { 8 | inTree 9 | 10 | leadComments *node 11 | name *node 12 | expr *node 13 | lineComments *node 14 | } 15 | 16 | func newAttribute() *Attribute { 17 | return &Attribute{ 18 | inTree: newInTree(), 19 | } 20 | } 21 | 22 | func (a *Attribute) init(name string, expr *Expression) { 23 | expr.assertUnattached() 24 | 25 | nameTok := newIdentToken(name) 26 | nameObj := newIdentifier(nameTok) 27 | a.leadComments = a.children.Append(newComments(nil)) 28 | a.name = a.children.Append(nameObj) 29 | a.children.AppendUnstructuredTokens(Tokens{ 30 | { 31 | Type: hclsyntax.TokenEqual, 32 | Bytes: []byte{'='}, 33 | }, 34 | }) 35 | a.expr = a.children.Append(expr) 36 | a.expr.list = a.children 37 | a.lineComments = a.children.Append(newComments(nil)) 38 | a.children.AppendUnstructuredTokens(Tokens{ 39 | { 40 | Type: hclsyntax.TokenNewline, 41 | Bytes: []byte{'\n'}, 42 | }, 43 | }) 44 | } 45 | 46 | func (a *Attribute) Expr() *Expression { 47 | return a.expr.content.(*Expression) 48 | } 49 | -------------------------------------------------------------------------------- /hcl/hclsyntax/public_test.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestValidIdentifier(t *testing.T) { 8 | tests := []struct { 9 | Input string 10 | Want bool 11 | }{ 12 | {"", false}, 13 | {"hello", true}, 14 | {"hello.world", false}, 15 | {"hello ", false}, 16 | {" hello", false}, 17 | {"hello\n", false}, 18 | {"hello world", false}, 19 | {"aws_instance", true}, 20 | {"aws.instance", false}, 21 | {"foo-bar", true}, 22 | {"foo--bar", true}, 23 | {"foo_", true}, 24 | {"foo-", true}, 25 | {"_foobar", true}, 26 | {"-foobar", false}, 27 | {"blah1", true}, 28 | {"blah1blah", true}, 29 | {"1blah1blah", false}, 30 | {"héllo", true}, // combining acute accent 31 | {"Χαίρετε", true}, 32 | {"звать", true}, 33 | {"今日は", true}, 34 | {"\x80", false}, // UTF-8 continuation without an introducer 35 | {"a\x80", false}, // UTF-8 continuation after a non-introducer 36 | } 37 | 38 | for _, test := range tests { 39 | t.Run(test.Input, func(t *testing.T) { 40 | got := ValidIdentifier(test.Input) 41 | if got != test.Want { 42 | t.Errorf("wrong result %#v; want %#v", got, test.Want) 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /hclwrite/ast_test.go: -------------------------------------------------------------------------------- 1 | package hclwrite 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type TestTreeNode struct { 9 | Type string 10 | Val string 11 | 12 | Children []TestTreeNode 13 | } 14 | 15 | func makeTestTree(n *node) (root TestTreeNode) { 16 | const us = "hclwrite." 17 | const usPtr = "*hclwrite." 18 | root.Type = fmt.Sprintf("%T", n.content) 19 | if strings.HasPrefix(root.Type, us) { 20 | root.Type = root.Type[len(us):] 21 | } else if strings.HasPrefix(root.Type, usPtr) { 22 | root.Type = root.Type[len(usPtr):] 23 | } 24 | 25 | type WithVal interface { 26 | testValue() string 27 | } 28 | hasTestVal := false 29 | if withVal, ok := n.content.(WithVal); ok { 30 | root.Val = withVal.testValue() 31 | hasTestVal = true 32 | } 33 | 34 | n.content.walkChildNodes(func(n *node) { 35 | root.Children = append(root.Children, makeTestTree(n)) 36 | }) 37 | 38 | // If we didn't end up with any children then this is probably a leaf 39 | // node, so we'll set its content value to it raw bytes if we didn't 40 | // already set a test value. 41 | if !hasTestVal && len(root.Children) == 0 { 42 | toks := n.content.BuildTokens(nil) 43 | root.Val = toks.testValue() 44 | } 45 | 46 | return root 47 | } 48 | -------------------------------------------------------------------------------- /hcl/json/navigation_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | func TestNavigationContextString(t *testing.T) { 10 | src := ` 11 | { 12 | "version": 1, 13 | "resource": { 14 | "null_resource": { 15 | "baz": { 16 | "id": "foo" 17 | }, 18 | "boz": [ 19 | { 20 | "ov": { } 21 | } 22 | ] 23 | } 24 | } 25 | } 26 | ` 27 | file, diags := Parse([]byte(src), "test.json") 28 | if len(diags) != 0 { 29 | fmt.Printf("offset %d\n", diags[0].Subject.Start.Byte) 30 | t.Errorf("Unexpected diagnostics: %s", diags) 31 | } 32 | if file == nil { 33 | t.Fatalf("Got nil file") 34 | } 35 | nav := file.Nav.(navigation) 36 | 37 | tests := []struct { 38 | Offset int 39 | Want string 40 | }{ 41 | {0, ``}, 42 | {8, ``}, 43 | {36, `resource`}, 44 | {60, `resource.null_resource`}, 45 | {89, `resource.null_resource.baz`}, 46 | {141, `resource.null_resource.boz`}, 47 | } 48 | 49 | for _, test := range tests { 50 | t.Run(strconv.Itoa(test.Offset), func(t *testing.T) { 51 | got := nav.ContextString(test.Offset) 52 | 53 | if got != test.Want { 54 | t.Errorf("wrong result\ngot: %s\nwant: %s", got, test.Want) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /specsuite/tests/expressions/heredoc.t: -------------------------------------------------------------------------------- 1 | result = { 2 | normal = { 3 | basic = "Foo\nBar\nBaz\n" 4 | indented = " Foo\n Bar\n Baz\n" 5 | indented_more = " Foo\n Bar\n Baz\n" 6 | interp = " Foo\n Bar\n Baz\n" 7 | newlines_between = "Foo\n\nBar\n\nBaz\n" 8 | indented_newlines_between = " Foo\n\n Bar\n\n Baz\n" 9 | 10 | marker_at_suffix = " NOT EOT\n" 11 | } 12 | flush = { 13 | basic = "Foo\nBar\nBaz\n" 14 | indented = "Foo\nBar\nBaz\n" 15 | indented_more = "Foo\n Bar\nBaz\n" 16 | indented_less = " Foo\nBar\n Baz\n" 17 | interp = "Foo\nBar\nBaz\n" 18 | interp_indented_more = "Foo\n Bar\nBaz\n" 19 | interp_indented_less = " Foo\n Bar\n Baz\n" 20 | tabs = "Foo\n Bar\n Baz\n" 21 | unicode_spaces = " Foo (there's two \"em spaces\" before Foo there)\nBar\nBaz\n" 22 | newlines_between = "Foo\n\nBar\n\nBaz\n" 23 | indented_newlines_between = "Foo\n\nBar\n\nBaz\n" 24 | } 25 | } 26 | result_type = object({ 27 | normal = map(string) 28 | flush = map(string) 29 | }) 30 | -------------------------------------------------------------------------------- /hcl/json/didyoumean.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "github.com/agext/levenshtein" 5 | ) 6 | 7 | var keywords = []string{"false", "true", "null"} 8 | 9 | // keywordSuggestion tries to find a valid JSON keyword that is close to the 10 | // given string and returns it if found. If no keyword is close enough, returns 11 | // the empty string. 12 | func keywordSuggestion(given string) string { 13 | return nameSuggestion(given, keywords) 14 | } 15 | 16 | // nameSuggestion tries to find a name from the given slice of suggested names 17 | // that is close to the given name and returns it if found. If no suggestion 18 | // is close enough, returns the empty string. 19 | // 20 | // The suggestions are tried in order, so earlier suggestions take precedence 21 | // if the given string is similar to two or more suggestions. 22 | // 23 | // This function is intended to be used with a relatively-small number of 24 | // suggestions. It's not optimized for hundreds or thousands of them. 25 | func nameSuggestion(given string, suggestions []string) string { 26 | for _, suggestion := range suggestions { 27 | dist := levenshtein.Distance(given, suggestion, nil) 28 | if dist < 3 { // threshold determined experimentally 29 | return suggestion 30 | } 31 | } 32 | return "" 33 | } 34 | -------------------------------------------------------------------------------- /ext/transform/transformer.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | // A Transformer takes a given body, applies some (possibly no-op) 8 | // transform to it, and returns the new body. 9 | // 10 | // It must _not_ mutate the given body in-place. 11 | // 12 | // The transform call cannot fail, but it _can_ return a body that immediately 13 | // returns diagnostics when its methods are called. NewErrorBody is a utility 14 | // to help with this. 15 | type Transformer interface { 16 | TransformBody(hcl.Body) hcl.Body 17 | } 18 | 19 | // TransformerFunc is a function type that implements Transformer. 20 | type TransformerFunc func(hcl.Body) hcl.Body 21 | 22 | // TransformBody is an implementation of Transformer.TransformBody. 23 | func (f TransformerFunc) TransformBody(in hcl.Body) hcl.Body { 24 | return f(in) 25 | } 26 | 27 | type chain []Transformer 28 | 29 | // Chain takes a slice of transformers and returns a single new 30 | // Transformer that applies each of the given transformers in sequence. 31 | func Chain(c []Transformer) Transformer { 32 | return chain(c) 33 | } 34 | 35 | func (c chain) TransformBody(body hcl.Body) hcl.Body { 36 | for _, t := range c { 37 | body = t.TransformBody(body) 38 | } 39 | return body 40 | } 41 | -------------------------------------------------------------------------------- /hcldec/variables.go: -------------------------------------------------------------------------------- 1 | package hcldec 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | // Variables processes the given body with the given spec and returns a 8 | // list of the variable traversals that would be required to decode 9 | // the same pairing of body and spec. 10 | // 11 | // This can be used to conditionally populate the variables in the EvalContext 12 | // passed to Decode, for applications where a static scope is insufficient. 13 | // 14 | // If the given body is not compliant with the given schema, the result may 15 | // be incomplete, but that's assumed to be okay because the eventual call 16 | // to Decode will produce error diagnostics anyway. 17 | func Variables(body hcl.Body, spec Spec) []hcl.Traversal { 18 | var vars []hcl.Traversal 19 | schema := ImpliedSchema(spec) 20 | content, _, _ := body.PartialContent(schema) 21 | 22 | if vs, ok := spec.(specNeedingVariables); ok { 23 | vars = append(vars, vs.variablesNeeded(content)...) 24 | } 25 | 26 | var visitFn visitFunc 27 | visitFn = func(s Spec) { 28 | if vs, ok := s.(specNeedingVariables); ok { 29 | vars = append(vars, vs.variablesNeeded(content)...) 30 | } 31 | s.visitSameBodyChildren(visitFn) 32 | } 33 | spec.visitSameBodyChildren(visitFn) 34 | 35 | return vars 36 | } 37 | -------------------------------------------------------------------------------- /ext/dynblock/expr_wrap.go: -------------------------------------------------------------------------------- 1 | package dynblock 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | type exprWrap struct { 9 | hcl.Expression 10 | i *iteration 11 | } 12 | 13 | func (e exprWrap) Variables() []hcl.Traversal { 14 | raw := e.Expression.Variables() 15 | ret := make([]hcl.Traversal, 0, len(raw)) 16 | 17 | // Filter out traversals that refer to our iterator name or any 18 | // iterator we've inherited; we're going to provide those in 19 | // our Value wrapper, so the caller doesn't need to know about them. 20 | for _, traversal := range raw { 21 | rootName := traversal.RootName() 22 | if rootName == e.i.IteratorName { 23 | continue 24 | } 25 | if _, inherited := e.i.Inherited[rootName]; inherited { 26 | continue 27 | } 28 | ret = append(ret, traversal) 29 | } 30 | return ret 31 | } 32 | 33 | func (e exprWrap) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { 34 | extCtx := e.i.EvalContext(ctx) 35 | return e.Expression.Value(extCtx) 36 | } 37 | 38 | // UnwrapExpression returns the expression being wrapped by this instance. 39 | // This allows the original expression to be recovered by hcl.UnwrapExpression. 40 | func (e exprWrap) UnwrapExpression() hcl.Expression { 41 | return e.Expression 42 | } 43 | -------------------------------------------------------------------------------- /hcl/hclsyntax/navigation.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/hashicorp/hcl2/hcl" 8 | ) 9 | 10 | type navigation struct { 11 | root *Body 12 | } 13 | 14 | // Implementation of hcled.ContextString 15 | func (n navigation) ContextString(offset int) string { 16 | // We will walk our top-level blocks until we find one that contains 17 | // the given offset, and then construct a representation of the header 18 | // of the block. 19 | 20 | var block *Block 21 | for _, candidate := range n.root.Blocks { 22 | if candidate.Range().ContainsOffset(offset) { 23 | block = candidate 24 | break 25 | } 26 | } 27 | 28 | if block == nil { 29 | return "" 30 | } 31 | 32 | if len(block.Labels) == 0 { 33 | // Easy case! 34 | return block.Type 35 | } 36 | 37 | buf := &bytes.Buffer{} 38 | buf.WriteString(block.Type) 39 | for _, label := range block.Labels { 40 | fmt.Fprintf(buf, " %q", label) 41 | } 42 | return buf.String() 43 | } 44 | 45 | func (n navigation) ContextDefRange(offset int) hcl.Range { 46 | var block *Block 47 | for _, candidate := range n.root.Blocks { 48 | if candidate.Range().ContainsOffset(offset) { 49 | block = candidate 50 | break 51 | } 52 | } 53 | 54 | if block == nil { 55 | return hcl.Range{} 56 | } 57 | 58 | return block.DefRange() 59 | } 60 | -------------------------------------------------------------------------------- /hcl/expr_list.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | 3 | // ExprList tests if the given expression is a static list construct and, 4 | // if so, extracts the expressions that represent the list elements. 5 | // If the given expression is not a static list, error diagnostics are 6 | // returned. 7 | // 8 | // A particular Expression implementation can support this function by 9 | // offering a method called ExprList that takes no arguments and returns 10 | // []Expression. This method should return nil if a static list cannot 11 | // be extracted. Alternatively, an implementation can support 12 | // UnwrapExpression to delegate handling of this function to a wrapped 13 | // Expression object. 14 | func ExprList(expr Expression) ([]Expression, Diagnostics) { 15 | type exprList interface { 16 | ExprList() []Expression 17 | } 18 | 19 | physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool { 20 | _, supported := expr.(exprList) 21 | return supported 22 | }) 23 | 24 | if exL, supported := physExpr.(exprList); supported { 25 | if list := exL.ExprList(); list != nil { 26 | return list, nil 27 | } 28 | } 29 | return nil, Diagnostics{ 30 | &Diagnostic{ 31 | Severity: DiagError, 32 | Summary: "Invalid expression", 33 | Detail: "A static list expression is required.", 34 | Subject: expr.StartRange().Ptr(), 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /hcl/hclsyntax/walk.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | // VisitFunc is the callback signature for VisitAll. 8 | type VisitFunc func(node Node) hcl.Diagnostics 9 | 10 | // VisitAll is a basic way to traverse the AST beginning with a particular 11 | // node. The given function will be called once for each AST node in 12 | // depth-first order, but no context is provided about the shape of the tree. 13 | // 14 | // The VisitFunc may return diagnostics, in which case they will be accumulated 15 | // and returned as a single set. 16 | func VisitAll(node Node, f VisitFunc) hcl.Diagnostics { 17 | diags := f(node) 18 | node.walkChildNodes(func(node Node) { 19 | diags = append(diags, VisitAll(node, f)...) 20 | }) 21 | return diags 22 | } 23 | 24 | // Walker is an interface used with Walk. 25 | type Walker interface { 26 | Enter(node Node) hcl.Diagnostics 27 | Exit(node Node) hcl.Diagnostics 28 | } 29 | 30 | // Walk is a more complex way to traverse the AST starting with a particular 31 | // node, which provides information about the tree structure via separate 32 | // Enter and Exit functions. 33 | func Walk(node Node, w Walker) hcl.Diagnostics { 34 | diags := w.Enter(node) 35 | node.walkChildNodes(func(node Node) { 36 | diags = append(diags, Walk(node, w)...) 37 | }) 38 | moreDiags := w.Exit(node) 39 | diags = append(diags, moreDiags...) 40 | return diags 41 | } 42 | -------------------------------------------------------------------------------- /extras/grammar/HCL.yaml-tmLanguage: -------------------------------------------------------------------------------- 1 | name: HCL 2 | scopeName: source.hcl 3 | fileTypes: [hcl, hcldec] 4 | uuid: 55e8075d-e2e3-4e44-8446-744a9860e476 5 | 6 | patterns: 7 | 8 | - comment: Comments 9 | name: comment.line.hcl 10 | begin: '#|//' 11 | end: $\n? 12 | captures: 13 | '0': {name: punctuation.definition.comment.hcl} 14 | 15 | - comment: Block comments 16 | name: comment.block.hcl 17 | begin: /\* 18 | end: \*/ 19 | captures: 20 | '0': {name: punctuation.definition.comment.hcl} 21 | 22 | - comment: Nested Blocks 23 | name: meta.block.hcl 24 | begin: "{" 25 | beginCaptures: 26 | '0': {name: punctuation.definition.block.hcl} 27 | end: "}" 28 | endCaptures: 29 | '0': {name: punctuation.definition.block.hcl} 30 | patterns: 31 | - include: "$self" 32 | 33 | - comment: Quoted Block Labels 34 | match: '(")([^"]+)(")' 35 | captures: 36 | '1': {name: string.hcl punctuation.definition.string.begin.hcl} 37 | '2': {name: string.value.hcl} 38 | '3': {name: string.hcl punctuation.definition.string.end.hcl} 39 | 40 | - comment: Attribute Definitions 41 | name: meta.attr.hcl 42 | begin: '(\w+)\s*(=)\s*' 43 | beginCaptures: 44 | '1': {name: variable.other.assignment.hcl} 45 | '2': {name: keyword.operator.hcl} 46 | end: '$' 47 | patterns: 48 | - include: "source.hclexpr" 49 | 50 | - comment: Keywords 51 | match: '[-\w]+' 52 | captures: 53 | '0': {name: keyword.other.hcl} 54 | -------------------------------------------------------------------------------- /hclwrite/public.go: -------------------------------------------------------------------------------- 1 | package hclwrite 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/hashicorp/hcl2/hcl" 7 | ) 8 | 9 | // NewFile creates a new file object that is empty and ready to have constructs 10 | // added t it. 11 | func NewFile() *File { 12 | body := &Body{ 13 | inTree: newInTree(), 14 | items: newNodeSet(), 15 | } 16 | file := &File{ 17 | inTree: newInTree(), 18 | } 19 | file.body = file.inTree.children.Append(body) 20 | return file 21 | } 22 | 23 | // ParseConfig interprets the given source bytes into a *hclwrite.File. The 24 | // resulting AST can be used to perform surgical edits on the source code 25 | // before turning it back into bytes again. 26 | func ParseConfig(src []byte, filename string, start hcl.Pos) (*File, hcl.Diagnostics) { 27 | return parse(src, filename, start) 28 | } 29 | 30 | // Format takes source code and performs simple whitespace changes to transform 31 | // it to a canonical layout style. 32 | // 33 | // Format skips constructing an AST and works directly with tokens, so it 34 | // is less expensive than formatting via the AST for situations where no other 35 | // changes will be made. It also ignores syntax errors and can thus be applied 36 | // to partial source code, although the result in that case may not be 37 | // desirable. 38 | func Format(src []byte) []byte { 39 | tokens := lexConfig(src) 40 | format(tokens) 41 | buf := &bytes.Buffer{} 42 | tokens.WriteTo(buf) 43 | return buf.Bytes() 44 | } 45 | -------------------------------------------------------------------------------- /guide/index.rst: -------------------------------------------------------------------------------- 1 | HCL Config Language Toolkit 2 | =========================== 3 | 4 | HCL is a toolkit for creating structured configuration languages that are both 5 | human- and machine-friendly, for use with command-line tools, servers, etc. 6 | 7 | HCL has both a native syntax, intended to be pleasant to read and write for 8 | humans, and a JSON-based variant that is easier for machines to generate and 9 | parse. The native syntax is inspired by libucl_, `nginx configuration`_, and 10 | others. 11 | 12 | It includes an expression syntax that allows basic inline computation and, with 13 | support from the calling application, use of variables and functions for more 14 | dynamic configuration languages. 15 | 16 | HCL provides a set of constructs that can be used by a calling application to 17 | construct a configuration language. The application defines which argument 18 | names and nested block types are expected, and HCL parses the configuration 19 | file, verifies that it conforms to the expected structure, and returns 20 | high-level objects that the application can use for further processing. 21 | 22 | At present, HCL is primarily intended for use in applications written in Go_, 23 | via its library API. 24 | 25 | .. toctree:: 26 | :maxdepth: 1 27 | :caption: Contents: 28 | 29 | intro 30 | go 31 | language_design 32 | 33 | .. _libucl: https://github.com/vstakhov/libucl 34 | .. _`nginx configuration`: http://nginx.org/en/docs/beginners_guide.html#conf_structure 35 | .. _Go: https://golang.org/ 36 | -------------------------------------------------------------------------------- /gohcl/encode_test.go: -------------------------------------------------------------------------------- 1 | package gohcl_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl2/gohcl" 7 | "github.com/hashicorp/hcl2/hclwrite" 8 | ) 9 | 10 | func ExampleEncodeIntoBody() { 11 | type Service struct { 12 | Name string `hcl:"name,label"` 13 | Exe []string `hcl:"executable"` 14 | } 15 | type Constraints struct { 16 | OS string `hcl:"os"` 17 | Arch string `hcl:"arch"` 18 | } 19 | type App struct { 20 | Name string `hcl:"name"` 21 | Desc string `hcl:"description"` 22 | Constraints *Constraints `hcl:"constraints,block"` 23 | Services []Service `hcl:"service,block"` 24 | } 25 | 26 | app := App{ 27 | Name: "awesome-app", 28 | Desc: "Such an awesome application", 29 | Constraints: &Constraints{ 30 | OS: "linux", 31 | Arch: "amd64", 32 | }, 33 | Services: []Service{ 34 | { 35 | Name: "web", 36 | Exe: []string{"./web", "--listen=:8080"}, 37 | }, 38 | { 39 | Name: "worker", 40 | Exe: []string{"./worker"}, 41 | }, 42 | }, 43 | } 44 | 45 | f := hclwrite.NewEmptyFile() 46 | gohcl.EncodeIntoBody(&app, f.Body()) 47 | fmt.Printf("%s", f.Bytes()) 48 | 49 | // Output: 50 | // name = "awesome-app" 51 | // description = "Such an awesome application" 52 | // 53 | // constraints { 54 | // os = "linux" 55 | // arch = "amd64" 56 | // } 57 | // 58 | // service "web" { 59 | // executable = ["./web", "--listen=:8080"] 60 | // } 61 | // service "worker" { 62 | // executable = ["./worker"] 63 | // } 64 | } 65 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // WARNING: This module will move to a new path when it transitions from 2 | // being "experimental" to being released. 3 | module github.com/hashicorp/hcl2 4 | 5 | require ( 6 | github.com/agext/levenshtein v1.2.1 7 | github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 8 | github.com/apparentlymart/go-textseg v1.0.0 9 | github.com/bsm/go-vlq v0.0.0-20150828105119-ec6e8d4f5f4e 10 | github.com/davecgh/go-spew v1.1.1 11 | github.com/go-test/deep v1.0.3 12 | github.com/google/go-cmp v0.2.0 13 | github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357 // indirect 14 | github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0 15 | github.com/kr/pretty v0.1.0 16 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 17 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 18 | github.com/onsi/ginkgo v1.7.0 // indirect 19 | github.com/onsi/gomega v1.4.3 // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | github.com/sergi/go-diff v1.0.0 22 | github.com/spf13/pflag v1.0.2 23 | github.com/stretchr/testify v1.2.2 // indirect 24 | github.com/zclconf/go-cty v1.0.0 25 | golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 26 | golang.org/x/net v0.0.0-20190502183928-7f726cade0ab // indirect 27 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect 28 | golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 // indirect 29 | golang.org/x/text v0.3.2 // indirect 30 | gopkg.in/yaml.v2 v2.2.2 31 | howett.net/plist v0.0.0-20181124034731-591f970eefbb 32 | ) 33 | -------------------------------------------------------------------------------- /hcl/expr_map.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | 3 | // ExprMap tests if the given expression is a static map construct and, 4 | // if so, extracts the expressions that represent the map elements. 5 | // If the given expression is not a static map, error diagnostics are 6 | // returned. 7 | // 8 | // A particular Expression implementation can support this function by 9 | // offering a method called ExprMap that takes no arguments and returns 10 | // []KeyValuePair. This method should return nil if a static map cannot 11 | // be extracted. Alternatively, an implementation can support 12 | // UnwrapExpression to delegate handling of this function to a wrapped 13 | // Expression object. 14 | func ExprMap(expr Expression) ([]KeyValuePair, Diagnostics) { 15 | type exprMap interface { 16 | ExprMap() []KeyValuePair 17 | } 18 | 19 | physExpr := UnwrapExpressionUntil(expr, func(expr Expression) bool { 20 | _, supported := expr.(exprMap) 21 | return supported 22 | }) 23 | 24 | if exM, supported := physExpr.(exprMap); supported { 25 | if pairs := exM.ExprMap(); pairs != nil { 26 | return pairs, nil 27 | } 28 | } 29 | return nil, Diagnostics{ 30 | &Diagnostic{ 31 | Severity: DiagError, 32 | Summary: "Invalid expression", 33 | Detail: "A static map expression is required.", 34 | Subject: expr.StartRange().Ptr(), 35 | }, 36 | } 37 | } 38 | 39 | // KeyValuePair represents a pair of expressions that serve as a single item 40 | // within a map or object definition construct. 41 | type KeyValuePair struct { 42 | Key Expression 43 | Value Expression 44 | } 45 | -------------------------------------------------------------------------------- /specsuite/tests/structure/blocks/single_oneline_invalid.t: -------------------------------------------------------------------------------- 1 | diagnostics { 2 | error { 3 | # Message like "Only one argument is allowed in a single-line block definition" 4 | from { 5 | line = 1 6 | column = 14 7 | byte = 13 8 | } 9 | to { 10 | line = 1 11 | column = 15 12 | byte = 14 13 | } 14 | } 15 | error { 16 | # Message like "The closing brace for a single-line block definition must be on the same line" 17 | from { 18 | line = 2 19 | column = 14 20 | byte = 40 21 | } 22 | to { 23 | line = 3 24 | column = 1 25 | byte = 41 26 | } 27 | } 28 | error { 29 | # Message like "The closing brace for a single-line block definition must be on the same line" 30 | from { 31 | line = 4 32 | column = 14 33 | byte = 56 34 | } 35 | to { 36 | line = 5 37 | column = 1 38 | byte = 57 39 | } 40 | } 41 | error { 42 | # Message like "The closing brace for a single-line block definition must be on the same line" 43 | from { 44 | line = 6 45 | column = 14 46 | byte = 84 47 | } 48 | to { 49 | line = 7 50 | column = 1 51 | byte = 85 52 | } 53 | } 54 | error { 55 | # Message like "A single-line block definition cannot contain another block definition" 56 | from { 57 | line = 9 58 | column = 5 59 | byte = 103 60 | } 61 | to { 62 | line = 9 63 | column = 8 64 | byte = 106 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /specsuite/tests/expressions/heredoc.hcl: -------------------------------------------------------------------------------- 1 | normal = { 2 | basic = < 0 && ret[0] == '.' { 27 | ret = ret[1:] 28 | } 29 | return ret 30 | } 31 | 32 | func navigationStepsRev(v node, offset int) []string { 33 | switch tv := v.(type) { 34 | case *objectVal: 35 | // Do any of our properties have an object that contains the target 36 | // offset? 37 | for _, attr := range tv.Attrs { 38 | k := attr.Name 39 | av := attr.Value 40 | 41 | switch av.(type) { 42 | case *objectVal, *arrayVal: 43 | // okay 44 | default: 45 | continue 46 | } 47 | 48 | if av.Range().ContainsOffset(offset) { 49 | return append(navigationStepsRev(av, offset), "."+k) 50 | } 51 | } 52 | case *arrayVal: 53 | // Do any of our elements contain the target offset? 54 | for i, elem := range tv.Values { 55 | 56 | switch elem.(type) { 57 | case *objectVal, *arrayVal: 58 | // okay 59 | default: 60 | continue 61 | } 62 | 63 | if elem.Range().ContainsOffset(offset) { 64 | return append(navigationStepsRev(elem, offset), fmt.Sprintf("[%d]", i)) 65 | } 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /hclpack/expression_test.go: -------------------------------------------------------------------------------- 1 | package hclpack 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/hcl2/hcl" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestExpressionValue(t *testing.T) { 11 | tests := map[string]struct { 12 | Expr *Expression 13 | Ctx *hcl.EvalContext 14 | Want cty.Value 15 | }{ 16 | "simple literal expr": { 17 | &Expression{ 18 | Source: []byte(`"hello"`), 19 | SourceType: ExprNative, 20 | }, 21 | nil, 22 | cty.StringVal("hello"), 23 | }, 24 | "simple literal template": { 25 | &Expression{ 26 | Source: []byte(`hello ${5}`), 27 | SourceType: ExprTemplate, 28 | }, 29 | nil, 30 | cty.StringVal("hello 5"), 31 | }, 32 | "expr with variable": { 33 | &Expression{ 34 | Source: []byte(`foo`), 35 | SourceType: ExprNative, 36 | }, 37 | &hcl.EvalContext{ 38 | Variables: map[string]cty.Value{ 39 | "foo": cty.StringVal("bar"), 40 | }, 41 | }, 42 | cty.StringVal("bar"), 43 | }, 44 | "template with variable": { 45 | &Expression{ 46 | Source: []byte(`foo ${foo}`), 47 | SourceType: ExprTemplate, 48 | }, 49 | &hcl.EvalContext{ 50 | Variables: map[string]cty.Value{ 51 | "foo": cty.StringVal("bar"), 52 | }, 53 | }, 54 | cty.StringVal("foo bar"), 55 | }, 56 | } 57 | 58 | for name, test := range tests { 59 | t.Run(name, func(t *testing.T) { 60 | got, diags := test.Expr.Value(test.Ctx) 61 | for _, diag := range diags { 62 | t.Errorf("unexpected diagnostic: %s", diag.Error()) 63 | } 64 | 65 | if !test.Want.RawEquals(got) { 66 | t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 67 | } 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /extras/grammar/HCLTemplate.yaml-tmLanguage: -------------------------------------------------------------------------------- 1 | name: HCL Template 2 | scopeName: source.hcltemplate 3 | fileTypes: [tmpl] 4 | uuid: ac6be18e-d44f-4a73-bd8f-b973fd26df05 5 | 6 | patterns: 7 | 8 | - comment: Interpolation Sequences 9 | name: meta.interp.hcltemplate 10 | begin: '[^\$]?(\$\{~?)' 11 | beginCaptures: 12 | '1': {name: entity.tag.embedded.start.hcltemplate} 13 | end: '~?}' 14 | endCaptures: 15 | '0': {name: entity.tag.embedded.end.hcltemplate} 16 | patterns: 17 | - include: "source.hclexpr" 18 | 19 | - comment: Control Sequences 20 | name: meta.control.hcltemplate 21 | begin: '[^\%]?(\%\{~?)' 22 | beginCaptures: 23 | '1': {name: entity.tag.embedded.start.hcltemplate} 24 | end: '~?}' 25 | endCaptures: 26 | '0': {name: entity.tag.embedded.end.hcltemplate} 27 | patterns: 28 | - include: "#templateif" 29 | - include: "#templatefor" 30 | - include: "#templatesimplekw" 31 | 32 | repository: 33 | 34 | templateif: 35 | name: meta.templateif.hcltemplate 36 | begin: '(if)\s*' 37 | beginCaptures: 38 | '1': {name: keyword.control.hcltemplate} 39 | end: '(?=~?\})' 40 | patterns: 41 | - include: "source.hclexpr" 42 | 43 | templatefor: 44 | name: meta.templatefor.hcltemplate 45 | begin: '(for)\s*(\w+)\s*(,\s*(\w+)\s*)?(in)' 46 | beginCaptures: 47 | '1': {name: keyword.control.hcltemplate} 48 | '2': {name: variable.other.hcl} 49 | '4': {name: variable.other.hcl} 50 | '5': {name: keyword.control.hcltemplate} 51 | end: '(?=~?\})' 52 | patterns: 53 | - include: "source.hclexpr" 54 | 55 | templatesimplekw: 56 | match: (else|endif|endfor) 57 | captures: 58 | '0': {name: keyword.control.hcl} 59 | -------------------------------------------------------------------------------- /cmd/hclspecsuite/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | "golang.org/x/crypto/ssh/terminal" 9 | 10 | "github.com/hashicorp/hcl2/hcl" 11 | "github.com/hashicorp/hcl2/hclparse" 12 | ) 13 | 14 | func main() { 15 | os.Exit(realMain(os.Args[1:])) 16 | } 17 | 18 | func realMain(args []string) int { 19 | if len(args) != 2 { 20 | fmt.Fprintf(os.Stderr, "Usage: hclspecsuite \n") 21 | return 2 22 | } 23 | 24 | testsDir := args[0] 25 | hcldecPath := args[1] 26 | 27 | hcldecPath, err := exec.LookPath(hcldecPath) 28 | if err != nil { 29 | fmt.Fprintf(os.Stderr, "%s\n", err) 30 | return 2 31 | } 32 | 33 | parser := hclparse.NewParser() 34 | 35 | color := terminal.IsTerminal(int(os.Stderr.Fd())) 36 | w, _, err := terminal.GetSize(int(os.Stdout.Fd())) 37 | if err != nil { 38 | w = 80 39 | } 40 | diagWr := hcl.NewDiagnosticTextWriter(os.Stderr, parser.Files(), uint(w), color) 41 | var diagCount int 42 | 43 | runner := &Runner{ 44 | parser: parser, 45 | hcldecPath: hcldecPath, 46 | baseDir: testsDir, 47 | logBegin: func(name string, file *TestFile) { 48 | fmt.Printf("- %s\n", name) 49 | }, 50 | logProblems: func(name string, file *TestFile, diags hcl.Diagnostics) { 51 | if len(diags) != 0 { 52 | os.Stderr.WriteString("\n") 53 | diagWr.WriteDiagnostics(diags) 54 | diagCount += len(diags) 55 | } 56 | fmt.Printf("- %s\n", name) 57 | }, 58 | } 59 | diags := runner.Run() 60 | 61 | if len(diags) != 0 { 62 | os.Stderr.WriteString("\n\n\n== Test harness problems:\n\n") 63 | diagWr.WriteDiagnostics(diags) 64 | diagCount += len(diags) 65 | } 66 | 67 | if diagCount > 0 { 68 | return 2 69 | } 70 | return 0 71 | } 72 | -------------------------------------------------------------------------------- /ext/dynblock/variables_hcldec.go: -------------------------------------------------------------------------------- 1 | package dynblock 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/hashicorp/hcl2/hcldec" 6 | ) 7 | 8 | // VariablesHCLDec is a wrapper around WalkVariables that uses the given hcldec 9 | // specification to automatically drive the recursive walk through nested 10 | // blocks in the given body. 11 | // 12 | // This is a drop-in replacement for hcldec.Variables which is able to treat 13 | // blocks of type "dynamic" in the same special way that dynblock.Expand would, 14 | // exposing both the variables referenced in the "for_each" and "labels" 15 | // arguments and variables used in the nested "content" block. 16 | func VariablesHCLDec(body hcl.Body, spec hcldec.Spec) []hcl.Traversal { 17 | rootNode := WalkVariables(body) 18 | return walkVariablesWithHCLDec(rootNode, spec) 19 | } 20 | 21 | // ExpandVariablesHCLDec is like VariablesHCLDec but it includes only the 22 | // minimal set of variables required to call Expand, ignoring variables that 23 | // are referenced only inside normal block contents. See WalkExpandVariables 24 | // for more information. 25 | func ExpandVariablesHCLDec(body hcl.Body, spec hcldec.Spec) []hcl.Traversal { 26 | rootNode := WalkExpandVariables(body) 27 | return walkVariablesWithHCLDec(rootNode, spec) 28 | } 29 | 30 | func walkVariablesWithHCLDec(node WalkVariablesNode, spec hcldec.Spec) []hcl.Traversal { 31 | vars, children := node.Visit(hcldec.ImpliedSchema(spec)) 32 | 33 | if len(children) > 0 { 34 | childSpecs := hcldec.ChildBlockTypes(spec) 35 | for _, child := range children { 36 | if childSpec, exists := childSpecs[child.BlockTypeName]; exists { 37 | vars = append(vars, walkVariablesWithHCLDec(child.Node, childSpec)...) 38 | } 39 | } 40 | } 41 | 42 | return vars 43 | } 44 | -------------------------------------------------------------------------------- /ext/include/file_resolver.go: -------------------------------------------------------------------------------- 1 | package include 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/hashicorp/hcl2/hcl" 8 | "github.com/hashicorp/hcl2/hclparse" 9 | ) 10 | 11 | // FileResolver creates and returns a Resolver that interprets include paths 12 | // as filesystem paths relative to the calling configuration file. 13 | // 14 | // When an include is requested, the source filename of the calling config 15 | // file is first interpreted relative to the given basePath, and then the 16 | // path given in configuration is interpreted relative to the resulting 17 | // absolute caller configuration directory. 18 | // 19 | // This resolver assumes that all calling bodies are loaded from local files 20 | // and that the paths to these files were correctly provided to the parser, 21 | // either absolute or relative to the given basePath. 22 | // 23 | // If the path given in configuration ends with ".json" then the referenced 24 | // file is interpreted as JSON. Otherwise, it is interpreted as HCL native 25 | // syntax. 26 | func FileResolver(baseDir string, parser *hclparse.Parser) Resolver { 27 | return &fileResolver{ 28 | BaseDir: baseDir, 29 | Parser: parser, 30 | } 31 | } 32 | 33 | type fileResolver struct { 34 | BaseDir string 35 | Parser *hclparse.Parser 36 | } 37 | 38 | func (r fileResolver) ResolveBodyPath(path string, refRange hcl.Range) (hcl.Body, hcl.Diagnostics) { 39 | callerFile := filepath.Join(r.BaseDir, refRange.Filename) 40 | callerDir := filepath.Dir(callerFile) 41 | targetFile := filepath.Join(callerDir, path) 42 | 43 | var f *hcl.File 44 | var diags hcl.Diagnostics 45 | if strings.HasSuffix(targetFile, ".json") { 46 | f, diags = r.Parser.ParseJSONFile(targetFile) 47 | } else { 48 | f, diags = r.Parser.ParseHCLFile(targetFile) 49 | } 50 | 51 | return f.Body, diags 52 | } 53 | -------------------------------------------------------------------------------- /hcl/hclsyntax/expression_vars.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | // Generated by expression_vars_get.go. DO NOT EDIT. 4 | // Run 'go generate' on this package to update the set of functions here. 5 | 6 | import ( 7 | "github.com/hashicorp/hcl2/hcl" 8 | ) 9 | 10 | func (e *AnonSymbolExpr) Variables() []hcl.Traversal { 11 | return Variables(e) 12 | } 13 | 14 | func (e *BinaryOpExpr) Variables() []hcl.Traversal { 15 | return Variables(e) 16 | } 17 | 18 | func (e *ConditionalExpr) Variables() []hcl.Traversal { 19 | return Variables(e) 20 | } 21 | 22 | func (e *ForExpr) Variables() []hcl.Traversal { 23 | return Variables(e) 24 | } 25 | 26 | func (e *FunctionCallExpr) Variables() []hcl.Traversal { 27 | return Variables(e) 28 | } 29 | 30 | func (e *IndexExpr) Variables() []hcl.Traversal { 31 | return Variables(e) 32 | } 33 | 34 | func (e *LiteralValueExpr) Variables() []hcl.Traversal { 35 | return Variables(e) 36 | } 37 | 38 | func (e *ObjectConsExpr) Variables() []hcl.Traversal { 39 | return Variables(e) 40 | } 41 | 42 | func (e *ObjectConsKeyExpr) Variables() []hcl.Traversal { 43 | return Variables(e) 44 | } 45 | 46 | func (e *RelativeTraversalExpr) Variables() []hcl.Traversal { 47 | return Variables(e) 48 | } 49 | 50 | func (e *ScopeTraversalExpr) Variables() []hcl.Traversal { 51 | return Variables(e) 52 | } 53 | 54 | func (e *SplatExpr) Variables() []hcl.Traversal { 55 | return Variables(e) 56 | } 57 | 58 | func (e *TemplateExpr) Variables() []hcl.Traversal { 59 | return Variables(e) 60 | } 61 | 62 | func (e *TemplateJoinExpr) Variables() []hcl.Traversal { 63 | return Variables(e) 64 | } 65 | 66 | func (e *TemplateWrapExpr) Variables() []hcl.Traversal { 67 | return Variables(e) 68 | } 69 | 70 | func (e *TupleConsExpr) Variables() []hcl.Traversal { 71 | return Variables(e) 72 | } 73 | 74 | func (e *UnaryOpExpr) Variables() []hcl.Traversal { 75 | return Variables(e) 76 | } 77 | -------------------------------------------------------------------------------- /hclpack/pack_native.go: -------------------------------------------------------------------------------- 1 | package hclpack 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/hashicorp/hcl2/hcl/hclsyntax" 6 | ) 7 | 8 | // PackNativeFile parses the given source code as HCL native syntax and packs 9 | // it into a hclpack Body ready to be marshalled. 10 | // 11 | // If the given source code contains syntax errors then error diagnostics will 12 | // be returned. A non-nil body might still be returned in this case, which 13 | // allows a cautious caller to still do certain analyses on the result. 14 | func PackNativeFile(src []byte, filename string, start hcl.Pos) (*Body, hcl.Diagnostics) { 15 | f, diags := hclsyntax.ParseConfig(src, filename, start) 16 | rootBody := f.Body.(*hclsyntax.Body) 17 | return packNativeBody(rootBody, src), diags 18 | } 19 | 20 | func packNativeBody(body *hclsyntax.Body, src []byte) *Body { 21 | ret := &Body{} 22 | for name, attr := range body.Attributes { 23 | exprRng := attr.Expr.Range() 24 | exprStartRng := attr.Expr.StartRange() 25 | exprSrc := exprRng.SliceBytes(src) 26 | ret.setAttribute(name, Attribute{ 27 | Expr: Expression{ 28 | Source: exprSrc, 29 | SourceType: ExprNative, 30 | 31 | Range_: exprRng, 32 | StartRange_: exprStartRng, 33 | }, 34 | Range: attr.Range(), 35 | NameRange: attr.NameRange, 36 | }) 37 | } 38 | 39 | for _, block := range body.Blocks { 40 | childBody := packNativeBody(block.Body, src) 41 | defRange := block.TypeRange 42 | if len(block.LabelRanges) > 0 { 43 | defRange = hcl.RangeBetween(defRange, block.LabelRanges[len(block.LabelRanges)-1]) 44 | } 45 | ret.appendBlock(Block{ 46 | Type: block.Type, 47 | Labels: block.Labels, 48 | Body: *childBody, 49 | TypeRange: block.TypeRange, 50 | DefRange: defRange, 51 | LabelRanges: block.LabelRanges, 52 | }) 53 | } 54 | 55 | ret.MissingItemRange_ = body.EndRange 56 | 57 | return ret 58 | } 59 | -------------------------------------------------------------------------------- /cmd/hcldec/vars.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/hashicorp/hcl2/hcl" 8 | "github.com/zclconf/go-cty/cty" 9 | ) 10 | 11 | func parseVarsArg(src string, argIdx int) (map[string]cty.Value, hcl.Diagnostics) { 12 | fakeFn := fmt.Sprintf("", argIdx) 13 | f, diags := parser.ParseJSON([]byte(src), fakeFn) 14 | if f == nil { 15 | return nil, diags 16 | } 17 | vals, valsDiags := parseVarsBody(f.Body) 18 | diags = append(diags, valsDiags...) 19 | return vals, diags 20 | } 21 | 22 | func parseVarsFile(filename string) (map[string]cty.Value, hcl.Diagnostics) { 23 | var f *hcl.File 24 | var diags hcl.Diagnostics 25 | 26 | if strings.HasSuffix(filename, ".json") { 27 | f, diags = parser.ParseJSONFile(filename) 28 | } else { 29 | f, diags = parser.ParseHCLFile(filename) 30 | } 31 | 32 | if f == nil { 33 | return nil, diags 34 | } 35 | 36 | vals, valsDiags := parseVarsBody(f.Body) 37 | diags = append(diags, valsDiags...) 38 | return vals, diags 39 | 40 | } 41 | 42 | func parseVarsBody(body hcl.Body) (map[string]cty.Value, hcl.Diagnostics) { 43 | attrs, diags := body.JustAttributes() 44 | if attrs == nil { 45 | return nil, diags 46 | } 47 | 48 | vals := make(map[string]cty.Value, len(attrs)) 49 | for name, attr := range attrs { 50 | val, valDiags := attr.Expr.Value(nil) 51 | diags = append(diags, valDiags...) 52 | vals[name] = val 53 | } 54 | return vals, diags 55 | } 56 | 57 | // varSpecs is an implementation of pflag.Value that accumulates a list of 58 | // raw values, ignoring any quoting. This is similar to pflag.StringSlice 59 | // but does not complain if there are literal quotes inside the value, which 60 | // is important for us to accept JSON literals here. 61 | type varSpecs []string 62 | 63 | func (vs *varSpecs) String() string { 64 | return strings.Join([]string(*vs), ", ") 65 | } 66 | 67 | func (vs *varSpecs) Set(new string) error { 68 | *vs = append(*vs, new) 69 | return nil 70 | } 71 | 72 | func (vs *varSpecs) Type() string { 73 | return "json-or-file" 74 | } 75 | -------------------------------------------------------------------------------- /hclwrite/ast_block.go: -------------------------------------------------------------------------------- 1 | package hclwrite 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl/hclsyntax" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | type Block struct { 9 | inTree 10 | 11 | leadComments *node 12 | typeName *node 13 | labels nodeSet 14 | open *node 15 | body *node 16 | close *node 17 | } 18 | 19 | func newBlock() *Block { 20 | return &Block{ 21 | inTree: newInTree(), 22 | labels: newNodeSet(), 23 | } 24 | } 25 | 26 | // NewBlock constructs a new, empty block with the given type name and labels. 27 | func NewBlock(typeName string, labels []string) *Block { 28 | block := newBlock() 29 | block.init(typeName, labels) 30 | return block 31 | } 32 | 33 | func (b *Block) init(typeName string, labels []string) { 34 | nameTok := newIdentToken(typeName) 35 | nameObj := newIdentifier(nameTok) 36 | b.leadComments = b.children.Append(newComments(nil)) 37 | b.typeName = b.children.Append(nameObj) 38 | for _, label := range labels { 39 | labelToks := TokensForValue(cty.StringVal(label)) 40 | labelObj := newQuoted(labelToks) 41 | labelNode := b.children.Append(labelObj) 42 | b.labels.Add(labelNode) 43 | } 44 | b.open = b.children.AppendUnstructuredTokens(Tokens{ 45 | { 46 | Type: hclsyntax.TokenOBrace, 47 | Bytes: []byte{'{'}, 48 | }, 49 | { 50 | Type: hclsyntax.TokenNewline, 51 | Bytes: []byte{'\n'}, 52 | }, 53 | }) 54 | body := newBody() // initially totally empty; caller can append to it subsequently 55 | b.body = b.children.Append(body) 56 | b.close = b.children.AppendUnstructuredTokens(Tokens{ 57 | { 58 | Type: hclsyntax.TokenCBrace, 59 | Bytes: []byte{'}'}, 60 | }, 61 | { 62 | Type: hclsyntax.TokenNewline, 63 | Bytes: []byte{'\n'}, 64 | }, 65 | }) 66 | } 67 | 68 | // Body returns the body that represents the content of the receiving block. 69 | // 70 | // Appending to or otherwise modifying this body will make changes to the 71 | // tokens that are generated between the blocks open and close braces. 72 | func (b *Block) Body() *Body { 73 | return b.body.content.(*Body) 74 | } 75 | -------------------------------------------------------------------------------- /ext/userfunc/public.go: -------------------------------------------------------------------------------- 1 | package userfunc 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/zclconf/go-cty/cty/function" 6 | ) 7 | 8 | // A ContextFunc is a callback used to produce the base EvalContext for 9 | // running a particular set of functions. 10 | // 11 | // This is a function rather than an EvalContext directly to allow functions 12 | // to be decoded before their context is complete. This will be true, for 13 | // example, for applications that wish to allow functions to refer to themselves. 14 | // 15 | // The simplest use of a ContextFunc is to give user functions access to the 16 | // same global variables and functions available elsewhere in an application's 17 | // configuration language, but more complex applications may use different 18 | // contexts to support lexical scoping depending on where in a configuration 19 | // structure a function declaration is found, etc. 20 | type ContextFunc func() *hcl.EvalContext 21 | 22 | // DecodeUserFunctions looks for blocks of the given type in the given body 23 | // and, for each one found, interprets it as a custom function definition. 24 | // 25 | // On success, the result is a mapping of function names to implementations, 26 | // along with a new body that represents the remaining content of the given 27 | // body which can be used for further processing. 28 | // 29 | // The result expression of each function is parsed during decoding but not 30 | // evaluated until the function is called. 31 | // 32 | // If the given ContextFunc is non-nil, it will be called to obtain the 33 | // context in which the function result expressions will be evaluated. If nil, 34 | // or if it returns nil, the result expression will have access only to 35 | // variables named after the declared parameters. A non-nil context turns 36 | // the returned functions into closures, bound to the given context. 37 | // 38 | // If the returned diagnostics set has errors then the function map and 39 | // remain body may be nil or incomplete. 40 | func DecodeUserFunctions(body hcl.Body, blockType string, context ContextFunc) (funcs map[string]function.Function, remain hcl.Body, diags hcl.Diagnostics) { 41 | return decodeUserFunctions(body, blockType, context) 42 | } 43 | -------------------------------------------------------------------------------- /hcl/json/fuzz/README.md: -------------------------------------------------------------------------------- 1 | # JSON syntax fuzzing utilities 2 | 3 | This directory contains helper functions and corpuses that can be used to 4 | fuzz-test the HCL JSON parser using [go-fuzz](https://github.com/dvyukov/go-fuzz). 5 | 6 | To fuzz, first install go-fuzz and its build tool in your `GOPATH`: 7 | 8 | ``` 9 | $ make tools 10 | ``` 11 | 12 | Now you can fuzz one or all of the parsers: 13 | 14 | ``` 15 | $ make fuzz-config FUZZ_WORK_DIR=/tmp/hcl2-fuzz-config 16 | ``` 17 | 18 | In all cases, set `FUZZ_WORK_DIR` to a directory where `go-fuzz` can keep state 19 | as it works. This should ideally be in a ramdisk for efficiency, and should 20 | probably _not_ be on an SSD to avoid thrashing it. 21 | 22 | ## Understanding the result 23 | 24 | A small number of subdirectories will be created in the work directory. 25 | 26 | If you let `go-fuzz` run for a few minutes (the more minutes the better) it 27 | may detect "crashers", which are inputs that caused the parser to panic. Details 28 | about these are written to `$FUZZ_WORK_DIR/crashers`: 29 | 30 | ``` 31 | $ ls /tmp/hcl2-fuzz-config/crashers 32 | 7f5e9ec80c89da14b8b0b238ec88969f658f5a2d 33 | 7f5e9ec80c89da14b8b0b238ec88969f658f5a2d.output 34 | 7f5e9ec80c89da14b8b0b238ec88969f658f5a2d.quoted 35 | ``` 36 | 37 | The base file above (with no extension) is the input that caused a crash. The 38 | `.output` file contains the panic stack trace, which you can use as a clue to 39 | figure out what caused the crash. 40 | 41 | A good first step to fixing a detected crasher is to copy the failing input 42 | into one of the unit tests in the `hcl/json` package and see it crash there 43 | too. After that, it's easy to re-run the test as you try to fix it. The 44 | file with the `.quoted` extension contains a form of the input that is quoted 45 | in Go syntax for easy copy-paste into a test case, even if the input contains 46 | non-printable characters or other inconvenient symbols. 47 | 48 | ## Rebuilding for new Upstream Code 49 | 50 | An archive file is created for `go-fuzz` to use on the first run of each 51 | of the above, as a `.zip` file created in this directory. If upstream code 52 | is changed these will need to be deleted to cause them to be rebuilt with 53 | the latest code: 54 | 55 | ``` 56 | $ make clean 57 | ``` 58 | -------------------------------------------------------------------------------- /extras/grammar/HCL.json-tmLanguage: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [ 3 | "hcl", 4 | "hcldec" 5 | ], 6 | "name": "HCL", 7 | "patterns": [ 8 | { 9 | "begin": "#|//", 10 | "captures": { 11 | "0": { 12 | "name": "punctuation.definition.comment.hcl" 13 | } 14 | }, 15 | "comment": "Comments", 16 | "end": "$\\n?", 17 | "name": "comment.line.hcl" 18 | }, 19 | { 20 | "begin": "/\\*", 21 | "captures": { 22 | "0": { 23 | "name": "punctuation.definition.comment.hcl" 24 | } 25 | }, 26 | "comment": "Block comments", 27 | "end": "\\*/", 28 | "name": "comment.block.hcl" 29 | }, 30 | { 31 | "begin": "{", 32 | "beginCaptures": { 33 | "0": { 34 | "name": "punctuation.definition.block.hcl" 35 | } 36 | }, 37 | "comment": "Nested Blocks", 38 | "end": "}", 39 | "endCaptures": { 40 | "0": { 41 | "name": "punctuation.definition.block.hcl" 42 | } 43 | }, 44 | "name": "meta.block.hcl", 45 | "patterns": [ 46 | { 47 | "include": "$self" 48 | } 49 | ] 50 | }, 51 | { 52 | "captures": { 53 | "1": { 54 | "name": "string.hcl punctuation.definition.string.begin.hcl" 55 | }, 56 | "2": { 57 | "name": "string.value.hcl" 58 | }, 59 | "3": { 60 | "name": "string.hcl punctuation.definition.string.end.hcl" 61 | } 62 | }, 63 | "comment": "Quoted Block Labels", 64 | "match": "(\")([^\"]+)(\")" 65 | }, 66 | { 67 | "begin": "(\\w+)\\s*(=)\\s*", 68 | "beginCaptures": { 69 | "1": { 70 | "name": "variable.other.assignment.hcl" 71 | }, 72 | "2": { 73 | "name": "keyword.operator.hcl" 74 | } 75 | }, 76 | "comment": "Attribute Definitions", 77 | "end": "$", 78 | "name": "meta.attr.hcl", 79 | "patterns": [ 80 | { 81 | "include": "source.hclexpr" 82 | } 83 | ] 84 | }, 85 | { 86 | "captures": { 87 | "0": { 88 | "name": "keyword.other.hcl" 89 | } 90 | }, 91 | "comment": "Keywords", 92 | "match": "[-\\w]+" 93 | } 94 | ], 95 | "scopeName": "source.hcl", 96 | "uuid": "55e8075d-e2e3-4e44-8446-744a9860e476" 97 | } -------------------------------------------------------------------------------- /hcl/hclsyntax/fuzz/README.md: -------------------------------------------------------------------------------- 1 | # hclsyntax fuzzing utilities 2 | 3 | This directory contains helper functions and corpuses that can be used to 4 | fuzz-test the `hclsyntax` parsers using [go-fuzz](https://github.com/dvyukov/go-fuzz). 5 | 6 | To fuzz, first install go-fuzz and its build tool in your `GOPATH`: 7 | 8 | ``` 9 | $ make tools 10 | ``` 11 | 12 | Now you can fuzz one or all of the parsers: 13 | 14 | ``` 15 | $ make fuzz-config FUZZ_WORK_DIR=/tmp/hcl2-fuzz-config 16 | $ make fuzz-expr FUZZ_WORK_DIR=/tmp/hcl2-fuzz-expr 17 | $ make fuzz-template FUZZ_WORK_DIR=/tmp/hcl2-fuzz-template 18 | $ make fuzz-traversal FUZZ_WORK_DIR=/tmp/hcl2-fuzz-traversal 19 | ``` 20 | 21 | In all cases, set `FUZZ_WORK_DIR` to a directory where `go-fuzz` can keep state 22 | as it works. This should ideally be in a ramdisk for efficiency, and should 23 | probably _not_ be on an SSD to avoid thrashing it. 24 | 25 | ## Understanding the result 26 | 27 | A small number of subdirectories will be created in the work directory. 28 | 29 | If you let `go-fuzz` run for a few minutes (the more minutes the better) it 30 | may detect "crashers", which are inputs that caused the parser to panic. Details 31 | about these are written to `$FUZZ_WORK_DIR/crashers`: 32 | 33 | ``` 34 | $ ls /tmp/hcl2-fuzz-config/crashers 35 | 7f5e9ec80c89da14b8b0b238ec88969f658f5a2d 36 | 7f5e9ec80c89da14b8b0b238ec88969f658f5a2d.output 37 | 7f5e9ec80c89da14b8b0b238ec88969f658f5a2d.quoted 38 | ``` 39 | 40 | The base file above (with no extension) is the input that caused a crash. The 41 | `.output` file contains the panic stack trace, which you can use as a clue to 42 | figure out what caused the crash. 43 | 44 | A good first step to fixing a detected crasher is to copy the failing input 45 | into one of the unit tests in the `hclsyntax` package and see it crash there 46 | too. After that, it's easy to re-run the test as you try to fix it. The 47 | file with the `.quoted` extension contains a form of the input that is quoted 48 | in Go syntax for easy copy-paste into a test case, even if the input contains 49 | non-printable characters or other inconvenient symbols. 50 | 51 | ## Rebuilding for new Upstream Code 52 | 53 | An archive file is created for `go-fuzz` to use on the first run of each 54 | of the above, as a `.zip` file created in this directory. If upstream code 55 | is changed these will need to be deleted to cause them to be rebuilt with 56 | the latest code: 57 | 58 | ``` 59 | $ make clean 60 | ``` 61 | -------------------------------------------------------------------------------- /ext/typeexpr/type_string_test.go: -------------------------------------------------------------------------------- 1 | package typeexpr 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | ) 8 | 9 | func TestTypeString(t *testing.T) { 10 | tests := []struct { 11 | Type cty.Type 12 | Want string 13 | }{ 14 | { 15 | cty.DynamicPseudoType, 16 | "any", 17 | }, 18 | { 19 | cty.String, 20 | "string", 21 | }, 22 | { 23 | cty.Number, 24 | "number", 25 | }, 26 | { 27 | cty.Bool, 28 | "bool", 29 | }, 30 | { 31 | cty.List(cty.Number), 32 | "list(number)", 33 | }, 34 | { 35 | cty.Set(cty.Bool), 36 | "set(bool)", 37 | }, 38 | { 39 | cty.Map(cty.String), 40 | "map(string)", 41 | }, 42 | { 43 | cty.EmptyObject, 44 | "object({})", 45 | }, 46 | { 47 | cty.Object(map[string]cty.Type{"foo": cty.Bool}), 48 | "object({foo=bool})", 49 | }, 50 | { 51 | cty.Object(map[string]cty.Type{"foo": cty.Bool, "bar": cty.String}), 52 | "object({bar=string,foo=bool})", 53 | }, 54 | { 55 | cty.EmptyTuple, 56 | "tuple([])", 57 | }, 58 | { 59 | cty.Tuple([]cty.Type{cty.Bool}), 60 | "tuple([bool])", 61 | }, 62 | { 63 | cty.Tuple([]cty.Type{cty.Bool, cty.String}), 64 | "tuple([bool,string])", 65 | }, 66 | { 67 | cty.List(cty.DynamicPseudoType), 68 | "list(any)", 69 | }, 70 | { 71 | cty.Tuple([]cty.Type{cty.DynamicPseudoType}), 72 | "tuple([any])", 73 | }, 74 | { 75 | cty.Object(map[string]cty.Type{"foo": cty.DynamicPseudoType}), 76 | "object({foo=any})", 77 | }, 78 | { 79 | // We don't expect to find attributes that aren't valid identifiers 80 | // because we only promise to support types that this package 81 | // would've created, but we allow this situation during rendering 82 | // just because it's convenient for applications trying to produce 83 | // error messages about mismatched types. Note that the quoted 84 | // attribute name is not actually accepted by our Type and 85 | // TypeConstraint functions, so this is one situation where the 86 | // TypeString result cannot be re-parsed by those functions. 87 | cty.Object(map[string]cty.Type{"foo bar baz": cty.String}), 88 | `object({"foo bar baz"=string})`, 89 | }, 90 | } 91 | 92 | for _, test := range tests { 93 | t.Run(test.Type.GoString(), func(t *testing.T) { 94 | got := TypeString(test.Type) 95 | if got != test.Want { 96 | t.Errorf("wrong result\ntype: %#v\ngot: %s\nwant: %s", test.Type, got, test.Want) 97 | } 98 | }) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /hclwrite/examples_test.go: -------------------------------------------------------------------------------- 1 | package hclwrite_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/hcl2/hcl" 7 | "github.com/hashicorp/hcl2/hclwrite" 8 | "github.com/zclconf/go-cty/cty" 9 | ) 10 | 11 | func Example_generateFromScratch() { 12 | f := hclwrite.NewEmptyFile() 13 | rootBody := f.Body() 14 | rootBody.SetAttributeValue("string", cty.StringVal("bar")) // this is overwritten later 15 | rootBody.AppendNewline() 16 | rootBody.SetAttributeValue("object", cty.ObjectVal(map[string]cty.Value{ 17 | "foo": cty.StringVal("foo"), 18 | "bar": cty.NumberIntVal(5), 19 | "baz": cty.True, 20 | })) 21 | rootBody.SetAttributeValue("string", cty.StringVal("foo")) 22 | rootBody.SetAttributeValue("bool", cty.False) 23 | rootBody.SetAttributeTraversal("path", hcl.Traversal{ 24 | hcl.TraverseRoot{ 25 | Name: "env", 26 | }, 27 | hcl.TraverseAttr{ 28 | Name: "PATH", 29 | }, 30 | }) 31 | rootBody.AppendNewline() 32 | fooBlock := rootBody.AppendNewBlock("foo", nil) 33 | fooBody := fooBlock.Body() 34 | rootBody.AppendNewBlock("empty", nil) 35 | rootBody.AppendNewline() 36 | barBlock := rootBody.AppendNewBlock("bar", []string{"a", "b"}) 37 | barBody := barBlock.Body() 38 | 39 | fooBody.SetAttributeValue("hello", cty.StringVal("world")) 40 | 41 | bazBlock := barBody.AppendNewBlock("baz", nil) 42 | bazBody := bazBlock.Body() 43 | bazBody.SetAttributeValue("foo", cty.NumberIntVal(10)) 44 | bazBody.SetAttributeValue("beep", cty.StringVal("boop")) 45 | bazBody.SetAttributeValue("baz", cty.ListValEmpty(cty.String)) 46 | 47 | fmt.Printf("%s", f.Bytes()) 48 | // Output: 49 | // string = "foo" 50 | // 51 | // object = { bar = 5, baz = true, foo = "foo" } 52 | // bool = false 53 | // path = env.PATH 54 | // 55 | // foo { 56 | // hello = "world" 57 | // } 58 | // empty { 59 | // } 60 | // 61 | // bar "a" "b" { 62 | // baz { 63 | // foo = 10 64 | // beep = "boop" 65 | // baz = [] 66 | // } 67 | // } 68 | } 69 | 70 | func ExampleExpression_RenameVariablePrefix() { 71 | src := []byte( 72 | "foo = a.x + a.y * b.c\n" + 73 | "bar = max(a.z, b.c)\n", 74 | ) 75 | f, diags := hclwrite.ParseConfig(src, "", hcl.Pos{Line: 1, Column: 1}) 76 | if diags.HasErrors() { 77 | fmt.Printf("errors: %s", diags) 78 | return 79 | } 80 | 81 | // Rename references of variable "a" to "z" 82 | for _, attr := range f.Body().Attributes() { 83 | attr.Expr().RenameVariablePrefix( 84 | []string{"a"}, 85 | []string{"z"}, 86 | ) 87 | } 88 | 89 | fmt.Printf("%s", f.Bytes()) 90 | // Output: 91 | // foo = z.x + z.y * b.c 92 | // bar = max(z.z, b.c) 93 | } 94 | -------------------------------------------------------------------------------- /hcl/spectests/spec_test.go: -------------------------------------------------------------------------------- 1 | package spectests 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestMain(m *testing.M) { 16 | // The test harness is an external program that also expects to have 17 | // hcldec built as an external program, so we'll build both into 18 | // temporary files in our working directory before running our tests 19 | // here, to ensure that we're always running a build of the latest code. 20 | err := build() 21 | if err != nil { 22 | fmt.Fprintf(os.Stderr, "%s\n", err.Error()) 23 | os.Exit(1) 24 | } 25 | 26 | // Now we can run the tests 27 | os.Exit(m.Run()) 28 | } 29 | 30 | func build() error { 31 | err := goBuild("github.com/hashicorp/hcl2/cmd/hcldec", "tmp_hcldec") 32 | if err != nil { 33 | return fmt.Errorf("error building hcldec: %s", err) 34 | } 35 | 36 | err = goBuild("github.com/hashicorp/hcl2/cmd/hclspecsuite", "tmp_hclspecsuite") 37 | if err != nil { 38 | return fmt.Errorf("error building hcldec: %s", err) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func TestSpec(t *testing.T) { 45 | suiteDir := filepath.Clean("../../specsuite/tests") 46 | harness := "./tmp_hclspecsuite" 47 | hcldec := "./tmp_hcldec" 48 | 49 | cmd := exec.Command(harness, suiteDir, hcldec) 50 | out, err := cmd.CombinedOutput() 51 | if _, isExit := err.(*exec.ExitError); err != nil && !isExit { 52 | t.Errorf("failed to run harness: %s", err) 53 | } 54 | failed := err != nil 55 | 56 | sc := bufio.NewScanner(bytes.NewReader(out)) 57 | var lines []string 58 | for sc.Scan() { 59 | lines = append(lines, sc.Text()) 60 | } 61 | 62 | i := 0 63 | for i < len(lines) { 64 | cur := lines[i] 65 | if strings.HasPrefix(cur, "- ") { 66 | testName := cur[2:] 67 | t.Run(testName, func(t *testing.T) { 68 | i++ 69 | for i < len(lines) { 70 | cur := lines[i] 71 | if strings.HasPrefix(cur, "- ") || strings.HasPrefix(cur, "==") { 72 | return 73 | } 74 | t.Error(cur) 75 | i++ 76 | } 77 | }) 78 | } else { 79 | if !strings.HasPrefix(cur, "==") { // not the "test harness problems" report, then 80 | t.Log(cur) 81 | } 82 | i++ 83 | } 84 | } 85 | 86 | if failed { 87 | t.Error("specsuite failed") 88 | } 89 | } 90 | 91 | func goBuild(pkg, outFile string) error { 92 | if runtime.GOOS == "windows" { 93 | outFile += ".exe" 94 | } 95 | 96 | cmd := exec.Command("go", "build", "-i", "-o", outFile, pkg) 97 | cmd.Stderr = os.Stderr 98 | cmd.Stdout = os.Stdout 99 | return cmd.Run() 100 | } 101 | -------------------------------------------------------------------------------- /guide/go_parsing.rst: -------------------------------------------------------------------------------- 1 | .. _go-parsing: 2 | 3 | Parsing HCL Input 4 | ================= 5 | 6 | The first step in processing HCL input provided by a user is to parse it. 7 | Parsing turns the raw bytes from an input file into a higher-level 8 | representation of the arguments and blocks, ready to be *decoded* into an 9 | application-specific form. 10 | 11 | The main entry point into HCL parsing is :go:pkg:`hclparse`, which provides 12 | :go:type:`hclparse.Parser`: 13 | 14 | .. code-block:: go 15 | 16 | parser := hclparse.NewParser() 17 | f, diags := parser.ParseHCLFile("server.conf") 18 | 19 | Variable ``f`` is then a pointer to an :go:type:`hcl.File`, which is an 20 | opaque abstract representation of the file, ready to be decoded. 21 | 22 | Variable ``diags`` describes any errors or warnings that were encountered 23 | during processing; HCL conventionally uses this in place of the usual ``error`` 24 | return value in Go, to allow returning a mixture of multiple errors and 25 | warnings together with enough information to present good error messages to the 26 | user. We'll cover this in more detail in the next section, 27 | :ref:`go-diagnostics`. 28 | 29 | .. go:package:: hclparse 30 | 31 | Package ``hclparse`` 32 | -------------------- 33 | 34 | .. go:type:: Parser 35 | 36 | .. go:function:: func NewParser() *Parser 37 | 38 | Constructs a new parser object. Each parser contains a cache of files 39 | that have already been read, so repeated calls to load the same file 40 | will return the same object. 41 | 42 | .. go:function:: func (*Parser) ParseHCL(src []byte, filename string) (*hcl.File, hcl.Diagnostics) 43 | 44 | Parse the given source code as HCL native syntax, saving the result into 45 | the parser's file cache under the given filename. 46 | 47 | .. go:function:: func (*Parser) ParseHCLFile(filename string) (*hcl.File, hcl.Diagnostics) 48 | 49 | Parse the contents of the given file as HCL native syntax. This is a 50 | convenience wrapper around ParseHCL that first reads the file into memory. 51 | 52 | .. go:function:: func (*Parser) ParseJSON(src []byte, filename string) (*hcl.File, hcl.Diagnostics) 53 | 54 | Parse the given source code as JSON syntax, saving the result into 55 | the parser's file cache under the given filename. 56 | 57 | .. go:function:: func (*Parser) ParseJSONFile(filename string) (*hcl.File, hcl.Diagnostics) 58 | 59 | Parse the contents of the given file as JSON syntax. This is a 60 | convenience wrapper around ParseJSON that first reads the file into memory. 61 | 62 | The above list just highlights the main functions in this package. 63 | For full documentation, see 64 | `the hclparse godoc `_. 65 | -------------------------------------------------------------------------------- /cmd/hcldec/diags_json.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "github.com/hashicorp/hcl2/hcl" 8 | ) 9 | 10 | type jsonDiagWriter struct { 11 | w io.Writer 12 | diags hcl.Diagnostics 13 | } 14 | 15 | var _ hcl.DiagnosticWriter = &jsonDiagWriter{} 16 | 17 | func (wr *jsonDiagWriter) WriteDiagnostic(diag *hcl.Diagnostic) error { 18 | wr.diags = append(wr.diags, diag) 19 | return nil 20 | } 21 | 22 | func (wr *jsonDiagWriter) WriteDiagnostics(diags hcl.Diagnostics) error { 23 | wr.diags = append(wr.diags, diags...) 24 | return nil 25 | } 26 | 27 | func (wr *jsonDiagWriter) Flush() error { 28 | if len(wr.diags) == 0 { 29 | return nil 30 | } 31 | 32 | type PosJSON struct { 33 | Line int `json:"line"` 34 | Column int `json:"column"` 35 | Byte int `json:"byte"` 36 | } 37 | type RangeJSON struct { 38 | Filename string `json:"filename"` 39 | Start PosJSON `json:"start"` 40 | End PosJSON `json:"end"` 41 | } 42 | type DiagnosticJSON struct { 43 | Severity string `json:"severity"` 44 | Summary string `json:"summary"` 45 | Detail string `json:"detail,omitempty"` 46 | Subject *RangeJSON `json:"subject,omitempty"` 47 | } 48 | type DiagnosticsJSON struct { 49 | Diagnostics []DiagnosticJSON `json:"diagnostics"` 50 | } 51 | 52 | diagsJSON := make([]DiagnosticJSON, 0, len(wr.diags)) 53 | for _, diag := range wr.diags { 54 | var diagJSON DiagnosticJSON 55 | 56 | switch diag.Severity { 57 | case hcl.DiagError: 58 | diagJSON.Severity = "error" 59 | case hcl.DiagWarning: 60 | diagJSON.Severity = "warning" 61 | default: 62 | diagJSON.Severity = "(unknown)" // should never happen 63 | } 64 | 65 | diagJSON.Summary = diag.Summary 66 | diagJSON.Detail = diag.Detail 67 | if diag.Subject != nil { 68 | diagJSON.Subject = &RangeJSON{} 69 | sJSON := diagJSON.Subject 70 | rng := diag.Subject 71 | sJSON.Filename = rng.Filename 72 | sJSON.Start.Line = rng.Start.Line 73 | sJSON.Start.Column = rng.Start.Column 74 | sJSON.Start.Byte = rng.Start.Byte 75 | sJSON.End.Line = rng.End.Line 76 | sJSON.End.Column = rng.End.Column 77 | sJSON.End.Byte = rng.End.Byte 78 | } 79 | 80 | diagsJSON = append(diagsJSON, diagJSON) 81 | } 82 | 83 | src, err := json.MarshalIndent(DiagnosticsJSON{diagsJSON}, "", " ") 84 | if err != nil { 85 | return err 86 | } 87 | _, err = wr.w.Write(src) 88 | wr.w.Write([]byte{'\n'}) 89 | return err 90 | } 91 | 92 | type flusher interface { 93 | Flush() error 94 | } 95 | 96 | func flush(maybeFlusher interface{}) error { 97 | if f, ok := maybeFlusher.(flusher); ok { 98 | return f.Flush() 99 | } 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /hcl/expr_unwrap.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | 3 | type unwrapExpression interface { 4 | UnwrapExpression() Expression 5 | } 6 | 7 | // UnwrapExpression removes any "wrapper" expressions from the given expression, 8 | // to recover the representation of the physical expression given in source 9 | // code. 10 | // 11 | // Sometimes wrapping expressions are used to modify expression behavior, e.g. 12 | // in extensions that need to make some local variables available to certain 13 | // sub-trees of the configuration. This can make it difficult to reliably 14 | // type-assert on the physical AST types used by the underlying syntax. 15 | // 16 | // Unwrapping an expression may modify its behavior by stripping away any 17 | // additional constraints or capabilities being applied to the Value and 18 | // Variables methods, so this function should generally only be used prior 19 | // to operations that concern themselves with the static syntax of the input 20 | // configuration, and not with the effective value of the expression. 21 | // 22 | // Wrapper expression types must support unwrapping by implementing a method 23 | // called UnwrapExpression that takes no arguments and returns the embedded 24 | // Expression. Implementations of this method should peel away only one level 25 | // of wrapping, if multiple are present. This method may return nil to 26 | // indicate _dynamically_ that no wrapped expression is available, for 27 | // expression types that might only behave as wrappers in certain cases. 28 | func UnwrapExpression(expr Expression) Expression { 29 | for { 30 | unwrap, wrapped := expr.(unwrapExpression) 31 | if !wrapped { 32 | return expr 33 | } 34 | innerExpr := unwrap.UnwrapExpression() 35 | if innerExpr == nil { 36 | return expr 37 | } 38 | expr = innerExpr 39 | } 40 | } 41 | 42 | // UnwrapExpressionUntil is similar to UnwrapExpression except it gives the 43 | // caller an opportunity to test each level of unwrapping to see each a 44 | // particular expression is accepted. 45 | // 46 | // This could be used, for example, to unwrap until a particular other 47 | // interface is satisfied, regardless of wrap wrapping level it is satisfied 48 | // at. 49 | // 50 | // The given callback function must return false to continue wrapping, or 51 | // true to accept and return the proposed expression given. If the callback 52 | // function rejects even the final, physical expression then the result of 53 | // this function is nil. 54 | func UnwrapExpressionUntil(expr Expression, until func(Expression) bool) Expression { 55 | for { 56 | if until(expr) { 57 | return expr 58 | } 59 | unwrap, wrapped := expr.(unwrapExpression) 60 | if !wrapped { 61 | return nil 62 | } 63 | expr = unwrap.UnwrapExpression() 64 | if expr == nil { 65 | return nil 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /hcl/json/ast.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/hashicorp/hcl2/hcl" 7 | ) 8 | 9 | type node interface { 10 | Range() hcl.Range 11 | StartRange() hcl.Range 12 | } 13 | 14 | type objectVal struct { 15 | Attrs []*objectAttr 16 | SrcRange hcl.Range // range of the entire object, brace-to-brace 17 | OpenRange hcl.Range // range of the opening brace 18 | CloseRange hcl.Range // range of the closing brace 19 | } 20 | 21 | func (n *objectVal) Range() hcl.Range { 22 | return n.SrcRange 23 | } 24 | 25 | func (n *objectVal) StartRange() hcl.Range { 26 | return n.OpenRange 27 | } 28 | 29 | type objectAttr struct { 30 | Name string 31 | Value node 32 | NameRange hcl.Range // range of the name string 33 | } 34 | 35 | func (n *objectAttr) Range() hcl.Range { 36 | return n.NameRange 37 | } 38 | 39 | func (n *objectAttr) StartRange() hcl.Range { 40 | return n.NameRange 41 | } 42 | 43 | type arrayVal struct { 44 | Values []node 45 | SrcRange hcl.Range // range of the entire object, bracket-to-bracket 46 | OpenRange hcl.Range // range of the opening bracket 47 | } 48 | 49 | func (n *arrayVal) Range() hcl.Range { 50 | return n.SrcRange 51 | } 52 | 53 | func (n *arrayVal) StartRange() hcl.Range { 54 | return n.OpenRange 55 | } 56 | 57 | type booleanVal struct { 58 | Value bool 59 | SrcRange hcl.Range 60 | } 61 | 62 | func (n *booleanVal) Range() hcl.Range { 63 | return n.SrcRange 64 | } 65 | 66 | func (n *booleanVal) StartRange() hcl.Range { 67 | return n.SrcRange 68 | } 69 | 70 | type numberVal struct { 71 | Value *big.Float 72 | SrcRange hcl.Range 73 | } 74 | 75 | func (n *numberVal) Range() hcl.Range { 76 | return n.SrcRange 77 | } 78 | 79 | func (n *numberVal) StartRange() hcl.Range { 80 | return n.SrcRange 81 | } 82 | 83 | type stringVal struct { 84 | Value string 85 | SrcRange hcl.Range 86 | } 87 | 88 | func (n *stringVal) Range() hcl.Range { 89 | return n.SrcRange 90 | } 91 | 92 | func (n *stringVal) StartRange() hcl.Range { 93 | return n.SrcRange 94 | } 95 | 96 | type nullVal struct { 97 | SrcRange hcl.Range 98 | } 99 | 100 | func (n *nullVal) Range() hcl.Range { 101 | return n.SrcRange 102 | } 103 | 104 | func (n *nullVal) StartRange() hcl.Range { 105 | return n.SrcRange 106 | } 107 | 108 | // invalidVal is used as a placeholder where a value is needed for a valid 109 | // parse tree but the input was invalid enough to prevent one from being 110 | // created. 111 | type invalidVal struct { 112 | SrcRange hcl.Range 113 | } 114 | 115 | func (n invalidVal) Range() hcl.Range { 116 | return n.SrcRange 117 | } 118 | 119 | func (n invalidVal) StartRange() hcl.Range { 120 | return n.SrcRange 121 | } 122 | -------------------------------------------------------------------------------- /ext/typeexpr/README.md: -------------------------------------------------------------------------------- 1 | # HCL Type Expressions Extension 2 | 3 | This HCL extension defines a convention for describing HCL types using function 4 | call and variable reference syntax, allowing configuration formats to include 5 | type information provided by users. 6 | 7 | The type syntax is processed statically from a hcl.Expression, so it cannot 8 | use any of the usual language operators. This is similar to type expressions 9 | in statically-typed programming languages. 10 | 11 | ```hcl 12 | variable "example" { 13 | type = list(string) 14 | } 15 | ``` 16 | 17 | The extension is built using the `hcl.ExprAsKeyword` and `hcl.ExprCall` 18 | functions, and so it relies on the underlying syntax to define how "keyword" 19 | and "call" are interpreted. The above shows how they are interpreted in 20 | the HCL native syntax, while the following shows the same information 21 | expressed in JSON: 22 | 23 | ```json 24 | { 25 | "variable": { 26 | "example": { 27 | "type": "list(string)" 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | Notice that since we have additional contextual information that we intend 34 | to allow only calls and keywords the JSON syntax is able to parse the given 35 | string directly as an expression, rather than as a template as would be 36 | the case for normal expression evaluation. 37 | 38 | For more information, see [the godoc reference](http://godoc.org/github.com/hashicorp/hcl2/ext/typeexpr). 39 | 40 | ## Type Expression Syntax 41 | 42 | When expressed in the native syntax, the following expressions are permitted 43 | in a type expression: 44 | 45 | * `string` - string 46 | * `bool` - boolean 47 | * `number` - number 48 | * `any` - `cty.DynamicPseudoType` (in function `TypeConstraint` only) 49 | * `list()` - list of the type given as an argument 50 | * `set()` - set of the type given as an argument 51 | * `map()` - map of the type given as an argument 52 | * `tuple([])` - tuple with the element types given in the single list argument 53 | * `object({=, ...}` - object with the attributes and corresponding types given in the single map argument 54 | 55 | For example: 56 | 57 | * `list(string)` 58 | * `object({name=string,age=number})` 59 | * `map(object({name=string,age=number}))` 60 | 61 | Note that the object constructor syntax is not fully-general for all possible 62 | object types because it requires the attribute names to be valid identifiers. 63 | In practice it is expected that any time an object type is being fixed for 64 | type checking it will be one that has identifiers as its attributes; object 65 | types with weird attributes generally show up only from arbitrary object 66 | constructors in configuration files, which are usually treated either as maps 67 | or as the dynamic pseudo-type. 68 | -------------------------------------------------------------------------------- /hcl/hclsyntax/variables.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | // Variables returns all of the variables referenced within a given experssion. 8 | // 9 | // This is the implementation of the "Variables" method on every native 10 | // expression. 11 | func Variables(expr Expression) []hcl.Traversal { 12 | var vars []hcl.Traversal 13 | 14 | walker := &variablesWalker{ 15 | Callback: func(t hcl.Traversal) { 16 | vars = append(vars, t) 17 | }, 18 | } 19 | 20 | Walk(expr, walker) 21 | 22 | return vars 23 | } 24 | 25 | // variablesWalker is a Walker implementation that calls its callback for any 26 | // root scope traversal found while walking. 27 | type variablesWalker struct { 28 | Callback func(hcl.Traversal) 29 | localScopes []map[string]struct{} 30 | } 31 | 32 | func (w *variablesWalker) Enter(n Node) hcl.Diagnostics { 33 | switch tn := n.(type) { 34 | case *ScopeTraversalExpr: 35 | t := tn.Traversal 36 | 37 | // Check if the given root name appears in any of the active 38 | // local scopes. We don't want to return local variables here, since 39 | // the goal of walking variables is to tell the calling application 40 | // which names it needs to populate in the _root_ scope. 41 | name := t.RootName() 42 | for _, names := range w.localScopes { 43 | if _, localized := names[name]; localized { 44 | return nil 45 | } 46 | } 47 | 48 | w.Callback(t) 49 | case ChildScope: 50 | w.localScopes = append(w.localScopes, tn.LocalNames) 51 | } 52 | return nil 53 | } 54 | 55 | func (w *variablesWalker) Exit(n Node) hcl.Diagnostics { 56 | switch n.(type) { 57 | case ChildScope: 58 | // pop the latest local scope, assuming that the walker will 59 | // behave symmetrically as promised. 60 | w.localScopes = w.localScopes[:len(w.localScopes)-1] 61 | } 62 | return nil 63 | } 64 | 65 | // ChildScope is a synthetic AST node that is visited during a walk to 66 | // indicate that its descendent will be evaluated in a child scope, which 67 | // may mask certain variables from the parent scope as locals. 68 | // 69 | // ChildScope nodes don't really exist in the AST, but are rather synthesized 70 | // on the fly during walk. Therefore it doesn't do any good to transform them; 71 | // instead, transform either parent node that created a scope or the expression 72 | // that the child scope struct wraps. 73 | type ChildScope struct { 74 | LocalNames map[string]struct{} 75 | Expr Expression 76 | } 77 | 78 | func (e ChildScope) walkChildNodes(w internalWalkFunc) { 79 | w(e.Expr) 80 | } 81 | 82 | // Range returns the range of the expression that the ChildScope is 83 | // encapsulating. It isn't really very useful to call Range on a ChildScope. 84 | func (e ChildScope) Range() hcl.Range { 85 | return e.Expr.Range() 86 | } 87 | -------------------------------------------------------------------------------- /hcl/json/public_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/hashicorp/hcl2/hcl" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | func TestParse_nonObject(t *testing.T) { 11 | src := `true` 12 | file, diags := Parse([]byte(src), "") 13 | if len(diags) != 1 { 14 | t.Errorf("got %d diagnostics; want 1", len(diags)) 15 | } 16 | if file == nil { 17 | t.Errorf("got nil File; want actual file") 18 | } 19 | if file.Body == nil { 20 | t.Fatalf("got nil Body; want actual body") 21 | } 22 | if file.Body.(*body).val == nil { 23 | t.Errorf("got nil Body object; want placeholder object") 24 | } 25 | } 26 | 27 | func TestParseTemplate(t *testing.T) { 28 | src := `{"greeting": "hello ${\"world\"}"}` 29 | file, diags := Parse([]byte(src), "") 30 | if len(diags) != 0 { 31 | t.Errorf("got %d diagnostics on parse; want 0", len(diags)) 32 | for _, diag := range diags { 33 | t.Logf("- %s", diag.Error()) 34 | } 35 | } 36 | if file == nil { 37 | t.Errorf("got nil File; want actual file") 38 | } 39 | if file.Body == nil { 40 | t.Fatalf("got nil Body; want actual body") 41 | } 42 | attrs, diags := file.Body.JustAttributes() 43 | if len(diags) != 0 { 44 | t.Errorf("got %d diagnostics on decode; want 0", len(diags)) 45 | for _, diag := range diags { 46 | t.Logf("- %s", diag.Error()) 47 | } 48 | } 49 | 50 | val, diags := attrs["greeting"].Expr.Value(&hcl.EvalContext{}) 51 | if len(diags) != 0 { 52 | t.Errorf("got %d diagnostics on eval; want 0", len(diags)) 53 | for _, diag := range diags { 54 | t.Logf("- %s", diag.Error()) 55 | } 56 | } 57 | 58 | if !val.RawEquals(cty.StringVal("hello world")) { 59 | t.Errorf("wrong result %#v; want %#v", val, cty.StringVal("hello world")) 60 | } 61 | } 62 | 63 | func TestParseTemplateUnwrap(t *testing.T) { 64 | src := `{"greeting": "${true}"}` 65 | file, diags := Parse([]byte(src), "") 66 | if len(diags) != 0 { 67 | t.Errorf("got %d diagnostics on parse; want 0", len(diags)) 68 | for _, diag := range diags { 69 | t.Logf("- %s", diag.Error()) 70 | } 71 | } 72 | if file == nil { 73 | t.Errorf("got nil File; want actual file") 74 | } 75 | if file.Body == nil { 76 | t.Fatalf("got nil Body; want actual body") 77 | } 78 | attrs, diags := file.Body.JustAttributes() 79 | if len(diags) != 0 { 80 | t.Errorf("got %d diagnostics on decode; want 0", len(diags)) 81 | for _, diag := range diags { 82 | t.Logf("- %s", diag.Error()) 83 | } 84 | } 85 | 86 | val, diags := attrs["greeting"].Expr.Value(&hcl.EvalContext{}) 87 | if len(diags) != 0 { 88 | t.Errorf("got %d diagnostics on eval; want 0", len(diags)) 89 | for _, diag := range diags { 90 | t.Logf("- %s", diag.Error()) 91 | } 92 | } 93 | 94 | if !val.RawEquals(cty.True) { 95 | t.Errorf("wrong result %#v; want %#v", val, cty.True) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /hclwrite/ast.go: -------------------------------------------------------------------------------- 1 | package hclwrite 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | type File struct { 9 | inTree 10 | 11 | srcBytes []byte 12 | body *node 13 | } 14 | 15 | // NewEmptyFile constructs a new file with no content, ready to be mutated 16 | // by other calls that append to its body. 17 | func NewEmptyFile() *File { 18 | f := &File{ 19 | inTree: newInTree(), 20 | } 21 | body := newBody() 22 | f.body = f.children.Append(body) 23 | return f 24 | } 25 | 26 | // Body returns the root body of the file, which contains the top-level 27 | // attributes and blocks. 28 | func (f *File) Body() *Body { 29 | return f.body.content.(*Body) 30 | } 31 | 32 | // WriteTo writes the tokens underlying the receiving file to the given writer. 33 | // 34 | // The tokens first have a simple formatting pass applied that adjusts only 35 | // the spaces between them. 36 | func (f *File) WriteTo(wr io.Writer) (int64, error) { 37 | tokens := f.inTree.children.BuildTokens(nil) 38 | format(tokens) 39 | return tokens.WriteTo(wr) 40 | } 41 | 42 | // Bytes returns a buffer containing the source code resulting from the 43 | // tokens underlying the receiving file. If any updates have been made via 44 | // the AST API, these will be reflected in the result. 45 | func (f *File) Bytes() []byte { 46 | buf := &bytes.Buffer{} 47 | f.WriteTo(buf) 48 | return buf.Bytes() 49 | } 50 | 51 | type comments struct { 52 | leafNode 53 | 54 | parent *node 55 | tokens Tokens 56 | } 57 | 58 | func newComments(tokens Tokens) *comments { 59 | return &comments{ 60 | tokens: tokens, 61 | } 62 | } 63 | 64 | func (c *comments) BuildTokens(to Tokens) Tokens { 65 | return c.tokens.BuildTokens(to) 66 | } 67 | 68 | type identifier struct { 69 | leafNode 70 | 71 | parent *node 72 | token *Token 73 | } 74 | 75 | func newIdentifier(token *Token) *identifier { 76 | return &identifier{ 77 | token: token, 78 | } 79 | } 80 | 81 | func (i *identifier) BuildTokens(to Tokens) Tokens { 82 | return append(to, i.token) 83 | } 84 | 85 | func (i *identifier) hasName(name string) bool { 86 | return name == string(i.token.Bytes) 87 | } 88 | 89 | type number struct { 90 | leafNode 91 | 92 | parent *node 93 | token *Token 94 | } 95 | 96 | func newNumber(token *Token) *number { 97 | return &number{ 98 | token: token, 99 | } 100 | } 101 | 102 | func (n *number) BuildTokens(to Tokens) Tokens { 103 | return append(to, n.token) 104 | } 105 | 106 | type quoted struct { 107 | leafNode 108 | 109 | parent *node 110 | tokens Tokens 111 | } 112 | 113 | func newQuoted(tokens Tokens) *quoted { 114 | return "ed{ 115 | tokens: tokens, 116 | } 117 | } 118 | 119 | func (q *quoted) BuildTokens(to Tokens) Tokens { 120 | return q.tokens.BuildTokens(to) 121 | } 122 | -------------------------------------------------------------------------------- /ext/transform/transform_test.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "testing" 5 | 6 | "reflect" 7 | 8 | "github.com/hashicorp/hcl2/hcl" 9 | "github.com/hashicorp/hcl2/hcltest" 10 | "github.com/zclconf/go-cty/cty" 11 | ) 12 | 13 | // Assert that deepWrapper implements Body 14 | var deepWrapperIsBody hcl.Body = deepWrapper{} 15 | 16 | func TestDeep(t *testing.T) { 17 | 18 | testTransform := TransformerFunc(func(body hcl.Body) hcl.Body { 19 | _, remain, diags := body.PartialContent(&hcl.BodySchema{ 20 | Blocks: []hcl.BlockHeaderSchema{ 21 | { 22 | Type: "remove", 23 | }, 24 | }, 25 | }) 26 | 27 | return BodyWithDiagnostics(remain, diags) 28 | }) 29 | 30 | src := hcltest.MockBody(&hcl.BodyContent{ 31 | Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ 32 | "true": hcltest.MockExprLiteral(cty.True), 33 | }), 34 | Blocks: []*hcl.Block{ 35 | { 36 | Type: "remove", 37 | Body: hcl.EmptyBody(), 38 | }, 39 | { 40 | Type: "child", 41 | Body: hcltest.MockBody(&hcl.BodyContent{ 42 | Blocks: []*hcl.Block{ 43 | { 44 | Type: "remove", 45 | }, 46 | }, 47 | }), 48 | }, 49 | }, 50 | }) 51 | 52 | wrapped := Deep(src, testTransform) 53 | 54 | rootContent, diags := wrapped.Content(&hcl.BodySchema{ 55 | Attributes: []hcl.AttributeSchema{ 56 | { 57 | Name: "true", 58 | }, 59 | }, 60 | Blocks: []hcl.BlockHeaderSchema{ 61 | { 62 | Type: "child", 63 | }, 64 | }, 65 | }) 66 | if len(diags) != 0 { 67 | t.Errorf("unexpected diagnostics for root content") 68 | for _, diag := range diags { 69 | t.Logf("- %s", diag) 70 | } 71 | } 72 | 73 | wantAttrs := hcltest.MockAttrs(map[string]hcl.Expression{ 74 | "true": hcltest.MockExprLiteral(cty.True), 75 | }) 76 | if !reflect.DeepEqual(rootContent.Attributes, wantAttrs) { 77 | t.Errorf("wrong root attributes\ngot: %#v\nwant: %#v", rootContent.Attributes, wantAttrs) 78 | } 79 | 80 | if got, want := len(rootContent.Blocks), 1; got != want { 81 | t.Fatalf("wrong number of root blocks %d; want %d", got, want) 82 | } 83 | if got, want := rootContent.Blocks[0].Type, "child"; got != want { 84 | t.Errorf("wrong block type %s; want %s", got, want) 85 | } 86 | 87 | childBlock := rootContent.Blocks[0] 88 | childContent, diags := childBlock.Body.Content(&hcl.BodySchema{}) 89 | if len(diags) != 0 { 90 | t.Errorf("unexpected diagnostics for child content") 91 | for _, diag := range diags { 92 | t.Logf("- %s", diag) 93 | } 94 | } 95 | 96 | if len(childContent.Attributes) != 0 { 97 | t.Errorf("unexpected attributes in child content; want empty content") 98 | } 99 | if len(childContent.Blocks) != 0 { 100 | t.Errorf("unexpected blocks in child content; want empty content") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /extras/grammar/HCLTemplate.json-tmLanguage: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [ 3 | "tmpl" 4 | ], 5 | "name": "HCL Template", 6 | "patterns": [ 7 | { 8 | "begin": "[^\\$]?(\\$\\{~?)", 9 | "beginCaptures": { 10 | "1": { 11 | "name": "entity.tag.embedded.start.hcltemplate" 12 | } 13 | }, 14 | "comment": "Interpolation Sequences", 15 | "end": "~?}", 16 | "endCaptures": { 17 | "0": { 18 | "name": "entity.tag.embedded.end.hcltemplate" 19 | } 20 | }, 21 | "name": "meta.interp.hcltemplate", 22 | "patterns": [ 23 | { 24 | "include": "source.hclexpr" 25 | } 26 | ] 27 | }, 28 | { 29 | "begin": "[^\\%]?(\\%\\{~?)", 30 | "beginCaptures": { 31 | "1": { 32 | "name": "entity.tag.embedded.start.hcltemplate" 33 | } 34 | }, 35 | "comment": "Control Sequences", 36 | "end": "~?}", 37 | "endCaptures": { 38 | "0": { 39 | "name": "entity.tag.embedded.end.hcltemplate" 40 | } 41 | }, 42 | "name": "meta.control.hcltemplate", 43 | "patterns": [ 44 | { 45 | "include": "#templateif" 46 | }, 47 | { 48 | "include": "#templatefor" 49 | }, 50 | { 51 | "include": "#templatesimplekw" 52 | } 53 | ] 54 | } 55 | ], 56 | "repository": { 57 | "templatefor": { 58 | "begin": "(for)\\s*(\\w+)\\s*(,\\s*(\\w+)\\s*)?(in)", 59 | "beginCaptures": { 60 | "1": { 61 | "name": "keyword.control.hcltemplate" 62 | }, 63 | "2": { 64 | "name": "variable.other.hcl" 65 | }, 66 | "4": { 67 | "name": "variable.other.hcl" 68 | }, 69 | "5": { 70 | "name": "keyword.control.hcltemplate" 71 | } 72 | }, 73 | "end": "(?=~?\\})", 74 | "name": "meta.templatefor.hcltemplate", 75 | "patterns": [ 76 | { 77 | "include": "source.hclexpr" 78 | } 79 | ] 80 | }, 81 | "templateif": { 82 | "begin": "(if)\\s*", 83 | "beginCaptures": { 84 | "1": { 85 | "name": "keyword.control.hcltemplate" 86 | } 87 | }, 88 | "end": "(?=~?\\})", 89 | "name": "meta.templateif.hcltemplate", 90 | "patterns": [ 91 | { 92 | "include": "source.hclexpr" 93 | } 94 | ] 95 | }, 96 | "templatesimplekw": { 97 | "captures": { 98 | "0": { 99 | "name": "keyword.control.hcl" 100 | } 101 | }, 102 | "match": "(else|endif|endfor)" 103 | } 104 | }, 105 | "scopeName": "source.hcltemplate", 106 | "uuid": "ac6be18e-d44f-4a73-bd8f-b973fd26df05" 107 | } -------------------------------------------------------------------------------- /extras/grammar/build.go: -------------------------------------------------------------------------------- 1 | // This is a helper to transform the HCL.yaml-tmLanguage file (the source of 2 | // record) into both HCL.json-tmLanguage and HCL.tmLanguage (in plist XML 3 | // format). 4 | // 5 | // Run this after making updates to HCL.yaml-tmLanguage to generate the other 6 | // formats. 7 | // 8 | // This file is intended to be run with "go run": 9 | // 10 | // go run ./build.go 11 | // 12 | // This file is also set up to run itself under "go generate": 13 | // 14 | // go generate . 15 | 16 | package main 17 | 18 | //go:generate go run ./build.go 19 | 20 | import ( 21 | "encoding/json" 22 | "fmt" 23 | "io/ioutil" 24 | "log" 25 | "os" 26 | 27 | yaml "gopkg.in/yaml.v2" 28 | plist "howett.net/plist" 29 | 30 | multierror "github.com/hashicorp/go-multierror" 31 | ) 32 | 33 | func main() { 34 | err := realMain() 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | os.Exit(0) 39 | } 40 | 41 | func realMain() error { 42 | var err error 43 | buildErr := build("HCL") 44 | if buildErr != nil { 45 | err = multierror.Append(err, fmt.Errorf("in HCL: %s", buildErr)) 46 | } 47 | buildErr = build("HCLTemplate") 48 | if buildErr != nil { 49 | err = multierror.Append(err, fmt.Errorf("in HCLTemplate: %s", buildErr)) 50 | } 51 | buildErr = build("HCLExpression") 52 | if buildErr != nil { 53 | err = multierror.Append(err, fmt.Errorf("in HCLExpression: %s", buildErr)) 54 | } 55 | return err 56 | } 57 | 58 | func build(basename string) error { 59 | yamlSrc, err := ioutil.ReadFile(basename + ".yaml-tmLanguage") 60 | if err != nil { 61 | return err 62 | } 63 | 64 | var content interface{} 65 | err = yaml.Unmarshal(yamlSrc, &content) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // Normalize the value so it's both JSON- and plist-friendly. 71 | content = prepare(content) 72 | 73 | jsonSrc, err := json.MarshalIndent(content, "", " ") 74 | if err != nil { 75 | return err 76 | } 77 | 78 | plistSrc, err := plist.MarshalIndent(content, plist.XMLFormat, " ") 79 | if err != nil { 80 | return err 81 | } 82 | 83 | err = ioutil.WriteFile(basename+".json-tmLanguage", jsonSrc, os.ModePerm) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | err = ioutil.WriteFile(basename+".tmLanguage", plistSrc, os.ModePerm) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func prepare(v interface{}) interface{} { 97 | switch tv := v.(type) { 98 | 99 | case map[interface{}]interface{}: 100 | var ret map[string]interface{} 101 | if len(tv) == 0 { 102 | return ret 103 | } 104 | ret = make(map[string]interface{}, len(tv)) 105 | for k, v := range tv { 106 | ret[k.(string)] = prepare(v) 107 | } 108 | return ret 109 | 110 | case []interface{}: 111 | for i := range tv { 112 | tv[i] = prepare(tv[i]) 113 | } 114 | return tv 115 | 116 | default: 117 | return v 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /hcl/json/public.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/hashicorp/hcl2/hcl" 9 | ) 10 | 11 | // Parse attempts to parse the given buffer as JSON and, if successful, returns 12 | // a hcl.File for the HCL configuration represented by it. 13 | // 14 | // This is not a generic JSON parser. Instead, it deals only with the profile 15 | // of JSON used to express HCL configuration. 16 | // 17 | // The returned file is valid only if the returned diagnostics returns false 18 | // from its HasErrors method. If HasErrors returns true, the file represents 19 | // the subset of data that was able to be parsed, which may be none. 20 | func Parse(src []byte, filename string) (*hcl.File, hcl.Diagnostics) { 21 | rootNode, diags := parseFileContent(src, filename) 22 | 23 | switch rootNode.(type) { 24 | case *objectVal, *arrayVal: 25 | // okay 26 | default: 27 | diags = diags.Append(&hcl.Diagnostic{ 28 | Severity: hcl.DiagError, 29 | Summary: "Root value must be object", 30 | Detail: "The root value in a JSON-based configuration must be either a JSON object or a JSON array of objects.", 31 | Subject: rootNode.StartRange().Ptr(), 32 | }) 33 | 34 | // Since we've already produced an error message for this being 35 | // invalid, we'll return an empty placeholder here so that trying to 36 | // extract content from our root body won't produce a redundant 37 | // error saying the same thing again in more general terms. 38 | fakePos := hcl.Pos{ 39 | Byte: 0, 40 | Line: 1, 41 | Column: 1, 42 | } 43 | fakeRange := hcl.Range{ 44 | Filename: filename, 45 | Start: fakePos, 46 | End: fakePos, 47 | } 48 | rootNode = &objectVal{ 49 | Attrs: []*objectAttr{}, 50 | SrcRange: fakeRange, 51 | OpenRange: fakeRange, 52 | } 53 | } 54 | 55 | file := &hcl.File{ 56 | Body: &body{ 57 | val: rootNode, 58 | }, 59 | Bytes: src, 60 | Nav: navigation{rootNode}, 61 | } 62 | return file, diags 63 | } 64 | 65 | // ParseFile is a convenience wrapper around Parse that first attempts to load 66 | // data from the given filename, passing the result to Parse if successful. 67 | // 68 | // If the file cannot be read, an error diagnostic with nil context is returned. 69 | func ParseFile(filename string) (*hcl.File, hcl.Diagnostics) { 70 | f, err := os.Open(filename) 71 | if err != nil { 72 | return nil, hcl.Diagnostics{ 73 | { 74 | Severity: hcl.DiagError, 75 | Summary: "Failed to open file", 76 | Detail: fmt.Sprintf("The file %q could not be opened.", filename), 77 | }, 78 | } 79 | } 80 | defer f.Close() 81 | 82 | src, err := ioutil.ReadAll(f) 83 | if err != nil { 84 | return nil, hcl.Diagnostics{ 85 | { 86 | Severity: hcl.DiagError, 87 | Summary: "Failed to read file", 88 | Detail: fmt.Sprintf("The file %q was opened, but an error occured while reading it.", filename), 89 | }, 90 | } 91 | } 92 | 93 | return Parse(src, filename) 94 | } 95 | -------------------------------------------------------------------------------- /hcl/hclsyntax/expression_vars_gen.go: -------------------------------------------------------------------------------- 1 | // This is a 'go generate'-oriented program for producing the "Variables" 2 | // method on every Expression implementation found within this package. 3 | // All expressions share the same implementation for this method, which 4 | // just wraps the package-level function "Variables" and uses an AST walk 5 | // to do its work. 6 | 7 | // +build ignore 8 | 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "go/ast" 14 | "go/parser" 15 | "go/token" 16 | "os" 17 | "sort" 18 | ) 19 | 20 | func main() { 21 | fs := token.NewFileSet() 22 | pkgs, err := parser.ParseDir(fs, ".", nil, 0) 23 | if err != nil { 24 | fmt.Fprintf(os.Stderr, "error while parsing: %s\n", err) 25 | os.Exit(1) 26 | } 27 | pkg := pkgs["hclsyntax"] 28 | 29 | // Walk all the files and collect the receivers of any "Value" methods 30 | // that look like they are trying to implement Expression. 31 | var recvs []string 32 | for _, f := range pkg.Files { 33 | for _, decl := range f.Decls { 34 | fd, ok := decl.(*ast.FuncDecl) 35 | if !ok { 36 | continue 37 | } 38 | if fd.Name.Name != "Value" { 39 | continue 40 | } 41 | results := fd.Type.Results.List 42 | if len(results) != 2 { 43 | continue 44 | } 45 | valResult := fd.Type.Results.List[0].Type.(*ast.SelectorExpr).X.(*ast.Ident) 46 | diagsResult := fd.Type.Results.List[1].Type.(*ast.SelectorExpr).X.(*ast.Ident) 47 | 48 | if valResult.Name != "cty" && diagsResult.Name != "hcl" { 49 | continue 50 | } 51 | 52 | // If we have a method called Value and it returns something in 53 | // "cty" followed by something in "hcl" then that's specific enough 54 | // for now, even though this is not 100% exact as a correct 55 | // implementation of Value. 56 | 57 | recvTy := fd.Recv.List[0].Type 58 | 59 | switch rtt := recvTy.(type) { 60 | case *ast.StarExpr: 61 | name := rtt.X.(*ast.Ident).Name 62 | recvs = append(recvs, fmt.Sprintf("*%s", name)) 63 | default: 64 | fmt.Fprintf(os.Stderr, "don't know what to do with a %T receiver\n", recvTy) 65 | } 66 | 67 | } 68 | } 69 | 70 | sort.Strings(recvs) 71 | 72 | of, err := os.OpenFile("expression_vars.go", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) 73 | if err != nil { 74 | fmt.Fprintf(os.Stderr, "failed to open output file: %s\n", err) 75 | os.Exit(1) 76 | } 77 | 78 | fmt.Fprint(of, outputPreamble) 79 | for _, recv := range recvs { 80 | fmt.Fprintf(of, outputMethodFmt, recv) 81 | } 82 | fmt.Fprint(of, "\n") 83 | 84 | } 85 | 86 | const outputPreamble = `package hclsyntax 87 | 88 | // Generated by expression_vars_get.go. DO NOT EDIT. 89 | // Run 'go generate' on this package to update the set of functions here. 90 | 91 | import ( 92 | "github.com/hashicorp/hcl2/hcl" 93 | )` 94 | 95 | const outputMethodFmt = ` 96 | 97 | func (e %s) Variables() []hcl.Traversal { 98 | return Variables(e) 99 | }` 100 | -------------------------------------------------------------------------------- /cmd/hcldec/examples/npm-package/spec.hcldec: -------------------------------------------------------------------------------- 1 | object { 2 | attr "name" { 3 | type = string 4 | required = true 5 | } 6 | attr "version" { 7 | type = string 8 | required = true 9 | } 10 | attr "description" { 11 | type = string 12 | } 13 | attr "keywords" { 14 | type = list(string) 15 | } 16 | attr "homepage" { 17 | # "homepage_url" in input file is translated to "homepage" in output 18 | name = "homepage_url" 19 | } 20 | block "bugs" { 21 | object { 22 | attr "url" { 23 | type = string 24 | } 25 | attr "email" { 26 | type = string 27 | } 28 | } 29 | } 30 | attr "license" { 31 | type = string 32 | } 33 | block "author" { 34 | object { 35 | attr "name" { 36 | type = string 37 | } 38 | attr "email" { 39 | type = string 40 | } 41 | attr "url" { 42 | type = string 43 | } 44 | } 45 | } 46 | block_list "contributors" { 47 | block_type = "contributor" 48 | object { 49 | attr "name" { 50 | type = string 51 | } 52 | attr "email" { 53 | type = string 54 | } 55 | attr "url" { 56 | type = string 57 | } 58 | } 59 | } 60 | attr "files" { 61 | type = list(string) 62 | } 63 | attr "main" { 64 | type = string 65 | } 66 | attr "bin" { 67 | type = map(string) 68 | } 69 | attr "man" { 70 | type = list(string) 71 | } 72 | attr "directories" { 73 | type = map(string) 74 | } 75 | block "repository" { 76 | object { 77 | attr "type" { 78 | type = string 79 | required = true 80 | } 81 | attr "url" { 82 | type = string 83 | required = true 84 | } 85 | } 86 | } 87 | attr "scripts" { 88 | type = map(string) 89 | } 90 | attr "config" { 91 | type = map(string) 92 | } 93 | attr "dependencies" { 94 | type = map(string) 95 | } 96 | attr "devDependencies" { 97 | name = "dev_dependencies" 98 | type = map(string) 99 | } 100 | attr "peerDependencies" { 101 | name = "peer_dependencies" 102 | type = map(string) 103 | } 104 | attr "bundledDependencies" { 105 | name = "bundled_dependencies" 106 | type = map(string) 107 | } 108 | attr "optionalDependencies" { 109 | name = "optional_dependencies" 110 | type = map(string) 111 | } 112 | attr "engines" { 113 | type = map(string) 114 | } 115 | attr "os" { 116 | type = list(string) 117 | } 118 | attr "cpu" { 119 | type = list(string) 120 | } 121 | attr "prefer_global" { 122 | type = bool 123 | } 124 | default "private" { 125 | attr { 126 | name = "private" 127 | type = bool 128 | } 129 | literal { 130 | value = false 131 | } 132 | } 133 | attr "publishConfig" { 134 | type = map(any) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /gohcl/doc.go: -------------------------------------------------------------------------------- 1 | // Package gohcl allows decoding HCL configurations into Go data structures. 2 | // 3 | // It provides a convenient and concise way of describing the schema for 4 | // configuration and then accessing the resulting data via native Go 5 | // types. 6 | // 7 | // A struct field tag scheme is used, similar to other decoding and 8 | // unmarshalling libraries. The tags are formatted as in the following example: 9 | // 10 | // ThingType string `hcl:"thing_type,attr"` 11 | // 12 | // Within each tag there are two comma-separated tokens. The first is the 13 | // name of the corresponding construct in configuration, while the second 14 | // is a keyword giving the kind of construct expected. The following 15 | // kind keywords are supported: 16 | // 17 | // attr (the default) indicates that the value is to be populated from an attribute 18 | // block indicates that the value is to populated from a block 19 | // label indicates that the value is to populated from a block label 20 | // remain indicates that the value is to be populated from the remaining body after populating other fields 21 | // 22 | // "attr" fields may either be of type *hcl.Expression, in which case the raw 23 | // expression is assigned, or of any type accepted by gocty, in which case 24 | // gocty will be used to assign the value to a native Go type. 25 | // 26 | // "block" fields may be of type *hcl.Block or hcl.Body, in which case the 27 | // corresponding raw value is assigned, or may be a struct that recursively 28 | // uses the same tags. Block fields may also be slices of any of these types, 29 | // in which case multiple blocks of the corresponding type are decoded into 30 | // the slice. 31 | // 32 | // "label" fields are considered only in a struct used as the type of a field 33 | // marked as "block", and are used sequentially to capture the labels of 34 | // the blocks being decoded. In this case, the name token is used only as 35 | // an identifier for the label in diagnostic messages. 36 | // 37 | // "remain" can be placed on a single field that may be either of type 38 | // hcl.Body or hcl.Attributes, in which case any remaining body content is 39 | // placed into this field for delayed processing. If no "remain" field is 40 | // present then any attributes or blocks not matched by another valid tag 41 | // will cause an error diagnostic. 42 | // 43 | // Only a subset of this tagging/typing vocabulary is supported for the 44 | // "Encode" family of functions. See the EncodeIntoBody docs for full details 45 | // on the constraints there. 46 | // 47 | // Broadly-speaking this package deals with two types of error. The first is 48 | // errors in the configuration itself, which are returned as diagnostics 49 | // written with the configuration author as the target audience. The second 50 | // is bugs in the calling program, such as invalid struct tags, which are 51 | // surfaced via panics since there can be no useful runtime handling of such 52 | // errors and they should certainly not be returned to the user as diagnostics. 53 | package gohcl 54 | -------------------------------------------------------------------------------- /ext/include/transformer_test.go: -------------------------------------------------------------------------------- 1 | package include 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/davecgh/go-spew/spew" 8 | "github.com/hashicorp/hcl2/gohcl" 9 | "github.com/hashicorp/hcl2/hcl" 10 | "github.com/hashicorp/hcl2/hcltest" 11 | "github.com/zclconf/go-cty/cty" 12 | ) 13 | 14 | func TestTransformer(t *testing.T) { 15 | caller := hcltest.MockBody(&hcl.BodyContent{ 16 | Blocks: hcl.Blocks{ 17 | { 18 | Type: "include", 19 | Body: hcltest.MockBody(&hcl.BodyContent{ 20 | Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ 21 | "path": hcltest.MockExprVariable("var_path"), 22 | }), 23 | }), 24 | }, 25 | { 26 | Type: "include", 27 | Body: hcltest.MockBody(&hcl.BodyContent{ 28 | Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ 29 | "path": hcltest.MockExprLiteral(cty.StringVal("include2")), 30 | }), 31 | }), 32 | }, 33 | { 34 | Type: "foo", 35 | Body: hcltest.MockBody(&hcl.BodyContent{ 36 | Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ 37 | "from": hcltest.MockExprLiteral(cty.StringVal("caller")), 38 | }), 39 | }), 40 | }, 41 | }, 42 | }) 43 | 44 | resolver := MapResolver(map[string]hcl.Body{ 45 | "include1": hcltest.MockBody(&hcl.BodyContent{ 46 | Blocks: hcl.Blocks{ 47 | { 48 | Type: "foo", 49 | Body: hcltest.MockBody(&hcl.BodyContent{ 50 | Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ 51 | "from": hcltest.MockExprLiteral(cty.StringVal("include1")), 52 | }), 53 | }), 54 | }, 55 | }, 56 | }), 57 | "include2": hcltest.MockBody(&hcl.BodyContent{ 58 | Blocks: hcl.Blocks{ 59 | { 60 | Type: "foo", 61 | Body: hcltest.MockBody(&hcl.BodyContent{ 62 | Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ 63 | "from": hcltest.MockExprLiteral(cty.StringVal("include2")), 64 | }), 65 | }), 66 | }, 67 | }, 68 | }), 69 | }) 70 | 71 | ctx := &hcl.EvalContext{ 72 | Variables: map[string]cty.Value{ 73 | "var_path": cty.StringVal("include1"), 74 | }, 75 | } 76 | 77 | transformer := Transformer("include", ctx, resolver) 78 | merged := transformer.TransformBody(caller) 79 | 80 | type foo struct { 81 | From string `hcl:"from,attr"` 82 | } 83 | type result struct { 84 | Foos []foo `hcl:"foo,block"` 85 | } 86 | var got result 87 | diags := gohcl.DecodeBody(merged, nil, &got) 88 | if len(diags) != 0 { 89 | t.Errorf("unexpected diags") 90 | for _, diag := range diags { 91 | t.Logf("- %s", diag) 92 | } 93 | } 94 | 95 | want := result{ 96 | Foos: []foo{ 97 | { 98 | From: "caller", 99 | }, 100 | { 101 | From: "include1", 102 | }, 103 | { 104 | From: "include2", 105 | }, 106 | }, 107 | } 108 | 109 | if !reflect.DeepEqual(got, want) { 110 | t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want)) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /cmd/hclspecsuite/diagnostics.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/hashicorp/hcl2/hcl" 8 | ) 9 | 10 | func decodeJSONDiagnostics(src []byte) hcl.Diagnostics { 11 | type PosJSON struct { 12 | Line int `json:"line"` 13 | Column int `json:"column"` 14 | Byte int `json:"byte"` 15 | } 16 | type RangeJSON struct { 17 | Filename string `json:"filename"` 18 | Start PosJSON `json:"start"` 19 | End PosJSON `json:"end"` 20 | } 21 | type DiagnosticJSON struct { 22 | Severity string `json:"severity"` 23 | Summary string `json:"summary"` 24 | Detail string `json:"detail,omitempty"` 25 | Subject *RangeJSON `json:"subject,omitempty"` 26 | } 27 | type DiagnosticsJSON struct { 28 | Diagnostics []DiagnosticJSON `json:"diagnostics"` 29 | } 30 | 31 | var raw DiagnosticsJSON 32 | var diags hcl.Diagnostics 33 | err := json.Unmarshal(src, &raw) 34 | if err != nil { 35 | diags = append(diags, &hcl.Diagnostic{ 36 | Severity: hcl.DiagError, 37 | Summary: "Failed to parse hcldec diagnostics result", 38 | Detail: fmt.Sprintf("Sub-program hcldec produced invalid diagnostics: %s.", err), 39 | }) 40 | return diags 41 | } 42 | 43 | if len(raw.Diagnostics) == 0 { 44 | return nil 45 | } 46 | 47 | diags = make(hcl.Diagnostics, 0, len(raw.Diagnostics)) 48 | for _, rawDiag := range raw.Diagnostics { 49 | var severity hcl.DiagnosticSeverity 50 | switch rawDiag.Severity { 51 | case "error": 52 | severity = hcl.DiagError 53 | case "warning": 54 | severity = hcl.DiagWarning 55 | default: 56 | diags = append(diags, &hcl.Diagnostic{ 57 | Severity: hcl.DiagError, 58 | Summary: "Failed to parse hcldec diagnostics result", 59 | Detail: fmt.Sprintf("Diagnostic has unsupported severity %q.", rawDiag.Severity), 60 | }) 61 | continue 62 | } 63 | 64 | diag := &hcl.Diagnostic{ 65 | Severity: severity, 66 | Summary: rawDiag.Summary, 67 | Detail: rawDiag.Detail, 68 | } 69 | if rawDiag.Subject != nil { 70 | rawRange := rawDiag.Subject 71 | diag.Subject = &hcl.Range{ 72 | Filename: rawRange.Filename, 73 | Start: hcl.Pos{ 74 | Line: rawRange.Start.Line, 75 | Column: rawRange.Start.Column, 76 | Byte: rawRange.Start.Byte, 77 | }, 78 | End: hcl.Pos{ 79 | Line: rawRange.End.Line, 80 | Column: rawRange.End.Column, 81 | Byte: rawRange.End.Byte, 82 | }, 83 | } 84 | } 85 | diags = append(diags, diag) 86 | } 87 | 88 | return diags 89 | } 90 | 91 | func severityString(severity hcl.DiagnosticSeverity) string { 92 | switch severity { 93 | case hcl.DiagError: 94 | return "error" 95 | case hcl.DiagWarning: 96 | return "warning" 97 | default: 98 | return "unsupported-severity" 99 | } 100 | } 101 | 102 | func rangeString(rng hcl.Range) string { 103 | return fmt.Sprintf( 104 | "from line %d column %d byte %d to line %d column %d byte %d", 105 | rng.Start.Line, rng.Start.Column, rng.Start.Byte, 106 | rng.End.Line, rng.End.Column, rng.End.Byte, 107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /ext/dynblock/unknown_body.go: -------------------------------------------------------------------------------- 1 | package dynblock 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | // unknownBody is a funny body that just reports everything inside it as 9 | // unknown. It uses a given other body as a sort of template for what attributes 10 | // and blocks are inside -- including source location information -- but 11 | // subsitutes unknown values of unknown type for all attributes. 12 | // 13 | // This rather odd process is used to handle expansion of dynamic blocks whose 14 | // for_each expression is unknown. Since a block cannot itself be unknown, 15 | // we instead arrange for everything _inside_ the block to be unknown instead, 16 | // to give the best possible approximation. 17 | type unknownBody struct { 18 | template hcl.Body 19 | } 20 | 21 | var _ hcl.Body = unknownBody{} 22 | 23 | func (b unknownBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { 24 | content, diags := b.template.Content(schema) 25 | content = b.fixupContent(content) 26 | 27 | // We're intentionally preserving the diagnostics reported from the 28 | // inner body so that we can still report where the template body doesn't 29 | // match the requested schema. 30 | return content, diags 31 | } 32 | 33 | func (b unknownBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { 34 | content, remain, diags := b.template.PartialContent(schema) 35 | content = b.fixupContent(content) 36 | remain = unknownBody{remain} // remaining content must also be wrapped 37 | 38 | // We're intentionally preserving the diagnostics reported from the 39 | // inner body so that we can still report where the template body doesn't 40 | // match the requested schema. 41 | return content, remain, diags 42 | } 43 | 44 | func (b unknownBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { 45 | attrs, diags := b.template.JustAttributes() 46 | attrs = b.fixupAttrs(attrs) 47 | 48 | // We're intentionally preserving the diagnostics reported from the 49 | // inner body so that we can still report where the template body doesn't 50 | // match the requested schema. 51 | return attrs, diags 52 | } 53 | 54 | func (b unknownBody) MissingItemRange() hcl.Range { 55 | return b.template.MissingItemRange() 56 | } 57 | 58 | func (b unknownBody) fixupContent(got *hcl.BodyContent) *hcl.BodyContent { 59 | ret := &hcl.BodyContent{} 60 | ret.Attributes = b.fixupAttrs(got.Attributes) 61 | if len(got.Blocks) > 0 { 62 | ret.Blocks = make(hcl.Blocks, 0, len(got.Blocks)) 63 | for _, gotBlock := range got.Blocks { 64 | new := *gotBlock // shallow copy 65 | new.Body = unknownBody{gotBlock.Body} // nested content must also be marked unknown 66 | ret.Blocks = append(ret.Blocks, &new) 67 | } 68 | } 69 | 70 | return ret 71 | } 72 | 73 | func (b unknownBody) fixupAttrs(got hcl.Attributes) hcl.Attributes { 74 | if len(got) == 0 { 75 | return nil 76 | } 77 | ret := make(hcl.Attributes, len(got)) 78 | for name, gotAttr := range got { 79 | new := *gotAttr // shallow copy 80 | new.Expr = hcl.StaticExpr(cty.DynamicVal, gotAttr.Expr.Range()) 81 | ret[name] = &new 82 | } 83 | return ret 84 | } 85 | -------------------------------------------------------------------------------- /extras/grammar/HCLExpression.yaml-tmLanguage: -------------------------------------------------------------------------------- 1 | name: HCL Expression 2 | scopeName: source.hclexpr 3 | fileTypes: [] 4 | uuid: 6c358551-0381-4128-9ea3-277b21943b5c 5 | 6 | patterns: 7 | 8 | - comment: Comments 9 | name: comment.line.hcl 10 | begin: '#|//' 11 | end: $\n? 12 | captures: 13 | '0': {name: punctuation.definition.comment.hcl} 14 | 15 | - comment: Block comments 16 | name: comment.block.hcl 17 | begin: /\* 18 | end: \*/ 19 | captures: 20 | '0': {name: punctuation.definition.comment.hcl} 21 | 22 | - comment: Language constants (true, false, null) 23 | name: constant.language.hcl 24 | match: \b(true|false|null)\b 25 | 26 | - comment: Numbers 27 | name: constant.numeric.hcl 28 | match: \b([0-9]+)(.[0-9]+)?([eE][0-9]+)?\b 29 | 30 | - comment: Function Calls 31 | begin: ([-\w]+)(\() 32 | beginCaptures: 33 | '1': {name: keyword.other.function.inline.hcl} 34 | '2': {name: keyword.other.section.begin.hcl} 35 | end: (\)) 36 | endCaptures: 37 | '1': {name: keyword.other.section.end.hcl} 38 | patterns: 39 | - include: '$self' 40 | 41 | - comment: Variables and Attribute Names 42 | match: '[-\w]+' 43 | captures: 44 | '0': {name: variable.other.hcl} 45 | 46 | - comment: Heredoc Templates 47 | begin: (?>\s*<<(\w+)) 48 | beginCaptures: 49 | '0': {name: punctuation.definition.string.begin.hcl} 50 | '1': {name: keyword.operator.heredoc.hcl} 51 | end: ^\s*\1$ 52 | endCaptures: 53 | '0': {name: punctuation.definition.string.end.hcl keyword.operator.heredoc.hcl} 54 | patterns: 55 | - include: 'source.hcltemplate' 56 | 57 | - comment: String Templates 58 | begin: \" 59 | beginCaptures: 60 | '0': {name: string.hcl punctuation.definition.string.begin.hcl} 61 | end: \" 62 | endCaptures: 63 | '0': {name: string.hcl punctuation.definition.string.end.hcl} 64 | patterns: 65 | - include: 'source.hcltemplate' 66 | - match: '(^"|$\{|%\{)+' 67 | name: "string.quoted.double.hcl" 68 | 69 | - comment: Operators 70 | match: '(!=|==|>=|<=|&&|\|\||[-+*/%<>!?:])' 71 | captures: 72 | '0': {name: keyword.operator.hcl} 73 | 74 | - comment: Parentheses 75 | begin: '\(' 76 | beginCaptures: 77 | '0': {name: meta.brace.round.hcl} 78 | end: '\)' 79 | endCaptures: 80 | '0': {name: meta.brace.round.hcl} 81 | patterns: 82 | - include: '$self' 83 | 84 | - comment: Tuple Constructor 85 | begin: '\[' 86 | beginCaptures: 87 | '0': {name: meta.brace.square.hcl} 88 | end: '\]' 89 | endCaptures: 90 | '0': {name: meta.brace.square.hcl} 91 | patterns: 92 | - match: '(for|in)' 93 | captures: 94 | '0': {name: keyword.control.hcl} 95 | - include: '$self' 96 | 97 | - comment: Object Constructor 98 | begin: '\{' 99 | beginCaptures: 100 | '0': {name: meta.brace.curly.hcl} 101 | end: '\}' 102 | endCaptures: 103 | '0': {name: meta.brace.curly.hcl} 104 | patterns: 105 | - match: '(for|in)' 106 | captures: 107 | '0': {name: keyword.control.hcl} 108 | - match: '(=>|\.\.\.)' 109 | captures: 110 | '0': {name: keyword.operator.hcl} 111 | - include: '$self' 112 | -------------------------------------------------------------------------------- /ext/transform/transform.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | ) 6 | 7 | // Shallow is equivalent to calling transformer.TransformBody(body), and 8 | // is provided only for completeness of the top-level API. 9 | func Shallow(body hcl.Body, transformer Transformer) hcl.Body { 10 | return transformer.TransformBody(body) 11 | } 12 | 13 | // Deep applies the given transform to the given body and then 14 | // wraps the result such that any descendent blocks that are decoded will 15 | // also have the transform applied to their bodies. 16 | // 17 | // This allows for language extensions that define a particular block type 18 | // for a particular body and all nested blocks within it. 19 | // 20 | // Due to the wrapping behavior, the body resulting from this function 21 | // will not be of the type returned by the transformer. Callers may call 22 | // only the methods defined for interface hcl.Body, and may not type-assert 23 | // to access other methods. 24 | func Deep(body hcl.Body, transformer Transformer) hcl.Body { 25 | return deepWrapper{ 26 | Transformed: transformer.TransformBody(body), 27 | Transformer: transformer, 28 | } 29 | } 30 | 31 | // deepWrapper is a hcl.Body implementation that ensures that a given 32 | // transformer is applied to another given body when content is extracted, 33 | // and that it recursively applies to any child blocks that are extracted. 34 | type deepWrapper struct { 35 | Transformed hcl.Body 36 | Transformer Transformer 37 | } 38 | 39 | func (w deepWrapper) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { 40 | content, diags := w.Transformed.Content(schema) 41 | content = w.transformContent(content) 42 | return content, diags 43 | } 44 | 45 | func (w deepWrapper) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { 46 | content, remain, diags := w.Transformed.PartialContent(schema) 47 | content = w.transformContent(content) 48 | return content, remain, diags 49 | } 50 | 51 | func (w deepWrapper) transformContent(content *hcl.BodyContent) *hcl.BodyContent { 52 | if len(content.Blocks) == 0 { 53 | // Easy path: if there are no blocks then there are no child bodies to wrap 54 | return content 55 | } 56 | 57 | // Since we're going to change things here, we'll be polite and clone the 58 | // structure so that we don't risk impacting any internal state of the 59 | // original body. 60 | ret := &hcl.BodyContent{ 61 | Attributes: content.Attributes, 62 | MissingItemRange: content.MissingItemRange, 63 | Blocks: make(hcl.Blocks, len(content.Blocks)), 64 | } 65 | 66 | for i, givenBlock := range content.Blocks { 67 | // Shallow-copy the block so we can mutate it 68 | newBlock := *givenBlock 69 | newBlock.Body = Deep(newBlock.Body, w.Transformer) 70 | ret.Blocks[i] = &newBlock 71 | } 72 | 73 | return ret 74 | } 75 | 76 | func (w deepWrapper) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { 77 | // Attributes can't have bodies or nested blocks, so this is just a thin wrapper. 78 | return w.Transformed.JustAttributes() 79 | } 80 | 81 | func (w deepWrapper) MissingItemRange() hcl.Range { 82 | return w.Transformed.MissingItemRange() 83 | } 84 | -------------------------------------------------------------------------------- /ext/include/transformer.go: -------------------------------------------------------------------------------- 1 | package include 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/ext/transform" 5 | "github.com/hashicorp/hcl2/gohcl" 6 | "github.com/hashicorp/hcl2/hcl" 7 | ) 8 | 9 | // Transformer builds a transformer that finds any "include" blocks in a body 10 | // and produces a merged body that contains the original content plus the 11 | // content of the other bodies referenced by the include blocks. 12 | // 13 | // blockType specifies the type of block to interpret. The conventional type name 14 | // is "include". 15 | // 16 | // ctx provides an evaluation context for the path expressions in include blocks. 17 | // If nil, path expressions may not reference variables nor functions. 18 | // 19 | // The given resolver is used to translate path strings (after expression 20 | // evaluation) into bodies. FileResolver returns a reasonable implementation for 21 | // applications that read configuration files from local disk. 22 | // 23 | // The returned Transformer can either be used directly to process includes 24 | // in a shallow fashion on a single body, or it can be used with 25 | // transform.Deep (from the sibling transform package) to allow includes 26 | // at all levels of a nested block structure: 27 | // 28 | // transformer = include.Transformer("include", nil, include.FileResolver(".", parser)) 29 | // body = transform.Deep(body, transformer) 30 | // // "body" will now have includes resolved in its own content and that 31 | // // of any descendent blocks. 32 | // 33 | func Transformer(blockType string, ctx *hcl.EvalContext, resolver Resolver) transform.Transformer { 34 | return &transformer{ 35 | Schema: &hcl.BodySchema{ 36 | Blocks: []hcl.BlockHeaderSchema{ 37 | { 38 | Type: blockType, 39 | }, 40 | }, 41 | }, 42 | Ctx: ctx, 43 | Resolver: resolver, 44 | } 45 | } 46 | 47 | type transformer struct { 48 | Schema *hcl.BodySchema 49 | Ctx *hcl.EvalContext 50 | Resolver Resolver 51 | } 52 | 53 | func (t *transformer) TransformBody(in hcl.Body) hcl.Body { 54 | content, remain, diags := in.PartialContent(t.Schema) 55 | 56 | if content == nil || len(content.Blocks) == 0 { 57 | // Nothing to do! 58 | return transform.BodyWithDiagnostics(remain, diags) 59 | } 60 | 61 | bodies := make([]hcl.Body, 1, len(content.Blocks)+1) 62 | bodies[0] = remain // content in "remain" takes priority over includes 63 | for _, block := range content.Blocks { 64 | incContent, incDiags := block.Body.Content(includeBlockSchema) 65 | diags = append(diags, incDiags...) 66 | if incDiags.HasErrors() { 67 | continue 68 | } 69 | 70 | pathExpr := incContent.Attributes["path"].Expr 71 | var path string 72 | incDiags = gohcl.DecodeExpression(pathExpr, t.Ctx, &path) 73 | diags = append(diags, incDiags...) 74 | if incDiags.HasErrors() { 75 | continue 76 | } 77 | 78 | incBody, incDiags := t.Resolver.ResolveBodyPath(path, pathExpr.Range()) 79 | bodies = append(bodies, transform.BodyWithDiagnostics(incBody, incDiags)) 80 | } 81 | 82 | return hcl.MergeBodies(bodies) 83 | } 84 | 85 | var includeBlockSchema = &hcl.BodySchema{ 86 | Attributes: []hcl.AttributeSchema{ 87 | { 88 | Name: "path", 89 | Required: true, 90 | }, 91 | }, 92 | } 93 | -------------------------------------------------------------------------------- /hclpack/example_test.go: -------------------------------------------------------------------------------- 1 | package hclpack_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/hashicorp/hcl2/hcl" 10 | "github.com/hashicorp/hcl2/hclpack" 11 | ) 12 | 13 | func Example_marshalJSON() { 14 | src := ` 15 | service "example" { 16 | priority = 2 17 | platform { 18 | os = "linux" 19 | arch = "amd64" 20 | } 21 | process "web" { 22 | exec = ["./webapp"] 23 | } 24 | process "worker" { 25 | exec = ["./worker"] 26 | } 27 | } 28 | ` 29 | 30 | body, diags := hclpack.PackNativeFile([]byte(src), "example.svc", hcl.Pos{Line: 1, Column: 1}) 31 | if diags.HasErrors() { 32 | fmt.Fprintf(os.Stderr, "Failed to parse: %s", diags.Error()) 33 | return 34 | } 35 | 36 | jb, err := body.MarshalJSON() 37 | if err != nil { 38 | fmt.Fprintf(os.Stderr, "Failed to marshal: %s", err) 39 | return 40 | } 41 | 42 | // Normally the compact form is best, but we'll indent just for the sake 43 | // of this example so the result is readable. 44 | var buf bytes.Buffer 45 | json.Indent(&buf, jb, "", " ") 46 | os.Stdout.Write(buf.Bytes()) 47 | 48 | // Output: 49 | // { 50 | // "r": { 51 | // "b": [ 52 | // { 53 | // "h": [ 54 | // "service", 55 | // "example" 56 | // ], 57 | // "b": { 58 | // "a": { 59 | // "priority": { 60 | // "s": "2", 61 | // "r": "ChAKDA4QDhA" 62 | // } 63 | // }, 64 | // "b": [ 65 | // { 66 | // "h": [ 67 | // "platform" 68 | // ], 69 | // "b": { 70 | // "a": { 71 | // "arch": { 72 | // "s": "\"amd64\"", 73 | // "r": "IiwiJCYsKCo" 74 | // }, 75 | // "os": { 76 | // "s": "\"linux\"", 77 | // "r": "FiAWGBogHB4" 78 | // } 79 | // }, 80 | // "r": "Li4" 81 | // }, 82 | // "r": "EhQSFA" 83 | // }, 84 | // { 85 | // "h": [ 86 | // "process", 87 | // "web" 88 | // ], 89 | // "b": { 90 | // "a": { 91 | // "exec": { 92 | // "s": "[\"./webapp\"]", 93 | // "r": "OEA4OjxAPD4" 94 | // } 95 | // }, 96 | // "r": "QkI" 97 | // }, 98 | // "r": "MDYwMjQ2" 99 | // }, 100 | // { 101 | // "h": [ 102 | // "process", 103 | // "worker" 104 | // ], 105 | // "b": { 106 | // "a": { 107 | // "exec": { 108 | // "s": "[\"./worker\"]", 109 | // "r": "TFRMTlBUUFI" 110 | // } 111 | // }, 112 | // "r": "VlY" 113 | // }, 114 | // "r": "REpERkhK" 115 | // } 116 | // ], 117 | // "r": "WFg" 118 | // }, 119 | // "r": "AggCBAYI" 120 | // } 121 | // ], 122 | // "r": "Wlo" 123 | // }, 124 | // "s": [ 125 | // "example.svc" 126 | // ], 127 | // "p": "BAQEAA4OAAICABISAggMABAQAAYGAAICAggIABAQAgYKAAQEAAoKAAICAAoKAAICAgYGAAgIAAYGAAICAAoKAAICAgoKAggIAA4OAAICAAoKAgwQAAgIAAYGAAICABYWAgoKAggIAA4OAAICABAQAgwQAAgIAAYGAAICABYWAgoKAgYGAgQE" 128 | // } 129 | } 130 | -------------------------------------------------------------------------------- /hcldec/public.go: -------------------------------------------------------------------------------- 1 | package hcldec 2 | 3 | import ( 4 | "github.com/hashicorp/hcl2/hcl" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | // Decode interprets the given body using the given specification and returns 9 | // the resulting value. If the given body is not valid per the spec, error 10 | // diagnostics are returned and the returned value is likely to be incomplete. 11 | // 12 | // The ctx argument may be nil, in which case any references to variables or 13 | // functions will produce error diagnostics. 14 | func Decode(body hcl.Body, spec Spec, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { 15 | val, _, diags := decode(body, nil, ctx, spec, false) 16 | return val, diags 17 | } 18 | 19 | // PartialDecode is like Decode except that it permits "leftover" items in 20 | // the top-level body, which are returned as a new body to allow for 21 | // further processing. 22 | // 23 | // Any descendent block bodies are _not_ decoded partially and thus must 24 | // be fully described by the given specification. 25 | func PartialDecode(body hcl.Body, spec Spec, ctx *hcl.EvalContext) (cty.Value, hcl.Body, hcl.Diagnostics) { 26 | return decode(body, nil, ctx, spec, true) 27 | } 28 | 29 | // ImpliedType returns the value type that should result from decoding the 30 | // given spec. 31 | func ImpliedType(spec Spec) cty.Type { 32 | return impliedType(spec) 33 | } 34 | 35 | // SourceRange interprets the given body using the given specification and 36 | // then returns the source range of the value that would be used to 37 | // fulfill the spec. 38 | // 39 | // This can be used if application-level validation detects value errors, to 40 | // obtain a reasonable SourceRange to use for generated diagnostics. It works 41 | // best when applied to specific body items (e.g. using AttrSpec, BlockSpec, ...) 42 | // as opposed to entire bodies using ObjectSpec, TupleSpec. The result will 43 | // be less useful the broader the specification, so e.g. a spec that returns 44 | // the entirety of all of the blocks of a given type is likely to be 45 | // _particularly_ arbitrary and useless. 46 | // 47 | // If the given body is not valid per the given spec, the result is best-effort 48 | // and may not actually be something ideal. It's expected that an application 49 | // will already have used Decode or PartialDecode earlier and thus had an 50 | // opportunity to detect and report spec violations. 51 | func SourceRange(body hcl.Body, spec Spec) hcl.Range { 52 | return sourceRange(body, nil, spec) 53 | } 54 | 55 | // ChildBlockTypes returns a map of all of the child block types declared 56 | // by the given spec, with block type names as keys and the associated 57 | // nested body specs as values. 58 | func ChildBlockTypes(spec Spec) map[string]Spec { 59 | ret := map[string]Spec{} 60 | 61 | // visitSameBodyChildren walks through the spec structure, calling 62 | // the given callback for each descendent spec encountered. We are 63 | // interested in the specs that reference attributes and blocks. 64 | var visit visitFunc 65 | visit = func(s Spec) { 66 | if bs, ok := s.(blockSpec); ok { 67 | for _, blockS := range bs.blockHeaderSchemata() { 68 | nested := bs.nestedSpec() 69 | if nested != nil { // nil can be returned to dynamically opt out of this interface 70 | ret[blockS.Type] = nested 71 | } 72 | } 73 | } 74 | 75 | s.visitSameBodyChildren(visit) 76 | } 77 | 78 | visit(spec) 79 | 80 | return ret 81 | } 82 | -------------------------------------------------------------------------------- /hcl/hclsyntax/peeker_test.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func init() { 9 | // see the documentation of this variable for more information 10 | tracePeekerNewlinesStack = true 11 | } 12 | 13 | func TestPeeker(t *testing.T) { 14 | tokens := Tokens{ 15 | { 16 | Type: TokenIdent, 17 | }, 18 | { 19 | Type: TokenComment, 20 | }, 21 | { 22 | Type: TokenIdent, 23 | }, 24 | { 25 | Type: TokenComment, 26 | }, 27 | { 28 | Type: TokenIdent, 29 | }, 30 | { 31 | Type: TokenNewline, 32 | }, 33 | { 34 | Type: TokenIdent, 35 | }, 36 | { 37 | Type: TokenNewline, 38 | }, 39 | { 40 | Type: TokenIdent, 41 | }, 42 | { 43 | Type: TokenNewline, 44 | }, 45 | { 46 | Type: TokenEOF, 47 | }, 48 | } 49 | 50 | { 51 | peeker := newPeeker(tokens, true) 52 | 53 | wantTypes := []TokenType{ 54 | TokenIdent, 55 | TokenComment, 56 | TokenIdent, 57 | TokenComment, 58 | TokenIdent, 59 | TokenNewline, 60 | TokenIdent, 61 | TokenNewline, 62 | TokenIdent, 63 | TokenNewline, 64 | TokenEOF, 65 | } 66 | var gotTypes []TokenType 67 | 68 | for { 69 | peeked := peeker.Peek() 70 | read := peeker.Read() 71 | if peeked.Type != read.Type { 72 | t.Errorf("mismatched Peek %s and Read %s", peeked, read) 73 | } 74 | 75 | gotTypes = append(gotTypes, read.Type) 76 | 77 | if read.Type == TokenEOF { 78 | break 79 | } 80 | } 81 | 82 | if !reflect.DeepEqual(gotTypes, wantTypes) { 83 | t.Errorf("wrong types\ngot: %#v\nwant: %#v", gotTypes, wantTypes) 84 | } 85 | } 86 | 87 | { 88 | peeker := newPeeker(tokens, false) 89 | 90 | wantTypes := []TokenType{ 91 | TokenIdent, 92 | TokenIdent, 93 | TokenIdent, 94 | TokenNewline, 95 | TokenIdent, 96 | TokenNewline, 97 | TokenIdent, 98 | TokenNewline, 99 | TokenEOF, 100 | } 101 | var gotTypes []TokenType 102 | 103 | for { 104 | peeked := peeker.Peek() 105 | read := peeker.Read() 106 | if peeked.Type != read.Type { 107 | t.Errorf("mismatched Peek %s and Read %s", peeked, read) 108 | } 109 | 110 | gotTypes = append(gotTypes, read.Type) 111 | 112 | if read.Type == TokenEOF { 113 | break 114 | } 115 | } 116 | 117 | if !reflect.DeepEqual(gotTypes, wantTypes) { 118 | t.Errorf("wrong types\ngot: %#v\nwant: %#v", gotTypes, wantTypes) 119 | } 120 | } 121 | 122 | { 123 | peeker := newPeeker(tokens, false) 124 | 125 | peeker.PushIncludeNewlines(false) 126 | 127 | wantTypes := []TokenType{ 128 | TokenIdent, 129 | TokenIdent, 130 | TokenIdent, 131 | TokenIdent, 132 | TokenIdent, 133 | TokenNewline, // we'll pop off the PushIncludeNewlines before we get here 134 | TokenEOF, 135 | } 136 | var gotTypes []TokenType 137 | 138 | idx := 0 139 | for { 140 | peeked := peeker.Peek() 141 | read := peeker.Read() 142 | if peeked.Type != read.Type { 143 | t.Errorf("mismatched Peek %s and Read %s", peeked, read) 144 | } 145 | 146 | gotTypes = append(gotTypes, read.Type) 147 | 148 | if read.Type == TokenEOF { 149 | break 150 | } 151 | 152 | if idx == 4 { 153 | peeker.PopIncludeNewlines() 154 | } 155 | 156 | idx++ 157 | } 158 | 159 | if !reflect.DeepEqual(gotTypes, wantTypes) { 160 | t.Errorf("wrong types\ngot: %#v\nwant: %#v", gotTypes, wantTypes) 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /cmd/hcldec/README.md: -------------------------------------------------------------------------------- 1 | # hcldec 2 | 3 | `hcldec` is a command line tool that transforms HCL input into JSON output 4 | using a decoding specification given by the user. 5 | 6 | This tool is intended as a "glue" tool, with use-cases like the following: 7 | 8 | * Define a HCL-based configuration format for a third-party tool that takes 9 | JSON as input, and then translate the HCL configuration into JSON before 10 | running the tool. (See [the `npm-package` example](examples/npm-package).) 11 | 12 | * Use HCL from languages where a HCL parser/decoder is not yet available. 13 | At the time of writing, that's any language other than Go. 14 | 15 | * In particular, define a HCL-based configuration format for a shell script 16 | and then use `jq` to load the result into environment variables for 17 | further processing. (See [the `sh-config-file` example](examples/sh-config-file).) 18 | 19 | ## Installation 20 | 21 | If you have a working Go development environment, you can install this tool 22 | with `go get` in the usual way: 23 | 24 | ``` 25 | $ go get -u github.com/hashicorp/hcl2/cmd/hcldec 26 | ``` 27 | 28 | This will install `hcldec` in `$GOPATH/bin`, which usually places it into 29 | your shell `PATH` so you can then run it as `hcldec`. 30 | 31 | ## Usage 32 | 33 | ``` 34 | usage: hcldec --spec= [options] [hcl-file ...] 35 | -o, --out string write to the given file, instead of stdout 36 | -s, --spec string path to spec file (required) 37 | -V, --vars json-or-file provide variables to the given configuration file(s) 38 | -v, --version show the version number and immediately exit 39 | ``` 40 | 41 | The most important step in using `hcldec` is to write the specification that 42 | defines how to interpret the given configuration files and translate them 43 | into JSON. The following is a simple specification that creates a JSON 44 | object from two top-level attributes in the input configuration: 45 | 46 | ```hcl 47 | object { 48 | attr "name" { 49 | type = string 50 | required = true 51 | } 52 | attr "is_member" { 53 | type = bool 54 | } 55 | } 56 | ``` 57 | 58 | Specification files are conventionally kept in files with a `.hcldec` 59 | extension. We'll call this one `example.hcldec`. 60 | 61 | With the above specification, the following input file `example.conf` is 62 | valid: 63 | 64 | ```hcl 65 | name = "Raul" 66 | ``` 67 | 68 | The spec and the input file can then be provided to `hcldec` to extract a 69 | JSON representation: 70 | 71 | ``` 72 | $ hcldec --spec=example.hcldec example.conf 73 | {"name": "Raul"} 74 | ``` 75 | 76 | The specification defines both how to map the input into a JSON data structure 77 | and what input is valid. The `required = true` specified for the `name` 78 | allows `hcldec` to detect and raise an error when an attribute of that name 79 | is not provided: 80 | 81 | ``` 82 | $ hcldec --spec=example.hcldec typo.conf 83 | Error: Unsupported attribute 84 | 85 | on example.conf line 1: 86 | 1: namme = "Juan" 87 | 88 | An attribute named "namme" is not expected here. Did you mean "name"? 89 | 90 | Error: Missing required attribute 91 | 92 | on example.conf line 2: 93 | 94 | The attribute "name" is required, but no definition was found. 95 | ``` 96 | 97 | ## Further Reading 98 | 99 | For more details on the `.hcldec` specification file format, see 100 | [the spec file documentation](spec-format.md). 101 | -------------------------------------------------------------------------------- /hcl/hclsyntax/scan_string_lit.rl: -------------------------------------------------------------------------------- 1 | 2 | package hclsyntax 3 | 4 | // This file is generated from scan_string_lit.rl. DO NOT EDIT. 5 | %%{ 6 | # (except you are actually in scan_string_lit.rl here, so edit away!) 7 | 8 | machine hclstrtok; 9 | write data; 10 | }%% 11 | 12 | func scanStringLit(data []byte, quoted bool) [][]byte { 13 | var ret [][]byte 14 | 15 | %%{ 16 | include UnicodeDerived "unicode_derived.rl"; 17 | 18 | UTF8Cont = 0x80 .. 0xBF; 19 | AnyUTF8 = ( 20 | 0x00..0x7F | 21 | 0xC0..0xDF . UTF8Cont | 22 | 0xE0..0xEF . UTF8Cont . UTF8Cont | 23 | 0xF0..0xF7 . UTF8Cont . UTF8Cont . UTF8Cont 24 | ); 25 | BadUTF8 = any - AnyUTF8; 26 | 27 | Hex = ('0'..'9' | 'a'..'f' | 'A'..'F'); 28 | 29 | # Our goal with this patterns is to capture user intent as best as 30 | # possible, even if the input is invalid. The caller will then verify 31 | # whether each token is valid and generate suitable error messages 32 | # if not. 33 | UnicodeEscapeShort = "\\u" . Hex{0,4}; 34 | UnicodeEscapeLong = "\\U" . Hex{0,8}; 35 | UnicodeEscape = (UnicodeEscapeShort | UnicodeEscapeLong); 36 | SimpleEscape = "\\" . (AnyUTF8 - ('U'|'u'))?; 37 | TemplateEscape = ("$" . ("$" . ("{"?))?) | ("%" . ("%" . ("{"?))?); 38 | Newline = ("\r\n" | "\r" | "\n"); 39 | 40 | action Begin { 41 | // If te is behind p then we've skipped over some literal 42 | // characters which we must now return. 43 | if te < p { 44 | ret = append(ret, data[te:p]) 45 | } 46 | ts = p; 47 | } 48 | action End { 49 | te = p; 50 | ret = append(ret, data[ts:te]); 51 | } 52 | 53 | QuotedToken = (UnicodeEscape | SimpleEscape | TemplateEscape | Newline) >Begin %End; 54 | UnquotedToken = (TemplateEscape | Newline) >Begin %End; 55 | QuotedLiteral = (any - ("\\" | "$" | "%" | "\r" | "\n")); 56 | UnquotedLiteral = (any - ("$" | "%" | "\r" | "\n")); 57 | 58 | quoted := (QuotedToken | QuotedLiteral)**; 59 | unquoted := (UnquotedToken | UnquotedLiteral)**; 60 | 61 | }%% 62 | 63 | // Ragel state 64 | p := 0 // "Pointer" into data 65 | pe := len(data) // End-of-data "pointer" 66 | ts := 0 67 | te := 0 68 | eof := pe 69 | 70 | var cs int // current state 71 | switch { 72 | case quoted: 73 | cs = hclstrtok_en_quoted 74 | default: 75 | cs = hclstrtok_en_unquoted 76 | } 77 | 78 | // Make Go compiler happy 79 | _ = ts 80 | _ = eof 81 | 82 | /*token := func () { 83 | ret = append(ret, data[ts:te]) 84 | }*/ 85 | 86 | %%{ 87 | write init nocs; 88 | write exec; 89 | }%% 90 | 91 | if te < p { 92 | // Collect any leftover literal characters at the end of the input 93 | ret = append(ret, data[te:p]) 94 | } 95 | 96 | // If we fall out here without being in a final state then we've 97 | // encountered something that the scanner can't match, which should 98 | // be impossible (the scanner matches all bytes _somehow_) but we'll 99 | // tolerate it and let the caller deal with it. 100 | if cs < hclstrtok_first_final { 101 | ret = append(ret, data[p:len(data)]) 102 | } 103 | 104 | return ret 105 | } 106 | -------------------------------------------------------------------------------- /hcl/hclsyntax/navigation_test.go: -------------------------------------------------------------------------------- 1 | package hclsyntax 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/hashicorp/hcl2/hcl" 9 | ) 10 | 11 | func TestNavigationContextString(t *testing.T) { 12 | cfg := ` 13 | 14 | 15 | resource { 16 | } 17 | 18 | resource "random_type" { 19 | } 20 | 21 | resource "null_resource" "baz" { 22 | name = "foo" 23 | boz = { 24 | one = "111" 25 | two = "22222" 26 | } 27 | } 28 | 29 | data "another" "baz" { 30 | name = "foo" 31 | boz = { 32 | one = "111" 33 | two = "22222" 34 | } 35 | } 36 | ` 37 | file, diags := ParseConfig([]byte(cfg), "", hcl.Pos{Byte: 0, Line: 1, Column: 1}) 38 | if len(diags) != 0 { 39 | fmt.Printf("offset %d\n", diags[0].Subject.Start.Byte) 40 | t.Errorf("Unexpected diagnostics: %s", diags) 41 | } 42 | if file == nil { 43 | t.Fatalf("Got nil file") 44 | } 45 | nav := file.Nav.(navigation) 46 | 47 | testCases := []struct { 48 | Offset int 49 | Want string 50 | }{ 51 | {0, ``}, 52 | {2, ``}, 53 | {4, `resource`}, 54 | {17, `resource "random_type"`}, 55 | {25, `resource "random_type"`}, 56 | {45, `resource "null_resource" "baz"`}, 57 | {142, `data "another" "baz"`}, 58 | {180, `data "another" "baz"`}, 59 | {99999, ``}, 60 | } 61 | 62 | for _, tc := range testCases { 63 | t.Run(strconv.Itoa(tc.Offset), func(t *testing.T) { 64 | got := nav.ContextString(tc.Offset) 65 | 66 | if got != tc.Want { 67 | t.Errorf("wrong result\ngot: %s\nwant: %s", got, tc.Want) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestNavigationContextDefRange(t *testing.T) { 74 | cfg := ` 75 | 76 | 77 | resource { 78 | } 79 | 80 | resource "random_type" { 81 | } 82 | 83 | resource "null_resource" "baz" { 84 | name = "foo" 85 | boz = { 86 | one = "111" 87 | two = "22222" 88 | } 89 | } 90 | 91 | data "another" "baz" { 92 | name = "foo" 93 | boz = { 94 | one = "111" 95 | two = "22222" 96 | } 97 | } 98 | ` 99 | file, diags := ParseConfig([]byte(cfg), "", hcl.Pos{Byte: 0, Line: 1, Column: 1}) 100 | if len(diags) != 0 { 101 | fmt.Printf("offset %d\n", diags[0].Subject.Start.Byte) 102 | t.Errorf("Unexpected diagnostics: %s", diags) 103 | } 104 | if file == nil { 105 | t.Fatalf("Got nil file") 106 | } 107 | nav := file.Nav.(navigation) 108 | 109 | testCases := []struct { 110 | Offset int 111 | WantRange hcl.Range 112 | }{ 113 | {0, hcl.Range{}}, 114 | {2, hcl.Range{}}, 115 | {4, hcl.Range{Filename: "", Start: hcl.Pos{Line: 4, Column: 1, Byte: 3}, End: hcl.Pos{Line: 4, Column: 11, Byte: 13}}}, 116 | {17, hcl.Range{Filename: "", Start: hcl.Pos{Line: 7, Column: 1, Byte: 17}, End: hcl.Pos{Line: 7, Column: 25, Byte: 41}}}, 117 | {25, hcl.Range{Filename: "", Start: hcl.Pos{Line: 7, Column: 1, Byte: 17}, End: hcl.Pos{Line: 7, Column: 25, Byte: 41}}}, 118 | {45, hcl.Range{Filename: "", Start: hcl.Pos{Line: 10, Column: 1, Byte: 45}, End: hcl.Pos{Line: 10, Column: 33, Byte: 77}}}, 119 | {142, hcl.Range{Filename: "", Start: hcl.Pos{Line: 18, Column: 1, Byte: 142}, End: hcl.Pos{Line: 18, Column: 23, Byte: 164}}}, 120 | {180, hcl.Range{Filename: "", Start: hcl.Pos{Line: 18, Column: 1, Byte: 142}, End: hcl.Pos{Line: 18, Column: 23, Byte: 164}}}, 121 | {99999, hcl.Range{}}, 122 | } 123 | 124 | for _, tc := range testCases { 125 | t.Run(strconv.Itoa(tc.Offset), func(t *testing.T) { 126 | got := nav.ContextDefRange(tc.Offset) 127 | 128 | if got != tc.WantRange { 129 | t.Errorf("wrong range\ngot: %#v\nwant: %#v", got, tc.WantRange) 130 | } 131 | }) 132 | } 133 | } 134 | --------------------------------------------------------------------------------