Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion cli/hcl_to_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def main():
parser.add_argument(
"--no-explicit-blocks",
action="store_true",
help="Disable explicit block markers",
help="Disable explicit block markers. Note: round-trip through json_to_hcl is NOT supported with this option.",
)
parser.add_argument(
"--no-preserve-heredocs",
Expand All @@ -85,6 +85,11 @@ def main():
action="store_true",
help="Convert scientific notation to standard floats",
)
parser.add_argument(
"--strip-string-quotes",
action="store_true",
help="Strip surrounding double-quotes from serialized string values. Note: round-trip through json_to_hcl is NOT supported with this option.",
)

# JSON output formatting
parser.add_argument(
Expand All @@ -106,6 +111,7 @@ def main():
preserve_heredocs=not args.no_preserve_heredocs,
force_operation_parentheses=args.force_parens,
preserve_scientific_notation=not args.no_preserve_scientific,
strip_string_quotes=args.strip_string_quotes,
)
json_indent = args.json_indent

Expand Down
6 changes: 6 additions & 0 deletions hcl2/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,15 @@
class DeserializerOptions:
"""Options controlling how Python dicts are deserialized into LarkElement trees."""

# Convert heredoc values (<<EOF...EOF) to regular escaped strings during
# deserialization. When False, heredoc syntax is preserved as-is.
heredocs_to_strings: bool = False
# Convert multi-line escaped strings (containing \n) back into heredoc
# syntax (<<EOF...EOF) during deserialization.
strings_to_heredocs: bool = False
# Use colon (:) instead of equals (=) as the separator in object elements.
object_elements_colon: bool = False
# Append a trailing comma after each object element.
object_elements_trailing_comma: bool = True
# with_comments: bool = False # TODO

Expand Down
10 changes: 9 additions & 1 deletion hcl2/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,20 @@
class FormatterOptions:
"""Options controlling whitespace formatting of LarkElement trees."""

# Number of spaces per indentation level.
indent_length: int = 2
# Use multi-line format for empty blocks (opening brace on same line,
# closing brace on next). When False, empty blocks collapse to "{}".
open_empty_blocks: bool = True
# Use multi-line format for empty objects. When False, empty objects
# collapse to "{}".
open_empty_objects: bool = False
# Use multi-line format for empty tuples. When False, empty tuples
# collapse to "[]".
open_empty_tuples: bool = False

# Pad attribute equals signs so they align vertically within a block body.
vertically_align_attributes: bool = True
# Pad object element equals/colons so they align vertically within an object.
vertically_align_object_elements: bool = True


Expand Down
18 changes: 12 additions & 6 deletions hcl2/rules/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,10 @@ def serialize(
self, options=SerializationOptions(), context=SerializationContext()
) -> Any:
"""Serialize to a quoted string."""
return (
'"'
+ "".join(part.serialize(options, context) for part in self.string_parts)
+ '"'
)
inner = "".join(part.serialize(options, context) for part in self.string_parts)
if options.strip_string_quotes:
return inner
return '"' + inner + '"'


