python-hcl2: HCL2 Parser for Python

repository·main·Indexed 43 days ago

https://github.com/amplify-education/python-hcl2

A Python parser for HCL2 (HashiCorp Configuration Language) built on the Lark parsing library. It enables parsing HCL2 files (such as Terraform configs) into Python dictionaries, generating HCL2 from dictionaries via the Builder API, and provides CLI tools including hcl2tojson, jsontohcl2, and hq for structural queries and format conversion.

Tokens
17.2K
Snippets
62
Records
74
Agent score
54%

What's inside python-hcl2

  1. Skip HCL block labels using the tilde (~) operator

    main

    HCL blocks often have multiple labels (e.g., resource "aws_instance" "main"). Normally, you must traverse each label individually: resource.aws_instance.main.

    To skip all name labels and go directly to the block body, append a tilde (~) to the segment. This is useful for querying all blocks of a certain type regardless of their specific names or labels.

    Examples:

    # Without ~: must name every label
    hq 'resource.aws_instance.main.ami' main.tf
    
    # With ~: skip all labels, access body directly
    hq 'resource~.ami' main.tf
    
    # All resource blocks regardless of labels
    hq 'resource~[*]' main.tf
    
    # Filter blocks by body content
    hq 'resource~[select(.ami)]' main.tf
    hq 'resource~.ami' main.tf
    hq 'resource~[*]' main.tf
    hq 'resource~[select(.ami)]' main.tf
  2. Handle HCL Tuples and Objects with Container Views

    main

    When an attribute contains a collection, you can use specialized container views to navigate them.

    TupleView

    Used for HCL lists/tuples (e.g., [1, 2, 3]).

    • len(tv): Returns the number of elements.
    • tv[index]: Returns a NodeView for the element at that index.
    • tv.elements: Returns a List[NodeView] of all elements.

    ObjectView

    Used for HCL maps/objects (e.g., { Name = "web" }).

    • ov.keys: Returns a list of keys.
    • ov.get(key): Returns a NodeView for the value associated with the key.
    • ov.entries: Returns a List[Tuple[str, NodeView]] of all key-value pairs.
    from hcl2.query.containers import TupleView
    from hcl2.query.containers import ObjectView
    from hcl2.walk import find_first
    from hcl2.rules.containers import TupleRule, ObjectRule
    
    # Accessing a Tuple
    doc = DocumentView.parse('x = [1, 2, 3]\n')
    node = find_first(doc.attribute("x").raw, TupleRule)
    tv = TupleView(node)
    print(len(tv)) # 3
    
    # Accessing an Object
    doc = DocumentView.parse('tags = { Name = "web", Env = "prod" }\n')
    node = find_first(doc.attribute("tags").raw, ObjectRule)
    ov = ObjectView(node)
    print(ov.keys) # ["Name", "Env"]
    print(ov.get("Name")) # NodeView for "web"
  3. Understand the v8 bidirectional HCL2 pipeline

    main

    v8 exposes the HCL processing lifecycle as a series of composable functions. This allows for more granular control over the transformation stages.

    Forward Pipeline (HCL $\rightarrow$ Dict):

    1. parses_to_tree(text): HCL string $\rightarrow$ raw lark.Tree
    2. transform(lark_tree): raw lark.Tree $\rightarrow$ StartRule (typed IR)
    3. serialize(tree): StartRule $\rightarrow$ dict

    Reverse Pipeline (Dict $\rightarrow$ HCL):

    1. from_dict(data): dict $\rightarrow$ StartRule
    2. reconstruct(tree): StartRule $\rightarrow$ HCL string
  4. Understand the HCL2 processing pipeline

    main

    The python-hcl2 library operates through a multi-stage pipeline that can be used in its entirety or accessed at individual stages for advanced manipulation.

    Forward Pipeline (Parsing): HCL2 Text $\rightarrow$ Lark Parse Tree $\rightarrow$ LarkElement Tree $\rightarrow$ Python Dict

    Reverse Pipeline (Serialization): Python Dict $\rightarrow$ LarkElement Tree $\rightarrow$ HCL2 Text

  5. Performance and Output Mode tips

    main

    Output Formats

    • --ndjson: Use this for directory queries. It streams results, making it compatible with tools like head, grep, and jq. Recommended for large datasets.
    • --json: Use this for single files or when you require a single parseable JSON array.

    Parallelization

    hq automatically parallelizes queries across CPU cores when processing 20+ files. You can control this behavior with the --jobs flag:

    • --jobs 0: Force serial execution.
    • --jobs N: Set an explicit worker count (where N is the number of workers).
  6. Chain queries with pipes and use property accessors

    main

    You can chain query stages using the pipe | operator. Each stage feeds its results into the next.

    Pipeline Semantics:

    • AttributeView → unwraps to the value node (Object, Tuple, etc.).
    • BlockView → unwraps to the body (BodyView).
    • ExprTermRule → unwraps to the inner expression.

    Note: Label traversal should be done within a single stage. Do not split labels across pipes (e.g., resource | .aws_instance | .main will not work).

    Property Accessors: When a pipe stage doesn't match a structural path, it falls back to Python properties on the view:

    • BlockView: .block_type, .labels (all labels), .name_labels (labels after type).
    • AttributeView: .name, .value.
    • FunctionCallView: .name, .args, .has_ellipsis.
    • ObjectView: .keys, .entries.
    • All views: .type (returns a short string like "block", "attribute", etc.).

    Examples:

    # Navigate through body, then extract attribute
    hq 'resource.aws_instance.main | .ami' main.tf --value
    
    # Get block types
    hq 'resource[*] | .block_type' main.tf --value
    
    # Get all labels including block type
    hq 'resource[*] | .labels' main.tf --json
    hq 'resource.aws_instance.main | .ami' main.tf --value
    hq 'resource[*] | .block_type' main.tf --value
    hq 'resource[*] | .labels' main.tf --json
  7. Use Hybrid Queries for Python transformations

    main

    Hybrid mode allows you to combine structural HCL navigation with Python expressions. Use the :: separator to split the structural path (left) from the Python expression (right). In the expression, _ is bound to each result from the structural path.

    Expression Normalization: You don't need to prefix attributes with _. For example, name_labels is automatically treated as _.name_labels.

    # Get name_labels for all variables
    hq 'variable[*]::name_labels' variables.tf
    
    # Get block_type
    hq 'variable[*]::block_type' variables.tf --value
    
    # Call methods
    hq 'resource.aws_instance[*].tags::entries()' main.tf
    
    # Use builtins
    hq 'variable[*]::len(_.name_labels)' variables.tf --value
  8. Access comments in parsed HCL2

    main

    When with_comments=True (the default), comments are captured in __comments__ (standalone line comments) and __inline_comments__ (comments inside expressions). Each comment is an object with a "value" key containing the text.

    Note: Comments are read-only; they are captured during parsing but are not restored when converting a dict back to HCL2 via dump/dumps.

    from hcl2 import loads, SerializationOptions
    
    data = loads(
        "# Configure the provider\nx = 1\n",
        serialization_options=SerializationOptions(with_comments=True),
    )
    
    print(data["__comments__"])
    # Output: [{"value": "Configure the provider"}]
  9. Optimize hq performance with parallel processing

    main

    For large datasets (20+ files), hq automatically uses multiprocessing when using --json or --ndjson output modes. You can control the worker count using the --jobs flag.

    # Auto-parallel (default for 20+ files with --json/--ndjson)
    hq 'resource[*]' large-monorepo/ --ndjson
    
    # Force serial processing
    hq 'resource[*]' large-monorepo/ --ndjson --jobs 0
    
    # Explicit worker count
    hq 'resource[*]' large-monorepo/ --ndjson --jobs 8
  10. Extract multiple attributes using Object Construction

    main

    You can extract multiple fields per result in a single query by constructing an object {key: .field}. This is highly effective when using --ndjson for downstream processing.

    Example: Instance types for cost analysis

    hq 'resource.aws_instance~[*] | {name: .name_labels, type: .instance_type}' infra/ --ndjson

    Example: CPU and memory for ECS services

    hq 'module~[select(.cpu)] | {cpu, memory}' infra/ --ndjson

    Example: Resource inventory with tags

    hq 'resource~[*] | {type: .name_labels, tags}' infra/prod/ --ndjson

    # Extract name and type
    hq 'resource.aws_instance~[*] | {name: .name_labels, type: .instance_type}' infra/ --ndjson
    
    # Extract cpu and memory from modules
    hq 'module~[select(.cpu)] | {cpu, memory}' infra/ --ndjson
    
    # Extract type and tags
    hq 'resource~[*] | {type: .name_labels, tags}' infra/prod/ --ndjson
  11. Use NDJSON for streaming large HCL datasets

    main

    When querying large monorepos, use the --ndjson flag to emit one JSON object per line. This is ideal for streaming results to other tools like jq without loading the entire result set into memory.

    # One JSON per result, compact
    hq 'resource[*]' dir/ --ndjson
    
    # With source location
    hq 'resource[*]' dir/ --ndjson --with-location
    
    # Pipe to jq for further processing
    hq 'resource[*]' dir/ --ndjson | jq '.tags'
  12. Use the hq CLI for HCL2 querying

    main

    The hq CLI is a jq-like query tool designed for HCL2 files. It allows you to perform structural queries, hybrid Python expressions, or full Python evaluation on HCL2 data.

    Query Modes (use in order of preference):

    1. Structural (default): Uses jq-like syntax with pipes, select, and string functions. Recommended for most tasks.
    2. Hybrid (::): Combines a structural path on the left with a Python expression on the right. Use this when structural syntax cannot express a specific transformation.
    3. Eval (-e): Executes full Python expressions. This is a last resort as many operations are blocked for safety.

    Common CLI Flags:

    • --json: Output results as JSON.
    • --value: Output raw values only.
    • --no-filename: Suppress the filename: prefix in multi-file output.
    • ?: Append to a query (e.g., query?) to exit with code 0 even if no results are found.
    # Get all variable blocks
    hq 'variable[*]' variables.tf
    
    # Output as JSON
    hq 'variable[*]' variables.tf --json
    
    # Output raw values only
    hq 'resource.aws_instance.main.ami' main.tf --value