3 Commits

Author SHA1 Message Date
a7fa0def04 weird windows panic 2023-12-07 14:06:11 +01:00
c9815e8ba3 not supported on windows! 2023-12-07 13:55:12 +01:00
d0376a63e3 added commandline and stdin tests using testscript 2023-12-07 13:42:41 +01:00
12 changed files with 145 additions and 291 deletions

View File

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

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

View File

@@ -19,8 +19,6 @@ package main
import ( import (
"fmt" "fmt"
"strconv"
"strings"
"testing" "testing"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
@@ -77,9 +75,7 @@ func TestCommentsAndWhitespace(t *testing.T) {
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
for _, line := range tt.cmd { for _, line := range tt.cmd {
if err := calc.Eval(line); err != nil { calc.Eval(line)
t.Errorf(err.Error())
}
} }
got := calc.stack.Last() got := calc.stack.Last()
@@ -292,9 +288,7 @@ func TestCalc(t *testing.T) {
t.Run(testname, func(t *testing.T) { t.Run(testname, func(t *testing.T) {
calc.batch = tt.batch calc.batch = tt.batch
if err := calc.Eval(tt.cmd); err != nil { calc.Eval(tt.cmd)
t.Errorf(err.Error())
}
got := calc.Result() got := calc.Result()
calc.stack.Clear() calc.stack.Clear()
if got != tt.exp { 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( "<": NewFuncall(
func(arg Numbers) R { 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) return NewR(float64(int(arg[0])<<int(arg[1])), nil)
}, },
2), 2),
">": NewFuncall( ">": NewFuncall(
func(arg Numbers) R { 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) return NewR(float64(int(arg[0])>>int(arg[1])), nil)
}, },
2), 2),

24
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 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 it under the terms of the GNU General Public License as published by
@@ -30,7 +30,7 @@ import (
lua "github.com/yuin/gopher-lua" 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. const Usage string = `This is rpn, a reverse polish notation calculator cli.
@@ -43,14 +43,13 @@ Options:
-i --intermediate print intermediate results -i --intermediate print intermediate results
-m, --manual show manual -m, --manual show manual
-c, --config <file> load <file> containing LUA code -c, --config <file> load <file> containing LUA code
-p, --precision <int> floating point number precision (default 2)
-v, --version show version -v, --version show version
-h, --help show help -h, --help show help
When <operator> is given, batch mode ist automatically enabled. Use When <operator> is given, batch mode ist automatically enabled. Use
this only when working with stdin. E.g.: echo "2 3 4 5" | rpn + 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() { func main() {
os.Exit(Main()) os.Exit(Main())
@@ -75,7 +74,6 @@ func Main() int {
flag.BoolVarP(&showmanual, "manual", "m", false, "show manual") flag.BoolVarP(&showmanual, "manual", "m", false, "show manual")
flag.StringVarP(&configfile, "config", "c", flag.StringVarP(&configfile, "config", "c",
os.Getenv("HOME")+"/.rpn.lua", "config file (lua format)") os.Getenv("HOME")+"/.rpn.lua", "config file (lua format)")
flag.IntVarP(&calc.precision, "precision", "p", Precision, "floating point precision")
flag.Parse() flag.Parse()
@@ -121,11 +119,7 @@ func Main() int {
// commandline calc operation, no readline etc needed // commandline calc operation, no readline etc needed
// called like rpn 2 2 + // called like rpn 2 2 +
calc.stdin = true calc.stdin = true
if err := calc.Eval(strings.Join(flag.Args(), " ")); err != nil { calc.Eval(strings.Join(flag.Args(), " "))
fmt.Println(err)
return 1
}
return 0 return 0
} }
@@ -159,10 +153,7 @@ func Main() int {
break break
} }
err = calc.Eval(line) calc.Eval(line)
if err != nil {
fmt.Println(err)
}
rl.SetPrompt(calc.Prompt()) rl.SetPrompt(calc.Prompt())
} }
@@ -171,10 +162,7 @@ func Main() int {
// echo 1 2 3 4 | rpn + // echo 1 2 3 4 | rpn +
// batch mode enabled automatically // batch mode enabled automatically
calc.batch = true calc.batch = true
if err = calc.Eval(flag.Args()[0]); err != nil { calc.Eval(flag.Args()[0])
fmt.Println(err)
return 1
}
} }
return 0 return 0

13
rpn.go
View File

@@ -13,8 +13,6 @@ SYNOPSIS
-s, --stack show last 5 items of the stack (off by default) -s, --stack show last 5 items of the stack (off by default)
-i --intermediate print intermediate results -i --intermediate print intermediate results
-m, --manual show manual -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 -v, --version show version
-h, --help show help -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 So you can't open files, execute other programs or open a connection to
the outside! 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 GETTING HELP
In interactive mode you can enter the help command (or ?) to get a short 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. 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 This software is licensed under the GNU GENERAL PUBLIC LICENSE version
3. 3.
Copyright (c) 2023-2024 by Thomas von Dein Copyright (c) 2023 by Thomas von Dein
This software uses the following GO modules: 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) -s, --stack show last 5 items of the stack (off by default)
-i --intermediate print intermediate results -i --intermediate print intermediate results
-m, --manual show manual -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 -v, --version show version
-h, --help show help -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 though. So you can't open files, execute other programs or open a
connection to the outside!> 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 =head1 GETTING HELP
In interactive mode you can enter the B<help> command (or B<?>) to get 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. 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: This software uses the following GO modules:

View File

@@ -1,2 +1,2 @@
! exec testrpn 1 2 dumb exec testrpn 1 2 dumb
stdout 'unknown command or operator' 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 +1,2 @@
! exec testrpn 4 + exec testrpn 4 +
stdout 'stack doesn''t provide enough arguments' stdout 'stack doesn''t provide enough arguments'

View File

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

20
util.go
View File

@@ -23,24 +23,16 @@ import (
"strings" "strings"
) )
// find an item in a list, generic variant // find an item in a list
func contains[E comparable](s []E, v E) bool { func contains(s []string, e string) bool {
for _, vs := range s { for _, a := range s {
if v == vs { if a == e {
return true return true
} }
} }
return false return false
} }
// look if a key in a map exists, generic variant
func exists[K comparable, V any](m map[K]V, v K) bool {
if _, ok := m[v]; ok {
return true
}
return false
}
func const2num(name string) float64 { func const2num(name string) float64 {
switch name { switch name {
case "Pi": case "Pi":
@@ -71,7 +63,3 @@ func const2num(name string) float64 {
func list2str(list Numbers) string { func list2str(list Numbers) string {
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(list)), " "), "[]") return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(list)), " "), "[]")
} }
func Error(m string) error {
return fmt.Errorf("Error: %s!", m)
}