%YAML 1.2
---
name: Terraform
scope: source.terraform
version: 2
file_extensions:
- tf
- nomad
variables:
ID_Start: '[\p{ID_Start}_]'
ID_Continue: '[\p{ID_Continue}-]'
identifier: (?:{{ID_Start}}{{ID_Continue}}*)
exponent: ([Ee][+-]?)
char_escapes: \\[nrt"\\]|\\u(\h{8}|\h{4})
string_literal: '"(?:[^"]|{{char_escapes}})*"'
template_literal: '(?:\$\$|%%)\{'
label: (?:{{identifier}}|{{string_literal}})
named_values: (?:var|local|module|data|path|terraform|each|count|self)
terraform_typed_and_named_blocks: (?:data|ephemeral|resource)
terraform_named_blocks: (?:module|output|provider|variable)
terraform_other_blocks: (?:locals|terraform)
terraform_type_keywords: (?:any|string|number|bool)
builtin_functions: |-
(?x:
# numbers
abs | ceil | floor | log | max | min | parseint | pow | signum
# string
| chomp | endswith | format | formatlist | indent | join | lower | regex
| regexall | replace | split | startswith | strcontains | strrev | substr
| templatestring | title | trim | trimprefix | trimsuffix | trimspace | upper
# collection
| alltrue | anytrue | chunklist | coalesce | coalescelist | compact | concat
| contains | distinct | element | flatten | index | keys | length | list
| lookup | map | matchkeys | merge | one | range | reverse | setintersection
| setproduct | setsubtract | setunion | slice | sort | sum | transpose
| values | zipmap
# encoding
| base64decode | base64encode | base64gzip | csvdecode | jsondecode
| jsonencode | textdecodebase64 | textencodebase64 | urlencode | yamldecode
| yamlencode
# filesystem
| abspath | dirname | pathexpand | basename | file | fileexists | fileset
| filebase64 | templatefile
# date and time
| formatdate | plantimestamp | timeadd | timecmp | timestamp
# hash and crypto
| base64sha256 | base64sha512 | bcrypt | filebase64sha256
| filebase64sha512 | filemd5 | filesha1 | filesha256 | filesha512 | md5
| rsadecrypt | sha1 | sha256 | sha512 | uuid | uuidv5
# ip network
| cidrhost | cidrnetmask | cidrsubnet | cidrsubnets
# type conversion
| can | issensitive | nonsensitive | sensitive | tobool | tolist | tomap
| tonumber | toset | tostring | try | type
# terraform-specific
| provider::terraform::(?:encode_tfvars | decode_tfvars | encode_expr)
# deprecated/old
| filemd1
)
zero_to_32: (?:3[0-2]|[12][0-9]|[0-9])
zero_to_128: (?:12[0-8]|1[01][0-9]|[1-9][0-9]|[0-9])
zero_to_255: (?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9][0-9])|(?:[1-9][0-9])|[0-9])
zero_to_65535: (?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])
ipv4: (?:(?:{{zero_to_255}}\.){3}{{zero_to_255}})
ipv6: |-
(?xi:
(?:::(?:ffff(?::0{1,4}){0,1}:){0,1}{{ipv4}}) # ::255.255.255.255 ::ffff:255.255.255.255 ::ffff:0:255.255.255.255 (IPv4-mapped IPv6 addresses and IPv4-translated addresses)
|(?:(?:[0-9a-f]{1,4}:){1,4}:{{ipv4}}) # 2001:db8:3:4::192.0.2.33 64:ff9b::192.0.2.33 (IPv4-Embedded IPv6 Address)
|(?:fe80:(?::[0-9a-f]{1,4}){0,4}%[0-9a-z]{1,}) # fe80::7:8%eth0 fe80::7:8%1 (link-local IPv6 addresses with zone index)
|(?:(?:[0-9a-f]{1,4}:){7,7} [0-9a-f]{1,4}) # 1:2:3:4:5:6:7:8
| (?:[0-9a-f]{1,4}: (?::[0-9a-f]{1,4}){1,6}) # 1::3:4:5:6:7:8 1::3:4:5:6:7:8 1::8
|(?:(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5}) # 1::4:5:6:7:8 1:2::4:5:6:7:8 1:2::8
|(?:(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4}) # 1::5:6:7:8 1:2:3::5:6:7:8 1:2:3::8
|(?:(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3}) # 1::6:7:8 1:2:3:4::6:7:8 1:2:3:4::8
|(?:(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2}) # 1::7:8 1:2:3:4:5::7:8 1:2:3:4:5::8
|(?:(?:[0-9a-f]{1,4}:){1,6} :[0-9a-f]{1,4}) # 1::8 1:2:3:4:5:6::8 1:2:3:4:5:6::8
|(?:(?:[0-9a-f]{1,4}:){1,7} :) # 1:: 1:2:3:4:5:6:7::
|(?::(?:(?::[0-9a-f]{1,4}){1,7}|:)) # ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::
)
contexts:
prototype:
- include: comments
main:
- include: attribute-definition
- include: imports
- include: blocks
- include: expressions
comments:
- include: inline-comments
- include: block-comments
expressions:
- include: literals
- include: operators
- include: brackets
- include: objects
- include: attribute-access
- include: functions
- include: parens
- include: identifiers
- include: illegal-strays
comma:
- match: \,
comment: Commas - used in certain expressions
scope: punctuation.separator.terraform
parens:
- match: \(
scope: punctuation.section.parens.begin.terraform
comment: Parens - matched *after* function syntax
push: paren-body
paren-body:
- meta_scope: meta.parens.terraform
- match: \)
scope: punctuation.section.parens.end.terraform
pop: 1
- include: expressions
literals:
- include: numeric-literals
- include: language-constants
- include: string-literals
- include: heredoc
- include: type-keywords
- include: named-value-references
named-value-references:
- match: \b{{named_values}}\b
comment: Special variables available only to Terraform.
scope: variable.language.terraform
type-keywords:
- match: \b{{terraform_type_keywords}}\b
comment: Type keywords known to Terraform.
scope: storage.type.terraform
inline-comments:
- match: '#|//'
comment: Inline Comments
scope: punctuation.definition.comment.terraform
push: inline-comment-body
inline-comment-body:
- meta_scope: comment.line.terraform
- match: $\n?
scope: punctuation.definition.comment.terraform
pop: 1
block-comments:
- match: /\*
comment: Block comments
scope: punctuation.definition.comment.terraform
push: block-comments-body
block-comments-body:
- meta_scope: comment.block.terraform
- match: \*/
scope: punctuation.definition.comment.terraform
pop: 1
language-constants:
- match: \btrue\b
scope: constant.language.boolean.true.terraform
- match: \bfalse\b
scope: constant.language.boolean.false.terraform
- match: \bnull\b
scope: constant.language.null.terraform
numeric-literals:
- match: \b\d+{{exponent}}\d+\b
comment: Integer, no fraction, optional exponent
scope: meta.number.float.decimal.terraform constant.numeric.value.terraform
- match: \b\d+(\.)\d+(?:{{exponent}}\d+)?\b
comment: Integer, fraction, optional exponent
scope: meta.number.float.decimal.terraform constant.numeric.value.terraform
captures:
1: punctuation.separator.decimal.terraform
- match: \b\d+\b
comment: Integers
scope: meta.number.integer.decimal.terraform constant.numeric.value.terraform
string-literals:
- include: ip-address-strings
- match: \"
comment: Strings
scope: punctuation.definition.string.begin.terraform
push: string-body
string-body:
- meta_include_prototype: false
- meta_scope: meta.string.terraform string.quoted.double.terraform
- match: \"
scope: punctuation.definition.string.end.terraform
pop: 1
- match: \n
scope: invalid.illegal.unclosed-string.terraform
pop: 1
- include: character-escapes
- include: string-interpolation
- include: aws-acl
character-escapes:
- match: '{{char_escapes}}'
comment: Character Escapes
scope: constant.character.escape.terraform
template-literals:
- match: '{{template_literal}}'
comment: Template Literal
scope: constant.character.escape.terraform
aws-acl:
- match: (?=\barn:aws:)
push: aws-acl-body
aws-acl-body:
- meta_include_prototype: false
- clear_scopes: 1 - meta_scope: variable.language.acl.terraform
- match: ([$%]\{)(~)?
captures:
1: punctuation.section.interpolation.begin.terraform
2: keyword.operator.template.trim.left.terraform
push: acl-interpolation-body
- match: :|/
scope: punctuation.separator.sequence.terraform
- match: \*
scope: constant.other.wildcard.asterisk.terraform
- match: (?![\w-])
pop: 1
acl-interpolation-body:
- meta_scope: meta.interpolation.terraform
- meta_content_scope: source.terraform
- include: string-interpolation-body
ip-address-strings:
- match: (")({{ipv6}})(?:(/)({{zero_to_128}}))?(")
scope: meta.string.terraform string.quoted.double.terraform
captures:
1: punctuation.definition.string.begin.terraform
2: meta.number.integer.other.terraform constant.numeric.ip-address.v6.terraform
3: punctuation.separator.terraform
4: constant.other.range.terraform
5: punctuation.definition.string.end.terraform
- match: (")((\[){{ipv6}}(\]))(?:(/)({{zero_to_128}})|(:)({{zero_to_65535}}))?(")
scope: meta.string.terraform string.quoted.double.terraform
captures:
1: punctuation.definition.string.begin.terraform
2: meta.number.integer.other.terraform constant.numeric.ip-address.v6.terraform
3: punctuation.definition.constant.begin.terraform
4: punctuation.definition.constant.end.terraform
5: punctuation.separator.terraform
6: constant.other.range.terraform
7: punctuation.separator.terraform
8: constant.other.port.terraform
9: punctuation.definition.string.end.terraform
- match: (")({{ipv4}})(?:(/)({{zero_to_32}})|(:)({{zero_to_65535}}))?(")
scope: meta.string.terraform string.quoted.double.terraform
captures:
1: punctuation.definition.string.begin.terraform
2: meta.number.integer.other.terraform constant.numeric.ip-address.v4.terraform
3: punctuation.separator.terraform
4: constant.other.range.terraform
5: punctuation.separator.terraform
6: constant.other.port.terraform
7: punctuation.definition.string.end.terraform
string-interpolation:
- include: template-literals
- match: ([$%]\{)(~)?
captures:
1: punctuation.section.interpolation.begin.terraform
2: keyword.operator.template.trim.left.terraform
push: string-interpolation-body
string-interpolation-body:
- clear_scopes: 1 - meta_scope: meta.interpolation.terraform
- meta_content_scope: source.terraform
- match: (~)?(\})
captures:
1: keyword.operator.template.trim.right.terraform
2: punctuation.section.interpolation.end.terraform
pop: 1
- match: \bif\b
scope: keyword.control.conditional.if.terraform
- match: \belse\b
scope: keyword.control.conditional.else.terraform
- match: \bendif\b
scope: keyword.control.conditional.end.terraform
- match: \bfor\b
scope: keyword.control.loop.for.terraform
- match: \bendfor\b
scope: keyword.control.loop.end.terraform
- match: \bin\b
scope: keyword.control.loop.in.terraform
- include: expressions
heredoc:
- match: (\<\<\-?)\s*({{identifier}})\s*$
comment: String Heredoc's
captures:
1: keyword.operator.heredoc.terraform
2: keyword.control.heredoc.terraform
push: heredoc-body
heredoc-body:
- meta_include_prototype: false
- meta_content_scope: meta.string.terraform string.unquoted.heredoc.terraform
- match: ^\s*\2\s*$
comment: The heredoc token identifier (second capture above).
scope: keyword.control.heredoc.terraform
pop: 1
- include: string-interpolation
operators:
- match: \>\= scope: keyword.operator.comparison.terraform
- match: \<\= scope: keyword.operator.comparison.terraform
- match: \=\= scope: keyword.operator.comparison.terraform
- match: \!\= scope: keyword.operator.comparison.terraform
- match: \+ scope: keyword.operator.arithmetic.terraform
- match: \- scope: keyword.operator.arithmetic.terraform
- match: \* scope: keyword.operator.arithmetic.terraform
- match: \/ scope: keyword.operator.arithmetic.terraform
- match: \% scope: keyword.operator.arithmetic.terraform
- match: \&\& scope: keyword.operator.logical.terraform
- match: \|\| scope: keyword.operator.logical.terraform
- match: \! scope: keyword.operator.logical.terraform
- match: \> scope: keyword.operator.comparison.terraform
- match: \< scope: keyword.operator.comparison.terraform
- match: \? scope: keyword.operator.ternary.terraform
- match: \.\.\. scope: keyword.operator.terraform
- match: ':' scope: keyword.operator.ternary.terraform
imports:
- match: \b(terraform)\s+(import)\b
comment: Importing resources
captures:
1: support.constant.terraform
2: keyword.control.import.terraform
push: import-body
import-body:
- match: \"
comment: String literal label
scope: punctuation.definition.string.begin.terraform
push: literal-label-body
- match: '{{identifier}}'
comment: Identifier label
scope: entity.name.label.terraform
- include: numeric-literals
- include: attribute-access
- match: $\n?
comment: Pop at newline
pop: 1
literal-label-body:
- meta_scope: meta.string.terraform string.quoted.double.terraform
- match: \"
scope: punctuation.definition.string.end.terraform
pop: 1
brackets:
- match: \[
scope: punctuation.section.brackets.begin.terraform
push: bracket-body
bracket-body:
- meta_scope: meta.brackets.terraform
- match: (\*)?\]
comment: Full-splat operator
scope: punctuation.section.brackets.end.terraform
captures:
1: keyword.operator.splat.terraform
pop: 1
- include: comma
- include: tuple-for-expression
- include: expressions
objects:
- match: \{
scope: punctuation.section.braces.begin.terraform
push: object-body
object-body:
- meta_scope: meta.braces.terraform
- match: \}
scope: punctuation.section.braces.end.terraform
pop: 1
- include: object-for-expression
- match: (?=({{identifier}}|\".*?\")\s*=)
push:
- object-value
- assignment-operator
- object-literal-key
- match: \(
comment: Computed object key (any expression between parens)
scope: punctuation.section.parens.begin.terraform
push:
- object-value
- assignment-operator
- object-computed-key
- include: Packages/Terraform/JSON (Terraform).sublime-syntax#object-body
object-computed-key:
- meta_scope: meta.mapping.key.terraform meta.parens.terraform
- include: paren-body
object-literal-key:
- match: '{{identifier}}'
scope: meta.mapping.key.terraform meta.string.terraform string.unquoted.terraform
pop: 1
- match: (\").*?(\")
scope: meta.mapping.key.terraform meta.string.terraform string.quoted.double.terraform
captures:
1: punctuation.definition.string.begin.terraform
2: punctuation.definition.string.end.terraform
pop: 1
- include: else-pop
assignment-operator:
- match: =
scope: keyword.operator.assignment.terraform
pop: 1
- include: else-pop
object-value:
- match: \,
comment: Pop scope on comma.
scope: punctuation.separator.terraform
pop: 1
- match: $\n?
comment: Pop scope on EOL.
pop: 1
- match: (?=\})
comment: Lookahead (don't consume) and pop scope on a bracket.
scope: punctuation.section.braces.end.terraform
pop: 1
- include: expressions
attribute-access:
- match: \.
scope: punctuation.accessor.dot.terraform
push: member
member:
- match: '{{identifier}}'
comment: Attribute access
scope: variable.other.member.terraform
pop: 1
- match: \d+
comment: Subscript
scope: meta.number.integer.decimal.terraform constant.numeric.value.terraform
pop: 1
- match: \*
comment: Attribute-only splat
scope: keyword.operator.splat.terraform
pop: 1
- include: else-pop
attribute-definition:
- match: (?=(\()?({{identifier}})(\))?\s*(\=(?![\=\>])))
push:
- assignment-operator
- attribute-key
attribute-key:
- match: \((?={{identifier}}\))
scope: punctuation.section.parens.begin.terraform
set:
- attribute-key-end
- attribute-key
- match: for_each\b
scope: variable.declaration.terraform keyword.control.loop.for.terraform
pop: 1
- match: count\b
scope: variable.declaration.terraform keyword.control.conditional.terraform
pop: 1
- match: '{{identifier}}'
scope: variable.declaration.terraform variable.other.readwrite.terraform
pop: 1
attribute-key-end:
- meta_scope: meta.parens.terraform
- match: \)
scope: punctuation.section.parens.end.terraform
pop: 1
functions:
- match: (?={{builtin_functions}}\()
push: builtin-function-name
- match: (?=(?:{{identifier}}::)*{{identifier}}\()
push: other-function-name
builtin-function-name:
- meta_content_scope: meta.function-call.identifier.terraform
- include: function-argument-list
- include: namespaces
- match: '{{identifier}}'
scope: support.function.builtin.terraform
other-function-name:
- meta_content_scope: meta.function-call.identifier.terraform
- include: function-argument-list
- include: namespaces
- match: '{{identifier}}'
scope: variable.function.terraform
function-argument-list:
- match: \(
scope: punctuation.section.parens.begin.terraform
set: function-argument-list-body
function-argument-list-body:
- meta_scope: meta.function-call.arguments.terraform meta.parens.terraform
- match: \)
scope: punctuation.section.parens.end.terraform
pop: 1
- include: expressions
- include: comma
tuple-for-expression:
- match: \bfor\b
comment: for expression (arrays)
scope: keyword.control.loop.for.terraform
set: tuple-for-expression-body
tuple-for-expression-body:
- meta_content_scope: meta.brackets.terraform
- match: \]
scope: meta.brackets.terraform punctuation.section.brackets.end.terraform
pop: 1
- include: for-expression-body
object-for-expression:
- match: \bfor\b
comment: for expression (arrays)
scope: keyword.control.loop.for.terraform
set: object-for-expression-body
object-for-expression-body:
- meta_content_scope: meta.braces.terraform
- match: \}
scope: meta.braces.terraform punctuation.section.braces.end.terraform
pop: 1
- match: \=\>
scope: punctuation.separator.key-value.terraform
- include: for-expression-body
for-expression-body:
- match: \bin\b
scope: keyword.operator.iteration.in.terraform
- match: \bif\b
scope: keyword.control.conditional.terraform
- match: '\:'
scope: punctuation.section.block.loop.for.terraform
- include: expressions
- include: comma
namespaces:
- match: ({{identifier}})(::)
captures:
1: variable.namespace.terraform
2: punctuation.accessor.double-colon.terraform
identifiers:
- match: '{{identifier}}'
scope: variable.other.readwrite.terraform
illegal-strays:
- match: '[\])}]'
scope: invalid.illegal.stray.terraform
blocks:
- include: typed-and-named-blocks
- include: named-blocks
- include: generic-blocks
typed-and-named-blocks:
- match: (?=\b{{terraform_typed_and_named_blocks}}(\s+{{label}}){2}\s*\{)
push:
- body
- block-name-label
- block-type-label
- block-declaration
named-blocks:
- match: (?=\b{{terraform_named_blocks}}\s+{{label}}\s*\{)
push:
- body
- block-name-label
- block-declaration
generic-blocks:
- match: (?=\b({{identifier}})(\s+{{label}})*\s*\{)
push:
- body
- generic-block-labels
- block-declaration
block-declaration:
- match: '{{identifier}}'
scope: keyword.declaration.terraform
pop: 1
- include: else-pop
block-type-label:
- match: \"
scope: punctuation.definition.string.begin.terraform
set: block-type-label-body
- match: '{{identifier}}'
scope: support.type.terraform
pop: 1
- include: else-pop
block-type-label-body:
- meta_include_prototype: false
- meta_scope: meta.string.terraform
- meta_content_scope: support.type.terraform
- match: \"
scope: punctuation.definition.string.end.terraform
pop: 1
- include: character-escapes
block-name-label:
- match: \"
scope: punctuation.definition.string.begin.terraform
set: block-name-label-body
- match: '{{identifier}}'
scope: entity.name.label.terraform
pop: 1
- include: else-pop
block-name-label-body:
- meta_include_prototype: false
- meta_scope: meta.string.terraform
- meta_content_scope: entity.name.label.terraform
- match: \"
scope: punctuation.definition.string.end.terraform
pop: 1
- include: character-escapes
generic-block-labels:
- match: \"
scope: punctuation.definition.string.begin.terraform
push: generic-block-label-body
- match: '{{identifier}}'
scope: string.unquoted.double.terraform
- include: else-pop
generic-block-label-body:
- meta_scope: meta.string.terraform string.quoted.double.terraform
- match: \"
scope: punctuation.definition.string.end.terraform
pop: 1
- include: character-escapes
body:
- meta_content_scope: meta.block.head.terraform
- match: \{
scope: punctuation.section.block.begin.terraform
set: body-body
- include: else-pop
body-body:
- meta_scope: meta.block.body.terraform
- match: \}
scope: punctuation.section.block.end.terraform
pop: 1
- include: main
else-pop:
- match: (?=\S)
pop: 1