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/
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"

227
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,8 +239,100 @@ func (c *Calc) Eval(line string) error {
c.notdone = false
}
if err := c.EvalItem(item); err != nil {
return err
num, err := strconv.ParseFloat(item, 64)
if err == nil {
c.stack.Backup()
c.stack.Push(num)
} else {
// try hex
var i int
_, err := fmt.Sscanf(item, "0x%x", &i)
if err == nil {
c.stack.Backup()
c.stack.Push(float64(i))
continue
}
if contains(c.Constants, item) {
// put the constant onto the stack
c.stack.Backup()
c.stack.Push(const2num(item))
continue
}
if _, ok := c.Funcalls[item]; ok {
if err := c.DoFuncall(item); err != nil {
fmt.Println(err)
} else {
c.Result()
}
continue
}
if c.batch {
if _, ok := c.BatchFuncalls[item]; ok {
if err := c.DoFuncall(item); err != nil {
fmt.Println(err)
} else {
c.Result()
}
continue
}
} else {
if _, ok := c.BatchFuncalls[item]; ok {
fmt.Println("only supported in batch mode")
continue
}
}
if contains(c.LuaFunctions, item) {
// user provided custom lua functions
c.EvalLuaFunction(item)
continue
}
regmatches := c.Register.FindStringSubmatch(item)
if len(regmatches) == 3 {
switch regmatches[1] {
case ">":
c.PutVar(regmatches[2])
case "<":
c.GetVar(regmatches[2])
}
continue
}
// internal commands
if _, ok := c.Commands[item]; ok {
c.Commands[item].Func(c)
continue
}
if _, ok := c.ShowCommands[item]; ok {
c.ShowCommands[item].Func(c)
continue
}
if _, ok := c.StackCommands[item]; ok {
c.StackCommands[item].Func(c)
continue
}
if _, ok := c.SettingsCommands[item]; ok {
c.SettingsCommands[item].Func(c)
continue
}
switch item {
case "?":
fallthrough
case "help":
c.PrintHelp()
default:
fmt.Println("unknown command or operator!")
}
}
}
@@ -257,106 +345,6 @@ func (c *Calc) Eval(line string) error {
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 {
c.stack.Backup()
c.stack.Push(num)
} else {
// try hex
var i int
_, err := fmt.Sscanf(item, "0x%x", &i)
if err == nil {
c.stack.Backup()
c.stack.Push(float64(i))
return nil
}
if contains(c.Constants, item) {
// put the constant onto the stack
c.stack.Backup()
c.stack.Push(const2num(item))
return nil
}
if exists(c.Funcalls, item) {
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
} else {
c.Result()
}
return nil
}
if exists(c.BatchFuncalls, item) {
if !c.batch {
return Error("only supported in batch mode")
}
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
} else {
c.Result()
}
return nil
}
if contains(c.LuaFunctions, item) {
// user provided custom lua functions
c.EvalLuaFunction(item)
return nil
}
regmatches := c.Register.FindStringSubmatch(item)
if len(regmatches) == 3 {
switch regmatches[1] {
case ">":
c.PutVar(regmatches[2])
case "<":
c.GetVar(regmatches[2])
}
return nil
}
// internal commands
// FIXME: propagate errors
if exists(c.Commands, item) {
c.Commands[item].Func(c)
return nil
}
if exists(c.ShowCommands, item) {
c.ShowCommands[item].Func(c)
return nil
}
if exists(c.StackCommands, item) {
c.StackCommands[item].Func(c)
return nil
}
if exists(c.SettingsCommands, item) {
c.SettingsCommands[item].Func(c)
return nil
}
switch item {
case "?":
fallthrough
case "help":
c.PrintHelp()
default:
return Error("unknown command or operator")
}
}
return nil
}
// 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 {
return Error("function not defined but in completion list")
panic("function not defined but in completion list")
}
var args Numbers
@@ -442,16 +430,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]
@@ -528,7 +507,7 @@ func (c *Calc) PutVar(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.stack.Backup()
c.stack.Push(c.Vars[name])
@@ -541,9 +520,7 @@ func sortcommands(hash Commands) []string {
keys := make([]string, 0, len(hash))
for key := range hash {
if len(key) > 1 {
keys = append(keys, key)
}
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),

40
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,27 +30,26 @@ 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.
Usage: rpn [-bdvh] [<operator>]
Options:
-b, --batchmode enable batch mode
-d, --debug enable debug mode
-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
-b, --batchmode enable batch mode
-d, --debug enable debug mode
-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
-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())
@@ -75,7 +74,6 @@ 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()
@@ -121,11 +119,7 @@ func Main() int {
// 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
}
calc.Eval(strings.Join(flag.Args(), " "))
return 0
}
@@ -159,10 +153,7 @@ func Main() int {
break
}
err = calc.Eval(line)
if err != nil {
fmt.Println(err)
}
calc.Eval(line)
rl.SetPrompt(calc.Prompt())
}
@@ -171,10 +162,7 @@ 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

27
rpn.go
View File

@@ -8,15 +8,13 @@ SYNOPSIS
Usage: rpn [-bdvh] [<operator>]
Options:
-b, --batchmode enable batch mode
-d, --debug enable debug mode
-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
-b, --batchmode enable batch mode
-d, --debug enable debug mode
-s, --stack show last 5 items of the stack (off by default)
-i --intermediate print intermediate results
-m, --manual show manual
-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 +
@@ -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:

28
rpn.pod
View File

@@ -7,15 +7,13 @@ rpn - Programmable command-line calculator using reverse polish notation
Usage: rpn [-bdvh] [<operator>]
Options:
-b, --batchmode enable batch mode
-d, --debug enable debug mode
-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
-b, --batchmode enable batch mode
-d, --debug enable debug mode
-s, --stack show last 5 items of the stack (off by default)
-i --intermediate print intermediate results
-m, --manual show manual
-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 +
@@ -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 +1,2 @@
! exec testrpn 1 2 dumb
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 +1,2 @@
! exec testrpn 4 +
exec testrpn 4 +
stdout 'stack doesn''t provide enough arguments'

View File

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

20
util.go
View File

@@ -23,24 +23,16 @@ import (
"strings"
)
// find an item in a list, generic variant
func contains[E comparable](s []E, v E) bool {
for _, vs := range s {
if v == vs {
// find an item in a list
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
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 {
switch name {
case "Pi":
@@ -71,7 +63,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)
}