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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Fixed a bug in YAML and TOML parsers that caused them to fail when parsing non-base10 numbers (e.g. hex, binary, octal).
- Fixed a bug in the `toInt` function that caused it to fail when parsing non-base10 numbers (e.g. hex, binary, octal).
- XML child element ordering now has more comprehensive round-trip handling. Thanks @takeokunn.

## [v3.4.1] - 2026-03-30
Expand Down
2 changes: 1 addition & 1 deletion execution/func_to_int.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var FuncToInt = NewFunc(
return nil, err
}

i, err := strconv.ParseInt(stringValue, 10, 64)
i, err := strconv.ParseInt(stringValue, 0, 64)
Comment thread
TomWright marked this conversation as resolved.
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion parsing/toml/toml_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (j *tomlReader) readNode(p *unstable.Parser, n *unstable.Node) (string, *mo
}
return "", model.NewFloatValue(f), nil
case unstable.Integer:
i64, err := strconv.ParseInt(string(n.Data), 10, 64)
i64, err := strconv.ParseInt(string(n.Data), 0, 64)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not an expert in TOML, but I took a quick look at the spec

  • Please add tests for these:

Valid

42, -42, 0x12, 0o7, 0b111, 12_000, -12_000

Valid but as string: "042"

Invalid

-0x12, -0o7, -0b111, 042

All these values are valid with base 0 with ParseInt but not in TOML

Here are also feedbacks provided by an LLM

# ==========================================================
# HEXADECIMAL, OCTAL, AND BINARY BASES
# ==========================================================

# --- Valid (Case Insensitivity for Prefixes and Values) ---
hex_lowercase = 0xff12       # Standard hex
hex_uppercase = 0XFF12       # Uppercase prefix and values are VALID

bin_lowercase = 0b1101
bin_uppercase = 0B1101       # Uppercase 'B' is VALID
oct_lowercase = 0o755
oct_uppercase = 0O755        # Uppercase 'O' is VALID

# --- Invalid ---
# Negative signs are NOT supported for non-decimal bases
invalid_hex = -0xFF12        
invalid_oct = -0o755         
invalid_bin = -0b1101        

# ==========================================================
# SCIENTIFIC NOTATION (FLOATS)
# ==========================================================

# --- Valid ---
standard_sci = 1e6           # 1,000,000
positive_exp = 5e+10         # 50,000,000,000
negative_exp = -1.5E-4       # -0.00015 (Uppercase 'E' is VALID)
large_float  = 6_022.140e23  # Avogadro's number

# --- Invalid ---
# Decimal points must always have digits on both sides
invalid_point_1 = .1e2       
invalid_point_2 = 1.e2       

# Underscores are NOT allowed in the exponent part
invalid_underscore = 1e1_0   

if err != nil {
return "", nil, err
}
Expand Down
38 changes: 38 additions & 0 deletions parsing/toml/toml_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,44 @@ func TestTomlReader_EdgeCases(t *testing.T) {
})
}

func TestTomlReader_NonBase10Numbers(t *testing.T) {
t.Run("hex", tomlReaderTest([]byte(`val = 0x12`), func() *model.Value {
res := model.NewMapValue()
_ = res.SetMapKey("val", model.NewIntValue(18))
return res
}))

t.Run("octal", tomlReaderTest([]byte(`val = 0o7`), func() *model.Value {
res := model.NewMapValue()
_ = res.SetMapKey("val", model.NewIntValue(7))
return res
}))

t.Run("binary", tomlReaderTest([]byte(`val = 0b111`), func() *model.Value {
res := model.NewMapValue()
_ = res.SetMapKey("val", model.NewIntValue(7))
return res
}))

t.Run("underscore decimal", tomlReaderTest([]byte(`val = 12_000`), func() *model.Value {
res := model.NewMapValue()
_ = res.SetMapKey("val", model.NewIntValue(12000))
return res
}))

t.Run("negative underscore decimal", tomlReaderTest([]byte(`val = -12_000`), func() *model.Value {
res := model.NewMapValue()
_ = res.SetMapKey("val", model.NewIntValue(-12000))
return res
}))

t.Run("negative int", tomlReaderTest([]byte(`val = -42`), func() *model.Value {
res := model.NewMapValue()
_ = res.SetMapKey("val", model.NewIntValue(-42))
return res
}))
}

