package toml import ( "fmt" "strings" ) // ParseError is returned when there is an error parsing the TOML syntax such as // invalid syntax, duplicate keys, etc. // // In addition to the error message itself, you can also print detailed location // information with context by using [ErrorWithPosition]: // // toml: error: Key 'fruit' was already created and cannot be used as an array. // // At line 4, column 2-7: // // 2 | fruit = [] // 3 | // 4 | [[fruit]] # Not allowed // ^^^^^ // // [ErrorWithUsage] can be used to print the above with some more detailed usage // guidance: // // toml: error: newlines not allowed within inline tables // // At line 1, column 18: // // 1 | x = [{ key = 42 # // ^ // // Error help: // // Inline tables must always be on a single line: // // table = {key = 42, second = 43} // // It is invalid to split them over multiple lines like so: // // # INVALID // table = { // key = 42, // second = 43 // } // // Use regular for this: // // [table] // key = 42 // second = 43 type ParseError struct { Message string // Short technical message. Usage string // Longer message with usage guidance; may be blank. Position Position // Position of the error LastKey string // Last parsed key, may be blank. // Line the error occurred. // // Deprecated: use [Position]. Line int err error input string } // Position of an error. type Position struct { Line int // Line number, starting at 1. Start int // Start of error, as byte offset starting at 0. Len int // Lenght in bytes. } func (pe ParseError) Error() string { msg := pe.Message if msg == "" { // Error from errorf() msg = pe.err.Error() } if pe.LastKey == "" { return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg) } return fmt.Sprintf("toml: line %d (last key %q): %s", pe.Position.Line, pe.LastKey, msg) } // ErrorWithPosition returns the error with detailed location context. // // See the documentation on [ParseError]. func (pe ParseError) ErrorWithPosition() string { if pe.input == "" { // Should never happen, but just in case. return pe.Error() } var ( lines = strings.Split(pe.input, "\n") col = pe.column(lines) b = new(strings.Builder) ) msg := pe.Message if msg == "" { msg = pe.err.Error() } // TODO: don't show control characters as literals? This may not show up // well everywhere. if pe.Position.Len == 1 { fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n", msg, pe.Position.Line, col+1) } else { fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n", msg, pe.Position.Line, col, col+pe.Position.Len) } if pe.Position.Line > 2 { fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, lines[pe.Position.Line-3]) } if pe.Position.Line > 1 { fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, lines[pe.Position.Line-2]) } fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, lines[pe.Position.Line-1]) fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col), strings.Repeat("^", pe.Position.Len)) return b.String() } // ErrorWithUsage returns the error with detailed location context and usage // guidance. // // See the documentation on [ParseError]. func (pe ParseError) ErrorWithUsage() string { m := pe.ErrorWithPosition() if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" { lines := strings.Split(strings.TrimSpace(u.Usage()), "\n") for i := range lines { if lines[i] != "" { lines[i] = " " + lines[i] } } return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n" } return m } func (pe ParseError) column(lines []string) int { var pos, col int for i := range lines { ll := len(lines[i]) + 1 // +1 for the removed newline if pos+ll >= pe.Position.Start { col = pe.Position.Start - pos if col < 0 { // Should never happen, but just in case. col = 0 } break } pos += ll } return col } type ( errLexControl struct{ r rune } errLexEscape struct{ r rune } errLexUTF8 struct{ b byte } errLexInvalidNum struct{ v string } errLexInvalidDate struct{ v string } errLexInlineTableNL struct{} errLexStringNL struct{} errParseRange struct { i interface{} // int or float size string // "int64", "uint16", etc. } errParseDuration struct{ d string } ) func (e errLexControl) Error() string { return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r) } func (e errLexControl) Usage() string { return "" } func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) } func (e errLexEscape) Usage() string { return usageEscape } func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) } func (e errLexUTF8) Usage() string { return "" } func (e errLexInvalidNum) Error() string { return fmt.Sprintf("invalid number: %q", e.v) } func (e errLexInvalidNum) Usage() string { return "" } func (e errLexInvalidDate) Error() string { return fmt.Sprintf("invalid date: %q", e.v) } func (e errLexInvalidDate) Usage() string { return "" } func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" } func (e errLexInlineTableNL) Usage() string { return usageInlineNewline } func (e errLexStringNL) Error() string { return "strings cannot contain newlines" } func (e errLexStringNL) Usage() string { return usageStringNewline } func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) } func (e errParseRange) Usage() string { return usageIntOverflow } func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) } func (e errParseDuration) Usage() string { return usageDuration } const usageEscape = ` A '\' inside a "-delimited string is interpreted as an escape character. The following escape sequences are supported: \b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX To prevent a '\' from being recognized as an escape character, use either: - a ' or '''-delimited string; escape characters aren't processed in them; or - write two backslashes to get a single backslash: '\\'. If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/' instead of '\' will usually also work: "C:/Users/martin". ` const usageInlineNewline = ` Inline tables must always be on a single line: table = {key = 42, second = 43} It is invalid to split them over multiple lines like so: # INVALID table = { key = 42, second = 43 } Use regular for this: [table] key = 42 second = 43 ` const usageStringNewline = ` Strings must always be on a single line, and cannot span more than one line: # INVALID string = "Hello, world!" Instead use """ or ''' to split strings over multiple lines: string = """Hello, world!""" ` const usageIntOverflow = ` This number is too large; this may be an error in the TOML, but it can also be a bug in the program that uses too small of an integer. The maximum and minimum values are: size │ lowest │ highest ───────┼────────────────┼────────── int8 │ -128 │ 127 int16 │ -32,768 │ 32,767 int32 │ -2,147,483,648 │ 2,147,483,647 int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷ uint8 │ 0 │ 255 uint16 │ 0 │ 65535 uint32 │ 0 │ 4294967295 uint64 │ 0 │ 1.8 × 10¹⁸ int refers to int32 on 32-bit systems and int64 on 64-bit systems. ` const usageDuration = ` A duration must be as "number", without any spaces. Valid units are: ns nanoseconds (billionth of a second) us, µs microseconds (millionth of a second) ms milliseconds (thousands of a second) s seconds m minutes h hours You can combine multiple units; for example "5m10s" for 5 minutes and 10 seconds. `