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
3 changes: 3 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ nav_order: 9

### Misc. changes

- Add support for pretty error reporting, can be controlled through
the use of `--raw-errors` (disable) and `--color`/`--colour`

### Docs changes

## Butane 0.28.0 (2026-05-19)
Expand Down
36 changes: 35 additions & 1 deletion internal/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/coreos/butane/config"
"github.com/coreos/butane/config/common"
breport "github.com/coreos/butane/internal/report"
"github.com/coreos/butane/internal/version"
)

Expand All @@ -31,14 +32,25 @@ func fail(format string, args ...interface{}) {
os.Exit(1)
}

func isCharDevice(f *os.File) bool {
stat, err := f.Stat()
if err != nil {
return false
}
return stat.Mode()&os.ModeCharDevice != 0
}

func main() {
var (
input string
output string
colorFlag string
check bool
strict bool
helpFlag bool
versionFlag bool
rawErrors bool
colorize bool
)
options := common.TranslateBytesOptions{}
pflag.BoolVarP(&helpFlag, "help", "h", false, "show usage and exit")
Expand All @@ -49,6 +61,12 @@ func main() {
pflag.BoolVarP(&strict, "strict", "s", false, "fail on any warning")
pflag.BoolVarP(&options.Pretty, "pretty", "p", false, "output formatted json")
pflag.BoolVarP(&options.Raw, "raw", "r", false, "never wrap in a MachineConfig; force Ignition output")
pflag.BoolVar(&rawErrors, "raw-errors", false, "show raw errors, rather than pretty printing them")
pflag.StringVar(&colorFlag, "color", "auto", `control color output: "auto", "always", or "never"`)
pflag.Lookup("color").NoOptDefVal = "always"
pflag.StringVar(&colorFlag, "colour", "auto", `control color output: "auto", "always", or "never"`)
pflag.Lookup("colour").NoOptDefVal = "always"
pflag.Lookup("colour").Hidden = true
pflag.StringVar(&input, "input", "", "read from input file instead of stdin")
pflag.Lookup("input").Deprecated = "specify filename directly on command line"
pflag.Lookup("input").Hidden = true
Expand All @@ -62,6 +80,17 @@ func main() {
}
pflag.Parse()

switch colorFlag {
case "always", "yes":
colorize = true
case "never", "no":
colorize = false
case "auto":
_, noColorSet := os.LookupEnv("NO_COLOR")
isTTY := isCharDevice(os.Stderr)
colorize = !noColorSet && isTTY
}

args := pflag.Args()
if len(args) == 1 && input == "" {
input = args[0]
Expand All @@ -82,13 +111,15 @@ func main() {
}

infile := os.Stdin
filename := "<stdin>"
if input != "" {
var err error
infile, err = os.Open(input)
if err != nil {
fail("failed to open %s: %v\n", input, err)
}
defer infile.Close()
filename = input
}

dataIn, err := io.ReadAll(infile)
Expand All @@ -97,7 +128,10 @@ func main() {
}

dataOut, r, err := config.TranslateBytes(dataIn, options)
fmt.Fprintf(os.Stderr, "%s", r.String())

errorString := breport.FormatError(r, filename, dataIn, colorize, rawErrors)
fmt.Fprintf(os.Stderr, "%s", errorString)

if err != nil {
fail("Error translating config: %v\n", err)
}
Expand Down
157 changes: 157 additions & 0 deletions internal/report/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2020 Red Hat, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.)

// Package report allows butane to pretty print errors, the error format is as follows:
package report

import (
"fmt"
"strings"
"unicode"

"github.com/coreos/vcontext/report"
Comment thread
angelcerveraroldan marked this conversation as resolved.
)

const (
red = "\033[1;31m"
yellow = "\033[1;33m"
cyan = "\033[1;36m"
blue = "\033[1;34m"
reset = "\033[0m"
)

func FormatError(r report.Report, fileName string, source []byte, colorize, rawErrors bool) string {
if rawErrors {
return formatErrorSimple(r)
} else {
return formatErrorPretty(r, fileName, source, colorize)
}
}

func formatErrorSimple(r report.Report) string {
return r.String()
}

func formatErrorPretty(r report.Report, fileName string, source []byte, colorize bool) string {
lines := strings.Split(string(source), "\n")
var buf strings.Builder
for i, entry := range r.Entries {
if i > 0 {
buf.WriteString("\n")
}
buf.WriteString(formatErrorEntry(entry, fileName, lines, colorize))
}
return buf.String()
}

func color(text, code string, colorize bool) string {
if !colorize {
return text
}
return code + text + reset
}

func severityColor(kind report.EntryKind) string {
switch kind {
case report.Error:
return red
case report.Info:
return cyan
case report.Warn:
return yellow
default:
return reset
}
}

func writeUnderline(buf *strings.Builder, col, gutterWidth int, message, line string, colorize bool) {
underlineStart := col - 1
rest := line[underlineStart:]
nextWhitespace := strings.IndexFunc(rest, unicode.IsSpace)
if nextWhitespace == -1 {
// If we didn't find whitespace then that means that we need to underline the entire string
nextWhitespace = len(rest)
}
underlineEnd := underlineStart + nextWhitespace
padding := strings.Repeat(" ", underlineStart)
underline := strings.Repeat("^", underlineEnd-underlineStart)
underline = color(underline, blue, colorize)

fmt.Fprintf(buf, " %s | %s%s %s\n",
strings.Repeat(" ", gutterWidth), padding, underline, message)
}

// formatErrorEntry will try to return the error as a pretty string in the following form
//
// error[$.boot_device.layout]:
//
// --> ../testing.bu:4:11
// |
// 2 | version: 1.6.0
// 3 | boot_device:
// 4 | layout: s390x-virt
// | ^^^^^^^^^^ mirroring not supported on layouts: s390x-eckd, s390x-zfcp, s390x-virt
// 5 | mirror:
// 6 | devices:
// |
func formatErrorEntry(entry report.Entry, filename string, lines []string, colorize bool) string {
if entry.Marker.StartP == nil {
return entry.String() + "\n"
}

line := int(entry.Marker.StartP.Line)
col := int(entry.Marker.StartP.Column)

// this should never happen as lines and cols are 1 indexed, but we'll add a check in case the vcontext library ever changes
if line < 1 || line > len(lines) || col < 1 || col > len(lines[line-1]) {
return entry.String() + "\n"
}

var buf strings.Builder
kindColor := severityColor(entry.Kind)
kindStr := color(entry.Kind.String(), kindColor, colorize)

path := ""
if entry.Context.Len() > 0 {
path = "[" + entry.Context.String() + "]"
}
fmt.Fprintf(&buf, "%s%s:\n", kindStr, path)
// Add information about the location of the error in the following form:
//
// " --> testing.bu:10:4"
arrow := color("-->", blue, colorize)
fmt.Fprintf(&buf, " %s %s:%d:%d\n", arrow, filename, line, col)

// Number of lines to show before and after the error location
contextLines := 2
contextLineInit := max(1, line-contextLines)
contextLineEnd := min(len(lines), line+contextLines)

// width of the largest line number
gutterWidth := len(fmt.Sprintf("%d", contextLineEnd))
fmt.Fprintf(&buf, " %s |\n", strings.Repeat(" ", gutterWidth))
for lineNumber := contextLineInit; lineNumber <= contextLineEnd; lineNumber++ {
lineNum := color(fmt.Sprintf("%*d", gutterWidth, lineNumber), blue, colorize)

fmt.Fprintf(&buf, " %s | %s\n", lineNum, lines[lineNumber-1])
// Underline the error and write the error message
if lineNumber == line {
writeUnderline(&buf, col, gutterWidth, entry.Message, lines[lineNumber-1], colorize)
}
}

// Empty line at the end
fmt.Fprintf(&buf, " %s |\n", strings.Repeat(" ", gutterWidth))
return buf.String()
}
Loading