1 Commits

Author SHA1 Message Date
d9a0d61efc use generics for contains() and add generic exists() 2023-12-07 12:16:49 +01:00
29 changed files with 140 additions and 404 deletions

View File

@@ -51,17 +51,14 @@ install: buildlocal
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean:
rm -rf $(tool) coverage.out testdata
rm -rf $(tool) coverage.out
test: clean
go test ./... $(ARGS)
testfuzzy: clean
go test -fuzz ./... $(ARGS)
test:
go test -v ./...
singletest:
@echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v"
go test -run $(TEST) $(ARGS)
@echo "Call like this: ''make singletest TEST=TestPrepareColumns"
go test -run $(TEST)
cover-report:
go test ./... -cover -coverprofile=coverage.out

87
calc.go
View File

@@ -1,5 +1,5 @@
/*
Copyright © 2023-2024 Thomas von Dein
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@ package main
import (
"errors"
"fmt"
"math"
"regexp"
"sort"
"strconv"
@@ -36,8 +35,6 @@ type Calc struct {
showstack bool
intermediate bool
notdone bool // set to true as long as there are items left in the eval loop
precision int
stack *Stack
history []string
completer readline.AutoCompleter
@@ -94,7 +91,6 @@ Register variables:
const (
//Commands string = `dump reverse clear shift undo help history manual exit quit swap debug undebug nodebug batch nobatch showstack noshowstack vars`
Constants string = `Pi Phi Sqrt2 SqrtE SqrtPi SqrtPhi Ln2 Log2E Ln10 Log10E`
Precision int = 2
)
// That way we can add custom functions to completion
@@ -154,7 +150,7 @@ func (c *Calc) GetCompleteCustomFuncalls() func(string) []string {
}
func NewCalc() *Calc {
c := Calc{stack: NewStack(), debug: false, precision: Precision}
c := Calc{stack: NewStack(), debug: false}
c.Funcalls = DefineFunctions()
c.BatchFuncalls = DefineBatchFunctions()
@@ -226,12 +222,12 @@ func (c *Calc) Prompt() string {
}
// the actual work horse, evaluate a line of calc command[s]
func (c *Calc) Eval(line string) error {
func (c *Calc) Eval(line string) {
// remove surrounding whitespace and comments, if any
line = strings.TrimSpace(c.Comment.ReplaceAllString(line, ""))
if line == "" {
return nil
return
}
items := c.Space.Split(line, -1)
@@ -243,25 +239,6 @@ func (c *Calc) Eval(line string) error {
c.notdone = false
}
if err := c.EvalItem(item); err != nil {
return err
}
}
if c.showstack && !c.stdin {
dots := ""
if c.stack.Len() > 5 {
dots = "... "
}
last := c.stack.Last(5)
fmt.Printf("stack: %s%s\n", dots, list2str(last))
}
return nil
}
func (c *Calc) EvalItem(item string) error {
num, err := strconv.ParseFloat(item, 64)
if err == nil {
@@ -274,42 +251,43 @@ func (c *Calc) EvalItem(item string) error {
if err == nil {
c.stack.Backup()
c.stack.Push(float64(i))
return nil
continue
}
if contains(c.Constants, item) {
// put the constant onto the stack
c.stack.Backup()
c.stack.Push(const2num(item))
return nil
continue
}
if exists(c.Funcalls, item) {
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
fmt.Println(err)
} else {
c.Result()
}
return nil
continue
}
if exists(c.BatchFuncalls, item) {
if !c.batch {
return Error("only supported in batch mode")
fmt.Println("only supported in batch mode")
continue
}
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
fmt.Println(err)
} else {
c.Result()
}
return nil
continue
}
if contains(c.LuaFunctions, item) {
// user provided custom lua functions
c.EvalLuaFunction(item)
return nil
continue
}
regmatches := c.Register.FindStringSubmatch(item)
@@ -320,29 +298,28 @@ func (c *Calc) EvalItem(item string) error {
case "<":
c.GetVar(regmatches[2])
}
return nil
continue
}
// internal commands
// FIXME: propagate errors
if exists(c.Commands, item) {
c.Commands[item].Func(c)
return nil
continue
}
if exists(c.ShowCommands, item) {
c.ShowCommands[item].Func(c)
return nil
continue
}
if exists(c.StackCommands, item) {
c.StackCommands[item].Func(c)
return nil
continue
}
if exists(c.SettingsCommands, item) {
c.SettingsCommands[item].Func(c)
return nil
continue
}
switch item {
@@ -352,11 +329,20 @@ func (c *Calc) EvalItem(item string) error {
c.PrintHelp()
default:
return Error("unknown command or operator")
fmt.Println("unknown command or operator!")
}
}
}
return nil
if c.showstack && !c.stdin {
dots := ""
if c.stack.Len() > 5 {
dots = "... "
}
last := c.stack.Last(5)
fmt.Printf("stack: %s%s\n", dots, list2str(last))
}
}
// Execute a math function, check if it is defined just in case
@@ -369,7 +355,7 @@ func (c *Calc) DoFuncall(funcname string) error {
}
if function == nil {
return Error("function not defined but in completion list")
panic("function not defined but in completion list")
}
var args Numbers
@@ -442,16 +428,7 @@ func (c *Calc) Result() float64 {
fmt.Print("= ")
}
result := c.stack.Last()[0]
truncated := math.Trunc(result)
precision := c.precision
if result == truncated {
precision = 0
}
format := fmt.Sprintf("%%.%df\n", precision)
fmt.Printf(format, result)
fmt.Println(c.stack.Last()[0])
}
return c.stack.Last()[0]
@@ -541,10 +518,8 @@ func sortcommands(hash Commands) []string {
keys := make([]string, 0, len(hash))
for key := range hash {
if len(key) > 1 {
keys = append(keys, key)
}
}
sort.Strings(keys)

View File

@@ -19,8 +19,6 @@ package main
import (
"fmt"
"strconv"
"strings"
"testing"
lua "github.com/yuin/gopher-lua"
@@ -77,9 +75,7 @@ func TestCommentsAndWhitespace(t *testing.T) {
t.Run(testname, func(t *testing.T) {
for _, line := range tt.cmd {
if err := calc.Eval(line); err != nil {
t.Errorf(err.Error())
}
calc.Eval(line)
}
got := calc.stack.Last()
@@ -292,9 +288,7 @@ func TestCalc(t *testing.T) {
t.Run(testname, func(t *testing.T) {
calc.batch = tt.batch
if err := calc.Eval(tt.cmd); err != nil {
t.Errorf(err.Error())
}
calc.Eval(tt.cmd)
got := calc.Result()
calc.stack.Clear()
if got != tt.exp {
@@ -356,60 +350,3 @@ func TestCalcLua(t *testing.T) {
})
}
}
func FuzzEval(f *testing.F) {
legal := []string{
"dump",
"showstack",
"help",
"Pi 31 *",
"SqrtE Pi /",
"55.5 yards-to-meters",
"2 4 +",
"7 8 batch sum",
"7 8 %-",
"7 8 clear",
"7 8 /",
"b",
"#444",
"<X",
}
for _, item := range legal {
f.Add(item)
}
calc := NewCalc()
var i int
f.Fuzz(func(t *testing.T, line string) {
t.Logf("Stack:\n%v\n", calc.stack.All())
if err := calc.EvalItem(line); err == nil {
t.Logf("given: <%s>", line)
// not corpus and empty?
if !contains(legal, line) && len(line) > 0 {
item := strings.TrimSpace(calc.Comment.ReplaceAllString(line, ""))
_, hexerr := fmt.Sscanf(item, "0x%x", &i)
// no comment?
if len(item) > 0 {
// no known command or function?
if _, err := strconv.ParseFloat(item, 64); err != nil {
if !contains(calc.Constants, item) &&
!exists(calc.Funcalls, item) &&
!exists(calc.BatchFuncalls, item) &&
!contains(calc.LuaFunctions, item) &&
!exists(calc.Commands, item) &&
!exists(calc.ShowCommands, item) &&
!exists(calc.SettingsCommands, item) &&
!exists(calc.StackCommands, item) &&
!calc.Register.MatchString(item) &&
item != "?" && item != "help" &&
hexerr != nil {
t.Errorf("Fuzzy input accepted: <%s>", line)
}
}
}
}
}
})
}

View File

@@ -457,20 +457,12 @@ func DefineFunctions() Funcalls {
"<": NewFuncall(
func(arg Numbers) R {
// Shift by negative number provibited, so check it.
// Note that we check agains uint64 overflow as well here
if arg[1] < 0 || uint64(arg[1]) > math.MaxInt64 {
return NewR(0, errors.New("negative shift amount"))
}
return NewR(float64(int(arg[0])<<int(arg[1])), nil)
},
2),
">": NewFuncall(
func(arg Numbers) R {
if arg[1] < 0 || uint64(arg[1]) > math.MaxInt64 {
return NewR(0, errors.New("negative shift amount"))
}
return NewR(float64(int(arg[0])>>int(arg[1])), nil)
},
2),

4
go.mod
View File

@@ -4,9 +4,7 @@ go 1.20
require (
github.com/chzyer/readline v1.5.1 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/yuin/gopher-lua v1.1.0 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
)

6
go.sum
View File

@@ -2,15 +2,9 @@ github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwys
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

46
main.go
View File

@@ -1,5 +1,5 @@
/*
Copyright © 2023-2024 Thomas von Dein
Copyright © 2023 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -30,7 +30,7 @@ import (
lua "github.com/yuin/gopher-lua"
)
const VERSION string = "2.1.0"
const VERSION string = "2.0.11"
const Usage string = `This is rpn, a reverse polish notation calculator cli.
@@ -42,21 +42,15 @@ Options:
-s, --stack show last 5 items of the stack (off by default)
-i --intermediate print intermediate results
-m, --manual show manual
-c, --config <file> load <file> containing LUA code
-p, --precision <int> floating point number precision (default 2)
-v, --version show version
-h, --help show help
When <operator> is given, batch mode ist automatically enabled. Use
this only when working with stdin. E.g.: echo "2 3 4 5" | rpn +
Copyright (c) 2023-2024 T.v.Dein`
Copyright (c) 2023 T.v.Dein`
func main() {
os.Exit(Main())
}
func Main() int {
calc := NewCalc()
showversion := false
@@ -75,18 +69,17 @@ func Main() int {
flag.BoolVarP(&showmanual, "manual", "m", false, "show manual")
flag.StringVarP(&configfile, "config", "c",
os.Getenv("HOME")+"/.rpn.lua", "config file (lua format)")
flag.IntVarP(&calc.precision, "precision", "p", Precision, "floating point precision")
flag.Parse()
if showversion {
fmt.Printf("This is rpn version %s\n", VERSION)
return 0
return
}
if showhelp {
fmt.Println(Usage)
return 0
return
}
if enabledebug {
@@ -95,7 +88,7 @@ func Main() int {
if showmanual {
man()
return 0
os.Exit(0)
}
// the lua state object is global, instanciate it early
@@ -108,25 +101,14 @@ func Main() int {
luarunner := NewInterpreter(configfile, enabledebug)
luarunner.InitLua()
calc.SetInt(luarunner)
if calc.debug {
fmt.Println("loaded config")
}
} else {
if calc.debug {
fmt.Println(err)
}
}
if len(flag.Args()) > 1 {
// commandline calc operation, no readline etc needed
// called like rpn 2 2 +
calc.stdin = true
if err := calc.Eval(strings.Join(flag.Args(), " ")); err != nil {
fmt.Println(err)
return 1
}
return 0
calc.Eval(strings.Join(flag.Args(), " "))
return
}
// interactive mode, need readline
@@ -159,10 +141,7 @@ func Main() int {
break
}
err = calc.Eval(line)
if err != nil {
fmt.Println(err)
}
calc.Eval(line)
rl.SetPrompt(calc.Prompt())
}
@@ -171,15 +150,10 @@ func Main() int {
// echo 1 2 3 4 | rpn +
// batch mode enabled automatically
calc.batch = true
if err = calc.Eval(flag.Args()[0]); err != nil {
fmt.Println(err)
return 1
calc.Eval(flag.Args()[0])
}
}
return 0
}
func inputIsStdin() bool {
stat, _ := os.Stdin.Stat()
return (stat.Mode() & os.ModeCharDevice) == 0

View File

@@ -1,20 +0,0 @@
package main
import (
"os"
"testing"
"github.com/rogpeppe/go-internal/testscript"
)
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"testrpn": Main,
}))
}
func TestRpn(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "t",
})
}

13
rpn.go
View File

@@ -13,8 +13,6 @@ SYNOPSIS
-s, --stack show last 5 items of the stack (off by default)
-i --intermediate print intermediate results
-m, --manual show manual
-c, --config <file> load <file> containing LUA code
-p, --precision <int> floating point number precision (default 2)
-v, --version show version
-h, --help show help
@@ -311,15 +309,6 @@ EXTENDING RPN USING LUA
So you can't open files, execute other programs or open a connection to
the outside!
CONFIGURATION
rpn can be configured via command line flags (see usage above). Most of
the flags are also available as interactive commands, such as "--batch"
has the same effect as the batch command.
The floating point number precision option "-p, --precision" however is
not available as interactive command, it MUST be configured on the
command line, if needed. The default precision is 2.
GETTING HELP
In interactive mode you can enter the help command (or ?) to get a short
help along with a list of all supported operators and functions.
@@ -339,7 +328,7 @@ LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version
3.
Copyright (c) 2023-2024 by Thomas von Dein
Copyright (c) 2023 by Thomas von Dein
This software uses the following GO modules:

14
rpn.pod
View File

@@ -12,8 +12,6 @@ rpn - Programmable command-line calculator using reverse polish notation
-s, --stack show last 5 items of the stack (off by default)
-i --intermediate print intermediate results
-m, --manual show manual
-c, --config <file> load <file> containing LUA code
-p, --precision <int> floating point number precision (default 2)
-v, --version show version
-h, --help show help
@@ -344,16 +342,6 @@ B<Please note, that io, networking and system stuff is not allowed
though. So you can't open files, execute other programs or open a
connection to the outside!>
=head1 CONFIGURATION
B<rpn> can be configured via command line flags (see usage
above). Most of the flags are also available as interactive commands,
such as C<--batch> has the same effect as the B<batch> command.
The floating point number precision option C<-p, --precision> however
is not available as interactive command, it MUST be configured on the
command line, if needed. The default precision is 2.
=head1 GETTING HELP
In interactive mode you can enter the B<help> command (or B<?>) to get
@@ -376,7 +364,7 @@ L<https://github.com/TLINDEN/rpnc/issues>.
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.
Copyright (c) 2023-2024 by Thomas von Dein
Copyright (c) 2023 by Thomas von Dein
This software uses the following GO modules:

View File

@@ -1,2 +0,0 @@
exec testrpn 1 2 dump
stdout 'Stack revision 2 .0x'

View File

@@ -1,2 +0,0 @@
! exec testrpn 1 2 dumb
stdout 'unknown command or operator'

View File

@@ -1,2 +0,0 @@
exec testrpn -p 4 2 3 /
stdout '0.6667\n'

View File

@@ -1,2 +0,0 @@
! exec testrpn 4 +
stdout 'stack doesn''t provide enough arguments'

View File

@@ -1,2 +0,0 @@
exec testrpn -d 44 55 *
stdout 'push to stack: 2420.00\n'

View File

@@ -1,2 +0,0 @@
! exec testrpn 100 50 50 - /
stdout 'division by null'

View File

@@ -1,16 +0,0 @@
exec testrpn -d -c test.lua 3 5 lower
stdout '3\n'
-- test.lua --
function lower(a,b)
if a < b then
return a
else
return b
end
end
function init()
-- expects 2 args
register("lower", 2, "lower")
end

View File

@@ -1,2 +0,0 @@
exec testrpn 44 55 *
stdout '2420\n'

View File

@@ -1,2 +0,0 @@
exec testrpn -m
stdout 'This software is licensed under the GNU GENERAL PUBLIC LICENSE'

View File

@@ -1,2 +0,0 @@
exec testrpn -h
stdout 'This is rpn'

View File

@@ -1,2 +0,0 @@
exec testrpn -v
stdout 'This is rpn version'

View File

@@ -1,4 +0,0 @@
exec echo 1 2 3 4 5 batch median
stdin stdout
exec testrpn
[unix] stdout '3\n'

View File

@@ -1,4 +0,0 @@
exec echo 1 2 3 4 5
stdin stdout
[unix] exec testrpn median
[unix] stdout '3\n'

View File

@@ -1,4 +0,0 @@
exec echo 10 10 +
stdin stdout
exec testrpn
[unix] stdout '20\n'

View File

@@ -1,6 +0,0 @@
stdin input.txt
exec testrpn
[unix] stdout 'Available configuration commands'
-- input.txt --
?

View File

@@ -1,13 +0,0 @@
stdin input.txt
exec testrpn
[unix] stdout '28\n'
-- input.txt --
10
10
+
>SUM
clear
8
<SUM
+

View File

@@ -1,4 +0,0 @@
exec echo 1 2 3 4 5 median
stdin stdout
exec testrpn -b
[unix] stdout '3\n'

View File

@@ -1,13 +0,0 @@
-- simple function, return the lower number of the two operands
function lower(a,b)
if a < b then
return a
else
return b
end
end
function init()
-- expects 2 args
register("lower", 2, "lower")
end

View File

@@ -71,7 +71,3 @@ func const2num(name string) float64 {
func list2str(list Numbers) string {
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(list)), " "), "[]")
}
func Error(m string) error {
return fmt.Errorf("Error: %s!", m)
}