Files
gosh/arith.go
T
Florian WaltherandClaude Opus 5.5 1f507f6562 add parameter expansion, arithmetic, set -e/-x/pipefail, trap, eval, exec
- ${#v}, ${v#p} ${v##p} ${v%p} ${v%%p}, ${v/p/r} ${v//p/r} ${v/#p/r}
  ${v/%p/r}, ${v:off:len}, ${v-w} ${v:=w} ${v:+w} ${v:?msg}; nested ${}
- arithmetic expansion $((...)) with the C operators, ** and assignments
- set -e, set -x, set -o pipefail (also as gosh options, passed on to
  subshells); set shows variables, set -o the options
- trap for EXIT and signals; scripts with an EXIT trap run it on
  SIGINT/SIGTERM/SIGHUP too
- eval, exec (also exec > file without a command), $!, wait pid

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:15:24 +02:00

459 lines
9.5 KiB
Go

package main
// Arithmetic expansion $((expression)) with 64-bit integers and the
// operators of C (and ** for powers), in the order of precedence:
//
// x++ x--, ++x --x, + - ! ~ (unary), **, * / %, + -, << >>,
// < <= > >=, == !=, &, ^, |, &&, ||, ?:, = += -= *= /= %= <<= >>= &= ^= |=, ","
//
// Variables can be used with or without $; their values are evaluated as
// expressions, an empty value is 0, an unset variable is an error (like
// everywhere in gosh). Numbers are decimal, 0x... hexadecimal or 0...
// octal.
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
)
// arithEnd returns the index of the last ) of $((...)) at s[i], or -1 if
// there is no arithmetic expansion (e.g. $( (cmd) ), a command substitution
// of a subshell).
func arithEnd(s string, i int) int {
if !strings.HasPrefix(s[i:], "$((") {
return -1
}
end := substEnd(s, i)
if end < 0 {
return -1
}
// The inner ( must end right before the outer )
if inner := substEnd(s, i+1); inner != end-1 {
return -1
}
return end
}
// evalArithText expands an arithmetic expression (variables, $(...), quotes)
// and evaluates it.
func evalArithText(text string) (int64, error) {
expr, err := expandWordText(text)
if err != nil {
return 0, err
}
return evalArith(expr, 0)
}
// maxArithDepth limits the recursion of variables whose values are
// expressions with variables.
const maxArithDepth = 32
func evalArith(expr string, depth int) (int64, error) {
if depth > maxArithDepth {
return 0, errors.New("arithmetic: expression recursion level exceeded")
}
toks, err := lexArith(expr)
if err != nil {
return 0, err
}
a := &arith{toks: toks, depth: depth, expr: expr}
if len(toks) == 0 {
return 0, nil
}
v, err := a.comma()
if err != nil {
return 0, err
}
if a.pos < len(a.toks) {
return 0, a.syntaxErr()
}
return v, nil
}
type arithToken struct {
kind byte // 'n' number, 'v' variable, 'o' operator
text string
num int64
}
var arithOps = []string{
"<<=", ">>=", "**", "++", "--", "<<", ">>", "<=", ">=", "==", "!=", "&&", "||",
"+=", "-=", "*=", "/=", "%=", "&=", "^=", "|=",
"+", "-", "*", "/", "%", "<", ">", "&", "|", "^", "!", "~", "?", ":", "=", "(", ")", ",",
}
func lexArith(s string) ([]arithToken, error) {
var toks []arithToken
for i := 0; i < len(s); {
ch := s[i]
switch {
case ch == ' ' || ch == '\t' || ch == '\n':
i++
case ch >= '0' && ch <= '9':
j := i
for j < len(s) && (isVarNameByte(s[j], false)) {
j++
}
n, err := strconv.ParseInt(s[i:j], 0, 64)
if err != nil {
return nil, fmt.Errorf("%s: value too great for base (error token is \"%s\")", strings.TrimSpace(s), s[i:j])
}
toks = append(toks, arithToken{kind: 'n', text: s[i:j], num: n})
i = j
case isVarNameByte(ch, true):
j := i
for j < len(s) && isVarNameByte(s[j], false) {
j++
}
toks = append(toks, arithToken{kind: 'v', text: s[i:j]})
i = j
default:
found := false
for _, op := range arithOps {
if strings.HasPrefix(s[i:], op) {
toks = append(toks, arithToken{kind: 'o', text: op})
i += len(op)
found = true
break
}
}
if !found {
return nil, fmt.Errorf("%s: syntax error: invalid arithmetic operator (error token is \"%s\")", strings.TrimSpace(s), sanitize(s[i:]))
}
}
}
return toks, nil
}
type arith struct {
toks []arithToken
pos int
skip int // > 0 while a branch is evaluated that has no effect (&&, ||, ?:)
depth int
expr string
}
func (a *arith) peekOp(ops ...string) string {
if a.pos < len(a.toks) && a.toks[a.pos].kind == 'o' {
for _, op := range ops {
if a.toks[a.pos].text == op {
return op
}
}
}
return ""
}
func (a *arith) syntaxErr() error {
token := "end of expression"
if a.pos < len(a.toks) {
token = a.toks[a.pos].text
}
return fmt.Errorf("%s: arithmetic syntax error (error token is \"%s\")", strings.TrimSpace(a.expr), sanitize(token))
}
// variable returns the value of a variable in an expression.
func (a *arith) variable(name string) (int64, error) {
if a.skip > 0 {
return 0, nil
}
value, ok := os.LookupEnv(name)
if !ok {
return 0, fmt.Errorf("%s: unbound variable", name)
}
if strings.TrimSpace(value) == "" {
return 0, nil
}
if n, err := strconv.ParseInt(strings.TrimSpace(value), 0, 64); err == nil {
return n, nil
}
return evalArith(value, a.depth+1)
}
func (a *arith) setVariable(name string, v int64) {
if a.skip == 0 {
os.Setenv(name, strconv.FormatInt(v, 10))
}
}
func (a *arith) comma() (int64, error) {
v, err := a.assign()
for err == nil && a.peekOp(",") != "" {
a.pos++
v, err = a.assign()
}
return v, err
}
func (a *arith) assign() (int64, error) {
if a.pos+1 < len(a.toks) && a.toks[a.pos].kind == 'v' && a.toks[a.pos+1].kind == 'o' {
name, op := a.toks[a.pos].text, a.toks[a.pos+1].text
if op == "=" || len(op) >= 2 && strings.HasSuffix(op, "=") && op != "==" && op != "!=" && op != "<=" && op != ">=" {
a.pos += 2
rhs, err := a.assign()
if err != nil {
return 0, err
}
v := rhs
if op != "=" {
old, err := a.variable(name)
if err != nil {
return 0, err
}
if v, err = a.apply(strings.TrimSuffix(op, "="), old, rhs); err != nil {
return 0, err
}
}
a.setVariable(name, v)
return v, nil
}
}
return a.ternary()
}
func (a *arith) ternary() (int64, error) {
cond, err := a.binary(0)
if err != nil || a.peekOp("?") == "" {
return cond, err
}
a.pos++
if cond == 0 {
a.skip++
}
yes, err := a.assign()
if cond == 0 {
a.skip--
}
if err != nil {
return 0, err
}
if a.peekOp(":") == "" {
return 0, a.syntaxErr()
}
a.pos++
if cond != 0 {
a.skip++
}
no, err := a.assign()
if cond != 0 {
a.skip--
}
if err != nil {
return 0, err
}
if cond != 0 {
return yes, nil
}
return no, nil
}
// binaryLevels are the binary operators from the lowest precedence on.
var binaryLevels = [][]string{
{"||"}, {"&&"}, {"|"}, {"^"}, {"&"}, {"==", "!="}, {"<", "<=", ">", ">="},
{"<<", ">>"}, {"+", "-"}, {"*", "/", "%"},
}
func (a *arith) binary(level int) (int64, error) {
if level == len(binaryLevels) {
return a.power()
}
lhs, err := a.binary(level + 1)
if err != nil {
return 0, err
}
for {
op := a.peekOp(binaryLevels[level]...)
if op == "" {
return lhs, nil
}
a.pos++
// && and || do not evaluate the right side if the result is known
short := op == "&&" && lhs == 0 || op == "||" && lhs != 0
if short {
a.skip++
}
rhs, err := a.binary(level + 1)
if short {
a.skip--
}
if err != nil {
return 0, err
}
if lhs, err = a.apply(op, lhs, rhs); err != nil {
return 0, err
}
}
}
func boolInt(b bool) int64 {
if b {
return 1
}
return 0
}
// apply applies a binary operator.
func (a *arith) apply(op string, x, y int64) (int64, error) {
switch op {
case "||":
return boolInt(x != 0 || y != 0), nil
case "&&":
return boolInt(x != 0 && y != 0), nil
case "|":
return x | y, nil
case "^":
return x ^ y, nil
case "&":
return x & y, nil
case "==":
return boolInt(x == y), nil
case "!=":
return boolInt(x != y), nil
case "<":
return boolInt(x < y), nil
case "<=":
return boolInt(x <= y), nil
case ">":
return boolInt(x > y), nil
case ">=":
return boolInt(x >= y), nil
case "<<":
return x << uint64(y&63), nil
case ">>":
return x >> uint64(y&63), nil
case "+":
return x + y, nil
case "-":
return x - y, nil
case "*":
return x * y, nil
case "/", "%":
if y == 0 {
if a.skip > 0 {
return 0, nil
}
return 0, fmt.Errorf("%s: division by 0", strings.TrimSpace(a.expr))
}
if op == "/" {
return x / y, nil
}
return x % y, nil
case "**":
if y < 0 {
if a.skip > 0 {
return 0, nil
}
return 0, fmt.Errorf("%s: exponent less than 0", strings.TrimSpace(a.expr))
}
result := int64(1)
for ; y > 0; y-- {
result *= x
}
return result, nil
}
return 0, a.syntaxErr()
}
// power parses x ** y (right associative, binds tighter than * but less
// than the unary operators).
func (a *arith) power() (int64, error) {
base, err := a.unary()
if err != nil || a.peekOp("**") == "" {
return base, err
}
a.pos++
exp, err := a.power()
if err != nil {
return 0, err
}
return a.apply("**", base, exp)
}
func (a *arith) unary() (int64, error) {
switch op := a.peekOp("+", "-", "!", "~", "++", "--"); op {
case "":
return a.postfix()
case "++", "--":
a.pos++
if a.pos >= len(a.toks) || a.toks[a.pos].kind != 'v' {
return 0, a.syntaxErr()
}
name := a.toks[a.pos].text
a.pos++
v, err := a.variable(name)
if err != nil {
return 0, err
}
if op == "++" {
v++
} else {
v--
}
a.setVariable(name, v)
return v, nil
default:
a.pos++
v, err := a.unary()
if err != nil {
return 0, err
}
switch op {
case "-":
return -v, nil
case "!":
return boolInt(v == 0), nil
case "~":
return ^v, nil
}
return v, nil
}
}
func (a *arith) postfix() (int64, error) {
if a.pos < len(a.toks) && a.toks[a.pos].kind == 'v' {
name := a.toks[a.pos].text
a.pos++
v, err := a.variable(name)
if err != nil {
return 0, err
}
if op := a.peekOp("++", "--"); op != "" {
a.pos++
n := v + 1
if op == "--" {
n = v - 1
}
a.setVariable(name, n)
}
return v, nil
}
return a.primary()
}
func (a *arith) primary() (int64, error) {
if a.pos >= len(a.toks) {
return 0, a.syntaxErr()
}
t := a.toks[a.pos]
switch {
case t.kind == 'n':
a.pos++
return t.num, nil
case t.kind == 'o' && t.text == "(":
a.pos++
v, err := a.comma()
if err != nil {
return 0, err
}
if a.peekOp(")") == "" {
return 0, a.syntaxErr()
}
a.pos++
return v, nil
}
return 0, a.syntaxErr()
}