diff --git a/cmd/utils.go b/cmd/utils.go index 0b37b3ee..4cecccfc 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -205,6 +205,7 @@ func configureEncoder() (yqlib.Encoder, error) { yqlib.ConfiguredYamlPreferences.ColorsEnabled = colorsEnabled yqlib.ConfiguredJSONPreferences.ColorsEnabled = colorsEnabled + yqlib.ConfiguredHclPreferences.ColorsEnabled = colorsEnabled yqlib.ConfiguredYamlPreferences.PrintDocSeparators = !noDocSeparators diff --git a/pkg/yqlib/encoder_hcl.go b/pkg/yqlib/encoder_hcl.go index b61cd0f7..8c72fe0f 100644 --- a/pkg/yqlib/encoder_hcl.go +++ b/pkg/yqlib/encoder_hcl.go @@ -8,6 +8,7 @@ import ( "regexp" "strings" + "github.com/fatih/color" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" hclwrite "github.com/hashicorp/hcl/v2/hclwrite" @@ -15,11 +16,12 @@ import ( ) type hclEncoder struct { + prefs HclPreferences } // NewHclEncoder creates a new HCL encoder -func NewHclEncoder() Encoder { - return &hclEncoder{} +func NewHclEncoder(prefs HclPreferences) Encoder { + return &hclEncoder{prefs: prefs} } func (he *hclEncoder) CanHandleAliases() bool { @@ -55,6 +57,12 @@ func (he *hclEncoder) Encode(writer io.Writer, node *CandidateNode) error { // Inject comments back into the output finalOutput := he.injectComments(compactOutput, commentMap) + if he.prefs.ColorsEnabled { + colorized := he.colorizeHcl(finalOutput) + _, err := writer.Write(colorized) + return err + } + _, err := writer.Write(finalOutput) return err } @@ -151,6 +159,108 @@ func (he *hclEncoder) injectComments(output []byte, commentMap map[string]string return []byte(result) } +// colorizeHcl applies syntax highlighting to HCL output using fatih/color +func (he *hclEncoder) colorizeHcl(input []byte) []byte { + hcl := string(input) + result := strings.Builder{} + + // Create color functions for different token types + commentColor := color.New(color.FgHiBlack).SprintFunc() + stringColor := color.New(color.FgGreen).SprintFunc() + numberColor := color.New(color.FgHiMagenta).SprintFunc() + keyColor := color.New(color.FgCyan).SprintFunc() + boolColor := color.New(color.FgHiMagenta).SprintFunc() + + // Simple tokenization for HCL coloring + i := 0 + for i < len(hcl) { + ch := hcl[i] + + // Comments - from # to end of line + if ch == '#' { + end := i + for end < len(hcl) && hcl[end] != '\n' { + end++ + } + result.WriteString(commentColor(hcl[i:end])) + i = end + continue + } + + // Strings - quoted text + if ch == '"' || ch == '\'' { + quote := ch + end := i + 1 + for end < len(hcl) && hcl[end] != quote { + if hcl[end] == '\\' { + end++ // skip escaped char + } + end++ + } + if end < len(hcl) { + end++ // include closing quote + } + result.WriteString(stringColor(hcl[i:end])) + i = end + continue + } + + // Numbers - sequences of digits, possibly with decimal point or minus + if (ch >= '0' && ch <= '9') || (ch == '-' && i+1 < len(hcl) && hcl[i+1] >= '0' && hcl[i+1] <= '9') { + end := i + if ch == '-' { + end++ + } + for end < len(hcl) && ((hcl[end] >= '0' && hcl[end] <= '9') || hcl[end] == '.') { + end++ + } + result.WriteString(numberColor(hcl[i:end])) + i = end + continue + } + + // Identifiers/keys - alphanumeric + underscore + if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' { + end := i + for end < len(hcl) && ((hcl[end] >= 'a' && hcl[end] <= 'z') || + (hcl[end] >= 'A' && hcl[end] <= 'Z') || + (hcl[end] >= '0' && hcl[end] <= '9') || + hcl[end] == '_' || hcl[end] == '-') { + end++ + } + ident := hcl[i:end] + + // Check if this is a keyword/reserved word + switch ident { + case "true", "false", "null": + result.WriteString(boolColor(ident)) + default: + // Check if followed by = (it's a key) + j := end + for j < len(hcl) && (hcl[j] == ' ' || hcl[j] == '\t') { + j++ + } + if j < len(hcl) && hcl[j] == '=' { + result.WriteString(keyColor(ident)) + } else if j < len(hcl) && hcl[j] == '{' { + // Block type + result.WriteString(keyColor(ident)) + } else { + result.WriteString(ident) // plain text for other identifiers + } + } + i = end + continue + } + + // Everything else (whitespace, operators, brackets) - no color + result.WriteByte(ch) + i++ + } + + return []byte(result.String()) +} + // Helper runes for unquoted identifiers func isHCLIdentifierStart(r rune) bool { return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' diff --git a/pkg/yqlib/format.go b/pkg/yqlib/format.go index 033f424d..442eabf1 100644 --- a/pkg/yqlib/format.go +++ b/pkg/yqlib/format.go @@ -68,7 +68,7 @@ var TomlFormat = &Format{"toml", []string{}, } var HclFormat = &Format{"hcl", []string{"h", "hcl"}, - func() Encoder { return NewHclEncoder() }, + func() Encoder { return NewHclEncoder(ConfiguredHclPreferences) }, func() Decoder { return NewHclDecoder() }, } diff --git a/pkg/yqlib/hcl.go b/pkg/yqlib/hcl.go new file mode 100644 index 00000000..74ddf7dc --- /dev/null +++ b/pkg/yqlib/hcl.go @@ -0,0 +1,15 @@ +package yqlib + +type HclPreferences struct { + ColorsEnabled bool +} + +func NewDefaultHclPreferences() HclPreferences { + return HclPreferences{ColorsEnabled: false} +} + +func (p *HclPreferences) Copy() HclPreferences { + return HclPreferences{ColorsEnabled: p.ColorsEnabled} +} + +var ConfiguredHclPreferences = NewDefaultHclPreferences() diff --git a/pkg/yqlib/hcl_test.go b/pkg/yqlib/hcl_test.go index 26314ef2..69c4455d 100644 --- a/pkg/yqlib/hcl_test.go +++ b/pkg/yqlib/hcl_test.go @@ -281,7 +281,7 @@ func testHclScenario(t *testing.T, s formatScenario) { result := mustProcessFormatScenario(s, NewHclDecoder(), NewYamlEncoder(ConfiguredYamlPreferences)) test.AssertResultWithContext(t, s.expected, result, s.description) case "roundtrip": - test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewHclDecoder(), NewHclEncoder()), s.description) + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewHclDecoder(), NewHclEncoder(ConfiguredHclPreferences)), s.description) } } diff --git a/pkg/yqlib/no_hcl.go b/pkg/yqlib/no_hcl.go index a9ef3f9e..0db24f87 100644 --- a/pkg/yqlib/no_hcl.go +++ b/pkg/yqlib/no_hcl.go @@ -6,6 +6,6 @@ func NewHclDecoder() Decoder { return nil } -func NewHclEncoder() Encoder { +func NewHclEncoder(_ HclPreferences) Encoder { return nil }