5 Commits

Author SHA1 Message Date
T.v.Dein
54d59c81a2 Merge branch 'master' into feature/add-converters 2023-11-14 19:58:12 +01:00
768803072b forgot to commit 2023-11-14 19:55:16 +01:00
243a4f762f added tests for new functions 2023-11-13 15:47:35 +01:00
11eabee480 bump version 2023-11-13 15:42:07 +01:00
ad3a49715c added:
- converters
- bitwise operators
- hex input and output support
2023-11-13 11:02:38 +01:00
29 changed files with 134 additions and 518 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

98
calc.go
View File

@@ -121,28 +121,20 @@ func (c *Calc) GetCompleteCustomFuncalls() func(string) []string {
}
for command := range c.SettingsCommands {
if len(command) > 1 {
completions = append(completions, command)
}
}
for command := range c.ShowCommands {
if len(command) > 1 {
completions = append(completions, command)
}
}
for command := range c.StackCommands {
if len(command) > 1 {
completions = append(completions, command)
}
}
for command := range c.Commands {
if len(command) > 1 {
completions = append(completions, command)
}
}
return completions
}
@@ -222,12 +214,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)
@@ -239,25 +231,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 {
@@ -270,42 +243,45 @@ 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 _, ok := c.Funcalls[item]; ok {
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
fmt.Println(err)
} else {
c.Result()
}
return nil
}
if exists(c.BatchFuncalls, item) {
if !c.batch {
return Error("only supported in batch mode")
continue
}
if c.batch {
if _, ok := c.BatchFuncalls[item]; ok {
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
fmt.Println(err)
} else {
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) {
// user provided custom lua functions
c.EvalLuaFunction(item)
return nil
continue
}
regmatches := c.Register.FindStringSubmatch(item)
@@ -316,29 +292,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) {
if _, ok := c.Commands[item]; ok {
c.Commands[item].Func(c)
return nil
continue
}
if exists(c.ShowCommands, item) {
if _, ok := c.ShowCommands[item]; ok {
c.ShowCommands[item].Func(c)
return nil
continue
}
if exists(c.StackCommands, item) {
if _, ok := c.StackCommands[item]; ok {
c.StackCommands[item].Func(c)
return nil
continue
}
if exists(c.SettingsCommands, item) {
if _, ok := c.SettingsCommands[item]; ok {
c.SettingsCommands[item].Func(c)
return nil
continue
}
switch item {
@@ -348,11 +323,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
@@ -365,7 +349,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
@@ -515,7 +499,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])
@@ -528,10 +512,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

@@ -18,12 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
)
type CommandFunction func(*Calc)
@@ -188,102 +184,6 @@ func (c *Calc) SetCommands() {
}
},
),
"edit": NewCommand(
"edit the stack interactively",
func(c *Calc) {
if c.stack.Len() == 0 {
fmt.Println("empty stack")
return
}
c.stack.Backup()
// put the stack contents into a tmp file
tmp, err := os.CreateTemp("", "stack")
if err != nil {
fmt.Println(err)
return
}
defer os.Remove(tmp.Name())
comment := `# add or remove numbers as you wish.
# each number must be on its own line.
# numbers must be floating point formatted.
`
_, err = tmp.WriteString(comment)
if err != nil {
fmt.Println(err)
return
}
for _, item := range c.stack.All() {
_, err = fmt.Fprintf(tmp, "%f\n", item)
if err != nil {
fmt.Println(err)
return
}
}
tmp.Close()
// determine which editor to use
editor := "vi"
enveditor, present := os.LookupEnv("EDITOR")
if present {
if editor != "" {
if _, err := os.Stat(editor); err == nil {
editor = enveditor
}
}
}
// execute editor with our tmp file containing current stack
cmd := exec.Command(editor, tmp.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
fmt.Println("could not run editor command: ", err)
return
}
// read the file back in
modified, err := os.Open(tmp.Name())
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer modified.Close()
// reset the stack
c.stack.Clear()
// and put the new contents (if legit) back onto the stack
scanner := bufio.NewScanner(modified)
for scanner.Scan() {
line := strings.TrimSpace(c.Comment.ReplaceAllString(scanner.Text(), ""))
if line == "" {
continue
}
num, err := strconv.ParseFloat(line, 64)
if err != nil {
fmt.Printf("%s is not a floating point number!\n", line)
continue
}
c.stack.Push(num)
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading from file:", err)
}
},
),
}
// general commands
@@ -305,15 +205,6 @@ func (c *Calc) SetCommands() {
// aliases
c.Commands["quit"] = c.Commands["exit"]
c.SettingsCommands["d"] = c.SettingsCommands["debug"]
c.SettingsCommands["b"] = c.SettingsCommands["batch"]
c.SettingsCommands["s"] = c.SettingsCommands["showstack"]
c.ShowCommands["h"] = c.ShowCommands["history"]
c.ShowCommands["p"] = c.ShowCommands["dump"]
c.ShowCommands["v"] = c.ShowCommands["vars"]
c.StackCommands["c"] = c.StackCommands["clear"]
c.StackCommands["u"] = c.StackCommands["undo"]
c.SettingsCommands["undebug"] = c.SettingsCommands["nodebug"]
c.SettingsCommands["show"] = c.SettingsCommands["showstack"]
}

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=

