Skip to content

Commit d96931f

Browse files
committed
expand: Support integer bases in arithmetic
The existing arithmetic logic is hard coded to always treat strings as base 10 integers. This commit updates that logic to support what bash supports based on its manual page. [1] References 1. https://www.man7.org/linux/man-pages/man1/bash.1.html
1 parent 88fac5c commit d96931f

2 files changed

Lines changed: 92 additions & 2 deletions

File tree

expand/arith.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,48 @@ func oneIf(b bool) int {
111111
return 0
112112
}
113113

114-
// atoi is like [strconv.ParseInt](s, 10, 64), but it ignores errors and trims whitespace.
114+
// atoi is like [strconv.ParseInt](s, BASE, 64), but it handles integer
115+
// base prefixes according to bash-shell's rules, ignores errors, and
116+
// trims whitespace.
117+
//
118+
// Here is how bash handles integer parsing according to its manual page:
119+
//
120+
// Integer constants follow the C language definition, without
121+
// suffixes or character constants. Constants with a leading 0 are
122+
// interpreted as octal numbers. A leading 0x or 0X denotes
123+
// hexadecimal. Otherwise, numbers take the form [base#]n, where the
124+
// optional base is a decimal number between 2 and 64 representing
125+
// the arithmetic base, and n is a number in that base. If base# is
126+
// omitted, then base 10 is used. When specifying n, if a non-digit
127+
// is required, the digits greater than 9 are represented by the
128+
// lowercase letters, the uppercase letters, @, and _, in that order.
129+
// If base is less than or equal to 36, lowercase and uppercase
130+
// letters may be used interchangeably to represent numbers between
131+
// 10 and 35.
132+
//
133+
// - https://www.man7.org/linux/man-pages/man1/bash.1.html
115134
func atoi(s string) int64 {
116135
s = strings.TrimSpace(s)
117-
n, _ := strconv.ParseInt(s, 10, 64)
136+
base := int64(10)
137+
switch {
138+
case strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X"):
139+
base = 16
140+
s = s[2:]
141+
case strings.HasPrefix(s, "0"):
142+
base = 8
143+
s = s[1:]
144+
default:
145+
baseStr, intStr, hasSep := strings.Cut(s, "#")
146+
if hasSep {
147+
var err error
148+
base, err = strconv.ParseInt(baseStr, 10, 8)
149+
if err != nil || base > 64 {
150+
return 0
151+
}
152+
s = intStr
153+
}
154+
}
155+
n, _ := strconv.ParseInt(s, int(base), 64)
118156
return n
119157
}
120158

expand/expand_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,58 @@ func Test_glob(t *testing.T) {
133133
}
134134
}
135135

136+
func TestArith(t *testing.T) {
137+
tests := []struct {
138+
src string
139+
want []string
140+
}{
141+
{
142+
"$((255+1))",
143+
[]string{"256"},
144+
},
145+
{
146+
"$((0xff+1))",
147+
[]string{"256"},
148+
},
149+
{
150+
"$((0377+1))",
151+
[]string{"256"},
152+
},
153+
{
154+
"$((10#255+1))",
155+
[]string{"256"},
156+
},
157+
{
158+
"$((16#ff+1))",
159+
[]string{"256"},
160+
},
161+
{
162+
"$((2#11111111+1))",
163+
[]string{"256"},
164+
},
165+
{
166+
"$((16#badc0ffee+1))",
167+
[]string{"50159747055"},
168+
},
169+
{
170+
"$((nope+1))",
171+
[]string{"1"}, // Yes, this is what bash does.
172+
},
173+
}
174+
for _, tc := range tests {
175+
word := parseWord(t, tc.src)
176+
for range 2 {
177+
got, err := Fields(nil, word)
178+
if err != nil {
179+
t.Fatalf("did not want error, got %v", err)
180+
}
181+
if !reflect.DeepEqual(got, tc.want) {
182+
t.Fatalf("wanted %q, got %q", tc.want, got)
183+
}
184+
}
185+
}
186+
}
187+
136188
type mockFileInfo struct {
137189
name string
138190
typ fs.FileMode

0 commit comments

Comments
 (0)