class HeredocTemplateRule(LarkRule):
Expand Down Expand Up @@ -129,9 +128,13 @@ def serialize(
heredoc = (
heredoc.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
)
if options.strip_string_quotes:
return heredoc
return f'"{heredoc}"'

result = heredoc.rstrip(self._trim_chars)
if options.strip_string_quotes:
return result
return f'"{result}"'


Expand Down Expand Up @@ -178,4 +181,7 @@ def serialize(
lines = [line.replace("\\", "\\\\").replace('"', '\\"') for line in lines]

sep = "\\n" if not options.preserve_heredocs else "\n"
return '"' + sep.join(lines) + '"'
inner = sep.join(lines)
if options.strip_string_quotes:
return inner
return '"' + inner + '"'
17 changes: 17 additions & 0 deletions hcl2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,31 @@
class SerializationOptions:
"""Options controlling how LarkElement trees are serialized to Python dicts."""

# Include __comments__ and __inline_comments__ keys in the output.
with_comments: bool = True
# Add __start_line__ and __end_line__ metadata to each block/attribute.
with_meta: bool = False
# Serialize nested objects as inline HCL strings (e.g. "${{key = value}}")
# instead of Python dicts.
wrap_objects: bool = False
# Serialize tuples as inline HCL strings (e.g. "${[1, 2, 3]}")
# instead of Python lists.
wrap_tuples: bool = False
# Add __is_block__ markers to distinguish blocks from plain objects.
# Note: round-trip through from_dict/dumps is NOT supported WITHOUT this option.
explicit_blocks: bool = True
# Keep heredoc syntax (<<EOF...EOF) in output. When False, heredocs are
# converted to regular escaped strings.
preserve_heredocs: bool = True
# Wrap all binary/unary operations in parentheses for explicit precedence.
force_operation_parentheses: bool = False
# Keep scientific notation for floats (e.g. 1e10). When False, expand to
# standard decimal form.
preserve_scientific_notation: bool = True
# Remove surrounding double-quotes from serialized string values,
# producing backwards-compatible output (e.g. "hello" instead of '"hello"').
# Note: round-trip through from_dict/dumps is NOT supported WITH this option.
strip_string_quotes: bool = False


@dataclass
Expand Down
44 changes: 44 additions & 0 deletions test/unit/rules/test_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,26 @@ def test_serialize_only_interpolation(self):
rule = _make_string([_make_string_part_interpolation("x")])
self.assertEqual(rule.serialize(), '"${x}"')

def test_serialize_strip_string_quotes(self):
rule = _make_string([_make_string_part_chars("hello")])
opts = SerializationOptions(strip_string_quotes=True)
self.assertEqual(rule.serialize(opts), "hello")

def test_serialize_strip_string_quotes_empty(self):
rule = _make_string([])
opts = SerializationOptions(strip_string_quotes=True)
self.assertEqual(rule.serialize(opts), "")

def test_serialize_strip_string_quotes_with_interpolation(self):
rule = _make_string(
[
_make_string_part_chars("prefix:"),
_make_string_part_interpolation("var.name"),
]
)
opts = SerializationOptions(strip_string_quotes=True)
self.assertEqual(rule.serialize(opts), "prefix:${var.name}")


# --- HeredocTemplateRule tests ---

Expand Down Expand Up @@ -237,6 +257,18 @@ def test_serialize_no_preserve_invalid_raises(self):
with self.assertRaises(RuntimeError):
rule.serialize(opts)

def test_serialize_strip_string_quotes_preserve(self):
token = HEREDOC_TEMPLATE("<<EOF\nhello world\nEOF")
rule = HeredocTemplateRule([token])
opts = SerializationOptions(preserve_heredocs=True, strip_string_quotes=True)
self.assertEqual(rule.serialize(opts), "<<EOF\nhello world\nEOF")

def test_serialize_strip_string_quotes_no_preserve(self):
token = HEREDOC_TEMPLATE("<<EOF\nline1\nline2\nEOF")
rule = HeredocTemplateRule([token])
opts = SerializationOptions(preserve_heredocs=False, strip_string_quotes=True)
self.assertEqual(rule.serialize(opts), "line1\\nline2")


# --- HeredocTrimTemplateRule tests ---

Expand Down Expand Up @@ -310,3 +342,15 @@ def test_serialize_no_preserve_invalid_raises(self):
opts = SerializationOptions(preserve_heredocs=False)
with self.assertRaises(RuntimeError):
rule.serialize(opts)

def test_serialize_strip_string_quotes_preserve(self):
token = HEREDOC_TRIM_TEMPLATE("<<-EOF\n line1\n line2\nEOF")
rule = HeredocTrimTemplateRule([token])
opts = SerializationOptions(preserve_heredocs=True, strip_string_quotes=True)
self.assertEqual(rule.serialize(opts), "<<-EOF\n line1\n line2\nEOF")

def test_serialize_strip_string_quotes_no_preserve(self):
token = HEREDOC_TRIM_TEMPLATE("<<-EOF\n line1\n line2\nEOF")
rule = HeredocTrimTemplateRule([token])
opts = SerializationOptions(preserve_heredocs=False, strip_string_quotes=True)
self.assertEqual(rule.serialize(opts), "line1\\nline2")
20 changes: 20 additions & 0 deletions test/unit/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ def test_block_parsing(self):
result = loads(BLOCK_HCL)
self.assertIn("resource", result)

def test_strip_string_quotes(self):
result = loads(
BLOCK_HCL,
serialization_options=SerializationOptions(
strip_string_quotes=True, explicit_blocks=False
),
)
resource_list = result["resource"]
self.assertEqual(len(resource_list), 1)
block = resource_list[0]
# Block label should have no surrounding quotes
self.assertIn("aws_instance", block)
inner = block["aws_instance"]
self.assertIn("example", inner)
body = inner["example"]
# Attribute value should have no surrounding quotes
self.assertEqual(body["ami"], "abc-123")
# No __is_block__ marker
self.assertNotIn("__is_block__", body)


class TestLoad(TestCase):
def test_from_file(self):
Expand Down