40
main.go
View File

@@ -30,7 +30,7 @@ import (
lua "github.com/yuin/gopher-lua"
)
const VERSION string = "2.0.13"
const VERSION string = "2.0.10"
const Usage string = `This is rpn, a reverse polish notation calculator cli.
@@ -42,7 +42,6 @@ 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
-v, --version show version
-h, --help show help
@@ -52,10 +51,6 @@ this only when working with stdin. E.g.: echo "2 3 4 5" | rpn +
Copyright (c) 2023 T.v.Dein`
func main() {
os.Exit(Main())
}
func Main() int {
calc := NewCalc()
showversion := false
@@ -79,12 +74,12 @@ func Main() int {
if showversion {
fmt.Printf("This is rpn version %s\n", VERSION)
return 0
return
}
if showhelp {
fmt.Println(Usage)
return 0
return
}
if enabledebug {
@@ -93,7 +88,7 @@ func Main() int {
if showmanual {
man()
return 0
os.Exit(0)
}
// the lua state object is global, instanciate it early
@@ -106,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
@@ -157,10 +141,7 @@ func Main() int {
break
}
err = calc.Eval(line)
if err != nil {
fmt.Println(err)
}
calc.Eval(line)
rl.SetPrompt(calc.Prompt())
}
@@ -169,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",
})
}

38
rpn.go
View File

@@ -178,27 +178,16 @@ DESCRIPTION
[no]debug toggle debug output (nodebug turns it off)
[no]showstack show the last 5 items of the stack (noshowtack turns it off)
Show commands:
Show commands: dump display the stack contents hex show last stack item
in hex form (converted to int) history display calculation history vars
show list of variables
dump display the stack contents
hex show last stack item in hex form (converted to int)
history display calculation history
vars show list of variables
Stack manipulation commands: clear clear the whole stack shift remove
the last element of the stack reverse reverse the stack elements swap
exchange the last two stack elements dup duplicate last stack item undo
undo last operation
Stack manipulation commands:
clear clear the whole stack
shift remove the last element of the stack
reverse reverse the stack elements
swap exchange the last two stack elements
dup duplicate last stack item
undo undo last operation
edit edit the stack interactively using vi or $EDITOR
Other commands:
help|? show this message
manual show manual
Other commands: help|? show this message manual show manual
quit|exit|c-d|c-c exit program
Register variables:
@@ -208,17 +197,6 @@ DESCRIPTION
Refer to https://pkg.go.dev/math for details about those functions.
There are also a number of shortcuts for some commands available:
d debug
b batch
s showstack
h history
p dump (aka print)
v vars
c clear
u undo
INTERACTIVE REPL
While you can use rpn in the command-line, the best experience you'll
have is the interactive repl (read eval print loop). Just execute "rpn"

15
rpn.pod
View File

@@ -186,24 +186,20 @@ Configuration Commands:
[no]showstack show the last 5 items of the stack (noshowtack turns it off)
Show commands:
dump display the stack contents
hex show last stack item in hex form (converted to int)
history display calculation history
vars show list of variables
Stack manipulation commands:
clear clear the whole stack
shift remove the last element of the stack
reverse reverse the stack elements
swap exchange the last two stack elements
dup duplicate last stack item
undo undo last operation
edit edit the stack interactively using vi or $EDITOR
Other commands:
help|? show this message
manual show manual
quit|exit|c-d|c-c exit program
@@ -216,17 +212,6 @@ Register variables:
Refer to https://pkg.go.dev/math for details about those functions.
There are also a number of shortcuts for some commands available:
d debug
b batch
s showstack
h history
p dump (aka print)
v vars
c clear
u undo
=head1 INTERACTIVE REPL
While you can use rpn in the command-line, the best experience you'll

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

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)
}