func TestTomlReader_TimeStrings(t *testing.T) {
// Local date
t.Run("local date string", func(t *testing.T) {
Expand Down
27 changes: 25 additions & 2 deletions parsing/yaml/yaml_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"strconv"
"strings"

"github.com/tomwright/dasel/v3/model"
"github.com/tomwright/dasel/v3/parsing"
Expand Down Expand Up @@ -96,11 +97,11 @@ func (yv *yamlValue) UnmarshalYAML(value *yaml.Node) error {
case "!!bool":
yv.value = model.NewBoolValue(value.Value == "true")
case "!!int":
i, err := strconv.Atoi(value.Value)
i, err := parseYAMLInt(value.Value)
if err != nil {
return err
}
yv.value = model.NewIntValue(int64(i))
yv.value = model.NewIntValue(i)
case "!!float":
f, err := strconv.ParseFloat(value.Value, 64)
if err != nil {
Expand Down Expand Up @@ -186,3 +187,25 @@ func (yv *yamlValue) UnmarshalYAML(value *yaml.Node) error {
}
return nil
}

func parseYAMLInt(s string) (int64, error) {
// Strip leading sign for prefix detection.
clean := s
if len(clean) > 0 && (clean[0] == '+' || clean[0] == '-') {
clean = clean[1:]
}

switch {
case strings.HasPrefix(clean, "0x") || strings.HasPrefix(clean, "0X"):
return strconv.ParseInt(s, 0, 64)
case strings.HasPrefix(clean, "0o") || strings.HasPrefix(clean, "0O"):
return strconv.ParseInt(s, 0, 64)
case strings.HasPrefix(clean, "0b") || strings.HasPrefix(clean, "0B"):
return strconv.ParseInt(s, 0, 64)
default:
// YAML 1.2 allows underscores in decimal integers (e.g. 1_000).
// strconv.ParseInt with base 10 does not support underscores,
// so we strip them before parsing.
return strconv.ParseInt(strings.ReplaceAll(s, "_", ""), 10, 64)
}
}
155 changes: 155 additions & 0 deletions parsing/yaml/yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,161 @@ name2: Tom
`,
}.run)

t.Run("base numbers", func(t *testing.T) {
Comment thread
TomWright marked this conversation as resolved.
t.Run("standard", rwTestCase{
in: `10
`,
out: `10
`,
}.run)

t.Run("zero", rwTestCase{
in: `0
`,
out: `0
`,
}.run)

t.Run("negative", rwTestCase{
in: `-42
`,
out: `-42
`,
}.run)

t.Run("hex lowercase", rwTestCase{
in: `0x10
`,
out: `16
`,
}.run)

t.Run("hex uppercase letters", rwTestCase{
in: `0xff
`,
out: `255
`,
}.run)

t.Run("octal", rwTestCase{
in: `0o10
`,
out: `8
`,
}.run)

t.Run("binary", rwTestCase{
in: `0b10
`,
out: `2
`,
}.run)

t.Run("leading zero is decimal", rwTestCase{
in: `010
`,
out: `10
`,
}.run)

t.Run("hex in map", rwTestCase{
in: `val: 0x10
`,
out: `val: 16
`,
}.run)

t.Run("octal in map", rwTestCase{
in: `val: 0o77
`,
out: `val: 63
`,
}.run)

t.Run("mixed types in map", rwTestCase{
in: `dec: 42
hex: 0xff
oct: 0o77
bin: 0b1010
`,
out: `dec: 42
hex: 255
oct: 63
bin: 10
`,
}.run)

t.Run("positive sign", rwTestCase{
in: `+42
`,
out: `42
`,
}.run)

t.Run("positive hex", rwTestCase{
in: `+0x10
`,
out: `16
`,
}.run)

t.Run("positive octal", rwTestCase{
in: `+0o10
`,
out: `8
`,
}.run)

t.Run("positive binary", rwTestCase{
in: `+0b10
`,
out: `2
`,
}.run)

t.Run("negative hex", rwTestCase{
in: `-0x10
`,
out: `-16
`,
}.run)

t.Run("negative octal", rwTestCase{
in: `-0o10
`,
out: `-8
`,
}.run)

t.Run("negative binary", rwTestCase{
in: `-0b10
`,
out: `-2
`,
}.run)

t.Run("underscore decimal", rwTestCase{
in: `1_000
`,
out: `1000
`,
}.run)

t.Run("underscore hex", rwTestCase{
in: `0xFF_FF
`,
out: `65535
`,
}.run)

t.Run("underscore binary", rwTestCase{
in: `0b1010_1010
`,
out: `170
`,
}.run)
})

t.Run("bounded yaml expansion", func(t *testing.T) {
in := `a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
Expand Down
4 changes: 4 additions & 0 deletions selector/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ func TestParser_Parse_HappyPath(t *testing.T) {
input: "42",
expected: ast.NumberIntExpr{Value: 42},
}.run)
t.Run("leading zero decimal", happyTestCase{
input: "010",
expected: ast.NumberIntExpr{Value: 10},
}.run)
t.Run("float", happyTestCase{
input: "42.1",
expected: ast.NumberFloatExpr{Value: 42.1},
Expand Down
Loading