mirror of https://github.com/deuill/grawkit.git
play: Update to latest Go and GoAWK versions
And vendor in dependencies, as is appropriate for end-products.
This commit is contained in:
parent
5500c9c667
commit
ec3cfbdf0e
|
@ -1,5 +1,5 @@
|
|||
module github.com/deuill/grawkit/play
|
||||
|
||||
go 1.12
|
||||
go 1.17
|
||||
|
||||
require github.com/benhoyt/goawk v1.6.0
|
||||
require github.com/benhoyt/goawk v1.13.0
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
github.com/benhoyt/goawk v1.6.0 h1:6oHKBL2BAvYiKroi8RhmpnhyvMGeiW5u/WEaxyOcKRQ=
|
||||
github.com/benhoyt/goawk v1.6.0/go.mod h1:krl47rWeW8s+kD3dtHYm6aq4MBGRzQD5PGkZaRm38Uk=
|
||||
github.com/benhoyt/goawk v1.13.0 h1:/Iu42ErHsT5vHrpWyewpI98hB2PHBk66o+oLZs4drPs=
|
||||
github.com/benhoyt/goawk v1.13.0/go.mod h1:UKzPyqDh9O7HZ/ftnU33MYlAP2rPbXdwQ+OVlEOPsjM=
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Ben Hoyt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,557 @@
|
|||
// GoAWK parser - abstract syntax tree structs
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
. "github.com/benhoyt/goawk/lexer"
|
||||
)
|
||||
|
||||
// Stmts is a block containing multiple statements.
|
||||
type Stmts []Stmt
|
||||
|
||||
func (ss Stmts) String() string {
|
||||
lines := []string{}
|
||||
for _, s := range ss {
|
||||
subLines := strings.Split(s.String(), "\n")
|
||||
for _, sl := range subLines {
|
||||
lines = append(lines, " "+sl+"\n")
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "")
|
||||
}
|
||||
|
||||
// Action is pattern-action section of a program.
|
||||
type Action struct {
|
||||
Pattern []Expr
|
||||
Stmts Stmts
|
||||
}
|
||||
|
||||
func (a *Action) String() string {
|
||||
patterns := make([]string, len(a.Pattern))
|
||||
for i, p := range a.Pattern {
|
||||
patterns[i] = p.String()
|
||||
}
|
||||
sep := ""
|
||||
if len(patterns) > 0 && a.Stmts != nil {
|
||||
sep = " "
|
||||
}
|
||||
stmtsStr := ""
|
||||
if a.Stmts != nil {
|
||||
stmtsStr = "{\n" + a.Stmts.String() + "}"
|
||||
}
|
||||
return strings.Join(patterns, ", ") + sep + stmtsStr
|
||||
}
|
||||
|
||||
// Expr is the abstract syntax tree for any AWK expression.
|
||||
type Expr interface {
|
||||
expr()
|
||||
String() string
|
||||
}
|
||||
|
||||
// All these types implement the Expr interface.
|
||||
func (e *FieldExpr) expr() {}
|
||||
func (e *UnaryExpr) expr() {}
|
||||
func (e *BinaryExpr) expr() {}
|
||||
func (e *ArrayExpr) expr() {}
|
||||
func (e *InExpr) expr() {}
|
||||
func (e *CondExpr) expr() {}
|
||||
func (e *NumExpr) expr() {}
|
||||
func (e *StrExpr) expr() {}
|
||||
func (e *RegExpr) expr() {}
|
||||
func (e *VarExpr) expr() {}
|
||||
func (e *IndexExpr) expr() {}
|
||||
func (e *AssignExpr) expr() {}
|
||||
func (e *AugAssignExpr) expr() {}
|
||||
func (e *IncrExpr) expr() {}
|
||||
func (e *CallExpr) expr() {}
|
||||
func (e *UserCallExpr) expr() {}
|
||||
func (e *MultiExpr) expr() {}
|
||||
func (e *GetlineExpr) expr() {}
|
||||
|
||||
// FieldExpr is an expression like $0.
|
||||
type FieldExpr struct {
|
||||
Index Expr
|
||||
}
|
||||
|
||||
func (e *FieldExpr) String() string {
|
||||
return "$" + e.Index.String()
|
||||
}
|
||||
|
||||
// UnaryExpr is an expression like -1234.
|
||||
type UnaryExpr struct {
|
||||
Op Token
|
||||
Value Expr
|
||||
}
|
||||
|
||||
func (e *UnaryExpr) String() string {
|
||||
return e.Op.String() + e.Value.String()
|
||||
}
|
||||
|
||||
// BinaryExpr is an expression like 1 + 2.
|
||||
type BinaryExpr struct {
|
||||
Left Expr
|
||||
Op Token
|
||||
Right Expr
|
||||
}
|
||||
|
||||
func (e *BinaryExpr) String() string {
|
||||
var opStr string
|
||||
if e.Op == CONCAT {
|
||||
opStr = " "
|
||||
} else {
|
||||
opStr = " " + e.Op.String() + " "
|
||||
}
|
||||
return "(" + e.Left.String() + opStr + e.Right.String() + ")"
|
||||
}
|
||||
|
||||
// ArrayExpr is an array reference. Not really a stand-alone
|
||||
// expression, except as an argument to split() or a user function
|
||||
// call.
|
||||
type ArrayExpr struct {
|
||||
Scope VarScope
|
||||
Index int
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e *ArrayExpr) String() string {
|
||||
return e.Name
|
||||
}
|
||||
|
||||
// InExpr is an expression like (index in array).
|
||||
type InExpr struct {
|
||||
Index []Expr
|
||||
Array *ArrayExpr
|
||||
}
|
||||
|
||||
func (e *InExpr) String() string {
|
||||
if len(e.Index) == 1 {
|
||||
return "(" + e.Index[0].String() + " in " + e.Array.String() + ")"
|
||||
}
|
||||
indices := make([]string, len(e.Index))
|
||||
for i, index := range e.Index {
|
||||
indices[i] = index.String()
|
||||
}
|
||||
return "((" + strings.Join(indices, ", ") + ") in " + e.Array.String() + ")"
|
||||
}
|
||||
|
||||
// CondExpr is an expression like cond ? 1 : 0.
|
||||
type CondExpr struct {
|
||||
Cond Expr
|
||||
True Expr
|
||||
False Expr
|
||||
}
|
||||
|
||||
func (e *CondExpr) String() string {
|
||||
return "(" + e.Cond.String() + " ? " + e.True.String() + " : " + e.False.String() + ")"
|
||||
}
|
||||
|
||||
// NumExpr is a literal number like 1234.
|
||||
type NumExpr struct {
|
||||
Value float64
|
||||
}
|
||||
|
||||
func (e *NumExpr) String() string {
|
||||
return fmt.Sprintf("%.6g", e.Value)
|
||||
}
|
||||
|
||||
// StrExpr is a literal string like "foo".
|
||||
type StrExpr struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (e *StrExpr) String() string {
|
||||
return strconv.Quote(e.Value)
|
||||
}
|
||||
|
||||
// RegExpr is a stand-alone regex expression, equivalent to:
|
||||
// $0 ~ /regex/.
|
||||
type RegExpr struct {
|
||||
Regex string
|
||||
}
|
||||
|
||||
func (e *RegExpr) String() string {
|
||||
escaped := strings.Replace(e.Regex, "/", `\/`, -1)
|
||||
return "/" + escaped + "/"
|
||||
}
|
||||
|
||||
type VarScope int
|
||||
|
||||
const (
|
||||
ScopeSpecial VarScope = iota
|
||||
ScopeGlobal
|
||||
ScopeLocal
|
||||
)
|
||||
|
||||
// VarExpr is a variable reference (special var, global, or local).
|
||||
// Index is the resolved variable index used by the interpreter; Name
|
||||
// is the original name used by String().
|
||||
type VarExpr struct {
|
||||
Scope VarScope
|
||||
Index int
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e *VarExpr) String() string {
|
||||
return e.Name
|
||||
}
|
||||
|
||||
// IndexExpr is an expression like a[k] (rvalue or lvalue).
|
||||
type IndexExpr struct {
|
||||
Array *ArrayExpr
|
||||
Index []Expr
|
||||
}
|
||||
|
||||
func (e *IndexExpr) String() string {
|
||||
indices := make([]string, len(e.Index))
|
||||
for i, index := range e.Index {
|
||||
indices[i] = index.String()
|
||||
}
|
||||
return e.Array.String() + "[" + strings.Join(indices, ", ") + "]"
|
||||
}
|
||||
|
||||
// AssignExpr is an expression like x = 1234.
|
||||
type AssignExpr struct {
|
||||
Left Expr // can be one of: var, array[x], $n
|
||||
Right Expr
|
||||
}
|
||||
|
||||
func (e *AssignExpr) String() string {
|
||||
return e.Left.String() + " = " + e.Right.String()
|
||||
}
|
||||
|
||||
// AugAssignExpr is an assignment expression like x += 5.
|
||||
type AugAssignExpr struct {
|
||||
Left Expr // can be one of: var, array[x], $n
|
||||
Op Token
|
||||
Right Expr
|
||||
}
|
||||
|
||||
func (e *AugAssignExpr) String() string {
|
||||
return e.Left.String() + " " + e.Op.String() + "= " + e.Right.String()
|
||||
}
|
||||
|
||||
// IncrExpr is an increment or decrement expression like x++ or --y.
|
||||
type IncrExpr struct {
|
||||
Expr Expr
|
||||
Op Token
|
||||
Pre bool
|
||||
}
|
||||
|
||||
func (e *IncrExpr) String() string {
|
||||
if e.Pre {
|
||||
return e.Op.String() + e.Expr.String()
|
||||
} else {
|
||||
return e.Expr.String() + e.Op.String()
|
||||
}
|
||||
}
|
||||
|
||||
// CallExpr is a builtin function call like length($1).
|
||||
type CallExpr struct {
|
||||
Func Token
|
||||
Args []Expr
|
||||
}
|
||||
|
||||
func (e *CallExpr) String() string {
|
||||
args := make([]string, len(e.Args))
|
||||
for i, a := range e.Args {
|
||||
args[i] = a.String()
|
||||
}
|
||||
return e.Func.String() + "(" + strings.Join(args, ", ") + ")"
|
||||
}
|
||||
|
||||
// UserCallExpr is a user-defined function call like my_func(1, 2, 3)
|
||||
//
|
||||
// Index is the resolved function index used by the interpreter; Name
|
||||
// is the original name used by String().
|
||||
type UserCallExpr struct {
|
||||
Native bool // false = AWK-defined function, true = native Go func
|
||||
Index int
|
||||
Name string
|
||||
Args []Expr
|
||||
}
|
||||
|
||||
func (e *UserCallExpr) String() string {
|
||||
args := make([]string, len(e.Args))
|
||||
for i, a := range e.Args {
|
||||
args[i] = a.String()
|
||||
}
|
||||
return e.Name + "(" + strings.Join(args, ", ") + ")"
|
||||
}
|
||||
|
||||
// MultiExpr isn't an interpretable expression, but it's used as a
|
||||
// pseudo-expression for print[f] parsing.
|
||||
type MultiExpr struct {
|
||||
Exprs []Expr
|
||||
}
|
||||
|
||||
func (e *MultiExpr) String() string {
|
||||
exprs := make([]string, len(e.Exprs))
|
||||
for i, e := range e.Exprs {
|
||||
exprs[i] = e.String()
|
||||
}
|
||||
return "(" + strings.Join(exprs, ", ") + ")"
|
||||
}
|
||||
|
||||
// GetlineExpr is an expression read from file or pipe input.
|
||||
type GetlineExpr struct {
|
||||
Command Expr
|
||||
Target Expr
|
||||
File Expr
|
||||
}
|
||||
|
||||
func (e *GetlineExpr) String() string {
|
||||
s := ""
|
||||
if e.Command != nil {
|
||||
s += e.Command.String() + " |"
|
||||
}
|
||||
s += "getline"
|
||||
if e.Target != nil {
|
||||
s += " " + e.Target.String()
|
||||
}
|
||||
if e.File != nil {
|
||||
s += " <" + e.File.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// IsLValue returns true if the given expression can be used as an
|
||||
// lvalue (on the left-hand side of an assignment, in a ++ or --
|
||||
// operation, or as the third argument to sub or gsub).
|
||||
func IsLValue(expr Expr) bool {
|
||||
switch expr.(type) {
|
||||
case *VarExpr, *IndexExpr, *FieldExpr:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Stmt is the abstract syntax tree for any AWK statement.
|
||||
type Stmt interface {
|
||||
stmt()
|
||||
String() string
|
||||
}
|
||||
|
||||
// All these types implement the Stmt interface.
|
||||
func (s *PrintStmt) stmt() {}
|
||||
func (s *PrintfStmt) stmt() {}
|
||||
func (s *ExprStmt) stmt() {}
|
||||
func (s *IfStmt) stmt() {}
|
||||
func (s *ForStmt) stmt() {}
|
||||
func (s *ForInStmt) stmt() {}
|
||||
func (s *WhileStmt) stmt() {}
|
||||
func (s *DoWhileStmt) stmt() {}
|
||||
func (s *BreakStmt) stmt() {}
|
||||
func (s *ContinueStmt) stmt() {}
|
||||
func (s *NextStmt) stmt() {}
|
||||
func (s *ExitStmt) stmt() {}
|
||||
func (s *DeleteStmt) stmt() {}
|
||||
func (s *ReturnStmt) stmt() {}
|
||||
func (s *BlockStmt) stmt() {}
|
||||
|
||||
// PrintStmt is a statement like print $1, $3.
|
||||
type PrintStmt struct {
|
||||
Args []Expr
|
||||
Redirect Token
|
||||
Dest Expr
|
||||
}
|
||||
|
||||
func (s *PrintStmt) String() string {
|
||||
return printString("print", s.Args, s.Redirect, s.Dest)
|
||||
}
|
||||
|
||||
func printString(f string, args []Expr, redirect Token, dest Expr) string {
|
||||
parts := make([]string, len(args))
|
||||
for i, a := range args {
|
||||
parts[i] = a.String()
|
||||
}
|
||||
str := f + " " + strings.Join(parts, ", ")
|
||||
if dest != nil {
|
||||
str += " " + redirect.String() + dest.String()
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// PrintfStmt is a statement like printf "%3d", 1234.
|
||||
type PrintfStmt struct {
|
||||
Args []Expr
|
||||
Redirect Token
|
||||
Dest Expr
|
||||
}
|
||||
|
||||
func (s *PrintfStmt) String() string {
|
||||
return printString("printf", s.Args, s.Redirect, s.Dest)
|
||||
}
|
||||
|
||||
// ExprStmt is statement like a bare function call: my_func(x).
|
||||
type ExprStmt struct {
|
||||
Expr Expr
|
||||
}
|
||||
|
||||
func (s *ExprStmt) String() string {
|
||||
return s.Expr.String()
|
||||
}
|
||||
|
||||
// IfStmt is an if or if-else statement.
|
||||
type IfStmt struct {
|
||||
Cond Expr
|
||||
Body Stmts
|
||||
Else Stmts
|
||||
}
|
||||
|
||||
func (s *IfStmt) String() string {
|
||||
str := "if (" + trimParens(s.Cond.String()) + ") {\n" + s.Body.String() + "}"
|
||||
if len(s.Else) > 0 {
|
||||
str += " else {\n" + s.Else.String() + "}"
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// ForStmt is a C-like for loop: for (i=0; i<10; i++) print i.
|
||||
type ForStmt struct {
|
||||
Pre Stmt
|
||||
Cond Expr
|
||||
Post Stmt
|
||||
Body Stmts
|
||||
}
|
||||
|
||||
func (s *ForStmt) String() string {
|
||||
preStr := ""
|
||||
if s.Pre != nil {
|
||||
preStr = s.Pre.String()
|
||||
}
|
||||
condStr := ""
|
||||
if s.Cond != nil {
|
||||
condStr = " " + trimParens(s.Cond.String())
|
||||
}
|
||||
postStr := ""
|
||||
if s.Post != nil {
|
||||
postStr = " " + s.Post.String()
|
||||
}
|
||||
return "for (" + preStr + ";" + condStr + ";" + postStr + ") {\n" + s.Body.String() + "}"
|
||||
}
|
||||
|
||||
// ForInStmt is a for loop like for (k in a) print k, a[k].
|
||||
type ForInStmt struct {
|
||||
Var *VarExpr
|
||||
Array *ArrayExpr
|
||||
Body Stmts
|
||||
}
|
||||
|
||||
func (s *ForInStmt) String() string {
|
||||
return "for (" + s.Var.String() + " in " + s.Array.String() + ") {\n" + s.Body.String() + "}"
|
||||
}
|
||||
|
||||
// WhileStmt is a while loop.
|
||||
type WhileStmt struct {
|
||||
Cond Expr
|
||||
Body Stmts
|
||||
}
|
||||
|
||||
func (s *WhileStmt) String() string {
|
||||
return "while (" + trimParens(s.Cond.String()) + ") {\n" + s.Body.String() + "}"
|
||||
}
|
||||
|
||||
// DoWhileStmt is a do-while loop.
|
||||
type DoWhileStmt struct {
|
||||
Body Stmts
|
||||
Cond Expr
|
||||
}
|
||||
|
||||
func (s *DoWhileStmt) String() string {
|
||||
return "do {\n" + s.Body.String() + "} while (" + trimParens(s.Cond.String()) + ")"
|
||||
}
|
||||
|
||||
// BreakStmt is a break statement.
|
||||
type BreakStmt struct{}
|
||||
|
||||
func (s *BreakStmt) String() string {
|
||||
return "break"
|
||||
}
|
||||
|
||||
// ContinueStmt is a continue statement.
|
||||
type ContinueStmt struct{}
|
||||
|
||||
func (s *ContinueStmt) String() string {
|
||||
return "continue"
|
||||
}
|
||||
|
||||
// NextStmt is a next statement.
|
||||
type NextStmt struct{}
|
||||
|
||||
func (s *NextStmt) String() string {
|
||||
return "next"
|
||||
}
|
||||
|
||||
// ExitStmt is an exit statement.
|
||||
type ExitStmt struct {
|
||||
Status Expr
|
||||
}
|
||||
|
||||
func (s *ExitStmt) String() string {
|
||||
var statusStr string
|
||||
if s.Status != nil {
|
||||
statusStr = " " + s.Status.String()
|
||||
}
|
||||
return "exit" + statusStr
|
||||
}
|
||||
|
||||
// DeleteStmt is a statement like delete a[k].
|
||||
type DeleteStmt struct {
|
||||
Array *ArrayExpr
|
||||
Index []Expr
|
||||
}
|
||||
|
||||
func (s *DeleteStmt) String() string {
|
||||
indices := make([]string, len(s.Index))
|
||||
for i, index := range s.Index {
|
||||
indices[i] = index.String()
|
||||
}
|
||||
return "delete " + s.Array.String() + "[" + strings.Join(indices, ", ") + "]"
|
||||
}
|
||||
|
||||
// ReturnStmt is a return statement.
|
||||
type ReturnStmt struct {
|
||||
Value Expr
|
||||
}
|
||||
|
||||
func (s *ReturnStmt) String() string {
|
||||
var valueStr string
|
||||
if s.Value != nil {
|
||||
valueStr = " " + s.Value.String()
|
||||
}
|
||||
return "return" + valueStr
|
||||
}
|
||||
|
||||
// BlockStmt is a stand-alone block like { print "x" }.
|
||||
type BlockStmt struct {
|
||||
Body Stmts
|
||||
}
|
||||
|
||||
func (s *BlockStmt) String() string {
|
||||
return "{\n" + s.Body.String() + "}"
|
||||
}
|
||||
|
||||
// Function is the AST for a user-defined function.
|
||||
type Function struct {
|
||||
Name string
|
||||
Params []string
|
||||
Arrays []bool
|
||||
Body Stmts
|
||||
}
|
||||
|
||||
func (f *Function) String() string {
|
||||
return "function " + f.Name + "(" + strings.Join(f.Params, ", ") + ") {\n" +
|
||||
f.Body.String() + "}"
|
||||
}
|
||||
|
||||
func trimParens(s string) string {
|
||||
if strings.HasPrefix(s, "(") && strings.HasSuffix(s, ")") {
|
||||
s = s[1 : len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// Special variable constants
|
||||
|
||||
package ast
|
||||
|
||||
const (
|
||||
V_ILLEGAL = iota
|
||||
V_ARGC
|
||||
V_CONVFMT
|
||||
V_FILENAME
|
||||
V_FNR
|
||||
V_FS
|
||||
V_NF
|
||||
V_NR
|
||||
V_OFMT
|
||||
V_OFS
|
||||
V_ORS
|
||||
V_RLENGTH
|
||||
V_RS
|
||||
V_RSTART
|
||||
V_RT
|
||||
V_SUBSEP
|
||||
|
||||
V_LAST = V_SUBSEP
|
||||
)
|
||||
|
||||
var specialVars = map[string]int{
|
||||
"ARGC": V_ARGC,
|
||||
"CONVFMT": V_CONVFMT,
|
||||
"FILENAME": V_FILENAME,
|
||||
"FNR": V_FNR,
|
||||
"FS": V_FS,
|
||||
"NF": V_NF,
|
||||
"NR": V_NR,
|
||||
"OFMT": V_OFMT,
|
||||
"OFS": V_OFS,
|
||||
"ORS": V_ORS,
|
||||
"RLENGTH": V_RLENGTH,
|
||||
"RS": V_RS,
|
||||
"RSTART": V_RSTART,
|
||||
"RT": V_RT,
|
||||
"SUBSEP": V_SUBSEP,
|
||||
}
|
||||
|
||||
// SpecialVarIndex returns the "index" of the special variable, or 0
|
||||
// if it's not a special variable.
|
||||
func SpecialVarIndex(name string) int {
|
||||
return specialVars[name]
|
||||
}
|
|
@ -0,0 +1,789 @@
|
|||
// Evaluate builtin and user-defined function calls
|
||||
|
||||
package interp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
. "github.com/benhoyt/goawk/internal/ast"
|
||||
. "github.com/benhoyt/goawk/lexer"
|
||||
)
|
||||
|
||||
// Call builtin function specified by "op" with given args
|
||||
func (p *interp) callBuiltin(op Token, argExprs []Expr) (value, error) {
|
||||
// split() has an array arg (not evaluated) and [g]sub() have an
|
||||
// lvalue arg, so handle them as special cases
|
||||
switch op {
|
||||
case F_SPLIT:
|
||||
strValue, err := p.eval(argExprs[0])
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
str := p.toString(strValue)
|
||||
var fieldSep string
|
||||
if len(argExprs) == 3 {
|
||||
sepValue, err := p.eval(argExprs[2])
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
fieldSep = p.toString(sepValue)
|
||||
} else {
|
||||
fieldSep = p.fieldSep
|
||||
}
|
||||
arrayExpr := argExprs[1].(*ArrayExpr)
|
||||
n, err := p.split(str, arrayExpr.Scope, arrayExpr.Index, fieldSep)
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
return num(float64(n)), nil
|
||||
|
||||
case F_SUB, F_GSUB:
|
||||
regexValue, err := p.eval(argExprs[0])
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
regex := p.toString(regexValue)
|
||||
replValue, err := p.eval(argExprs[1])
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
repl := p.toString(replValue)
|
||||
var in string
|
||||
if len(argExprs) == 3 {
|
||||
inValue, err := p.eval(argExprs[2])
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
in = p.toString(inValue)
|
||||
} else {
|
||||
in = p.line
|
||||
}
|
||||
out, n, err := p.sub(regex, repl, in, op == F_GSUB)
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
if len(argExprs) == 3 {
|
||||
err := p.assign(argExprs[2], str(out))
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
} else {
|
||||
p.setLine(out, true)
|
||||
}
|
||||
return num(float64(n)), nil
|
||||
}
|
||||
|
||||
// Now evaluate the argExprs (calls with up to 7 args don't
|
||||
// require heap allocation)
|
||||
args := make([]value, 0, 7)
|
||||
for _, a := range argExprs {
|
||||
arg, err := p.eval(a)
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
args = append(args, arg)
|
||||
}
|
||||
|
||||
// Then switch on the function for the ordinary functions
|
||||
switch op {
|
||||
case F_LENGTH:
|
||||
var s string
|
||||
if len(args) > 0 {
|
||||
s = p.toString(args[0])
|
||||
} else {
|
||||
s = p.line
|
||||
}
|
||||
var n int
|
||||
if p.bytes {
|
||||
n = len(s)
|
||||
} else {
|
||||
n = utf8.RuneCountInString(s)
|
||||
}
|
||||
return num(float64(n)), nil
|
||||
|
||||
case F_MATCH:
|
||||
re, err := p.compileRegex(p.toString(args[1]))
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
s := p.toString(args[0])
|
||||
loc := re.FindStringIndex(s)
|
||||
if loc == nil {
|
||||
p.matchStart = 0
|
||||
p.matchLength = -1
|
||||
return num(0), nil
|
||||
}
|
||||
if p.bytes {
|
||||
p.matchStart = loc[0] + 1
|
||||
p.matchLength = loc[1] - loc[0]
|
||||
} else {
|
||||
p.matchStart = utf8.RuneCountInString(s[:loc[0]]) + 1
|
||||
p.matchLength = utf8.RuneCountInString(s[loc[0]:loc[1]])
|
||||
}
|
||||
return num(float64(p.matchStart)), nil
|
||||
|
||||
case F_SUBSTR:
|
||||
s := p.toString(args[0])
|
||||
pos := int(args[1].num())
|
||||
if p.bytes {
|
||||
if pos > len(s) {
|
||||
pos = len(s) + 1
|
||||
}
|
||||
if pos < 1 {
|
||||
pos = 1
|
||||
}
|
||||
maxLength := len(s) - pos + 1
|
||||
length := maxLength
|
||||
if len(args) == 3 {
|
||||
length = int(args[2].num())
|
||||
if length < 0 {
|
||||
length = 0
|
||||
}
|
||||
if length > maxLength {
|
||||
length = maxLength
|
||||
}
|
||||
}
|
||||
return str(s[pos-1 : pos-1+length]), nil
|
||||
} else {
|
||||
// Count characters till we get to pos.
|
||||
chars := 1
|
||||
start := 0
|
||||
for start = range s {
|
||||
chars++
|
||||
if chars > pos {
|
||||
break
|
||||
}
|
||||
}
|
||||
if pos >= chars {
|
||||
start = len(s)
|
||||
}
|
||||
|
||||
// Count characters from start till we reach length.
|
||||
var end int
|
||||
if len(args) == 3 {
|
||||
length := int(args[2].num())
|
||||
chars = 0
|
||||
for end = range s[start:] {
|
||||
chars++
|
||||
if chars > length {
|
||||
break
|
||||
}
|
||||
}
|
||||
if length >= chars {
|
||||
end = len(s)
|
||||
} else {
|
||||
end += start
|
||||
}
|
||||
} else {
|
||||
end = len(s)
|
||||
}
|
||||
return str(s[start:end]), nil
|
||||
}
|
||||
|
||||
case F_SPRINTF:
|
||||
s, err := p.sprintf(p.toString(args[0]), args[1:])
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
return str(s), nil
|
||||
|
||||
case F_INDEX:
|
||||
s := p.toString(args[0])
|
||||
substr := p.toString(args[1])
|
||||
index := strings.Index(s, substr)
|
||||
if p.bytes {
|
||||
return num(float64(index + 1)), nil
|
||||
} else {
|
||||
if index < 0 {
|
||||
return num(float64(0)), nil
|
||||
}
|
||||
index = utf8.RuneCountInString(s[:index])
|
||||
return num(float64(index + 1)), nil
|
||||
}
|
||||
|
||||
case F_TOLOWER:
|
||||
return str(strings.ToLower(p.toString(args[0]))), nil
|
||||
case F_TOUPPER:
|
||||
return str(strings.ToUpper(p.toString(args[0]))), nil
|
||||
|
||||
case F_ATAN2:
|
||||
return num(math.Atan2(args[0].num(), args[1].num())), nil
|
||||
case F_COS:
|
||||
return num(math.Cos(args[0].num())), nil
|
||||
case F_EXP:
|
||||
return num(math.Exp(args[0].num())), nil
|
||||
case F_INT:
|
||||
return num(float64(int(args[0].num()))), nil
|
||||
case F_LOG:
|
||||
return num(math.Log(args[0].num())), nil
|
||||
case F_SQRT:
|
||||
return num(math.Sqrt(args[0].num())), nil
|
||||
case F_RAND:
|
||||
return num(p.random.Float64()), nil
|
||||
case F_SIN:
|
||||
return num(math.Sin(args[0].num())), nil
|
||||
|
||||
case F_SRAND:
|
||||
prevSeed := p.randSeed
|
||||
switch len(args) {
|
||||
case 0:
|
||||
p.random.Seed(time.Now().UnixNano())
|
||||
case 1:
|
||||
p.randSeed = args[0].num()
|
||||
p.random.Seed(int64(math.Float64bits(p.randSeed)))
|
||||
}
|
||||
return num(prevSeed), nil
|
||||
|
||||
case F_SYSTEM:
|
||||
if p.noExec {
|
||||
return null(), newError("can't call system() due to NoExec")
|
||||
}
|
||||
cmdline := p.toString(args[0])
|
||||
cmd := p.execShell(cmdline)
|
||||
cmd.Stdout = p.output
|
||||
cmd.Stderr = p.errorOutput
|
||||
_ = p.flushAll() // ensure synchronization
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
p.printErrorf("%s\n", err)
|
||||
return num(-1), nil
|
||||
}
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
code := exitErr.ProcessState.ExitCode()
|
||||
return num(float64(code)), nil
|
||||
} else {
|
||||
p.printErrorf("unexpected error running command %q: %v\n", cmdline, err)
|
||||
return num(-1), nil
|
||||
}
|
||||
}
|
||||
return num(0), nil
|
||||
|
||||
case F_CLOSE:
|
||||
name := p.toString(args[0])
|
||||
var c io.Closer = p.inputStreams[name]
|
||||
if c != nil {
|
||||
// Close input stream
|
||||
delete(p.inputStreams, name)
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
return num(-1), nil
|
||||
}
|
||||
return num(0), nil
|
||||
}
|
||||
c = p.outputStreams[name]
|
||||
if c != nil {
|
||||
// Close output stream
|
||||
delete(p.outputStreams, name)
|
||||
err := c.Close()
|
||||
if err != nil {
|
||||
return num(-1), nil
|
||||
}
|
||||
return num(0), nil
|
||||
}
|
||||
// Nothing to close
|
||||
return num(-1), nil
|
||||
|
||||
case F_FFLUSH:
|
||||
var name string
|
||||
if len(args) > 0 {
|
||||
name = p.toString(args[0])
|
||||
}
|
||||
var ok bool
|
||||
if name != "" {
|
||||
// Flush a single, named output stream
|
||||
ok = p.flushStream(name)
|
||||
} else {
|
||||
// fflush() or fflush("") flushes all output streams
|
||||
ok = p.flushAll()
|
||||
}
|
||||
if !ok {
|
||||
return num(-1), nil
|
||||
}
|
||||
return num(0), nil
|
||||
|
||||
default:
|
||||
// Shouldn't happen
|
||||
panic(fmt.Sprintf("unexpected function: %s", op))
|
||||
}
|
||||
}
|
||||
|
||||
// Executes code using configured system shell
|
||||
func (p *interp) execShell(code string) *exec.Cmd {
|
||||
executable := p.shellCommand[0]
|
||||
args := p.shellCommand[1:]
|
||||
args = append(args, code)
|
||||
cmd := exec.Command(executable, args...)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Call user-defined function with given index and arguments, return
|
||||
// its return value (or null value if it doesn't return anything)
|
||||
func (p *interp) callUser(index int, args []Expr) (value, error) {
|
||||
f := p.program.Functions[index]
|
||||
|
||||
if p.callDepth >= maxCallDepth {
|
||||
return null(), newError("calling %q exceeded maximum call depth of %d", f.Name, maxCallDepth)
|
||||
}
|
||||
|
||||
// Evaluate the arguments and push them onto the locals stack
|
||||
oldFrame := p.frame
|
||||
newFrameStart := len(p.stack)
|
||||
var arrays []int
|
||||
for i, arg := range args {
|
||||
if f.Arrays[i] {
|
||||
a := arg.(*VarExpr)
|
||||
arrays = append(arrays, p.getArrayIndex(a.Scope, a.Index))
|
||||
} else {
|
||||
argValue, err := p.eval(arg)
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
p.stack = append(p.stack, argValue)
|
||||
}
|
||||
}
|
||||
// Push zero value for any additional parameters (it's valid to
|
||||
// call a function with fewer arguments than it has parameters)
|
||||
oldArraysLen := len(p.arrays)
|
||||
for i := len(args); i < len(f.Params); i++ {
|
||||
if f.Arrays[i] {
|
||||
arrays = append(arrays, len(p.arrays))
|
||||
p.arrays = append(p.arrays, make(map[string]value))
|
||||
} else {
|
||||
p.stack = append(p.stack, null())
|
||||
}
|
||||
}
|
||||
p.frame = p.stack[newFrameStart:]
|
||||
p.localArrays = append(p.localArrays, arrays)
|
||||
|
||||
// Execute the function!
|
||||
p.callDepth++
|
||||
err := p.executes(f.Body)
|
||||
p.callDepth--
|
||||
|
||||
// Pop the locals off the stack
|
||||
p.stack = p.stack[:newFrameStart]
|
||||
p.frame = oldFrame
|
||||
p.localArrays = p.localArrays[:len(p.localArrays)-1]
|
||||
p.arrays = p.arrays[:oldArraysLen]
|
||||
|
||||
if r, ok := err.(returnValue); ok {
|
||||
return r.Value, nil
|
||||
}
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
return null(), nil
|
||||
}
|
||||
|
||||
// Call native-defined function with given name and arguments, return
|
||||
// its return value (or null value if it doesn't return anything).
|
||||
func (p *interp) callNative(index int, args []Expr) (value, error) {
|
||||
f := p.nativeFuncs[index]
|
||||
minIn := len(f.in) // Minimum number of args we should pass
|
||||
var variadicType reflect.Type
|
||||
if f.isVariadic {
|
||||
variadicType = f.in[len(f.in)-1].Elem()
|
||||
minIn--
|
||||
}
|
||||
|
||||
// Build list of args to pass to function
|
||||
values := make([]reflect.Value, 0, 7) // up to 7 args won't require heap allocation
|
||||
for i, arg := range args {
|
||||
a, err := p.eval(arg)
|
||||
if err != nil {
|
||||
return null(), err
|
||||
}
|
||||
var argType reflect.Type
|
||||
if !f.isVariadic || i < len(f.in)-1 {
|
||||
argType = f.in[i]
|
||||
} else {
|
||||
// Final arg(s) when calling a variadic are all of this type
|
||||
argType = variadicType
|
||||
}
|
||||
values = append(values, p.toNative(a, argType))
|
||||
}
|
||||
// Use zero value for any unspecified args
|
||||
for i := len(args); i < minIn; i++ {
|
||||
values = append(values, reflect.Zero(f.in[i]))
|
||||
}
|
||||
|
||||
// Call Go function, determine return value
|
||||
outs := f.value.Call(values)
|
||||
switch len(outs) {
|
||||
case 0:
|
||||
// No return value, return null value to AWK
|
||||
return null(), nil
|
||||
case 1:
|
||||
// Single return value
|
||||
return fromNative(outs[0]), nil
|
||||
case 2:
|
||||
// Two-valued return of (scalar, error)
|
||||
if !outs[1].IsNil() {
|
||||
return null(), outs[1].Interface().(error)
|
||||
}
|
||||
return fromNative(outs[0]), nil
|
||||
default:
|
||||
// Should never happen (checked at parse time)
|
||||
panic(fmt.Sprintf("unexpected number of return values: %d", len(outs)))
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from an AWK value to a native Go value
|
||||
func (p *interp) toNative(v value, typ reflect.Type) reflect.Value {
|
||||
switch typ.Kind() {
|
||||
case reflect.Bool:
|
||||
return reflect.ValueOf(v.boolean())
|
||||
case reflect.Int:
|
||||
return reflect.ValueOf(int(v.num()))
|
||||
case reflect.Int8:
|
||||
return reflect.ValueOf(int8(v.num()))
|
||||
case reflect.Int16:
|
||||
return reflect.ValueOf(int16(v.num()))
|
||||
case reflect.Int32:
|
||||
return reflect.ValueOf(int32(v.num()))
|
||||
case reflect.Int64:
|
||||
return reflect.ValueOf(int64(v.num()))
|
||||
case reflect.Uint:
|
||||
return reflect.ValueOf(uint(v.num()))
|
||||
case reflect.Uint8:
|
||||
return reflect.ValueOf(uint8(v.num()))
|
||||
case reflect.Uint16:
|
||||
return reflect.ValueOf(uint16(v.num()))
|
||||
case reflect.Uint32:
|
||||
return reflect.ValueOf(uint32(v.num()))
|
||||
case reflect.Uint64:
|
||||
return reflect.ValueOf(uint64(v.num()))
|
||||
case reflect.Float32:
|
||||
return reflect.ValueOf(float32(v.num()))
|
||||
case reflect.Float64:
|
||||
return reflect.ValueOf(v.num())
|
||||
case reflect.String:
|
||||
return reflect.ValueOf(p.toString(v))
|
||||
case reflect.Slice:
|
||||
if typ.Elem().Kind() != reflect.Uint8 {
|
||||
// Shouldn't happen: prevented by checkNativeFunc
|
||||
panic(fmt.Sprintf("unexpected argument slice: %s", typ.Elem().Kind()))
|
||||
}
|
||||
return reflect.ValueOf([]byte(p.toString(v)))
|
||||
default:
|
||||
// Shouldn't happen: prevented by checkNativeFunc
|
||||
panic(fmt.Sprintf("unexpected argument type: %s", typ.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from a native Go value to an AWK value
|
||||
func fromNative(v reflect.Value) value {
|
||||
switch v.Kind() {
|
||||
case reflect.Bool:
|
||||
return boolean(v.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return num(float64(v.Int()))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return num(float64(v.Uint()))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return num(v.Float())
|
||||
case reflect.String:
|
||||
return str(v.String())
|
||||
case reflect.Slice:
|
||||
if b, ok := v.Interface().([]byte); ok {
|
||||
return str(string(b))
|
||||
}
|
||||
// Shouldn't happen: prevented by checkNativeFunc
|
||||
panic(fmt.Sprintf("unexpected return slice: %s", v.Type().Elem().Kind()))
|
||||
default:
|
||||
// Shouldn't happen: prevented by checkNativeFunc
|
||||
panic(fmt.Sprintf("unexpected return type: %s", v.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// Used for caching native function type information on init
|
||||
type nativeFunc struct {
|
||||
isVariadic bool
|
||||
in []reflect.Type
|
||||
value reflect.Value
|
||||
}
|
||||
|
||||
// Check and initialize native functions
|
||||
func (p *interp) initNativeFuncs(funcs map[string]interface{}) error {
|
||||
for name, f := range funcs {
|
||||
err := checkNativeFunc(name, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Sort functions by name, then use those indexes to build slice
|
||||
// (this has to match how the parser sets the indexes).
|
||||
names := make([]string, 0, len(funcs))
|
||||
for name := range funcs {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
p.nativeFuncs = make([]nativeFunc, len(names))
|
||||
for i, name := range names {
|
||||
f := funcs[name]
|
||||
typ := reflect.TypeOf(f)
|
||||
in := make([]reflect.Type, typ.NumIn())
|
||||
for j := 0; j < len(in); j++ {
|
||||
in[j] = typ.In(j)
|
||||
}
|
||||
p.nativeFuncs[i] = nativeFunc{
|
||||
isVariadic: typ.IsVariadic(),
|
||||
in: in,
|
||||
value: reflect.ValueOf(f),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Got this trick from the Go stdlib text/template source
|
||||
var errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
|
||||
// Check that native function with given name is okay to call from
|
||||
// AWK, return an *interp.Error if not. This checks that f is actually
|
||||
// a function, and that its parameter and return types are good.
|
||||
func checkNativeFunc(name string, f interface{}) error {
|
||||
if KeywordToken(name) != ILLEGAL {
|
||||
return newError("can't use keyword %q as native function name", name)
|
||||
}
|
||||
|
||||
typ := reflect.TypeOf(f)
|
||||
if typ.Kind() != reflect.Func {
|
||||
return newError("native function %q is not a function", name)
|
||||
}
|
||||
for i := 0; i < typ.NumIn(); i++ {
|
||||
param := typ.In(i)
|
||||
if typ.IsVariadic() && i == typ.NumIn()-1 {
|
||||
param = param.Elem()
|
||||
}
|
||||
if !validNativeType(param) {
|
||||
return newError("native function %q param %d is not int or string", name, i)
|
||||
}
|
||||
}
|
||||
|
||||
switch typ.NumOut() {
|
||||
case 0:
|
||||
// No return value is fine
|
||||
case 1:
|
||||
// Single scalar return value is fine
|
||||
if !validNativeType(typ.Out(0)) {
|
||||
return newError("native function %q return value is not int or string", name)
|
||||
}
|
||||
case 2:
|
||||
// Returning (scalar, error) is handled too
|
||||
if !validNativeType(typ.Out(0)) {
|
||||
return newError("native function %q first return value is not int or string", name)
|
||||
}
|
||||
if typ.Out(1) != errorType {
|
||||
return newError("native function %q second return value is not an error", name)
|
||||
}
|
||||
default:
|
||||
return newError("native function %q returns more than two values", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return true if typ is a valid parameter or return type.
|
||||
func validNativeType(typ reflect.Type) bool {
|
||||
switch typ.Kind() {
|
||||
case reflect.Bool:
|
||||
return true
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return true
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return true
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
case reflect.String:
|
||||
return true
|
||||
case reflect.Slice:
|
||||
// Only allow []byte (convert to string in AWK)
|
||||
return typ.Elem().Kind() == reflect.Uint8
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Guts of the split() function
|
||||
func (p *interp) split(s string, scope VarScope, index int, fs string) (int, error) {
|
||||
var parts []string
|
||||
if fs == " " {
|
||||
parts = strings.Fields(s)
|
||||
} else if s == "" {
|
||||
// NF should be 0 on empty line
|
||||
} else if utf8.RuneCountInString(fs) <= 1 {
|
||||
parts = strings.Split(s, fs)
|
||||
} else {
|
||||
re, err := p.compileRegex(fs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
parts = re.Split(s, -1)
|
||||
}
|
||||
array := make(map[string]value, len(parts))
|
||||
for i, part := range parts {
|
||||
array[strconv.Itoa(i+1)] = numStr(part)
|
||||
}
|
||||
p.arrays[p.getArrayIndex(scope, index)] = array
|
||||
return len(array), nil
|
||||
}
|
||||
|
||||
// Guts of the sub() and gsub() functions
|
||||
func (p *interp) sub(regex, repl, in string, global bool) (out string, num int, err error) {
|
||||
re, err := p.compileRegex(regex)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
count := 0
|
||||
out = re.ReplaceAllStringFunc(in, func(s string) string {
|
||||
// Only do the first replacement for sub(), or all for gsub()
|
||||
if !global && count > 0 {
|
||||
return s
|
||||
}
|
||||
count++
|
||||
// Handle & (ampersand) properly in replacement string
|
||||
r := make([]byte, 0, 64) // Up to 64 byte replacement won't require heap allocation
|
||||
for i := 0; i < len(repl); i++ {
|
||||
switch repl[i] {
|
||||
case '&':
|
||||
r = append(r, s...)
|
||||
case '\\':
|
||||
i++
|
||||
if i < len(repl) {
|
||||
switch repl[i] {
|
||||
case '&':
|
||||
r = append(r, '&')
|
||||
case '\\':
|
||||
r = append(r, '\\')
|
||||
default:
|
||||
r = append(r, '\\', repl[i])
|
||||
}
|
||||
} else {
|
||||
r = append(r, '\\')
|
||||
}
|
||||
default:
|
||||
r = append(r, repl[i])
|
||||
}
|
||||
}
|
||||
return string(r)
|
||||
})
|
||||
return out, count, nil
|
||||
}
|
||||
|
||||
type cachedFormat struct {
|
||||
format string
|
||||
types []byte
|
||||
}
|
||||
|
||||
// Parse given sprintf format string into Go format string, along with
|
||||
// type conversion specifiers. Output is memoized in a simple cache
|
||||
// for performance.
|
||||
func (p *interp) parseFmtTypes(s string) (format string, types []byte, err error) {
|
||||
if item, ok := p.formatCache[s]; ok {
|
||||
return item.format, item.types, nil
|
||||
}
|
||||
|
||||
out := []byte(s)
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '%' {
|
||||
i++
|
||||
if i >= len(s) {
|
||||
return "", nil, errors.New("expected type specifier after %")
|
||||
}
|
||||
if s[i] == '%' {
|
||||
continue
|
||||
}
|
||||
for i < len(s) && bytes.IndexByte([]byte(" .-+*#0123456789"), s[i]) >= 0 {
|
||||
if s[i] == '*' {
|
||||
types = append(types, 'd')
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i >= len(s) {
|
||||
return "", nil, errors.New("expected type specifier after %")
|
||||
}
|
||||
var t byte
|
||||
switch s[i] {
|
||||
case 's':
|
||||
t = 's'
|
||||
case 'd', 'i', 'o', 'x', 'X':
|
||||
t = 'd'
|
||||
case 'f', 'e', 'E', 'g', 'G':
|
||||
t = 'f'
|
||||
case 'u':
|
||||
t = 'u'
|
||||
out[i] = 'd'
|
||||
case 'c':
|
||||
t = 'c'
|
||||
out[i] = 's'
|
||||
default:
|
||||
return "", nil, fmt.Errorf("invalid format type %q", s[i])
|
||||
}
|
||||
types = append(types, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Dumb, non-LRU cache: just cache the first N formats
|
||||
format = string(out)
|
||||
if len(p.formatCache) < maxCachedFormats {
|
||||
p.formatCache[s] = cachedFormat{format, types}
|
||||
}
|
||||
return format, types, nil
|
||||
}
|
||||
|
||||
// Guts of sprintf() function (also used by "printf" statement)
|
||||
func (p *interp) sprintf(format string, args []value) (string, error) {
|
||||
format, types, err := p.parseFmtTypes(format)
|
||||
if err != nil {
|
||||
return "", newError("format error: %s", err)
|
||||
}
|
||||
if len(types) > len(args) {
|
||||
return "", newError("format error: got %d args, expected %d", len(args), len(types))
|
||||
}
|
||||
converted := make([]interface{}, len(types))
|
||||
for i, t := range types {
|
||||
a := args[i]
|
||||
var v interface{}
|
||||
switch t {
|
||||
case 's':
|
||||
v = p.toString(a)
|
||||
case 'd':
|
||||
v = int(a.num())
|
||||
case 'f':
|
||||
v = a.num()
|
||||
case 'u':
|
||||
v = uint32(a.num())
|
||||
case 'c':
|
||||
var c []byte
|
||||
n, isStr := a.isTrueStr()
|
||||
if isStr {
|
||||
s := p.toString(a)
|
||||
if len(s) > 0 {
|
||||
c = []byte{s[0]}
|
||||
} else {
|
||||
c = []byte{0}
|
||||
}
|
||||
} else {
|
||||
// Follow the behaviour of awk and mawk, where %c
|
||||
// operates on bytes (0-255), not Unicode codepoints
|
||||
c = []byte{byte(n)}
|
||||
}
|
||||
v = c
|
||||
}
|
||||
converted[i] = v
|
||||
}
|
||||
return fmt.Sprintf(format, converted...), nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,561 @@
|
|||
// Input/output handling for GoAWK interpreter
|
||||
|
||||
package interp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
. "github.com/benhoyt/goawk/internal/ast"
|
||||
. "github.com/benhoyt/goawk/lexer"
|
||||
)
|
||||
|
||||
// Print a line of output followed by a newline
|
||||
func (p *interp) printLine(writer io.Writer, line string) error {
|
||||
err := writeOutput(writer, line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeOutput(writer, p.outputRecordSep)
|
||||
}
|
||||
|
||||
// Implement a buffered version of WriteCloser so output is buffered
|
||||
// when redirecting to a file (eg: print >"out")
|
||||
type bufferedWriteCloser struct {
|
||||
*bufio.Writer
|
||||
io.Closer
|
||||
}
|
||||
|
||||
func newBufferedWriteCloser(w io.WriteCloser) *bufferedWriteCloser {
|
||||
writer := bufio.NewWriterSize(w, outputBufSize)
|
||||
return &bufferedWriteCloser{writer, w}
|
||||
}
|
||||
|
||||
func (wc *bufferedWriteCloser) Close() error {
|
||||
err := wc.Writer.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return wc.Closer.Close()
|
||||
}
|
||||
|
||||
// Determine the output stream for given redirect token and
|
||||
// destination (file or pipe name)
|
||||
func (p *interp) getOutputStream(redirect Token, dest Expr) (io.Writer, error) {
|
||||
if redirect == ILLEGAL {
|
||||
// Token "ILLEGAL" means send to standard output
|
||||
return p.output, nil
|
||||
}
|
||||
|
||||
destValue, err := p.eval(dest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := p.toString(destValue)
|
||||
if _, ok := p.inputStreams[name]; ok {
|
||||
return nil, newError("can't write to reader stream")
|
||||
}
|
||||
if w, ok := p.outputStreams[name]; ok {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
switch redirect {
|
||||
case GREATER, APPEND:
|
||||
if name == "-" {
|
||||
// filename of "-" means write to stdout, eg: print "x" >"-"
|
||||
return p.output, nil
|
||||
}
|
||||
// Write or append to file
|
||||
if p.noFileWrites {
|
||||
return nil, newError("can't write to file due to NoFileWrites")
|
||||
}
|
||||
p.flushOutputAndError() // ensure synchronization
|
||||
flags := os.O_CREATE | os.O_WRONLY
|
||||
if redirect == GREATER {
|
||||
flags |= os.O_TRUNC
|
||||
} else {
|
||||
flags |= os.O_APPEND
|
||||
}
|
||||
w, err := os.OpenFile(name, flags, 0644)
|
||||
if err != nil {
|
||||
return nil, newError("output redirection error: %s", err)
|
||||
}
|
||||
buffered := newBufferedWriteCloser(w)
|
||||
p.outputStreams[name] = buffered
|
||||
return buffered, nil
|
||||
|
||||
case PIPE:
|
||||
// Pipe to command
|
||||
if p.noExec {
|
||||
return nil, newError("can't write to pipe due to NoExec")
|
||||
}
|
||||
cmd := p.execShell(name)
|
||||
w, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, newError("error connecting to stdin pipe: %v", err)
|
||||
}
|
||||
cmd.Stdout = p.output
|
||||
cmd.Stderr = p.errorOutput
|
||||
p.flushOutputAndError() // ensure synchronization
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
p.printErrorf("%s\n", err)
|
||||
return ioutil.Discard, nil
|
||||
}
|
||||
p.commands[name] = cmd
|
||||
buffered := newBufferedWriteCloser(w)
|
||||
p.outputStreams[name] = buffered
|
||||
return buffered, nil
|
||||
|
||||
default:
|
||||
// Should never happen
|
||||
panic(fmt.Sprintf("unexpected redirect type %s", redirect))
|
||||
}
|
||||
}
|
||||
|
||||
// Get input Scanner to use for "getline" based on file name
|
||||
func (p *interp) getInputScannerFile(name string) (*bufio.Scanner, error) {
|
||||
if _, ok := p.outputStreams[name]; ok {
|
||||
return nil, newError("can't read from writer stream")
|
||||
}
|
||||
if _, ok := p.inputStreams[name]; ok {
|
||||
return p.scanners[name], nil
|
||||
}
|
||||
if name == "-" {
|
||||
// filename of "-" means read from stdin, eg: getline <"-"
|
||||
if scanner, ok := p.scanners["-"]; ok {
|
||||
return scanner, nil
|
||||
}
|
||||
scanner := p.newScanner(p.stdin)
|
||||
p.scanners[name] = scanner
|
||||
return scanner, nil
|
||||
}
|
||||
if p.noFileReads {
|
||||
return nil, newError("can't read from file due to NoFileReads")
|
||||
}
|
||||
r, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err // *os.PathError is handled by caller (getline returns -1)
|
||||
}
|
||||
scanner := p.newScanner(r)
|
||||
p.scanners[name] = scanner
|
||||
p.inputStreams[name] = r
|
||||
return scanner, nil
|
||||
}
|
||||
|
||||
// Get input Scanner to use for "getline" based on pipe name
|
||||
func (p *interp) getInputScannerPipe(name string) (*bufio.Scanner, error) {
|
||||
if _, ok := p.outputStreams[name]; ok {
|
||||
return nil, newError("can't read from writer stream")
|
||||
}
|
||||
if _, ok := p.inputStreams[name]; ok {
|
||||
return p.scanners[name], nil
|
||||
}
|
||||
if p.noExec {
|
||||
return nil, newError("can't read from pipe due to NoExec")
|
||||
}
|
||||
cmd := p.execShell(name)
|
||||
cmd.Stdin = p.stdin
|
||||
cmd.Stderr = p.errorOutput
|
||||
r, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, newError("error connecting to stdout pipe: %v", err)
|
||||
}
|
||||
p.flushOutputAndError() // ensure synchronization
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
p.printErrorf("%s\n", err)
|
||||
return bufio.NewScanner(strings.NewReader("")), nil
|
||||
}
|
||||
scanner := p.newScanner(r)
|
||||
p.commands[name] = cmd
|
||||
p.inputStreams[name] = r
|
||||
p.scanners[name] = scanner
|
||||
return scanner, nil
|
||||
}
|
||||
|
||||
// Create a new buffered Scanner for reading input records
|
||||
func (p *interp) newScanner(input io.Reader) *bufio.Scanner {
|
||||
scanner := bufio.NewScanner(input)
|
||||
switch {
|
||||
case p.recordSep == "\n":
|
||||
// Scanner default is to split on newlines
|
||||
case p.recordSep == "":
|
||||
// Empty string for RS means split on \n\n (blank lines)
|
||||
splitter := blankLineSplitter{&p.recordTerminator}
|
||||
scanner.Split(splitter.scan)
|
||||
case len(p.recordSep) == 1:
|
||||
splitter := byteSplitter{p.recordSep[0]}
|
||||
scanner.Split(splitter.scan)
|
||||
case utf8.RuneCountInString(p.recordSep) >= 1:
|
||||
// Multi-byte and single char but multi-byte RS use regex
|
||||
splitter := regexSplitter{p.recordSepRegex, &p.recordTerminator}
|
||||
scanner.Split(splitter.scan)
|
||||
}
|
||||
buffer := make([]byte, inputBufSize)
|
||||
scanner.Buffer(buffer, maxRecordLength)
|
||||
return scanner
|
||||
}
|
||||
|
||||
// Copied from bufio/scan.go in the stdlib: I guess it's a bit more
|
||||
// efficient than bytes.TrimSuffix(data, []byte("\r"))
|
||||
func dropCR(data []byte) []byte {
|
||||
if len(data) > 0 && data[len(data)-1] == '\r' {
|
||||
return data[:len(data)-1]
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func dropLF(data []byte) []byte {
|
||||
if len(data) > 0 && data[len(data)-1] == '\n' {
|
||||
return data[:len(data)-1]
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
type blankLineSplitter struct {
|
||||
terminator *string
|
||||
}
|
||||
|
||||
func (s blankLineSplitter) scan(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Skip newlines at beginning of data
|
||||
i := 0
|
||||
for i < len(data) && (data[i] == '\n' || data[i] == '\r') {
|
||||
i++
|
||||
}
|
||||
if i >= len(data) {
|
||||
// At end of data after newlines, skip entire data block
|
||||
return i, nil, nil
|
||||
}
|
||||
start := i
|
||||
|
||||
// Try to find two consecutive newlines (or \n\r\n for Windows)
|
||||
for ; i < len(data); i++ {
|
||||
if data[i] != '\n' {
|
||||
continue
|
||||
}
|
||||
end := i
|
||||
if i+1 < len(data) && data[i+1] == '\n' {
|
||||
i += 2
|
||||
for i < len(data) && (data[i] == '\n' || data[i] == '\r') {
|
||||
i++ // Skip newlines at end of record
|
||||
}
|
||||
*s.terminator = string(data[end:i])
|
||||
return i, dropCR(data[start:end]), nil
|
||||
}
|
||||
if i+2 < len(data) && data[i+1] == '\r' && data[i+2] == '\n' {
|
||||
i += 3
|
||||
for i < len(data) && (data[i] == '\n' || data[i] == '\r') {
|
||||
i++ // Skip newlines at end of record
|
||||
}
|
||||
*s.terminator = string(data[end:i])
|
||||
return i, dropCR(data[start:end]), nil
|
||||
}
|
||||
}
|
||||
|
||||
// If we're at EOF, we have one final record; return it
|
||||
if atEOF {
|
||||
token = dropCR(dropLF(data[start:]))
|
||||
*s.terminator = string(data[len(token):])
|
||||
return len(data), token, nil
|
||||
}
|
||||
|
||||
// Request more data
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Splitter that splits records on the given separator byte
|
||||
type byteSplitter struct {
|
||||
sep byte
|
||||
}
|
||||
|
||||
func (s byteSplitter) scan(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
if i := bytes.IndexByte(data, s.sep); i >= 0 {
|
||||
// We have a full sep-terminated record
|
||||
return i + 1, data[:i], nil
|
||||
}
|
||||
// If at EOF, we have a final, non-terminated record; return it
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
// Request more data
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Splitter that splits records on the given regular expression
|
||||
type regexSplitter struct {
|
||||
re *regexp.Regexp
|
||||
terminator *string
|
||||
}
|
||||
|
||||
func (s regexSplitter) scan(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
loc := s.re.FindIndex(data)
|
||||
// Note: for a regex such as "()", loc[0]==loc[1]. Gawk behavior for this
|
||||
// case is to match the entire input.
|
||||
if loc != nil && loc[0] != loc[1] {
|
||||
*s.terminator = string(data[loc[0]:loc[1]]) // set RT special variable
|
||||
return loc[1], data[:loc[0]], nil
|
||||
}
|
||||
// If at EOF, we have a final, non-terminated record; return it
|
||||
if atEOF {
|
||||
*s.terminator = ""
|
||||
return len(data), data, nil
|
||||
}
|
||||
// Request more data
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
// Setup for a new input file with given name (empty string if stdin)
|
||||
func (p *interp) setFile(filename string) {
|
||||
p.filename = numStr(filename)
|
||||
p.fileLineNum = 0
|
||||
}
|
||||
|
||||
// Setup for a new input line (but don't parse it into fields till we
|
||||
// need to)
|
||||
func (p *interp) setLine(line string, isTrueStr bool) {
|
||||
p.line = line
|
||||
p.lineIsTrueStr = isTrueStr
|
||||
p.haveFields = false
|
||||
}
|
||||
|
||||
// Ensure that the current line is parsed into fields, splitting it
|
||||
// into fields if it hasn't been already
|
||||
func (p *interp) ensureFields() {
|
||||
if p.haveFields {
|
||||
return
|
||||
}
|
||||
p.haveFields = true
|
||||
|
||||
switch {
|
||||
case p.fieldSep == " ":
|
||||
// FS space (default) means split fields on any whitespace
|
||||
p.fields = strings.Fields(p.line)
|
||||
case p.line == "":
|
||||
p.fields = nil
|
||||
case utf8.RuneCountInString(p.fieldSep) <= 1:
|
||||
// 1-char FS is handled as plain split (not regex)
|
||||
p.fields = strings.Split(p.line, p.fieldSep)
|
||||
default:
|
||||
// Split on FS as a regex
|
||||
p.fields = p.fieldSepRegex.Split(p.line, -1)
|
||||
}
|
||||
|
||||
// Special case for when RS=="" and FS is single character,
|
||||
// split on newline in addition to FS. See more here:
|
||||
// https://www.gnu.org/software/gawk/manual/html_node/Multiple-Line.html
|
||||
if p.recordSep == "" && utf8.RuneCountInString(p.fieldSep) == 1 {
|
||||
fields := make([]string, 0, len(p.fields))
|
||||
for _, field := range p.fields {
|
||||
lines := strings.Split(field, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSuffix(line, "\r")
|
||||
fields = append(fields, trimmed)
|
||||
}
|
||||
}
|
||||
p.fields = fields
|
||||
}
|
||||
|
||||
p.fieldsIsTrueStr = make([]bool, len(p.fields))
|
||||
p.numFields = len(p.fields)
|
||||
}
|
||||
|
||||
// Fetch next line (record) of input from current input file, opening
|
||||
// next input file if done with previous one
|
||||
func (p *interp) nextLine() (string, error) {
|
||||
for {
|
||||
if p.scanner == nil {
|
||||
if prevInput, ok := p.input.(io.Closer); ok && p.input != p.stdin {
|
||||
// Previous input is file, close it
|
||||
_ = prevInput.Close()
|
||||
}
|
||||
if p.filenameIndex >= p.argc && !p.hadFiles {
|
||||
// Moved past number of ARGV args and haven't seen
|
||||
// any files yet, use stdin
|
||||
p.input = p.stdin
|
||||
p.setFile("")
|
||||
p.hadFiles = true
|
||||
} else {
|
||||
if p.filenameIndex >= p.argc {
|
||||
// Done with ARGV args, all done with input
|
||||
return "", io.EOF
|
||||
}
|
||||
// Fetch next filename from ARGV. Can't use
|
||||
// getArrayValue() here as it would set the value if
|
||||
// not present
|
||||
index := strconv.Itoa(p.filenameIndex)
|
||||
argvIndex := p.program.Arrays["ARGV"]
|
||||
argvArray := p.arrays[p.getArrayIndex(ScopeGlobal, argvIndex)]
|
||||
filename := p.toString(argvArray[index])
|
||||
p.filenameIndex++
|
||||
|
||||
// Is it actually a var=value assignment?
|
||||
matches := varRegex.FindStringSubmatch(filename)
|
||||
if len(matches) >= 3 {
|
||||
// Yep, set variable to value and keep going
|
||||
err := p.setVarByName(matches[1], matches[2])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
continue
|
||||
} else if filename == "" {
|
||||
// ARGV arg is empty string, skip
|
||||
p.input = nil
|
||||
continue
|
||||
} else if filename == "-" {
|
||||
// ARGV arg is "-" meaning stdin
|
||||
p.input = p.stdin
|
||||
p.setFile("")
|
||||
} else {
|
||||
// A regular file name, open it
|
||||
if p.noFileReads {
|
||||
return "", newError("can't read from file due to NoFileReads")
|
||||
}
|
||||
input, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p.input = input
|
||||
p.setFile(filename)
|
||||
p.hadFiles = true
|
||||
}
|
||||
}
|
||||
p.scanner = p.newScanner(p.input)
|
||||
}
|
||||
p.recordTerminator = p.recordSep // will be overridden if RS is "" or multiple chars
|
||||
if p.scanner.Scan() {
|
||||
// We scanned some input, break and return it
|
||||
break
|
||||
}
|
||||
err := p.scanner.Err()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading from input: %s", err)
|
||||
}
|
||||
// Signal loop to move onto next file
|
||||
p.scanner = nil
|
||||
}
|
||||
|
||||
// Got a line (record) of input, return it
|
||||
p.lineNum++
|
||||
p.fileLineNum++
|
||||
return p.scanner.Text(), nil
|
||||
}
|
||||
|
||||
// Write output string to given writer, producing correct line endings
|
||||
// on Windows (CR LF).
|
||||
func writeOutput(w io.Writer, s string) error {
|
||||
if crlfNewline {
|
||||
// First normalize to \n, then convert all newlines to \r\n
|
||||
// (on Windows). NOTE: creating two new strings is almost
|
||||
// certainly slow; would be better to create a custom Writer.
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
s = strings.Replace(s, "\n", "\r\n", -1)
|
||||
}
|
||||
_, err := io.WriteString(w, s)
|
||||
return err
|
||||
}
|
||||
|
||||
// Close all streams, commands, and so on (after program execution).
|
||||
func (p *interp) closeAll() {
|
||||
if prevInput, ok := p.input.(io.Closer); ok {
|
||||
_ = prevInput.Close()
|
||||
}
|
||||
for _, r := range p.inputStreams {
|
||||
_ = r.Close()
|
||||
}
|
||||
for _, w := range p.outputStreams {
|
||||
_ = w.Close()
|
||||
}
|
||||
for _, cmd := range p.commands {
|
||||
_ = cmd.Wait()
|
||||
}
|
||||
if f, ok := p.output.(flusher); ok {
|
||||
_ = f.Flush()
|
||||
}
|
||||
if f, ok := p.errorOutput.(flusher); ok {
|
||||
_ = f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// Flush all output streams as well as standard output. Report whether all
|
||||
// streams were flushed successfully (logging error(s) if not).
|
||||
func (p *interp) flushAll() bool {
|
||||
allGood := true
|
||||
for name, writer := range p.outputStreams {
|
||||
allGood = allGood && p.flushWriter(name, writer)
|
||||
}
|
||||
if _, ok := p.output.(flusher); ok {
|
||||
// User-provided output may or may not be flushable
|
||||
allGood = allGood && p.flushWriter("stdout", p.output)
|
||||
}
|
||||
return allGood
|
||||
}
|
||||
|
||||
// Flush a single, named output stream, and report whether it was flushed
|
||||
// successfully (logging an error if not).
|
||||
func (p *interp) flushStream(name string) bool {
|
||||
writer := p.outputStreams[name]
|
||||
if writer == nil {
|
||||
p.printErrorf("error flushing %q: not an output file or pipe\n", name)
|
||||
return false
|
||||
}
|
||||
return p.flushWriter(name, writer)
|
||||
}
|
||||
|
||||
type flusher interface {
|
||||
Flush() error
|
||||
}
|
||||
|
||||
// Flush given output writer, and report whether it was flushed successfully
|
||||
// (logging an error if not).
|
||||
func (p *interp) flushWriter(name string, writer io.Writer) bool {
|
||||
flusher, ok := writer.(flusher)
|
||||
if !ok {
|
||||
return true // not a flusher, don't error
|
||||
}
|
||||
err := flusher.Flush()
|
||||
if err != nil {
|
||||
p.printErrorf("error flushing %q: %v\n", name, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Flush output and error streams.
|
||||
func (p *interp) flushOutputAndError() {
|
||||
if flusher, ok := p.output.(flusher); ok {
|
||||
_ = flusher.Flush()
|
||||
}
|
||||
if flusher, ok := p.errorOutput.(flusher); ok {
|
||||
_ = flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// Print a message to the error output stream, flushing as necessary.
|
||||
func (p *interp) printErrorf(format string, args ...interface{}) {
|
||||
if flusher, ok := p.output.(flusher); ok {
|
||||
_ = flusher.Flush() // ensure synchronization
|
||||
}
|
||||
fmt.Fprintf(p.errorOutput, format, args...)
|
||||
if flusher, ok := p.errorOutput.(flusher); ok {
|
||||
_ = flusher.Flush()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
// GoAWK interpreter value type (not exported).
|
||||
|
||||
package interp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type valueType uint8
|
||||
|
||||
const (
|
||||
typeNull valueType = iota
|
||||
typeStr
|
||||
typeNum
|
||||
typeNumStr
|
||||
)
|
||||
|
||||
// An AWK value (these are passed around by value)
|
||||
type value struct {
|
||||
typ valueType // Type of value
|
||||
s string // String value (for typeStr and typeNumStr)
|
||||
n float64 // Numeric value (for typeNum)
|
||||
}
|
||||
|
||||
// Create a new null value
|
||||
func null() value {
|
||||
return value{}
|
||||
}
|
||||
|
||||
// Create a new number value
|
||||
func num(n float64) value {
|
||||
return value{typ: typeNum, n: n}
|
||||
}
|
||||
|
||||
// Create a new string value
|
||||
func str(s string) value {
|
||||
return value{typ: typeStr, s: s}
|
||||
}
|
||||
|
||||
// Create a new value to represent a "numeric string" from an input field
|
||||
func numStr(s string) value {
|
||||
return value{typ: typeNumStr, s: s}
|
||||
}
|
||||
|
||||
// Create a numeric value from a Go bool
|
||||
func boolean(b bool) value {
|
||||
if b {
|
||||
return num(1)
|
||||
}
|
||||
return num(0)
|
||||
}
|
||||
|
||||
// Return true if value is a "true string" (a string or a "numeric string"
|
||||
// from an input field that can't be converted to a number). If false,
|
||||
// also return the (possibly converted) number.
|
||||
func (v value) isTrueStr() (float64, bool) {
|
||||
switch v.typ {
|
||||
case typeStr:
|
||||
return 0, true
|
||||
case typeNumStr:
|
||||
f, err := strconv.ParseFloat(strings.TrimSpace(v.s), 64)
|
||||
if err != nil {
|
||||
return 0, true
|
||||
}
|
||||
return f, false
|
||||
default: // typeNum, typeNull
|
||||
return v.n, false
|
||||
}
|
||||
}
|
||||
|
||||
// Return Go bool value of AWK value. For numbers or numeric strings,
|
||||
// zero is false and everything else is true. For strings, empty
|
||||
// string is false and everything else is true.
|
||||
func (v value) boolean() bool {
|
||||
switch v.typ {
|
||||
case typeStr:
|
||||
return v.s != ""
|
||||
case typeNumStr:
|
||||
f, err := strconv.ParseFloat(strings.TrimSpace(v.s), 64)
|
||||
if err != nil {
|
||||
return v.s != ""
|
||||
}
|
||||
return f != 0
|
||||
default: // typeNum, typeNull
|
||||
return v.n != 0
|
||||
}
|
||||
}
|
||||
|
||||
// Return value's string value, or convert to a string using given
|
||||
// format if a number value. Integers are a special case and don't
|
||||
// use floatFormat.
|
||||
func (v value) str(floatFormat string) string {
|
||||
if v.typ == typeNum {
|
||||
switch {
|
||||
case math.IsNaN(v.n):
|
||||
return "nan"
|
||||
case math.IsInf(v.n, 0):
|
||||
if v.n < 0 {
|
||||
return "-inf"
|
||||
} else {
|
||||
return "inf"
|
||||
}
|
||||
case v.n == float64(int(v.n)):
|
||||
return strconv.Itoa(int(v.n))
|
||||
default:
|
||||
return fmt.Sprintf(floatFormat, v.n)
|
||||
}
|
||||
}
|
||||
// For typeStr and typeNumStr we already have the string, for
|
||||
// typeNull v.s == "".
|
||||
return v.s
|
||||
}
|
||||
|
||||
// Return value's number value, converting from string if necessary
|
||||
func (v value) num() float64 {
|
||||
switch v.typ {
|
||||
case typeStr, typeNumStr:
|
||||
// Ensure string starts with a float and convert it
|
||||
return parseFloatPrefix(v.s)
|
||||
default: // typeNum, typeNull
|
||||
return v.n
|
||||
}
|
||||
}
|
||||
|
||||
var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
|
||||
|
||||
// Like strconv.ParseFloat, but parses at the start of string and
|
||||
// allows things like "1.5foo"
|
||||
func parseFloatPrefix(s string) float64 {
|
||||
// Skip whitespace at start
|
||||
i := 0
|
||||
for i < len(s) && asciiSpace[s[i]] != 0 {
|
||||
i++
|
||||
}
|
||||
start := i
|
||||
|
||||
// Parse mantissa: optional sign, initial digit(s), optional '.',
|
||||
// then more digits
|
||||
gotDigit := false
|
||||
if i < len(s) && (s[i] == '+' || s[i] == '-') {
|
||||
i++
|
||||
}
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
gotDigit = true
|
||||
i++
|
||||
}
|
||||
if i < len(s) && s[i] == '.' {
|
||||
i++
|
||||
}
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
gotDigit = true
|
||||
i++
|
||||
}
|
||||
if !gotDigit {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Parse exponent ("1e" and similar are allowed, but ParseFloat
|
||||
// rejects them)
|
||||
end := i
|
||||
if i < len(s) && (s[i] == 'e' || s[i] == 'E') {
|
||||
i++
|
||||
if i < len(s) && (s[i] == '+' || s[i] == '-') {
|
||||
i++
|
||||
}
|
||||
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
|
||||
i++
|
||||
end = i
|
||||
}
|
||||
}
|
||||
|
||||
floatStr := s[start:end]
|
||||
f, _ := strconv.ParseFloat(floatStr, 64)
|
||||
return f // Returns infinity in case of "value out of range" error
|
||||
}
|
|
@ -0,0 +1,461 @@
|
|||
// Package lexer is an AWK lexer (tokenizer).
|
||||
//
|
||||
// The lexer turns a string of AWK source code into a stream of
|
||||
// tokens for parsing.
|
||||
//
|
||||
// To tokenize some source, create a new lexer with NewLexer(src) and
|
||||
// then call Scan() until the token type is EOF or ILLEGAL.
|
||||
//
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Lexer tokenizes a byte string of AWK source code. Use NewLexer to
|
||||
// actually create a lexer, and Scan() or ScanRegex() to get tokens.
|
||||
type Lexer struct {
|
||||
src []byte
|
||||
offset int
|
||||
ch byte
|
||||
pos Position
|
||||
nextPos Position
|
||||
hadSpace bool
|
||||
lastTok Token
|
||||
}
|
||||
|
||||
// Position stores the source line and column where a token starts.
|
||||
type Position struct {
|
||||
// Line number of the token (starts at 1).
|
||||
Line int
|
||||
// Column on the line (starts at 1). Note that this is the byte
|
||||
// offset into the line, not rune offset.
|
||||
Column int
|
||||
}
|
||||
|
||||
// NewLexer creates a new lexer that will tokenize the given source
|
||||
// code. See the module-level example for a working example.
|
||||
func NewLexer(src []byte) *Lexer {
|
||||
l := &Lexer{src: src}
|
||||
l.nextPos.Line = 1
|
||||
l.nextPos.Column = 1
|
||||
l.next()
|
||||
return l
|
||||
}
|
||||
|
||||
// HadSpace returns true if the previously-scanned token had
|
||||
// whitespace before it. Used by the parser because when calling a
|
||||
// user-defined function the grammar doesn't allow a space between
|
||||
// the function name and the left parenthesis.
|
||||
func (l *Lexer) HadSpace() bool {
|
||||
return l.hadSpace
|
||||
}
|
||||
|
||||
// Scan scans the next token and returns its position (line/column),
|
||||
// token value (one of the uppercase token constants), and the
|
||||
// string value of the token. For most tokens, the token value is
|
||||
// empty. For NAME, NUMBER, STRING, and REGEX tokens, it's the
|
||||
// token's value. For an ILLEGAL token, it's the error message.
|
||||
func (l *Lexer) Scan() (Position, Token, string) {
|
||||
pos, tok, val := l.scan()
|
||||
l.lastTok = tok
|
||||
return pos, tok, val
|
||||
}
|
||||
|
||||
// Does the real work of scanning. Scan() wraps this to more easily
|
||||
// set lastTok.
|
||||
func (l *Lexer) scan() (Position, Token, string) {
|
||||
// Skip whitespace (except newline, which is a token)
|
||||
l.hadSpace = false
|
||||
for l.ch == ' ' || l.ch == '\t' || l.ch == '\r' || l.ch == '\\' {
|
||||
l.hadSpace = true
|
||||
if l.ch == '\\' {
|
||||
l.next()
|
||||
if l.ch == '\r' {
|
||||
l.next()
|
||||
}
|
||||
if l.ch != '\n' {
|
||||
return l.pos, ILLEGAL, "expected \\n after \\ line continuation"
|
||||
}
|
||||
}
|
||||
l.next()
|
||||
}
|
||||
if l.ch == '#' {
|
||||
// Skip comment till end of line
|
||||
l.next()
|
||||
for l.ch != '\n' && l.ch != 0 {
|
||||
l.next()
|
||||
}
|
||||
}
|
||||
if l.ch == 0 {
|
||||
// l.next() reached end of input
|
||||
return l.pos, EOF, ""
|
||||
}
|
||||
|
||||
pos := l.pos
|
||||
tok := ILLEGAL
|
||||
val := ""
|
||||
|
||||
ch := l.ch
|
||||
l.next()
|
||||
|
||||
// Names: keywords and functions
|
||||
if isNameStart(ch) {
|
||||
start := l.offset - 2
|
||||
for isNameStart(l.ch) || isDigit(l.ch) {
|
||||
l.next()
|
||||
}
|
||||
name := string(l.src[start : l.offset-1])
|
||||
tok := KeywordToken(name)
|
||||
if tok == ILLEGAL {
|
||||
tok = NAME
|
||||
val = name
|
||||
}
|
||||
return pos, tok, val
|
||||
}
|
||||
|
||||
// These are ordered by my guess at frequency of use. Should run
|
||||
// through a corpus of real AWK programs to determine actual
|
||||
// frequency.
|
||||
switch ch {
|
||||
case '$':
|
||||
tok = DOLLAR
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.':
|
||||
// Avoid make/append and use l.offset directly for performance
|
||||
start := l.offset - 2
|
||||
gotDigit := false
|
||||
if ch != '.' {
|
||||
gotDigit = true
|
||||
for isDigit(l.ch) {
|
||||
l.next()
|
||||
}
|
||||
if l.ch == '.' {
|
||||
l.next()
|
||||
}
|
||||
}
|
||||
for isDigit(l.ch) {
|
||||
gotDigit = true
|
||||
l.next()
|
||||
}
|
||||
if !gotDigit {
|
||||
return l.pos, ILLEGAL, "expected digits"
|
||||
}
|
||||
if l.ch == 'e' || l.ch == 'E' {
|
||||
l.next()
|
||||
gotSign := false
|
||||
if l.ch == '+' || l.ch == '-' {
|
||||
gotSign = true
|
||||
l.next()
|
||||
}
|
||||
gotDigit = false
|
||||
for isDigit(l.ch) {
|
||||
l.next()
|
||||
gotDigit = true
|
||||
}
|
||||
// Per awk/gawk, "1e" is allowed and parsed as "1 e" (with "e"
|
||||
// considered a variable). "1e+" is parsed as "1e + ...".
|
||||
if !gotDigit {
|
||||
if gotSign {
|
||||
l.unread() // unread the '+' or '-'
|
||||
}
|
||||
l.unread() // unread the 'e' or 'E'
|
||||
}
|
||||
}
|
||||
tok = NUMBER
|
||||
val = string(l.src[start : l.offset-1])
|
||||
case '{':
|
||||
tok = LBRACE
|
||||
case '}':
|
||||
tok = RBRACE
|
||||
case '=':
|
||||
tok = l.choice('=', ASSIGN, EQUALS)
|
||||
case '<':
|
||||
tok = l.choice('=', LESS, LTE)
|
||||
case '>':
|
||||
switch l.ch {
|
||||
case '=':
|
||||
l.next()
|
||||
tok = GTE
|
||||
case '>':
|
||||
l.next()
|
||||
tok = APPEND
|
||||
default:
|
||||
tok = GREATER
|
||||
}
|
||||
case '"', '\'':
|
||||
// Note: POSIX awk spec doesn't allow single-quoted strings,
|
||||
// but this helps without quoting, especially on Windows
|
||||
// where the shell quote character is " (double quote).
|
||||
chars := make([]byte, 0, 32) // most won't require heap allocation
|
||||
for l.ch != ch {
|
||||
c := l.ch
|
||||
if c == 0 {
|
||||
return l.pos, ILLEGAL, "didn't find end quote in string"
|
||||
}
|
||||
if c == '\r' || c == '\n' {
|
||||
return l.pos, ILLEGAL, "can't have newline in string"
|
||||
}
|
||||
if c != '\\' {
|
||||
// Normal, non-escaped character
|
||||
chars = append(chars, c)
|
||||
l.next()
|
||||
continue
|
||||
}
|
||||
// Escape sequence, skip over \ and process
|
||||
l.next()
|
||||
switch l.ch {
|
||||
case 'n':
|
||||
c = '\n'
|
||||
l.next()
|
||||
case 't':
|
||||
c = '\t'
|
||||
l.next()
|
||||
case 'r':
|
||||
c = '\r'
|
||||
l.next()
|
||||
case 'a':
|
||||
c = '\a'
|
||||
l.next()
|
||||
case 'b':
|
||||
c = '\b'
|
||||
l.next()
|
||||
case 'f':
|
||||
c = '\f'
|
||||
l.next()
|
||||
case 'v':
|
||||
c = '\v'
|
||||
l.next()
|
||||
case 'x':
|
||||
// Hex byte of one of two hex digits
|
||||
l.next()
|
||||
digit := hexDigit(l.ch)
|
||||
if digit < 0 {
|
||||
return l.pos, ILLEGAL, "1 or 2 hex digits expected"
|
||||
}
|
||||
c = byte(digit)
|
||||
l.next()
|
||||
digit = hexDigit(l.ch)
|
||||
if digit >= 0 {
|
||||
c = c*16 + byte(digit)
|
||||
l.next()
|
||||
}
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||
// Octal byte of 1-3 octal digits
|
||||
c = l.ch - '0'
|
||||
l.next()
|
||||
for i := 0; i < 2 && l.ch >= '0' && l.ch <= '7'; i++ {
|
||||
c = c*8 + l.ch - '0'
|
||||
l.next()
|
||||
}
|
||||
default:
|
||||
// Any other escape character is just the char
|
||||
// itself, eg: "\z" is just "z"
|
||||
c = l.ch
|
||||
l.next()
|
||||
}
|
||||
chars = append(chars, c)
|
||||
}
|
||||
l.next()
|
||||
tok = STRING
|
||||
val = string(chars)
|
||||
case '(':
|
||||
tok = LPAREN
|
||||
case ')':
|
||||
tok = RPAREN
|
||||
case ',':
|
||||
tok = COMMA
|
||||
case ';':
|
||||
tok = SEMICOLON
|
||||
case '+':
|
||||
switch l.ch {
|
||||
case '+':
|
||||
l.next()
|
||||
tok = INCR
|
||||
case '=':
|
||||
l.next()
|
||||
tok = ADD_ASSIGN
|
||||
default:
|
||||
tok = ADD
|
||||
}
|
||||
case '-':
|
||||
switch l.ch {
|
||||
case '-':
|
||||
l.next()
|
||||
tok = DECR
|
||||
case '=':
|
||||
l.next()
|
||||
tok = SUB_ASSIGN
|
||||
default:
|
||||
tok = SUB
|
||||
}
|
||||
case '*':
|
||||
switch l.ch {
|
||||
case '*':
|
||||
l.next()
|
||||
tok = l.choice('=', POW, POW_ASSIGN)
|
||||
case '=':
|
||||
l.next()
|
||||
tok = MUL_ASSIGN
|
||||
default:
|
||||
tok = MUL
|
||||
}
|
||||
case '/':
|
||||
tok = l.choice('=', DIV, DIV_ASSIGN)
|
||||
case '%':
|
||||
tok = l.choice('=', MOD, MOD_ASSIGN)
|
||||
case '[':
|
||||
tok = LBRACKET
|
||||
case ']':
|
||||
tok = RBRACKET
|
||||
case '\n':
|
||||
tok = NEWLINE
|
||||
case '^':
|
||||
tok = l.choice('=', POW, POW_ASSIGN)
|
||||
case '!':
|
||||
switch l.ch {
|
||||
case '=':
|
||||
l.next()
|
||||
tok = NOT_EQUALS
|
||||
case '~':
|
||||
l.next()
|
||||
tok = NOT_MATCH
|
||||
default:
|
||||
tok = NOT
|
||||
}
|
||||
case '~':
|
||||
tok = MATCH
|
||||
case '?':
|
||||
tok = QUESTION
|
||||
case ':':
|
||||
tok = COLON
|
||||
case '&':
|
||||
tok = l.choice('&', ILLEGAL, AND)
|
||||
if tok == ILLEGAL {
|
||||
return l.pos, ILLEGAL, "unexpected char after '&'"
|
||||
}
|
||||
case '|':
|
||||
tok = l.choice('|', PIPE, OR)
|
||||
default:
|
||||
tok = ILLEGAL
|
||||
val = "unexpected char"
|
||||
}
|
||||
return pos, tok, val
|
||||
}
|
||||
|
||||
// ScanRegex parses an AWK regular expression in /slash/ syntax. The
|
||||
// AWK grammar has somewhat special handling of regex tokens, so the
|
||||
// parser can only call this after a DIV or DIV_ASSIGN token has just
|
||||
// been scanned.
|
||||
func (l *Lexer) ScanRegex() (Position, Token, string) {
|
||||
pos, tok, val := l.scanRegex()
|
||||
l.lastTok = tok
|
||||
return pos, tok, val
|
||||
}
|
||||
|
||||
// Does the real work of scanning a regex. ScanRegex() wraps this to
|
||||
// more easily set lastTok.
|
||||
func (l *Lexer) scanRegex() (Position, Token, string) {
|
||||
pos := l.pos
|
||||
chars := make([]byte, 0, 32) // most won't require heap allocation
|
||||
switch l.lastTok {
|
||||
case DIV:
|
||||
// Regex after '/' (the usual case)
|
||||
pos.Column -= 1
|
||||
case DIV_ASSIGN:
|
||||
// Regex after '/=' (happens when regex starts with '=')
|
||||
pos.Column -= 2
|
||||
chars = append(chars, '=')
|
||||
default:
|
||||
return l.pos, ILLEGAL, fmt.Sprintf("unexpected %s preceding regex", l.lastTok)
|
||||
}
|
||||
for l.ch != '/' {
|
||||
c := l.ch
|
||||
if c == 0 {
|
||||
return l.pos, ILLEGAL, "didn't find end slash in regex"
|
||||
}
|
||||
if c == '\r' || c == '\n' {
|
||||
return l.pos, ILLEGAL, "can't have newline in regex"
|
||||
}
|
||||
if c == '\\' {
|
||||
l.next()
|
||||
if l.ch != '/' {
|
||||
chars = append(chars, '\\')
|
||||
}
|
||||
c = l.ch
|
||||
}
|
||||
chars = append(chars, c)
|
||||
l.next()
|
||||
}
|
||||
l.next()
|
||||
return pos, REGEX, string(chars)
|
||||
}
|
||||
|
||||
// Load the next character into l.ch (or 0 on end of input) and update
|
||||
// line and column position.
|
||||
func (l *Lexer) next() {
|
||||
l.pos = l.nextPos
|
||||
if l.offset >= len(l.src) {
|
||||
// For last character, move offset 1 past the end as it
|
||||
// simplifies offset calculations in NAME and NUMBER
|
||||
if l.ch != 0 {
|
||||
l.ch = 0
|
||||
l.offset++
|
||||
l.nextPos.Column++
|
||||
}
|
||||
return
|
||||
}
|
||||
ch := l.src[l.offset]
|
||||
if ch == '\n' {
|
||||
l.nextPos.Line++
|
||||
l.nextPos.Column = 1
|
||||
} else if ch != '\r' {
|
||||
l.nextPos.Column++
|
||||
}
|
||||
l.ch = ch
|
||||
l.offset++
|
||||
}
|
||||
|
||||
// Un-read the character just scanned (doesn't handle line boundaries).
|
||||
func (l *Lexer) unread() {
|
||||
l.offset--
|
||||
l.pos.Column--
|
||||
l.nextPos.Column--
|
||||
l.ch = l.src[l.offset-1]
|
||||
}
|
||||
|
||||
func isNameStart(ch byte) bool {
|
||||
return ch == '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
|
||||
}
|
||||
|
||||
func isDigit(ch byte) bool {
|
||||
return ch >= '0' && ch <= '9'
|
||||
}
|
||||
|
||||
// Return the hex digit 0-15 corresponding to the given ASCII byte,
|
||||
// or -1 if it's not a valid hex digit.
|
||||
func hexDigit(ch byte) int {
|
||||
switch {
|
||||
case isDigit(ch):
|
||||
return int(ch - '0')
|
||||
case ch >= 'a' && ch <= 'f':
|
||||
return int(ch - 'a' + 10)
|
||||
case ch >= 'A' && ch <= 'F':
|
||||
return int(ch - 'A' + 10)
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) choice(ch byte, one, two Token) Token {
|
||||
if l.ch == ch {
|
||||
l.next()
|
||||
return two
|
||||
}
|
||||
return one
|
||||
}
|
||||
|
||||
// PeekByte returns the next unscanned byte; used when parsing
|
||||
// "getline lvalue" expressions. Returns 0 at end of input.
|
||||
func (l *Lexer) PeekByte() byte {
|
||||
return l.ch
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
// Lexer tokens
|
||||
|
||||
package lexer
|
||||
|
||||
// Token is the type of a single token.
|
||||
type Token int
|
||||
|
||||
const (
|
||||
ILLEGAL Token = iota
|
||||
EOF
|
||||
NEWLINE
|
||||
CONCAT // Not really a token, but used as an operator
|
||||
|
||||
// Symbols
|
||||
|
||||
ADD
|
||||
ADD_ASSIGN
|
||||
AND
|
||||
APPEND
|
||||
ASSIGN
|
||||
COLON
|
||||
COMMA
|
||||
DECR
|
||||
DIV
|
||||
DIV_ASSIGN
|
||||
DOLLAR
|
||||
EQUALS
|
||||
GTE
|
||||
GREATER
|
||||
INCR
|
||||
LBRACE
|
||||
LBRACKET
|
||||
LESS
|
||||
LPAREN
|
||||
LTE
|
||||
MATCH
|
||||
MOD
|
||||
MOD_ASSIGN
|
||||
MUL
|
||||
MUL_ASSIGN
|
||||
NOT_MATCH
|
||||
NOT
|
||||
NOT_EQUALS
|
||||
OR
|
||||
PIPE
|
||||
POW
|
||||
POW_ASSIGN
|
||||
QUESTION
|
||||
RBRACE
|
||||
RBRACKET
|
||||
RPAREN
|
||||
SEMICOLON
|
||||
SUB
|
||||
SUB_ASSIGN
|
||||
|
||||
// Keywords
|
||||
|
||||
BEGIN
|
||||
BREAK
|
||||
CONTINUE
|
||||
DELETE
|
||||
DO
|
||||
ELSE
|
||||
END
|
||||
EXIT
|
||||
FOR
|
||||
FUNCTION
|
||||
GETLINE
|
||||
IF
|
||||
IN
|
||||
NEXT
|
||||
PRINT
|
||||
PRINTF
|
||||
RETURN
|
||||
WHILE
|
||||
|
||||
// Built-in functions
|
||||
|
||||
F_ATAN2
|
||||
F_CLOSE
|
||||
F_COS
|
||||
F_EXP
|
||||
F_FFLUSH
|
||||
F_GSUB
|
||||
F_INDEX
|
||||
F_INT
|
||||
F_LENGTH
|
||||
F_LOG
|
||||
F_MATCH
|
||||
F_RAND
|
||||
F_SIN
|
||||
F_SPLIT
|
||||
F_SPRINTF
|
||||
F_SQRT
|
||||
F_SRAND
|
||||
F_SUB
|
||||
F_SUBSTR
|
||||
F_SYSTEM
|
||||
F_TOLOWER
|
||||
F_TOUPPER
|
||||
|
||||
// Literals and names (variables and arrays)
|
||||
|
||||
NAME
|
||||
NUMBER
|
||||
STRING
|
||||
REGEX
|
||||
|
||||
LAST = REGEX
|
||||
FIRST_FUNC = F_ATAN2
|
||||
LAST_FUNC = F_TOUPPER
|
||||
)
|
||||
|
||||
var keywordTokens = map[string]Token{
|
||||
"BEGIN": BEGIN,
|
||||
"break": BREAK,
|
||||
"continue": CONTINUE,
|
||||
"delete": DELETE,
|
||||
"do": DO,
|
||||
"else": ELSE,
|
||||
"END": END,
|
||||
"exit": EXIT,
|
||||
"for": FOR,
|
||||
"function": FUNCTION,
|
||||
"getline": GETLINE,
|
||||
"if": IF,
|
||||
"in": IN,
|
||||
"next": NEXT,
|
||||
"print": PRINT,
|
||||
"printf": PRINTF,
|
||||
"return": RETURN,
|
||||
"while": WHILE,
|
||||
|
||||
"atan2": F_ATAN2,
|
||||
"close": F_CLOSE,
|
||||
"cos": F_COS,
|
||||
"exp": F_EXP,
|
||||
"fflush": F_FFLUSH,
|
||||
"gsub": F_GSUB,
|
||||
"index": F_INDEX,
|
||||
"int": F_INT,
|
||||
"length": F_LENGTH,
|
||||
"log": F_LOG,
|
||||
"match": F_MATCH,
|
||||
"rand": F_RAND,
|
||||
"sin": F_SIN,
|
||||
"split": F_SPLIT,
|
||||
"sprintf": F_SPRINTF,
|
||||
"sqrt": F_SQRT,
|
||||
"srand": F_SRAND,
|
||||
"sub": F_SUB,
|
||||
"substr": F_SUBSTR,
|
||||
"system": F_SYSTEM,
|
||||
"tolower": F_TOLOWER,
|
||||
"toupper": F_TOUPPER,
|
||||
}
|
||||
|
||||
// KeywordToken returns the token associated with the given keyword
|
||||
// string, or ILLEGAL if given name is not a keyword.
|
||||
func KeywordToken(name string) Token {
|
||||
return keywordTokens[name]
|
||||
}
|
||||
|
||||
var tokenNames = map[Token]string{
|
||||
ILLEGAL: "<illegal>",
|
||||
EOF: "EOF",
|
||||
NEWLINE: "<newline>",
|
||||
CONCAT: "<concat>",
|
||||
|
||||
ADD: "+",
|
||||
ADD_ASSIGN: "+=",
|
||||
AND: "&&",
|
||||
APPEND: ">>",
|
||||
ASSIGN: "=",
|
||||
COLON: ":",
|
||||
COMMA: ",",
|
||||
DECR: "--",
|
||||
DIV: "/",
|
||||
DIV_ASSIGN: "/=",
|
||||
DOLLAR: "$",
|
||||
EQUALS: "==",
|
||||
GTE: ">=",
|
||||
GREATER: ">",
|
||||
INCR: "++",
|
||||
LBRACE: "{",
|
||||
LBRACKET: "[",
|
||||
LESS: "<",
|
||||
LPAREN: "(",
|
||||
LTE: "<=",
|
||||
MATCH: "~",
|
||||
MOD: "%",
|
||||
MOD_ASSIGN: "%=",
|
||||
MUL: "*",
|
||||
MUL_ASSIGN: "*=",
|
||||
NOT_MATCH: "!~",
|
||||
NOT: "!",
|
||||
NOT_EQUALS: "!=",
|
||||
OR: "||",
|
||||
PIPE: "|",
|
||||
POW: "^",
|
||||
POW_ASSIGN: "^=",
|
||||
QUESTION: "?",
|
||||
RBRACE: "}",
|
||||
RBRACKET: "]",
|
||||
RPAREN: ")",
|
||||
SEMICOLON: ";",
|
||||
SUB: "-",
|
||||
SUB_ASSIGN: "-=",
|
||||
|
||||
BEGIN: "BEGIN",
|
||||
BREAK: "break",
|
||||
CONTINUE: "continue",
|
||||
DELETE: "delete",
|
||||
DO: "do",
|
||||
ELSE: "else",
|
||||
END: "END",
|
||||
EXIT: "exit",
|
||||
FOR: "for",
|
||||
FUNCTION: "function",
|
||||
GETLINE: "getline",
|
||||
IF: "if",
|
||||
IN: "in",
|
||||
NEXT: "next",
|
||||
PRINT: "print",
|
||||
PRINTF: "printf",
|
||||
RETURN: "return",
|
||||
WHILE: "while",
|
||||
|
||||
F_ATAN2: "atan2",
|
||||
F_CLOSE: "close",
|
||||
F_COS: "cos",
|
||||
F_EXP: "exp",
|
||||
F_FFLUSH: "fflush",
|
||||
F_GSUB: "gsub",
|
||||
F_INDEX: "index",
|
||||
F_INT: "int",
|
||||
F_LENGTH: "length",
|
||||
F_LOG: "log",
|
||||
F_MATCH: "match",
|
||||
F_RAND: "rand",
|
||||
F_SIN: "sin",
|
||||
F_SPLIT: "split",
|
||||
F_SPRINTF: "sprintf",
|
||||
F_SQRT: "sqrt",
|
||||
F_SRAND: "srand",
|
||||
F_SUB: "sub",
|
||||
F_SUBSTR: "substr",
|
||||
F_SYSTEM: "system",
|
||||
F_TOLOWER: "tolower",
|
||||
F_TOUPPER: "toupper",
|
||||
|
||||
NAME: "name",
|
||||
NUMBER: "number",
|
||||
STRING: "string",
|
||||
REGEX: "regex",
|
||||
}
|
||||
|
||||
// String returns the string name of this token.
|
||||
func (t Token) String() string {
|
||||
return tokenNames[t]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,468 @@
|
|||
// Resolve function calls and variable types
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
. "github.com/benhoyt/goawk/internal/ast"
|
||||
. "github.com/benhoyt/goawk/lexer"
|
||||
)
|
||||
|
||||
type varType int
|
||||
|
||||
const (
|
||||
typeUnknown varType = iota
|
||||
typeScalar
|
||||
typeArray
|
||||
)
|
||||
|
||||
func (t varType) String() string {
|
||||
switch t {
|
||||
case typeScalar:
|
||||
return "Scalar"
|
||||
case typeArray:
|
||||
return "Array"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// typeInfo records type information for a single variable
|
||||
type typeInfo struct {
|
||||
typ varType
|
||||
ref *VarExpr
|
||||
scope VarScope
|
||||
index int
|
||||
callName string
|
||||
argIndex int
|
||||
}
|
||||
|
||||
// Used by printVarTypes when debugTypes is turned on
|
||||
func (t typeInfo) String() string {
|
||||
var scope string
|
||||
switch t.scope {
|
||||
case ScopeGlobal:
|
||||
scope = "Global"
|
||||
case ScopeLocal:
|
||||
scope = "Local"
|
||||
default:
|
||||
scope = "Special"
|
||||
}
|
||||
return fmt.Sprintf("typ=%s ref=%p scope=%s index=%d callName=%q argIndex=%d",
|
||||
t.typ, t.ref, scope, t.index, t.callName, t.argIndex)
|
||||
}
|
||||
|
||||
// A single variable reference (normally scalar)
|
||||
type varRef struct {
|
||||
funcName string
|
||||
ref *VarExpr
|
||||
isArg bool
|
||||
pos Position
|
||||
}
|
||||
|
||||
// A single array reference
|
||||
type arrayRef struct {
|
||||
funcName string
|
||||
ref *ArrayExpr
|
||||
pos Position
|
||||
}
|
||||
|
||||
// Initialize the resolver
|
||||
func (p *parser) initResolve() {
|
||||
p.varTypes = make(map[string]map[string]typeInfo)
|
||||
p.varTypes[""] = make(map[string]typeInfo) // globals
|
||||
p.functions = make(map[string]int)
|
||||
p.arrayRef("ARGV", Position{1, 1}) // interpreter relies on ARGV being present
|
||||
p.arrayRef("ENVIRON", Position{1, 1}) // and ENVIRON
|
||||
p.multiExprs = make(map[*MultiExpr]Position, 3)
|
||||
}
|
||||
|
||||
// Signal the start of a function
|
||||
func (p *parser) startFunction(name string, params []string) {
|
||||
p.funcName = name
|
||||
p.varTypes[name] = make(map[string]typeInfo)
|
||||
}
|
||||
|
||||
// Signal the end of a function
|
||||
func (p *parser) stopFunction() {
|
||||
p.funcName = ""
|
||||
}
|
||||
|
||||
// Add function by name with given index
|
||||
func (p *parser) addFunction(name string, index int) {
|
||||
p.functions[name] = index
|
||||
}
|
||||
|
||||
// Records a call to a user function (for resolving indexes later)
|
||||
type userCall struct {
|
||||
call *UserCallExpr
|
||||
pos Position
|
||||
inFunc string
|
||||
}
|
||||
|
||||
// Record a user call site
|
||||
func (p *parser) recordUserCall(call *UserCallExpr, pos Position) {
|
||||
p.userCalls = append(p.userCalls, userCall{call, pos, p.funcName})
|
||||
}
|
||||
|
||||
// After parsing, resolve all user calls to their indexes. Also
|
||||
// ensures functions called have actually been defined, and that
|
||||
// they're not being called with too many arguments.
|
||||
func (p *parser) resolveUserCalls(prog *Program) {
|
||||
// Number the native funcs (order by name to get consistent order)
|
||||
nativeNames := make([]string, 0, len(p.nativeFuncs))
|
||||
for name := range p.nativeFuncs {
|
||||
nativeNames = append(nativeNames, name)
|
||||
}
|
||||
sort.Strings(nativeNames)
|
||||
nativeIndexes := make(map[string]int, len(nativeNames))
|
||||
for i, name := range nativeNames {
|
||||
nativeIndexes[name] = i
|
||||
}
|
||||
|
||||
for _, c := range p.userCalls {
|
||||
// AWK-defined functions take precedence over native Go funcs
|
||||
index, ok := p.functions[c.call.Name]
|
||||
if !ok {
|
||||
f, haveNative := p.nativeFuncs[c.call.Name]
|
||||
if !haveNative {
|
||||
panic(p.posErrorf(c.pos, "undefined function %q", c.call.Name))
|
||||
}
|
||||
typ := reflect.TypeOf(f)
|
||||
if !typ.IsVariadic() && len(c.call.Args) > typ.NumIn() {
|
||||
panic(p.posErrorf(c.pos, "%q called with more arguments than declared", c.call.Name))
|
||||
}
|
||||
c.call.Native = true
|
||||
c.call.Index = nativeIndexes[c.call.Name]
|
||||
continue
|
||||
}
|
||||
function := prog.Functions[index]
|
||||
if len(c.call.Args) > len(function.Params) {
|
||||
panic(p.posErrorf(c.pos, "%q called with more arguments than declared", c.call.Name))
|
||||
}
|
||||
c.call.Index = index
|
||||
}
|
||||
}
|
||||
|
||||
// For arguments that are variable references, we don't know the
|
||||
// type based on context, so mark the types for these as unknown.
|
||||
func (p *parser) processUserCallArg(funcName string, arg Expr, index int) {
|
||||
if varExpr, ok := arg.(*VarExpr); ok {
|
||||
scope, varFuncName := p.getScope(varExpr.Name)
|
||||
ref := p.varTypes[varFuncName][varExpr.Name].ref
|
||||
if ref == varExpr {
|
||||
// Only applies if this is the first reference to this
|
||||
// variable (otherwise we know the type already)
|
||||
p.varTypes[varFuncName][varExpr.Name] = typeInfo{typeUnknown, ref, scope, 0, funcName, index}
|
||||
}
|
||||
// Mark the last related varRef (the most recent one) as a
|
||||
// call argument for later error handling
|
||||
p.varRefs[len(p.varRefs)-1].isArg = true
|
||||
}
|
||||
}
|
||||
|
||||
// Determine scope of given variable reference (and funcName if it's
|
||||
// a local, otherwise empty string)
|
||||
func (p *parser) getScope(name string) (VarScope, string) {
|
||||
switch {
|
||||
case p.locals[name]:
|
||||
return ScopeLocal, p.funcName
|
||||
case SpecialVarIndex(name) > 0:
|
||||
return ScopeSpecial, ""
|
||||
default:
|
||||
return ScopeGlobal, ""
|
||||
}
|
||||
}
|
||||
|
||||
// Record a variable (scalar) reference and return the *VarExpr (but
|
||||
// VarExpr.Index won't be set till later)
|
||||
func (p *parser) varRef(name string, pos Position) *VarExpr {
|
||||
scope, funcName := p.getScope(name)
|
||||
expr := &VarExpr{scope, 0, name}
|
||||
p.varRefs = append(p.varRefs, varRef{funcName, expr, false, pos})
|
||||
info := p.varTypes[funcName][name]
|
||||
if info.typ == typeUnknown {
|
||||
p.varTypes[funcName][name] = typeInfo{typeScalar, expr, scope, 0, info.callName, 0}
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
// Record an array reference and return the *ArrayExpr (but
|
||||
// ArrayExpr.Index won't be set till later)
|
||||
func (p *parser) arrayRef(name string, pos Position) *ArrayExpr {
|
||||
scope, funcName := p.getScope(name)
|
||||
if scope == ScopeSpecial {
|
||||
panic(p.errorf("can't use scalar %q as array", name))
|
||||
}
|
||||
expr := &ArrayExpr{scope, 0, name}
|
||||
p.arrayRefs = append(p.arrayRefs, arrayRef{funcName, expr, pos})
|
||||
info := p.varTypes[funcName][name]
|
||||
if info.typ == typeUnknown {
|
||||
p.varTypes[funcName][name] = typeInfo{typeArray, nil, scope, 0, info.callName, 0}
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
// Print variable type information (for debugging) on p.debugWriter
|
||||
func (p *parser) printVarTypes(prog *Program) {
|
||||
fmt.Fprintf(p.debugWriter, "scalars: %v\n", prog.Scalars)
|
||||
fmt.Fprintf(p.debugWriter, "arrays: %v\n", prog.Arrays)
|
||||
funcNames := []string{}
|
||||
for funcName := range p.varTypes {
|
||||
funcNames = append(funcNames, funcName)
|
||||
}
|
||||
sort.Strings(funcNames)
|
||||
for _, funcName := range funcNames {
|
||||
if funcName != "" {
|
||||
fmt.Fprintf(p.debugWriter, "function %s\n", funcName)
|
||||
} else {
|
||||
fmt.Fprintf(p.debugWriter, "globals\n")
|
||||
}
|
||||
varNames := []string{}
|
||||
for name := range p.varTypes[funcName] {
|
||||
varNames = append(varNames, name)
|
||||
}
|
||||
sort.Strings(varNames)
|
||||
for _, name := range varNames {
|
||||
info := p.varTypes[funcName][name]
|
||||
fmt.Fprintf(p.debugWriter, " %s: %s\n", name, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't finish resolving after this many iterations, give up
|
||||
const maxResolveIterations = 10000
|
||||
|
||||
// Resolve unknown variables types and generate variable indexes and
|
||||
// name-to-index mappings for interpreter
|
||||
func (p *parser) resolveVars(prog *Program) {
|
||||
// First go through all unknown types and try to determine the
|
||||
// type from the parameter type in that function definition. May
|
||||
// need multiple passes depending on the order of functions. This
|
||||
// is not particularly efficient, but on realistic programs it's
|
||||
// not an issue.
|
||||
for i := 0; ; i++ {
|
||||
progressed := false
|
||||
for funcName, infos := range p.varTypes {
|
||||
for name, info := range infos {
|
||||
if info.scope == ScopeSpecial || info.typ != typeUnknown {
|
||||
// It's a special var or type is already known
|
||||
continue
|
||||
}
|
||||
funcIndex, ok := p.functions[info.callName]
|
||||
if !ok {
|
||||
// Function being called is a native function
|
||||
continue
|
||||
}
|
||||
// Determine var type based on type of this parameter
|
||||
// in the called function (if we know that)
|
||||
paramName := prog.Functions[funcIndex].Params[info.argIndex]
|
||||
typ := p.varTypes[info.callName][paramName].typ
|
||||
if typ != typeUnknown {
|
||||
if p.debugTypes {
|
||||
fmt.Fprintf(p.debugWriter, "resolving %s:%s to %s\n",
|
||||
funcName, name, typ)
|
||||
}
|
||||
info.typ = typ
|
||||
p.varTypes[funcName][name] = info
|
||||
progressed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !progressed {
|
||||
// If we didn't progress we're done (or trying again is
|
||||
// not going to help)
|
||||
break
|
||||
}
|
||||
if i >= maxResolveIterations {
|
||||
panic(p.errorf("too many iterations trying to resolve variable types"))
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve global variables (iteration order is undefined, so
|
||||
// assign indexes basically randomly)
|
||||
prog.Scalars = make(map[string]int)
|
||||
prog.Arrays = make(map[string]int)
|
||||
for name, info := range p.varTypes[""] {
|
||||
_, isFunc := p.functions[name]
|
||||
if isFunc {
|
||||
// Global var can't also be the name of a function
|
||||
panic(p.errorf("global var %q can't also be a function", name))
|
||||
}
|
||||
var index int
|
||||
if info.scope == ScopeSpecial {
|
||||
index = SpecialVarIndex(name)
|
||||
} else if info.typ == typeArray {
|
||||
index = len(prog.Arrays)
|
||||
prog.Arrays[name] = index
|
||||
} else {
|
||||
index = len(prog.Scalars)
|
||||
prog.Scalars[name] = index
|
||||
}
|
||||
info.index = index
|
||||
p.varTypes[""][name] = info
|
||||
}
|
||||
|
||||
// Fill in unknown parameter types that are being called with arrays,
|
||||
// for example, as in the following code:
|
||||
//
|
||||
// BEGIN { arr[0]; f(arr) }
|
||||
// function f(a) { }
|
||||
for _, c := range p.userCalls {
|
||||
if c.call.Native {
|
||||
continue
|
||||
}
|
||||
function := prog.Functions[c.call.Index]
|
||||
for i, arg := range c.call.Args {
|
||||
varExpr, ok := arg.(*VarExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
funcName := p.getVarFuncName(prog, varExpr.Name, c.inFunc)
|
||||
argType := p.varTypes[funcName][varExpr.Name]
|
||||
paramType := p.varTypes[function.Name][function.Params[i]]
|
||||
if argType.typ == typeArray && paramType.typ == typeUnknown {
|
||||
paramType.typ = argType.typ
|
||||
p.varTypes[function.Name][function.Params[i]] = paramType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve local variables (assign indexes in order of params).
|
||||
// Also patch up Function.Arrays (tells interpreter which args
|
||||
// are arrays).
|
||||
for funcName, infos := range p.varTypes {
|
||||
if funcName == "" {
|
||||
continue
|
||||
}
|
||||
scalarIndex := 0
|
||||
arrayIndex := 0
|
||||
functionIndex := p.functions[funcName]
|
||||
function := prog.Functions[functionIndex]
|
||||
arrays := make([]bool, len(function.Params))
|
||||
for i, name := range function.Params {
|
||||
info := infos[name]
|
||||
var index int
|
||||
if info.typ == typeArray {
|
||||
index = arrayIndex
|
||||
arrayIndex++
|
||||
arrays[i] = true
|
||||
} else {
|
||||
// typeScalar or typeUnknown: variables may still be
|
||||
// of unknown type if they've never been referenced --
|
||||
// default to scalar in that case
|
||||
index = scalarIndex
|
||||
scalarIndex++
|
||||
}
|
||||
info.index = index
|
||||
p.varTypes[funcName][name] = info
|
||||
}
|
||||
prog.Functions[functionIndex].Arrays = arrays
|
||||
}
|
||||
|
||||
// Check that variables passed to functions are the correct type
|
||||
for _, c := range p.userCalls {
|
||||
// Check native function calls
|
||||
if c.call.Native {
|
||||
for _, arg := range c.call.Args {
|
||||
varExpr, ok := arg.(*VarExpr)
|
||||
if !ok {
|
||||
// Non-variable expression, must be scalar
|
||||
continue
|
||||
}
|
||||
funcName := p.getVarFuncName(prog, varExpr.Name, c.inFunc)
|
||||
info := p.varTypes[funcName][varExpr.Name]
|
||||
if info.typ == typeArray {
|
||||
panic(p.posErrorf(c.pos, "can't pass array %q to native function", varExpr.Name))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check AWK function calls
|
||||
function := prog.Functions[c.call.Index]
|
||||
for i, arg := range c.call.Args {
|
||||
varExpr, ok := arg.(*VarExpr)
|
||||
if !ok {
|
||||
if function.Arrays[i] {
|
||||
panic(p.posErrorf(c.pos, "can't pass scalar %s as array param", arg))
|
||||
}
|
||||
continue
|
||||
}
|
||||
funcName := p.getVarFuncName(prog, varExpr.Name, c.inFunc)
|
||||
info := p.varTypes[funcName][varExpr.Name]
|
||||
if info.typ == typeArray && !function.Arrays[i] {
|
||||
panic(p.posErrorf(c.pos, "can't pass array %q as scalar param", varExpr.Name))
|
||||
}
|
||||
if info.typ != typeArray && function.Arrays[i] {
|
||||
panic(p.posErrorf(c.pos, "can't pass scalar %q as array param", varExpr.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.debugTypes {
|
||||
p.printVarTypes(prog)
|
||||
}
|
||||
|
||||
// Patch up variable indexes (interpreter uses an index instead
|
||||
// of name for more efficient lookups)
|
||||
for _, varRef := range p.varRefs {
|
||||
info := p.varTypes[varRef.funcName][varRef.ref.Name]
|
||||
if info.typ == typeArray && !varRef.isArg {
|
||||
panic(p.posErrorf(varRef.pos, "can't use array %q as scalar", varRef.ref.Name))
|
||||
}
|
||||
varRef.ref.Index = info.index
|
||||
}
|
||||
for _, arrayRef := range p.arrayRefs {
|
||||
info := p.varTypes[arrayRef.funcName][arrayRef.ref.Name]
|
||||
if info.typ == typeScalar {
|
||||
panic(p.posErrorf(arrayRef.pos, "can't use scalar %q as array", arrayRef.ref.Name))
|
||||
}
|
||||
arrayRef.ref.Index = info.index
|
||||
}
|
||||
}
|
||||
|
||||
// If name refers to a local (in function inFunc), return that
|
||||
// function's name, otherwise return "" (meaning global).
|
||||
func (p *parser) getVarFuncName(prog *Program, name, inFunc string) string {
|
||||
if inFunc == "" {
|
||||
return ""
|
||||
}
|
||||
for _, param := range prog.Functions[p.functions[inFunc]].Params {
|
||||
if name == param {
|
||||
return inFunc
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Record a "multi expression" (comma-separated pseudo-expression
|
||||
// used to allow commas around print/printf arguments).
|
||||
func (p *parser) multiExpr(exprs []Expr, pos Position) Expr {
|
||||
expr := &MultiExpr{exprs}
|
||||
p.multiExprs[expr] = pos
|
||||
return expr
|
||||
}
|
||||
|
||||
// Mark the multi expression as used (by a print/printf statement).
|
||||
func (p *parser) useMultiExpr(expr *MultiExpr) {
|
||||
delete(p.multiExprs, expr)
|
||||
}
|
||||
|
||||
// Check that there are no unused multi expressions (syntax error).
|
||||
func (p *parser) checkMultiExprs() {
|
||||
if len(p.multiExprs) == 0 {
|
||||
return
|
||||
}
|
||||
// Show error on first comma-separated expression
|
||||
min := Position{1000000000, 1000000000}
|
||||
for _, pos := range p.multiExprs {
|
||||
if pos.Line < min.Line || (pos.Line == min.Line && pos.Column < min.Column) {
|
||||
min = pos
|
||||
}
|
||||
}
|
||||
panic(p.posErrorf(min, "unexpected comma-separated expression"))
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
# github.com/benhoyt/goawk v1.13.0
|
||||
## explicit; go 1.13
|
||||
github.com/benhoyt/goawk/internal/ast
|
||||
github.com/benhoyt/goawk/interp
|
||||
github.com/benhoyt/goawk/lexer
|
||||
github.com/benhoyt/goawk/parser
|
Loading…
Reference in New Issue