51 Commits

Author SHA1 Message Date
2fbf3ecb4e fix badge 2025-11-02 10:49:50 +01:00
0dbdb360c3 fix status badge 2025-11-02 10:46:07 +01:00
326b45b838 fix release link 2025-11-02 10:43:43 +01:00
T. von Dein
3e490a9fb5 move to codeberg (#40) 2025-11-02 10:42:32 +01:00
047920b665 add stew 2025-10-25 21:51:28 +02:00
dependabot[bot]
60df7086a6 Bump actions/checkout from 4 to 5 (#45) 2025-10-02 22:58:56 +02:00
dependabot[bot]
1f0e4626a9 Bump github.com/charmbracelet/bubbletea from 1.3.6 to 1.3.10 (#46) 2025-10-02 22:58:41 +02:00
dependabot[bot]
1a07ccc812 Bump github.com/spf13/pflag from 1.0.7 to 1.0.10 (#47) 2025-10-02 22:58:28 +02:00
dependabot[bot]
ee97c05443 Bump actions/setup-go from 5 to 6 (#44) 2025-10-02 22:58:17 +02:00
cc11f923b4 ci on push only 2025-10-02 22:56:42 +02:00
d449b4bd1f update go to 1.24.5 2025-10-02 22:54:31 +02:00
0688d6b213 refactored dir layout 2025-09-18 20:59:25 +02:00
T.v.Dein
06aad0649b update go to 1.24 (#43) 2025-09-18 20:41:38 +02:00
dependabot[bot]
ed69fbeeaa Bump golangci/golangci-lint-action from 6 to 7 (#41) 2025-09-18 20:34:44 +02:00
T.v.Dein
0bc23be919 Fix linter errors (#42)
* add gh-dash config
* fix linting errors
2025-09-18 20:31:40 +02:00
92cbc0f8dc also get rid of -m test 2025-08-08 13:13:28 +02:00
fa93b16d02 use internal pager for man page as well 2025-08-08 13:12:50 +02:00
15c40583a2 bump version 2025-08-08 12:50:53 +02:00
d7368374b6 avoid fuzzy tester to hit interactive pager 2025-08-08 12:48:05 +02:00
6094f480f1 fix linter 2025-08-08 12:30:32 +02:00
fd17211a53 not possible to test anymore 2025-08-08 12:25:49 +02:00
1f96e99da2 added more byte converters 2025-08-08 12:21:08 +02:00
f977b56815 added bubbletea pager 2025-08-08 12:20:59 +02:00
d430a45384 add changelog guilder and update release builder 2025-02-05 17:52:22 +01:00
433c5ede91 bump version 2025-02-01 18:15:37 +01:00
dependabot[bot]
b77ef061e6 Bump actions/checkout from 2 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 17:57:18 +01:00
dependabot[bot]
2a5e70279e Bump github.com/spf13/pflag from 1.0.5 to 1.0.6
Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.5 to 1.0.6.
- [Release notes](https://github.com/spf13/pflag/releases)
- [Commits](https://github.com/spf13/pflag/compare/v1.0.5...v1.0.6)

---
updated-dependencies:
- dependency-name: github.com/spf13/pflag
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 17:45:24 +01:00
dependabot[bot]
6c56ed9508 Bump actions/setup-go from 1 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 1 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v1...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 17:44:55 +01:00
ff76137986 build release binaries using ci workflow 2025-01-18 10:55:04 +01:00
e5dfad1e35 better switch 2025-01-15 10:28:35 +01:00
43fcf43d1f add time support 2024-11-18 13:18:51 +01:00
3a9d753720 bump version 2024-10-02 10:46:39 +02:00
5afe1275bc implemented #12: added toggle commands like togglebatch 2024-10-02 10:45:41 +02:00
dependabot[bot]
41b38191a5 Bump github.com/rogpeppe/go-internal from 1.11.0 to 1.13.1
Bumps [github.com/rogpeppe/go-internal](https://github.com/rogpeppe/go-internal) from 1.11.0 to 1.13.1.
- [Release notes](https://github.com/rogpeppe/go-internal/releases)
- [Commits](https://github.com/rogpeppe/go-internal/compare/v1.11.0...v1.13.1)

---
updated-dependencies:
- dependency-name: github.com/rogpeppe/go-internal
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-02 10:41:09 +02:00
8f2b6955ff fix spelling 2024-09-25 19:34:35 +02:00
9b244fc170 do not use formatter, specify build target 2024-09-25 19:29:49 +02:00
e4b2a4d6ea update lua 2024-09-25 19:24:17 +02:00
3ee4d4181a Merge branch 'master' of github.com:TLINDEN/rpnc 2024-09-25 19:20:51 +02:00
1a1670076a bump version, use go 1.22 2024-09-25 19:20:22 +02:00
7ccb05558f add dependabot 2024-09-25 19:18:55 +02:00
T.v.Dein
b38b431d29 Add demo mp4 2024-05-12 21:24:06 +02:00
62188dda0c fix linter errors 2024-01-26 13:10:15 +01:00
6a2a501e48 fix printing of fractionals (not scientific anymore), added -p flag 2024-01-26 08:19:01 +01:00
T.v.Dein
e81be12b19 Merge pull request #31 from TLINDEN/internal/fuzzytesting
* reorganized Eval() to return errors and call EvalItem() on each item
* fix negative shift amount error, found with fuzzy testing :)
* added fuzzy testing
2023-12-08 18:43:10 +01:00
222dc3a734 added fuzzy testing 2023-12-08 18:37:59 +01:00
49e01565b9 catch exec errors 2023-12-08 18:37:35 +01:00
e4a8af9b5b fix negative shift amount error, found with fuzzy testing :) 2023-12-08 18:36:33 +01:00
ac9d08d6fc reorganized Eval() return errors and call EvalItem() on each item 2023-12-08 18:35:56 +01:00
T.v.Dein
cb774b3b80 added commandline and stdin tests using testscript (#28)
* added commandline and stdin tests using testscript

---------

Co-authored-by: Thomas von Dein <tom@vondein.org>
2023-12-07 14:09:42 +01:00
T.v.Dein
846b3e63fc don't show shortcuts in help (clutters it) (#27)
* don't show shortcuts in help (clutters it)

* bump version

---------

Co-authored-by: Thomas von Dein <tom@vondein.org>
2023-12-07 13:47:32 +01:00
T.v.Dein
5557ad5f99 use generics for contains() and add generic exists() (#29)
Co-authored-by: Thomas von Dein <tom@vondein.org>
2023-12-07 13:47:04 +01:00
48 changed files with 1999 additions and 1317 deletions

96
.gh-dash.yml Normal file
View File

@@ -0,0 +1,96 @@
prSections:
- title: Responsible PRs
filters: repo:tlinden/rpnc is:open NOT dependabot
layout:
repoName:
hidden: true
- title: Responsible Dependabot PRs
filters: repo:tlinden/rpnc is:open dependabot
layout:
repoName:
hidden: true
issuesSections:
- title: Responsible Issues
filters: is:open repo:tlinden/rpnc -author:@me
layout:
repoName:
hidden: true
- title: Note-to-Self Issues
filters: is:open repo:tlinden/rpnc author:@me
layout:
creator:
hidden: true
repoName:
hidden: true
defaults:
preview:
open: false
width: 100
keybindings:
universal:
- key: "shift+down"
builtin: pageDown
- key: "shift+up"
builtin: pageUp
prs:
- key: g
name: gitu
command: >
cd {{.RepoPath}} && /home/scip/bin/gitu
- key: M
name: squash-merge
command: gh pr merge --rebase --squash --admin --repo {{.RepoName}} {{.PrNumber}}
- key: i
name: show ci checks
command: gh pr checks --repo {{.RepoName}} {{.PrNumber}} | glow -p
- key: e
name: edit pr
command: ~/.config/gh-dash/edit-gh-pr {{.RepoName}} {{.PrNumber}}
- key: E
name: open repo in emacs
command: emacsclient {{.RepoPath}} &
issues:
- key: v
name: view
command: gh issue view --repo {{.RepoName}} {{.IssueNumber}} | glow -p
- key: l
name: add label
command: gh issue --repo {{.RepoName}} edit {{.IssueNumber}} --add-label $(gum choose bug enhancement question dependencies wontfix)
- key: L
name: remove label
command: gh issue --repo {{.RepoName}} edit {{.IssueNumber}} --remove-label $(gum choose bug enhancement question dependencies wontfix)
- key: E
name: open repo in emacs
command: emacsclient {{.RepoPath}} &
theme:
ui:
sectionsShowCount: true
table:
compact: false
showSeparator: true
colors:
text:
primary: "#E2E1ED"
secondary: "#6770cb"
inverted: "#242347"
faint: "#b0793b"
warning: "#E0AF68"
success: "#3DF294"
background:
selected: "#1B1B33"
border:
primary: "#383B5B"
secondary: "#39386B"
faint: "#8d3e0b"
repoPaths:
:owner/:repo: ~/dev/:repo
pager:
diff: delta

View File

@@ -1,31 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[bug-report]"
labels: bug
assignees: TLINDEN
---
**Describtion**
<!-- Please provide a clear and concise description of the issue: -->
**Steps To Reproduce**
<!-- Please detail the steps to reproduce the behavior: -->
**Expected behavior**
<!-- What do you expected to happen instead? -->
**Version information**
<!--
Please provide as much version information as possible:
- if you have just installed a binary, provide the output of: rpn -v
- if you installed from source, provide the output of: make show-version
- provide additional details: operating system and version and shell environment
-->
**Additional informations**

View File

@@ -1,23 +0,0 @@
---
name: Feature request
about: Suggest a feature
title: "[feature-request]"
labels: feature-request
assignees: TLINDEN
---
**Describtion**
<!-- Please provide a clear and concise description of the feature you desire: -->
**Version information**
<!--
Just in case the feature is already present, please provide as
much version information as possible:
- if you have just installed a binary, provide the output of: rpn -v
- if you installed from source, provide the output of: make show-version
- provide additional details: operating system and version and shell environment
-->

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

View File

@@ -1,36 +0,0 @@
name: build-and-test-rpn
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
version: [1.21]
os: [ubuntu-latest, windows-latest, macos-latest]
name: Build
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.version }}
id: go
- name: checkout
uses: actions/checkout@v3
- name: build
run: go build
- name: test
run: make test
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.21
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3

69
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,69 @@
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
- go mod tidy
gitea_urls:
api: https://codeberg.org/api/v1
download: https://codeberg.org
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
- freebsd
archives:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}_{{ .Tag }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: [zip]
- goos: linux
formats: [tar.gz,binary]
files:
- src: "*.md"
strip_parent: true
- src: "docs/*"
strip_parent: true
- src: Makefile.dist
dst: Makefile
wrap_in_directory: true
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
groups:
- title: Improved
regexp: '^.*?(feat|add|new)(\([[:word:]]+\))??!?:.+$'
order: 0
- title: Fixed
regexp: '^.*?(bug|fix)(\([[:word:]]+\))??!?:.+$'
order: 1
- title: Changed
order: 999
release:
header: "# Release Notes"
footer: >-
---
Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/rpnc/compare/{{ .PreviousTag }}...{{ .Tag }})

27
.woodpecker/build.yaml Normal file
View File

@@ -0,0 +1,27 @@
matrix:
platform:
- linux/amd64
goversion:
- 1.24
labels:
platform: ${platform}
steps:
build:
when:
event: [push]
image: golang:${goversion}
commands:
- go get
- go build
- go test
linter:
when:
event: [push]
image: golang:${goversion}
commands:
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0
- golangci-lint --version
- golangci-lint run ./...

15
.woodpecker/release.yaml Normal file
View File

@@ -0,0 +1,15 @@
# build release
labels:
platform: linux/amd64
steps:
goreleaser:
image: goreleaser/goreleaser
when:
event: [tag]
environment:
GITEA_TOKEN:
from_secret: DEPLOY_TOKEN
commands:
- goreleaser release --clean --verbose

View File

@@ -25,20 +25,20 @@ UID = root
GID = 0
HAVE_POD := $(shell pod2text -h 2>/dev/null)
all: $(tool).1 $(tool).go buildlocal
all: $(tool).1 cmd/$(tool).go buildlocal
%.1: %.pod
ifdef HAVE_POD
pod2man -c "User Commands" -r 1 -s 1 $*.pod > $*.1
endif
%.go: %.pod
cmd/%.go: %.pod
ifdef HAVE_POD
echo "package main" > $*.go
echo >> $*.go
echo "var manpage = \`" >> $*.go
pod2text $*.pod >> $*.go
echo "\`" >> $*.go
echo "package main" > cmd/$*.go
echo >> cmd/$*.go
echo "var manpage = \`" >> cmd/$*.go
pod2text cmd/$*.pod >> cmd/$*.go
echo "\`" >> cmd/$*.go
endif
buildlocal:
@@ -51,14 +51,25 @@ install: buildlocal
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean:
rm -rf $(tool) coverage.out
rm -rf $(tool) coverage.out testdata
test:
go test -v ./...
test: clean
go test ./... $(ARGS)
testfuzzy: clean
go test -fuzz ./... $(ARGS)
testlint: test lint
lint:
golangci-lint run
lint-full:
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest,forbidigo,godox,dupword,forcetypeassert,goerr113,gomnd
singletest:
@echo "Call like this: ''make singletest TEST=TestPrepareColumns"
go test -run $(TEST)
@echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v"
go test -run $(TEST) $(ARGS)
cover-report:
go test ./... -cover -coverprofile=coverage.out
@@ -70,8 +81,8 @@ goupdate:
buildall:
./mkrel.sh $(tool) $(VERSION)
release: buildall
gh release create v$(VERSION) --generate-notes releases/*
release:
gh release create v$(VERSION) --generate-notes
show-versions: buildlocal
@echo "### rpn version:"

20
Makefile.dist Normal file
View File

@@ -0,0 +1,20 @@
# -*-make-*-
.PHONY: install all
tool = rpn
PREFIX = /usr/local
UID = root
GID = 0
all:
@echo "Type 'sudo make install' to install the tool."
@echo "To change prefix, type 'sudo make install PREFIX=/opt'"
install:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -d -o $(UID) -g $(GID) $(PREFIX)/share/doc
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
install -o $(UID) -g $(GID) -m 444 *.md $(PREFIX)/share/doc/

View File

@@ -1,8 +1,8 @@
## Programmable command-line calculator using reverse polish notation
[![status-badge](https://ci.codeberg.org/api/badges/15511/status.svg)](https://ci.codeberg.org/repos/15511)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://codeberg.org/scip/rpnc/raw/branch/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/codeberg.org/scip/rpnc)](https://goreportcard.com/report/codeberg.org/scip/rpnc)
[![Actions](https://github.com/tlinden/rpnc/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/rpnc/actions)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/rpnc/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/rpnc)](https://goreportcard.com/report/github.com/tlinden/rpnc)
## Programmable command-line calculator using reverse polish notation
This is a small commandline calculator which takes its input in
[reverse polish notation](https://en.wikipedia.org/wiki/Reverse_Polish_notation)
@@ -10,6 +10,7 @@ form.
Features:
- unlimited stack
- undo
- various stack manipulation commands
@@ -23,6 +24,7 @@ Features:
- history
- comments (comment character is `#`)
- variables
- help screen uses comfortable internal pager
## Demo
@@ -246,7 +248,12 @@ connection to the outside!**
There are multiple ways to install **rpn**:
- Go to the [latest release page](https://github.com/tlinden/rpn/releases/latest),
- You can use [stew](https://github.com/marwanhawari/stew) to install rpnc:
```default
stew install tlinden/rpnc
```
- Go to the [latest release page](https://codeberg.org/scip/rpn/releases/),
locate the binary for your operating system and platform.
Download it and put it into some directory within your `$PATH` variable.
@@ -259,7 +266,7 @@ There are multiple ways to install **rpn**:
- You can also install from source. Issue the following commands in your shell:
```
git clone https://github.com/TLINDEN/rpn.git
git clone https://codeberg.org/scip/rpn.git
cd rpn
make
sudo make install
@@ -273,7 +280,7 @@ hesitate to ask me about it, I'll add it.
The documentation is provided as a unix man-page. It will be
automatically installed if you install from source. However, you can
[read the man-page online](https://github.com/TLINDEN/rpnc/blob/master/rpn.pod)
[read the man-page online](https://codeberg.org/scip/rpnc/raw/branch/master/rpn.pod)
Or if you cloned the repository you can read it this way (perl needs
to be installed though): `perldoc rpn.pod`.
@@ -290,7 +297,7 @@ best way for me to forget to do something.
In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github:
https://github.com/TLINDEN/rpnc/issues.
https://codeberg.org/scip/rpnc/issues.
## Copyright and license
@@ -302,4 +309,4 @@ T.v.Dein <tom AT vondein DOT org>
## Project homepage
https://github.com/TLINDEN/rpnc
https://codeberg.org/scip/rpnc

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2023 Thomas von Dein
Copyright © 2023-2024 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
@@ -15,11 +15,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package cmd
import (
"errors"
"fmt"
"math"
"regexp"
"sort"
"strconv"
@@ -35,6 +36,8 @@ 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
@@ -66,7 +69,7 @@ Bitwise operators: and or xor < (left shift) > (right shift)
Percent functions:
% percent
%- substract percent
%- subtract percent
%+ add percent
Math functions (see https://pkg.go.dev/math):
@@ -75,6 +78,12 @@ erf erfc erfcinv erfinv exp exp2 expm1 floor gamma ilogb j0 j1 log
log10 log1p log2 logb pow round roundtoeven sin sinh tan tanh trunc y0
y1 copysign dim hypot
Converter functions:
cm-to-inch yards-to-meters bytes-to-kilobytes
inch-to-cm meters-to-yards bytes-to-megabytes
gallons-to-liters miles-to-kilometers bytes-to-gigabytes
liters-to-gallons kilometers-to-miles bytes-to-terabytes
Batch functions:
sum sum of all values (alias: +)
max max of all values
@@ -89,8 +98,9 @@ Register variables:
// commands, constants and operators, defined here to feed completion
// and our mode switch in Eval() dynamically
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
ShowStackLen int = 5
)
// That way we can add custom functions to completion
@@ -146,37 +156,36 @@ func (c *Calc) GetCompleteCustomFuncalls() func(string) []string {
return completions
}
}
func NewCalc() *Calc {
c := Calc{stack: NewStack(), debug: false}
calc := Calc{stack: NewStack(), debug: false, precision: Precision}
c.Funcalls = DefineFunctions()
c.BatchFuncalls = DefineBatchFunctions()
c.Vars = map[string]float64{}
calc.Funcalls = DefineFunctions()
calc.BatchFuncalls = DefineBatchFunctions()
calc.Vars = map[string]float64{}
c.completer = readline.NewPrefixCompleter(
calc.completer = readline.NewPrefixCompleter(
// custom lua functions
readline.PcItemDynamic(GetCompleteCustomFunctions()),
readline.PcItemDynamic(c.GetCompleteCustomFuncalls()),
readline.PcItemDynamic(calc.GetCompleteCustomFuncalls()),
)
c.Space = regexp.MustCompile(`\s+`)
c.Comment = regexp.MustCompile(`#.*`) // ignore everything after #
c.Register = regexp.MustCompile(`^([<>])([A-Z][A-Z0-9]*)`)
calc.Space = regexp.MustCompile(`\s+`)
calc.Comment = regexp.MustCompile(`#.*`) // ignore everything after #
calc.Register = regexp.MustCompile(`^([<>])([A-Z][A-Z0-9]*)`)
// pre-calculate mode switching arrays
c.Constants = strings.Split(Constants, " ")
calc.Constants = strings.Split(Constants, " ")
c.SetCommands()
calc.SetCommands()
return &c
return &calc
}
// setup the interpreter, called from main(), import lua functions
func (c *Calc) SetInt(I *Interpreter) {
c.interpreter = I
func (c *Calc) SetInt(interpreter *Interpreter) {
c.interpreter = interpreter
for name := range LuaFuncs {
c.LuaFunctions = append(c.LuaFunctions, name)
@@ -203,31 +212,31 @@ func (c *Calc) ToggleShow() {
}
func (c *Calc) Prompt() string {
p := "\033[31m»\033[0m "
b := ""
prompt := "\033[31m»\033[0m "
batch := ""
if c.batch {
b = "->batch"
batch = "->batch"
}
d := ""
v := ""
debug := ""
revision := ""
if c.debug {
d = "->debug"
v = fmt.Sprintf("/rev%d", c.stack.rev)
debug = "->debug"
revision = fmt.Sprintf("/rev%d", c.stack.rev)
}
return fmt.Sprintf("rpn%s%s [%d%s]%s", b, d, c.stack.Len(), v, p)
return fmt.Sprintf("rpn%s%s [%d%s]%s", batch, debug, c.stack.Len(), revision, prompt)
}
// the actual work horse, evaluate a line of calc command[s]
func (c *Calc) Eval(line string) {
func (c *Calc) Eval(line string) error {
// remove surrounding whitespace and comments, if any
line = strings.TrimSpace(c.Comment.ReplaceAllString(line, ""))
if line == "" {
return
return nil
}
items := c.Space.Split(line, -1)
@@ -239,57 +248,93 @@ func (c *Calc) Eval(line string) {
c.notdone = false
}
if err := c.EvalItem(item); err != nil {
return err
}
}
if c.showstack && !c.stdin {
dots := ""
if c.stack.Len() > ShowStackLen {
dots = "... "
}
last := c.stack.Last(ShowStackLen)
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 {
return nil
}
// try time
var hour, min int
_, err = fmt.Sscanf(item, "%d:%d", &hour, &min)
if err == nil {
c.stack.Backup()
c.stack.Push(float64(hour) + float64(min)/60)
return nil
}
// try hex
var i int
_, err := fmt.Sscanf(item, "0x%x", &i)
_, err = fmt.Sscanf(item, "0x%x", &i)
if err == nil {
c.stack.Backup()
c.stack.Push(float64(i))
continue
return nil
}
if contains(c.Constants, item) {
// put the constant onto the stack
c.stack.Backup()
c.stack.Push(const2num(item))
continue
return nil
}
if _, ok := c.Funcalls[item]; ok {
if exists(c.Funcalls, item) {
if err := c.DoFuncall(item); err != nil {
fmt.Println(err)
} else {
c.Result()
}
continue
return Error(err.Error())
}
if c.batch {
if _, ok := c.BatchFuncalls[item]; ok {
if err := c.DoFuncall(item); err != nil {
fmt.Println(err)
} else {
c.Result()
return nil
}
continue
if exists(c.BatchFuncalls, item) {
if !c.batch {
return Error("only supported in batch mode")
}
} else {
if _, ok := c.BatchFuncalls[item]; ok {
fmt.Println("only supported in batch mode")
continue
if err := c.DoFuncall(item); err != nil {
return Error(err.Error())
}
c.Result()
return nil
}
if contains(c.LuaFunctions, item) {
// user provided custom lua functions
c.EvalLuaFunction(item)
continue
return nil
}
regmatches := c.Register.FindStringSubmatch(item)
@@ -300,51 +345,45 @@ func (c *Calc) Eval(line string) {
case "<":
c.GetVar(regmatches[2])
}
continue
return nil
}
// internal commands
if _, ok := c.Commands[item]; ok {
// FIXME: propagate errors
if exists(c.Commands, item) {
c.Commands[item].Func(c)
continue
return nil
}
if _, ok := c.ShowCommands[item]; ok {
if exists(c.ShowCommands, item) {
c.ShowCommands[item].Func(c)
continue
return nil
}
if _, ok := c.StackCommands[item]; ok {
if exists(c.StackCommands, item) {
c.StackCommands[item].Func(c)
continue
return nil
}
if _, ok := c.SettingsCommands[item]; ok {
if exists(c.SettingsCommands, item) {
c.SettingsCommands[item].Func(c)
continue
return nil
}
switch item {
case "?":
fallthrough
case "help":
case "?", "help":
c.PrintHelp()
default:
fmt.Println("unknown command or operator!")
}
}
return Error("unknown command or operator")
}
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
}
// Execute a math function, check if it is defined just in case
@@ -357,10 +396,11 @@ func (c *Calc) DoFuncall(funcname string) error {
}
if function == nil {
panic("function not defined but in completion list")
return Error("function not defined but in completion list")
}
var args Numbers
batch := false
if function.Expectargs == -1 {
@@ -382,11 +422,11 @@ func (c *Calc) DoFuncall(funcname string) error {
// the actual lambda call, so to say. We provide a slice of
// the requested size, fetched from the stack (but not popped
// yet!)
R := function.Func(args)
funcresult := function.Func(args)
if R.Err != nil {
if funcresult.Err != nil {
// leave the stack untouched in case of any error
return R.Err
return funcresult.Err
}
// don't forget to backup!
@@ -402,10 +442,11 @@ func (c *Calc) DoFuncall(funcname string) error {
}
// save result
c.stack.Push(R.Res)
c.stack.Push(funcresult.Res)
// thanks a lot
c.SetHistory(funcname, args, R.Res)
c.SetHistory(funcname, args, funcresult.Res)
return nil
}
@@ -430,7 +471,16 @@ func (c *Calc) Result() float64 {
fmt.Print("= ")
}
fmt.Println(c.stack.Last()[0])
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)
}
return c.stack.Last()[0]
@@ -444,24 +494,26 @@ func (c *Calc) Debug(msg string) {
func (c *Calc) EvalLuaFunction(funcname string) {
// called from calc loop
var x float64
var luaresult float64
var err error
switch c.interpreter.FuncNumArgs(funcname) {
case 0:
fallthrough
case 1:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last())
luaresult, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last())
case 2:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last(2))
luaresult, err = c.interpreter.CallLuaFunc(funcname, c.stack.Last(2))
case -1:
x, err = c.interpreter.CallLuaFunc(funcname, c.stack.All())
luaresult, err = c.interpreter.CallLuaFunc(funcname, c.stack.All())
default:
x, err = 0, errors.New("invalid number of argument requested")
luaresult, err = 0, errors.New("invalid number of argument requested")
}
if err != nil {
fmt.Println(err)
return
}
@@ -472,24 +524,26 @@ func (c *Calc) EvalLuaFunction(funcname string) {
switch c.interpreter.FuncNumArgs(funcname) {
case 0:
a := c.stack.Last()
if len(a) == 1 {
c.History("%s(%f) = %f", funcname, a, x)
c.History("%s(%f) = %f", funcname, a, luaresult)
}
dopush = false
case 1:
a := c.stack.Pop()
c.History("%s(%f) = %f", funcname, a, x)
c.History("%s(%f) = %f", funcname, a, luaresult)
case 2:
a := c.stack.Pop()
b := c.stack.Pop()
c.History("%s(%f,%f) = %f", funcname, a, b, x)
c.History("%s(%f,%f) = %f", funcname, a, b, luaresult)
case -1:
c.stack.Clear()
c.History("%s(*) = %f", funcname, x)
c.History("%s(*) = %f", funcname, luaresult)
}
if dopush {
c.stack.Push(x)
c.stack.Push(luaresult)
}
c.Result()
@@ -507,7 +561,7 @@ func (c *Calc) PutVar(name string) {
}
func (c *Calc) GetVar(name string) {
if _, ok := c.Vars[name]; ok {
if exists(c.Vars, name) {
c.Debug(fmt.Sprintf("retrieve %.2f from %s", c.Vars[name], name))
c.stack.Backup()
c.stack.Push(c.Vars[name])
@@ -520,8 +574,10 @@ 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)
@@ -529,37 +585,40 @@ func sortcommands(hash Commands) []string {
}
func (c *Calc) PrintHelp() {
fmt.Println("Available configuration commands:")
output := "Available configuration commands:\n"
for _, name := range sortcommands(c.SettingsCommands) {
fmt.Printf("%-20s %s\n", name, c.SettingsCommands[name].Help)
output += fmt.Sprintf("%-20s %s\n", name, c.SettingsCommands[name].Help)
}
fmt.Println()
fmt.Println("Available show commands:")
output += "\nAvailable show commands:\n"
for _, name := range sortcommands(c.ShowCommands) {
fmt.Printf("%-20s %s\n", name, c.ShowCommands[name].Help)
output += fmt.Sprintf("%-20s %s\n", name, c.ShowCommands[name].Help)
}
fmt.Println()
fmt.Println("Available stack manipulation commands:")
output += "\nAvailable stack manipulation commands:\n"
for _, name := range sortcommands(c.StackCommands) {
fmt.Printf("%-20s %s\n", name, c.StackCommands[name].Help)
output += fmt.Sprintf("%-20s %s\n", name, c.StackCommands[name].Help)
}
fmt.Println()
fmt.Println("Other commands:")
output += "\nOther commands:\n"
for _, name := range sortcommands(c.Commands) {
fmt.Printf("%-20s %s\n", name, c.Commands[name].Help)
output += fmt.Sprintf("%-20s %s\n", name, c.Commands[name].Help)
}
fmt.Println()
fmt.Println(Help)
output += "\n" + Help
// append lua functions, if any
if len(LuaFuncs) > 0 {
fmt.Println("Lua functions:")
output += "\nLua functions:\n"
for name, function := range LuaFuncs {
fmt.Printf("%-20s %s\n", name, function.help)
output += fmt.Sprintf("%-20s %s\n", name, function.help)
}
}
Pager("rpn help overview", output)
}

View File

@@ -15,10 +15,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package cmd
import (
"fmt"
"strconv"
"strings"
"testing"
lua "github.com/yuin/gopher-lua"
@@ -69,20 +71,22 @@ func TestCommentsAndWhitespace(t *testing.T) {
},
}
for _, tt := range tests {
for _, test := range tests {
testname := fmt.Sprintf("%s .(expect %.2f)",
tt.name, tt.exp)
test.name, test.exp)
t.Run(testname, func(t *testing.T) {
for _, line := range tt.cmd {
calc.Eval(line)
for _, line := range test.cmd {
if err := calc.Eval(line); err != nil {
t.Error(err.Error())
}
}
got := calc.stack.Last()
if len(got) > 0 {
if got[0] != tt.exp {
if got[0] != test.exp {
t.Errorf("parsing failed:\n+++ got: %f\n--- want: %f",
got, tt.exp)
got, test.exp)
}
}
@@ -90,7 +94,6 @@ func TestCommentsAndWhitespace(t *testing.T) {
t.Errorf("invalid stack size:\n+++ got: %d\n--- want: 1",
calc.stack.Len())
}
})
calc.stack.Clear()
@@ -282,18 +285,20 @@ func TestCalc(t *testing.T) {
},
}
for _, tt := range tests {
for _, test := range tests {
testname := fmt.Sprintf("cmd-%s-expect-%.2f",
tt.name, tt.exp)
test.name, test.exp)
t.Run(testname, func(t *testing.T) {
calc.batch = tt.batch
calc.Eval(tt.cmd)
calc.batch = test.batch
if err := calc.Eval(test.cmd); err != nil {
t.Error(err.Error())
}
got := calc.Result()
calc.stack.Clear()
if got != tt.exp {
if got != test.exp {
t.Errorf("calc failed:\n+++ got: %f\n--- want: %f",
got, tt.exp)
got, test.exp)
}
})
}
@@ -318,23 +323,24 @@ func TestCalcLua(t *testing.T) {
}
calc := NewCalc()
L = lua.NewState(lua.Options{SkipOpenLibs: true})
defer L.Close()
luarunner := NewInterpreter("example.lua", false)
LuaInterpreter = lua.NewState(lua.Options{SkipOpenLibs: true})
defer LuaInterpreter.Close()
luarunner := NewInterpreter("../example.lua", false)
luarunner.InitLua()
calc.SetInt(luarunner)
for _, tt := range tests {
testname := fmt.Sprintf("lua-%s", tt.function)
for _, test := range tests {
testname := fmt.Sprintf("lua-%s", test.function)
t.Run(testname, func(t *testing.T) {
calc.stack.Clear()
for _, item := range tt.stack {
for _, item := range test.stack {
calc.stack.Push(item)
}
calc.EvalLuaFunction(tt.function)
calc.EvalLuaFunction(test.function)
got := calc.stack.Last()
@@ -343,10 +349,76 @@ func TestCalcLua(t *testing.T) {
calc.stack.Len())
}
if got[0] != tt.exp {
if got[0] != test.exp {
t.Errorf("lua function %s failed:\n+++ got: %f\n--- want: %f",
tt.function, got, tt.exp)
test.function, got, test.exp)
}
})
}
}
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",
"?",
"help",
}
for _, item := range legal {
f.Add(item)
}
calc := NewCalc()
var hexnum, hour, min int
f.Fuzz(func(t *testing.T, line string) {
t.Logf("Stack:\n%v\nLine: <%s>\n", calc.stack.All(), line)
switch line {
case "help", "?":
return
}
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", &hexnum)
_, timeerr := fmt.Sscanf(item, "%d:%d", &hour, &min)
// 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 &&
timeerr != nil {
t.Errorf("Fuzzy input accepted: <%s>", line)
}
}
}
}
}
})
}

View File

@@ -15,11 +15,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package cmd
import (
"bufio"
"fmt"
"log"
"os"
"os/exec"
"strconv"
@@ -42,9 +43,8 @@ func NewCommand(help string, function CommandFunction) *Command {
}
}
// define all management (that is: non calculation) commands
func (c *Calc) SetCommands() {
c.SettingsCommands = Commands{
func (c *Calc) SetSettingsCommands() Commands {
return Commands{
// Toggles
"debug": NewCommand(
"toggle debugging",
@@ -89,8 +89,10 @@ func (c *Calc) SetCommands() {
},
),
}
}
c.ShowCommands = Commands{
func (c *Calc) SetShowCommands() Commands {
return Commands{
// Display commands
"dump": NewCommand(
"display the stack contents",
@@ -131,8 +133,10 @@ func (c *Calc) SetCommands() {
},
),
}
}
c.StackCommands = Commands{
func (c *Calc) SetStackCommands() Commands {
return Commands{
"clear": NewCommand(
"clear the whole stack",
func(c *Calc) {
@@ -159,14 +163,7 @@ func (c *Calc) SetCommands() {
"swap": NewCommand(
"exchange the last two elements",
func(c *Calc) {
if c.stack.Len() < 2 {
fmt.Println("stack too small, can't swap")
} else {
c.stack.Backup()
c.stack.Swap()
}
},
CommandSwap,
),
"undo": NewCommand(
@@ -178,113 +175,21 @@ func (c *Calc) SetCommands() {
"dup": NewCommand(
"duplicate last stack item",
func(c *Calc) {
item := c.stack.Last()
if len(item) == 1 {
c.stack.Backup()
c.stack.Push(item[0])
} else {
fmt.Println("stack empty")
}
},
CommandDup,
),
"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)
}
},
CommandEdit,
),
}
}
// define all management (that is: non calculation) commands
func (c *Calc) SetCommands() {
c.SettingsCommands = c.SetSettingsCommands()
c.ShowCommands = c.SetShowCommands()
c.StackCommands = c.SetStackCommands()
// general commands
c.Commands = Commands{
@@ -310,6 +215,10 @@ func (c *Calc) SetCommands() {
c.SettingsCommands["b"] = c.SettingsCommands["batch"]
c.SettingsCommands["s"] = c.SettingsCommands["showstack"]
c.SettingsCommands["togglebatch"] = c.SettingsCommands["batch"]
c.SettingsCommands["toggledebug"] = c.SettingsCommands["debug"]
c.SettingsCommands["toggleshowstack"] = c.SettingsCommands["showstack"]
c.ShowCommands["h"] = c.ShowCommands["history"]
c.ShowCommands["p"] = c.ShowCommands["dump"]
c.ShowCommands["v"] = c.ShowCommands["vars"]
@@ -317,3 +226,136 @@ func (c *Calc) SetCommands() {
c.StackCommands["c"] = c.StackCommands["clear"]
c.StackCommands["u"] = c.StackCommands["undo"]
}
// added to the command map:
func CommandSwap(c *Calc) {
if c.stack.Len() < 2 {
fmt.Println("stack too small, can't swap")
} else {
c.stack.Backup()
c.stack.Swap()
}
}
func CommandDup(c *Calc) {
item := c.stack.Last()
if len(item) == 1 {
c.stack.Backup()
c.stack.Push(item[0])
} else {
fmt.Println("stack empty")
}
}
func CommandEdit(calc *Calc) {
if calc.stack.Len() == 0 {
fmt.Println("empty stack")
return
}
calc.stack.Backup()
// put the stack contents into a tmp file
tmp, err := os.CreateTemp("", "stack")
if err != nil {
fmt.Println(err)
return
}
defer func() {
if err := os.Remove(tmp.Name()); err != nil {
log.Fatal(err)
}
}()
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 calc.stack.All() {
_, err = fmt.Fprintf(tmp, "%f\n", item)
if err != nil {
fmt.Println(err)
return
}
}
if err := tmp.Close(); err != nil {
log.Fatal(err)
}
// 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 func() {
if err := modified.Close(); err != nil {
log.Fatal(err)
}
}()
// reset the stack
calc.stack.Clear()
// and put the new contents (if legit) back onto the stack
scanner := bufio.NewScanner(modified)
for scanner.Scan() {
line := strings.TrimSpace(calc.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
}
calc.stack.Push(num)
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading from file:", err)
}
}

578
cmd/funcs.go Normal file
View File

@@ -0,0 +1,578 @@
/*
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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"errors"
"math"
)
type Result struct {
Res float64
Err error
}
type Numbers []float64
type Function func(Numbers) Result
// every function we are able to call must be of type Funcall, which
// needs to specify how many numbers it expects and the actual go
// function to be executed.
//
// The function has to take a float slice as argument and return a
// float and an error object. The float slice is guaranteed to have
// the expected number of arguments.
//
// However, Lua functions are handled differently, see interpreter.go.
type Funcall struct {
Expectargs int // -1 means batch only mode, you'll get the whole stack as arg
Func Function
}
// will hold all hard coded functions and operators
type Funcalls map[string]*Funcall
// convenience function, create a new Funcall object, if expectargs
// was not specified, 2 is assumed.
func NewFuncall(function Function, expectargs ...int) *Funcall {
expect := 2
if len(expectargs) > 0 {
expect = expectargs[0]
}
return &Funcall{
Expectargs: expect,
Func: function,
}
}
// Convenience function, create new result
func NewResult(n float64, e error) Result {
return Result{Res: n, Err: e}
}
// the actual functions, called once during initialization.
func DefineFunctions() Funcalls {
funcmap := map[string]*Funcall{
// simple operators, they all expect 2 args
"+": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]+arg[1], nil)
},
),
"-": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]-arg[1], nil)
},
),
"x": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*arg[1], nil)
},
),
"/": NewFuncall(
func(arg Numbers) Result {
if arg[1] == 0 {
return NewResult(0, errors.New("division by null"))
}
return NewResult(arg[0]/arg[1], nil)
},
),
"^": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Pow(arg[0], arg[1]), nil)
},
),
"%": NewFuncall(
func(arg Numbers) Result {
return NewResult((arg[0]/100)*arg[1], nil)
},
),
"%-": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]-((arg[0]/100)*arg[1]), nil)
},
),
"%+": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]+((arg[0]/100)*arg[1]), nil)
},
),
"mod": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Remainder(arg[0], arg[1]), nil)
},
),
"sqrt": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Sqrt(arg[0]), nil)
},
1),
"abs": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Abs(arg[0]), nil)
},
1),
"acos": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Acos(arg[0]), nil)
},
1),
"acosh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Acosh(arg[0]), nil)
},
1),
"asin": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Asin(arg[0]), nil)
},
1),
"asinh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Asinh(arg[0]), nil)
},
1),
"atan": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Atan(arg[0]), nil)
},
1),
"atan2": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Atan2(arg[0], arg[1]), nil)
},
2),
"atanh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Atanh(arg[0]), nil)
},
1),
"cbrt": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Cbrt(arg[0]), nil)
},
1),
"ceil": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Ceil(arg[0]), nil)
},
1),
"cos": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Cos(arg[0]), nil)
},
1),
"cosh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Cosh(arg[0]), nil)
},
1),
"erf": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Erf(arg[0]), nil)
},
1),
"erfc": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Erfc(arg[0]), nil)
},
1),
"erfcinv": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Erfcinv(arg[0]), nil)
},
1),
"erfinv": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Erfinv(arg[0]), nil)
},
1),
"exp": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Exp(arg[0]), nil)
},
1),
"exp2": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Exp2(arg[0]), nil)
},
1),
"expm1": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Expm1(arg[0]), nil)
},
1),
"floor": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Floor(arg[0]), nil)
},
1),
"gamma": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Gamma(arg[0]), nil)
},
1),
"ilogb": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(math.Ilogb(arg[0])), nil)
},
1),
"j0": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.J0(arg[0]), nil)
},
1),
"j1": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.J1(arg[0]), nil)
},
1),
"log": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Log(arg[0]), nil)
},
1),
"log10": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Log10(arg[0]), nil)
},
1),
"log1p": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Log1p(arg[0]), nil)
},
1),
"log2": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Log2(arg[0]), nil)
},
1),
"logb": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Logb(arg[0]), nil)
},
1),
"pow": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Pow(arg[0], arg[1]), nil)
},
2),
"round": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Round(arg[0]), nil)
},
1),
"roundtoeven": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.RoundToEven(arg[0]), nil)
},
1),
"sin": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Sin(arg[0]), nil)
},
1),
"sinh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Sinh(arg[0]), nil)
},
1),
"tan": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Tan(arg[0]), nil)
},
1),
"tanh": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Tanh(arg[0]), nil)
},
1),
"trunc": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Trunc(arg[0]), nil)
},
1),
"y0": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Y0(arg[0]), nil)
},
1),
"y1": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Y1(arg[0]), nil)
},
1),
"copysign": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Copysign(arg[0], arg[1]), nil)
},
2),
"dim": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Dim(arg[0], arg[1]), nil)
},
2),
"hypot": NewFuncall(
func(arg Numbers) Result {
return NewResult(math.Hypot(arg[0], arg[1]), nil)
},
2),
// converters of all kinds
"cm-to-inch": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/2.54, nil)
},
1),
"inch-to-cm": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*2.54, nil)
},
1),
"gallons-to-liters": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*3.785, nil)
},
1),
"liters-to-gallons": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/3.785, nil)
},
1),
"yards-to-meters": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*91.44, nil)
},
1),
"meters-to-yards": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/91.44, nil)
},
1),
"miles-to-kilometers": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]*1.609, nil)
},
1),
"kilometers-to-miles": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1.609, nil)
},
1),
"bytes-to-kilobytes": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1024, nil)
},
1),
"bytes-to-megabytes": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1024/1024, nil)
},
1),
"bytes-to-gigabytes": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1024/1024/1024, nil)
},
1),
"bytes-to-terabytes": NewFuncall(
func(arg Numbers) Result {
return NewResult(arg[0]/1024/1024/1024/1024, nil)
},
1),
"or": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(int(arg[0])|int(arg[1])), nil)
},
2),
"and": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(int(arg[0])&int(arg[1])), nil)
},
2),
"xor": NewFuncall(
func(arg Numbers) Result {
return NewResult(float64(int(arg[0])^int(arg[1])), nil)
},
2),
"<": NewFuncall(
func(arg Numbers) Result {
// Shift by negative number provibited, so check it.
// Note that we check against uint64 overflow as well here
if arg[1] < 0 || uint64(arg[1]) > math.MaxInt64 {
return NewResult(0, errors.New("negative shift amount"))
}
return NewResult(float64(int(arg[0])<<int(arg[1])), nil)
},
2),
">": NewFuncall(
func(arg Numbers) Result {
if arg[1] < 0 || uint64(arg[1]) > math.MaxInt64 {
return NewResult(0, errors.New("negative shift amount"))
}
return NewResult(float64(int(arg[0])>>int(arg[1])), nil)
},
2),
}
// aliases
funcmap["*"] = funcmap["x"]
funcmap["remainder"] = funcmap["mod"]
return funcmap
}
func DefineBatchFunctions() Funcalls {
funcmap := map[string]*Funcall{
"median": NewFuncall(
func(args Numbers) Result {
middle := len(args) / 2
return NewResult(args[middle], nil)
},
-1),
"mean": NewFuncall(
func(args Numbers) Result {
var sum float64
for _, item := range args {
sum += item
}
return NewResult(sum/float64(len(args)), nil)
},
-1),
"min": NewFuncall(
func(args Numbers) Result {
var min float64
min, args = args[0], args[1:]
for _, item := range args {
if item < min {
min = item
}
}
return NewResult(min, nil)
},
-1),
"max": NewFuncall(
func(args Numbers) Result {
var max float64
max, args = args[0], args[1:]
for _, item := range args {
if item > max {
max = item
}
}
return NewResult(max, nil)
},
-1),
"sum": NewFuncall(
func(args Numbers) Result {
var sum float64
for _, item := range args {
sum += item
}
return NewResult(sum, nil)
},
-1),
}
// aliases
funcmap["+"] = funcmap["sum"]
funcmap["avg"] = funcmap["mean"]
return funcmap
}

View File

@@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package cmd
import (
"errors"
@@ -29,8 +29,8 @@ type Interpreter struct {
script string
}
// LUA interpreter, instanciated in main()
var L *lua.LState
// LuaInterpreter is the lua interpreter, instantiated in main()
var LuaInterpreter *lua.LState
// holds a user provided lua function
type LuaFunction struct {
@@ -39,8 +39,8 @@ type LuaFunction struct {
numargs int
}
// must be global since init() is being called from lua which doesn't
// have access to the interpreter instance
// LuaFuncs must be global since init() is being called from lua which
// doesn't have access to the interpreter instance
var LuaFuncs map[string]LuaFunction
func NewInterpreter(script string, debug bool) *Interpreter {
@@ -61,8 +61,8 @@ func (i *Interpreter) InitLua() {
{lua.DebugLibName, lua.OpenDebug},
{lua.MathLibName, lua.OpenMath},
} {
if err := L.CallByParam(lua.P{
Fn: L.NewFunction(pair.f),
if err := LuaInterpreter.CallByParam(lua.P{
Fn: LuaInterpreter.NewFunction(pair.f),
NRet: 0,
Protect: true,
}, lua.LString(pair.n)); err != nil {
@@ -71,19 +71,19 @@ func (i *Interpreter) InitLua() {
}
// load the lua config (which we expect to contain init() and math functions)
if err := L.DoFile(i.script); err != nil {
if err := LuaInterpreter.DoFile(i.script); err != nil {
panic(err)
}
// instanciate
// instantiate
LuaFuncs = map[string]LuaFunction{}
// that way the user can call register(...) from lua inside init()
L.SetGlobal("register", L.NewFunction(register))
LuaInterpreter.SetGlobal("register", LuaInterpreter.NewFunction(register))
// actually call init()
if err := L.CallByParam(lua.P{
Fn: L.GetGlobal("init"),
if err := LuaInterpreter.CallByParam(lua.P{
Fn: LuaInterpreter.GetGlobal("init"),
NRet: 0,
Protect: true,
}); err != nil {
@@ -108,56 +108,54 @@ func (i *Interpreter) FuncNumArgs(name string) int {
// arguments. 1 uses the last item of the stack, 2 the last two and -1
// all items (which translates to batch mode)
//
// The items array will be provded by calc.Eval(), these are
// The items array will be provided by calc.Eval(), these are
// non-popped stack items. So the items will only removed from the
// stack when the lua function execution is successfull.
// stack when the lua function execution is successful.
func (i *Interpreter) CallLuaFunc(funcname string, items []float64) (float64, error) {
i.Debug(fmt.Sprintf("calling lua func %s() with %d args",
funcname, LuaFuncs[funcname].numargs))
switch LuaFuncs[funcname].numargs {
case 0:
fallthrough
case 1:
case 0, 1:
// 1 arg variant
if err := L.CallByParam(lua.P{
Fn: L.GetGlobal(funcname),
if err := LuaInterpreter.CallByParam(lua.P{
Fn: LuaInterpreter.GetGlobal(funcname),
NRet: 1,
Protect: true,
}, lua.LNumber(items[0])); err != nil {
fmt.Println(err)
return 0, err
return 0, fmt.Errorf("failed to exec lua func %s: %w", funcname, err)
}
case 2:
// 2 arg variant
if err := L.CallByParam(lua.P{
Fn: L.GetGlobal(funcname),
if err := LuaInterpreter.CallByParam(lua.P{
Fn: LuaInterpreter.GetGlobal(funcname),
NRet: 1,
Protect: true,
}, lua.LNumber(items[0]), lua.LNumber(items[1])); err != nil {
return 0, err
return 0, fmt.Errorf("failed to exec lua func %s: %w", funcname, err)
}
case -1:
// batch variant, use lua table as array
tb := L.NewTable()
table := LuaInterpreter.NewTable()
// put the whole stack into it
for _, item := range items {
tb.Append(lua.LNumber(item))
table.Append(lua.LNumber(item))
}
if err := L.CallByParam(lua.P{
Fn: L.GetGlobal(funcname),
if err := LuaInterpreter.CallByParam(lua.P{
Fn: LuaInterpreter.GetGlobal(funcname),
NRet: 1,
Protect: true,
}, tb); err != nil {
return 0, err
}, table); err != nil {
return 0, fmt.Errorf("failed to exec lua func %s: %w", funcname, err)
}
}
// get result and cast to float64
if res, ok := L.Get(-1).(lua.LNumber); ok {
L.Pop(1)
if res, ok := LuaInterpreter.Get(-1).(lua.LNumber); ok {
LuaInterpreter.Pop(1)
return float64(res), nil
}
@@ -167,10 +165,10 @@ func (i *Interpreter) CallLuaFunc(funcname string, items []float64) (float64, er
// called from lua to register a math function numargs may be 1, 2 or
// -1, it denotes the number of items from the stack requested by the
// lua function. -1 means batch mode, that is all items
func register(L *lua.LState) int {
function := L.ToString(1)
numargs := L.ToInt(2)
help := L.ToString(3)
func register(lstate *lua.LState) int {
function := lstate.ToString(1)
numargs := lstate.ToInt(2)
help := lstate.ToString(3)
LuaFuncs[function] = LuaFunction{
name: function,

120
cmd/pager.go Normal file
View File

@@ -0,0 +1,120 @@
package cmd
// pager setup using bubbletea
// file shamlelessly copied from:
// https://github.com/charmbracelet/bubbletea/tree/main/examples/pager
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
titleStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Right = "├"
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
}()
infoStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Left = "┤"
return titleStyle.BorderStyle(b)
}()
)
type model struct {
content string
title string
ready bool
viewport viewport.Model
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.KeyMsg:
if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
return m, tea.Quit
}
case tea.WindowSizeMsg:
headerHeight := lipgloss.Height(m.headerView())
footerHeight := lipgloss.Height(m.footerView())
verticalMarginHeight := headerHeight + footerHeight
if !m.ready {
// Since this program is using the full size of the viewport we
// need to wait until we've received the window dimensions before
// we can initialize the viewport. The initial dimensions come in
// quickly, though asynchronously, which is why we wait for them
// here.
m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
m.viewport.YPosition = headerHeight
m.viewport.SetContent(m.content)
m.ready = true
} else {
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - verticalMarginHeight
}
}
// Handle keyboard and mouse events in the viewport
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m model) View() string {
if !m.ready {
return "\n Initializing..."
}
return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
}
func (m model) headerView() string {
// title := titleStyle.Render("RPN Help Overview")
title := titleStyle.Render(m.title)
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
}
func (m model) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func Pager(title, message string) {
p := tea.NewProgram(
model{content: message, title: title},
tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
)
if _, err := p.Run(); err != nil {
fmt.Println("could not run pager:", err)
os.Exit(1)
}
}

195
cmd/root.go Normal file
View File

@@ -0,0 +1,195 @@
/*
Copyright © 2023-2024 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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"fmt"
"log"
"os"
"strings"
"github.com/chzyer/readline"
flag "github.com/spf13/pflag"
lua "github.com/yuin/gopher-lua"
)
const VERSION string = "2.1.7"
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
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-2025 T.v.Dein`
func Main() int {
calc := NewCalc()
showversion := false
showhelp := false
showmanual := false
enabledebug := false
configfile := ""
flag.BoolVarP(&calc.batch, "batchmode", "b", false, "batch mode")
flag.BoolVarP(&calc.showstack, "show-stack", "s", false, "show stack")
flag.BoolVarP(&calc.intermediate, "showin-termediate", "i", false,
"show intermediate results")
flag.BoolVarP(&enabledebug, "debug", "d", false, "debug mode")
flag.BoolVarP(&showversion, "version", "v", false, "show version")
flag.BoolVarP(&showhelp, "help", "h", false, "show usage")
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
}
if showhelp {
fmt.Println(Usage)
return 0
}
if enabledebug {
calc.ToggleDebug()
}
if showmanual {
man()
return 0
}
// the lua state object is global, instantiate it early
LuaInterpreter = lua.NewState(lua.Options{SkipOpenLibs: true})
defer LuaInterpreter.Close()
// our config file is interpreted as lua code, only functions can
// be defined, init() will be called by InitLua().
if _, err := os.Stat(configfile); err == nil {
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
}
// interactive mode, need readline
reader, err := readline.NewEx(&readline.Config{
Prompt: calc.Prompt(),
HistoryFile: os.Getenv("HOME") + "/.rpn-history",
HistoryLimit: 500,
AutoComplete: calc.completer,
InterruptPrompt: "^C",
EOFPrompt: "exit",
HistorySearchFold: true,
})
if err != nil {
panic(err)
}
defer func() {
if err := reader.Close(); err != nil {
log.Fatal(err)
}
}()
reader.CaptureExitSignal()
if inputIsStdin() {
// commands are coming on stdin, however we will still enter
// the same loop since readline just reads fine from stdin
calc.ToggleStdin()
}
for {
// primary program repl
line, err := reader.Readline()
if err != nil {
break
}
err = calc.Eval(line)
if err != nil {
fmt.Println(err)
}
reader.SetPrompt(calc.Prompt())
}
if len(flag.Args()) > 0 {
// called like this:
// 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
}
}
return 0
}
func inputIsStdin() bool {
stat, _ := os.Stdin.Stat()
return (stat.Mode() & os.ModeCharDevice) == 0
}
func man() {
Pager("rpn manual page", manpage)
}

View File

@@ -1,4 +1,4 @@
package main
package cmd
var manpage = `
NAME
@@ -13,6 +13,8 @@ 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
@@ -106,7 +108,8 @@ DESCRIPTION
is enabled automatically, see last example.
You can enter integers, floating point numbers (positive or negative) or
hex numbers (prefixed with 0x).
hex numbers (prefixed with 0x). Time values in hh::mm format are
possible as well.
STACK MANIPULATION
There are lots of stack manipulation commands provided. The most
@@ -127,7 +130,7 @@ DESCRIPTION
Basic operators:
+ add
- substract
- subtract
/ divide
x multiply (alias: *)
^ power
@@ -143,7 +146,7 @@ DESCRIPTION
Percent functions:
% percent
%- substract percent
%- subtract percent
%+ add percent
Batch functions:
@@ -163,14 +166,10 @@ DESCRIPTION
Conversion functions:
cm-to-inch
inch-to-cm
gallons-to-liters
liters-to-gallons
yards-to-meters
meters-to-yards
miles-to-kilometers
kilometers-to-miles
cm-to-inch yards-to-meters bytes-to-kilobytes
inch-to-cm meters-to-yards bytes-to-megabytes
gallons-to-liters miles-to-kilometers bytes-to-gigabytes
liters-to-gallons kilometers-to-miles bytes-to-terabytes
Configuration Commands:
@@ -309,6 +308,15 @@ 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.
@@ -322,13 +330,13 @@ GETTING HELP
BUGS
In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github:
<https://github.com/TLINDEN/rpnc/issues>.
<https://codeberg.org/scip/rpnc/issues>.
LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version
3.
Copyright (c) 2023 by Thomas von Dein
Copyright (c) 2023-2024 by Thomas von Dein
This software uses the following GO modules:

View File

@@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package cmd
import (
"container/list"
@@ -64,14 +64,14 @@ func (s *Stack) Bump() {
}
// append an item to the stack
func (s *Stack) Push(x float64) {
func (s *Stack) Push(item float64) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.Debug(fmt.Sprintf(" push to stack: %.2f", x))
s.Debug(fmt.Sprintf(" push to stack: %.2f", item))
s.Bump()
s.linklist.PushBack(x)
s.linklist.PushBack(item)
}
// remove and return an item from the stack
@@ -90,6 +90,7 @@ func (s *Stack) Pop() float64 {
s.Debug(fmt.Sprintf(" remove from stack: %.2f", val))
s.Bump()
return val.(float64)
}
@@ -123,32 +124,33 @@ func (s *Stack) Swap() {
return
}
a := s.linklist.Back()
s.linklist.Remove(a)
prevA := s.linklist.Back()
s.linklist.Remove(prevA)
b := s.linklist.Back()
s.linklist.Remove(b)
prevB := s.linklist.Back()
s.linklist.Remove(prevB)
s.Debug(fmt.Sprintf("swapping %.2f with %.2f", b.Value, a.Value))
s.Debug(fmt.Sprintf("swapping %.2f with %.2f", prevB.Value, prevA.Value))
s.linklist.PushBack(a.Value)
s.linklist.PushBack(b.Value)
s.linklist.PushBack(prevA.Value)
s.linklist.PushBack(prevB.Value)
}
// Return the last num items from the stack w/o modifying it.
func (s *Stack) Last(num ...int) []float64 {
items := []float64{}
i := s.Len()
stacklen := s.Len()
count := 1
if len(num) > 0 {
count = num[0]
}
for e := s.linklist.Front(); e != nil; e = e.Next() {
if i <= count {
if stacklen <= count {
items = append(items, e.Value.(float64))
}
i--
stacklen--
}
return items
@@ -168,12 +170,14 @@ func (s *Stack) All() []float64 {
// dump the stack to stdout, including backup if debug is enabled
func (s *Stack) Dump() {
fmt.Printf("Stack revision %d (%p):\n", s.rev, &s.linklist)
for e := s.linklist.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
if s.debug {
fmt.Printf("Backup stack revision %d (%p):\n", s.backuprev, &s.backup)
for e := s.backup.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
@@ -215,6 +219,7 @@ func (s *Stack) Restore() {
if s.rev == 0 {
fmt.Println("error: stack is empty.")
return
}

View File

@@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package cmd
import (
"testing"
@@ -35,16 +35,16 @@ func TestPush(t *testing.T) {
func TestPop(t *testing.T) {
t.Run("pop", func(t *testing.T) {
s := NewStack()
s.Push(5)
got := s.Pop()
stack := NewStack()
stack.Push(5)
got := stack.Pop()
if got != 5.0 {
t.Errorf("pop failed:\n+++ got: %f\n--- want: %f",
got, 5.0)
}
if s.Len() != 0 {
if stack.Len() != 0 {
t.Errorf("stack not empty after pop()")
}
})
@@ -52,25 +52,25 @@ func TestPop(t *testing.T) {
func TestPops(t *testing.T) {
t.Run("pops", func(t *testing.T) {
s := NewStack()
s.Push(5)
s.Push(5)
s.Push(5)
s.Pop()
stack := NewStack()
stack.Push(5)
stack.Push(5)
stack.Push(5)
stack.Pop()
if s.Len() != 2 {
if stack.Len() != 2 {
t.Errorf("stack len not correct after pop:\n+++ got: %d\n--- want: %d",
s.Len(), 2)
stack.Len(), 2)
}
})
}
func TestShift(t *testing.T) {
t.Run("shift", func(t *testing.T) {
s := NewStack()
s.Shift()
stack := NewStack()
stack.Shift()
if s.Len() != 0 {
if stack.Len() != 0 {
t.Errorf("stack not empty after shift()")
}
})
@@ -78,13 +78,13 @@ func TestShift(t *testing.T) {
func TestClear(t *testing.T) {
t.Run("clear", func(t *testing.T) {
s := NewStack()
s.Push(5)
s.Push(5)
s.Push(5)
s.Clear()
stack := NewStack()
stack.Push(5)
stack.Push(5)
stack.Push(5)
stack.Clear()
if s.Len() != 0 {
if stack.Len() != 0 {
t.Errorf("stack not empty after clear()")
}
})
@@ -92,9 +92,9 @@ func TestClear(t *testing.T) {
func TestLast(t *testing.T) {
t.Run("last", func(t *testing.T) {
s := NewStack()
s.Push(5)
got := s.Last()
stack := NewStack()
stack.Push(5)
got := stack.Last()
if len(got) != 1 {
t.Errorf("last failed:\n+++ got: %d elements\n--- want: %d elements",
@@ -106,7 +106,7 @@ func TestLast(t *testing.T) {
got, 5.0)
}
if s.Len() != 1 {
if stack.Len() != 1 {
t.Errorf("stack modified after last()")
}
})
@@ -114,14 +114,14 @@ func TestLast(t *testing.T) {
func TestAll(t *testing.T) {
t.Run("all", func(t *testing.T) {
s := NewStack()
stack := NewStack()
list := []float64{2, 4, 6, 8}
for _, item := range list {
s.Push(item)
stack.Push(item)
}
got := s.All()
got := stack.All()
if len(got) != len(list) {
t.Errorf("all failed:\n+++ got: %d elements\n--- want: %d elements",
@@ -135,7 +135,7 @@ func TestAll(t *testing.T) {
}
}
if s.Len() != len(list) {
if stack.Len() != len(list) {
t.Errorf("stack modified after last()")
}
})
@@ -143,37 +143,37 @@ func TestAll(t *testing.T) {
func TestBackupRestore(t *testing.T) {
t.Run("shift", func(t *testing.T) {
s := NewStack()
s.Push(5)
s.Backup()
s.Clear()
s.Restore()
stack := NewStack()
stack.Push(5)
stack.Backup()
stack.Clear()
stack.Restore()
if s.Len() != 1 {
if stack.Len() != 1 {
t.Errorf("stack not correctly restored()")
}
a := s.Pop()
if a != 5.0 {
value := stack.Pop()
if value != 5.0 {
t.Errorf("stack not identical to old revision:\n+++ got: %f\n--- want: %f",
a, 5.0)
value, 5.0)
}
})
}
func TestReverse(t *testing.T) {
t.Run("reverse", func(t *testing.T) {
s := NewStack()
stack := NewStack()
list := []float64{2, 4, 6}
reverse := []float64{6, 4, 2}
for _, item := range list {
s.Push(item)
stack.Push(item)
}
s.Reverse()
stack.Reverse()
got := s.All()
got := stack.All()
if len(got) != len(list) {
t.Errorf("all failed:\n+++ got: %d elements\n--- want: %d elements",

View File

@@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package cmd
import (
"fmt"
@@ -23,13 +23,23 @@ import (
"strings"
)
// find an item in a list
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
// find an item in a list, generic variant
func contains[E comparable](s []E, v E) bool {
for _, vs := range s {
if v == vs {
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
}
@@ -63,3 +73,7 @@ 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)
}

View File

@@ -15,7 +15,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
package cmd
import (
"testing"

539
funcs.go
View File

@@ -1,539 +0,0 @@
/*
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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"errors"
"math"
)
type R struct {
Res float64
Err error
}
type Numbers []float64
type Function func(Numbers) R
// every function we are able to call must be of type Funcall, which
// needs to specify how many numbers it expects and the actual go
// function to be executed.
//
// The function has to take a float slice as argument and return a
// float and an error object. The float slice is guaranteed to have
// the expected number of arguments.
//
// However, Lua functions are handled differently, see interpreter.go.
type Funcall struct {
Expectargs int // -1 means batch only mode, you'll get the whole stack as arg
Func Function
}
// will hold all hard coded functions and operators
type Funcalls map[string]*Funcall
// convenience function, create a new Funcall object, if expectargs
// was not specified, 2 is assumed.
func NewFuncall(function Function, expectargs ...int) *Funcall {
expect := 2
if len(expectargs) > 0 {
expect = expectargs[0]
}
return &Funcall{
Expectargs: expect,
Func: function,
}
}
// Convenience function, create new result
func NewR(n float64, e error) R {
return R{Res: n, Err: e}
}
// the actual functions, called once during initialization.
func DefineFunctions() Funcalls {
f := map[string]*Funcall{
// simple operators, they all expect 2 args
"+": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]+arg[1], nil)
},
),
"-": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]-arg[1], nil)
},
),
"x": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]*arg[1], nil)
},
),
"/": NewFuncall(
func(arg Numbers) R {
if arg[1] == 0 {
return NewR(0, errors.New("division by null"))
}
return NewR(arg[0]/arg[1], nil)
},
),
"^": NewFuncall(
func(arg Numbers) R {
return NewR(math.Pow(arg[0], arg[1]), nil)
},
),
"%": NewFuncall(
func(arg Numbers) R {
return NewR((arg[0]/100)*arg[1], nil)
},
),
"%-": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]-((arg[0]/100)*arg[1]), nil)
},
),
"%+": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]+((arg[0]/100)*arg[1]), nil)
},
),
"mod": NewFuncall(
func(arg Numbers) R {
return NewR(math.Remainder(arg[0], arg[1]), nil)
},
),
"sqrt": NewFuncall(
func(arg Numbers) R {
return NewR(math.Sqrt(arg[0]), nil)
},
1),
"abs": NewFuncall(
func(arg Numbers) R {
return NewR(math.Abs(arg[0]), nil)
},
1),
"acos": NewFuncall(
func(arg Numbers) R {
return NewR(math.Acos(arg[0]), nil)
},
1),
"acosh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Acosh(arg[0]), nil)
},
1),
"asin": NewFuncall(
func(arg Numbers) R {
return NewR(math.Asin(arg[0]), nil)
},
1),
"asinh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Asinh(arg[0]), nil)
},
1),
"atan": NewFuncall(
func(arg Numbers) R {
return NewR(math.Atan(arg[0]), nil)
},
1),
"atan2": NewFuncall(
func(arg Numbers) R {
return NewR(math.Atan2(arg[0], arg[1]), nil)
},
2),
"atanh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Atanh(arg[0]), nil)
},
1),
"cbrt": NewFuncall(
func(arg Numbers) R {
return NewR(math.Cbrt(arg[0]), nil)
},
1),
"ceil": NewFuncall(
func(arg Numbers) R {
return NewR(math.Ceil(arg[0]), nil)
},
1),
"cos": NewFuncall(
func(arg Numbers) R {
return NewR(math.Cos(arg[0]), nil)
},
1),
"cosh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Cosh(arg[0]), nil)
},
1),
"erf": NewFuncall(
func(arg Numbers) R {
return NewR(math.Erf(arg[0]), nil)
},
1),
"erfc": NewFuncall(
func(arg Numbers) R {
return NewR(math.Erfc(arg[0]), nil)
},
1),
"erfcinv": NewFuncall(
func(arg Numbers) R {
return NewR(math.Erfcinv(arg[0]), nil)
},
1),
"erfinv": NewFuncall(
func(arg Numbers) R {
return NewR(math.Erfinv(arg[0]), nil)
},
1),
"exp": NewFuncall(
func(arg Numbers) R {
return NewR(math.Exp(arg[0]), nil)
},
1),
"exp2": NewFuncall(
func(arg Numbers) R {
return NewR(math.Exp2(arg[0]), nil)
},
1),
"expm1": NewFuncall(
func(arg Numbers) R {
return NewR(math.Expm1(arg[0]), nil)
},
1),
"floor": NewFuncall(
func(arg Numbers) R {
return NewR(math.Floor(arg[0]), nil)
},
1),
"gamma": NewFuncall(
func(arg Numbers) R {
return NewR(math.Gamma(arg[0]), nil)
},
1),
"ilogb": NewFuncall(
func(arg Numbers) R {
return NewR(float64(math.Ilogb(arg[0])), nil)
},
1),
"j0": NewFuncall(
func(arg Numbers) R {
return NewR(math.J0(arg[0]), nil)
},
1),
"j1": NewFuncall(
func(arg Numbers) R {
return NewR(math.J1(arg[0]), nil)
},
1),
"log": NewFuncall(
func(arg Numbers) R {
return NewR(math.Log(arg[0]), nil)
},
1),
"log10": NewFuncall(
func(arg Numbers) R {
return NewR(math.Log10(arg[0]), nil)
},
1),
"log1p": NewFuncall(
func(arg Numbers) R {
return NewR(math.Log1p(arg[0]), nil)
},
1),
"log2": NewFuncall(
func(arg Numbers) R {
return NewR(math.Log2(arg[0]), nil)
},
1),
"logb": NewFuncall(
func(arg Numbers) R {
return NewR(math.Logb(arg[0]), nil)
},
1),
"pow": NewFuncall(
func(arg Numbers) R {
return NewR(math.Pow(arg[0], arg[1]), nil)
},
2),
"round": NewFuncall(
func(arg Numbers) R {
return NewR(math.Round(arg[0]), nil)
},
1),
"roundtoeven": NewFuncall(
func(arg Numbers) R {
return NewR(math.RoundToEven(arg[0]), nil)
},
1),
"sin": NewFuncall(
func(arg Numbers) R {
return NewR(math.Sin(arg[0]), nil)
},
1),
"sinh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Sinh(arg[0]), nil)
},
1),
"tan": NewFuncall(
func(arg Numbers) R {
return NewR(math.Tan(arg[0]), nil)
},
1),
"tanh": NewFuncall(
func(arg Numbers) R {
return NewR(math.Tanh(arg[0]), nil)
},
1),
"trunc": NewFuncall(
func(arg Numbers) R {
return NewR(math.Trunc(arg[0]), nil)
},
1),
"y0": NewFuncall(
func(arg Numbers) R {
return NewR(math.Y0(arg[0]), nil)
},
1),
"y1": NewFuncall(
func(arg Numbers) R {
return NewR(math.Y1(arg[0]), nil)
},
1),
"copysign": NewFuncall(
func(arg Numbers) R {
return NewR(math.Copysign(arg[0], arg[1]), nil)
},
2),
"dim": NewFuncall(
func(arg Numbers) R {
return NewR(math.Dim(arg[0], arg[1]), nil)
},
2),
"hypot": NewFuncall(
func(arg Numbers) R {
return NewR(math.Hypot(arg[0], arg[1]), nil)
},
2),
// converters of all kinds
"cm-to-inch": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]/2.54, nil)
},
1),
"inch-to-cm": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]*2.54, nil)
},
1),
"gallons-to-liters": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]*3.785, nil)
},
1),
"liters-to-gallons": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]/3.785, nil)
},
1),
"yards-to-meters": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]*91.44, nil)
},
1),
"meters-to-yards": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]/91.44, nil)
},
1),
"miles-to-kilometers": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]*1.609, nil)
},
1),
"kilometers-to-miles": NewFuncall(
func(arg Numbers) R {
return NewR(arg[0]/1.609, nil)
},
1),
"or": NewFuncall(
func(arg Numbers) R {
return NewR(float64(int(arg[0])|int(arg[1])), nil)
},
2),
"and": NewFuncall(
func(arg Numbers) R {
return NewR(float64(int(arg[0])&int(arg[1])), nil)
},
2),
"xor": NewFuncall(
func(arg Numbers) R {
return NewR(float64(int(arg[0])^int(arg[1])), nil)
},
2),
"<": NewFuncall(
func(arg Numbers) R {
return NewR(float64(int(arg[0])<<int(arg[1])), nil)
},
2),
">": NewFuncall(
func(arg Numbers) R {
return NewR(float64(int(arg[0])>>int(arg[1])), nil)
},
2),
}
// aliases
f["*"] = f["x"]
f["remainder"] = f["mod"]
return f
}
func DefineBatchFunctions() Funcalls {
f := map[string]*Funcall{
"median": NewFuncall(
func(args Numbers) R {
middle := len(args) / 2
return NewR(args[middle], nil)
},
-1),
"mean": NewFuncall(
func(args Numbers) R {
var sum float64
for _, item := range args {
sum += item
}
return NewR(sum/float64(len(args)), nil)
},
-1),
"min": NewFuncall(
func(args Numbers) R {
var min float64
min, args = args[0], args[1:]
for _, item := range args {
if item < min {
min = item
}
}
return NewR(min, nil)
},
-1),
"max": NewFuncall(
func(args Numbers) R {
var max float64
max, args = args[0], args[1:]
for _, item := range args {
if item > max {
max = item
}
}
return NewR(max, nil)
},
-1),
"sum": NewFuncall(
func(args Numbers) R {
var sum float64
for _, item := range args {
sum += item
}
return NewR(sum, nil)
},
-1),
}
// aliases
f["+"] = f["sum"]
f["avg"] = f["mean"]
return f
}

34
go.mod
View File

@@ -1,10 +1,34 @@
module rpn
go 1.20
go 1.24.5
require (
github.com/chzyer/readline v1.5.1 // 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-20220310020820-b874c991c1a5 // indirect
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/chzyer/readline v1.5.1
github.com/rogpeppe/go-internal v1.14.1
github.com/spf13/pflag v1.0.10
github.com/yuin/gopher-lua v1.1.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.3.8 // indirect
golang.org/x/tools v0.26.0 // indirect
)

60
go.sum
View File

@@ -1,10 +1,60 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
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 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
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=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=

156
main.go
View File

@@ -1,5 +1,5 @@
/*
Copyright © 2023 Thomas von Dein
Copyright © 2023-2024 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
@@ -18,160 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/chzyer/readline"
flag "github.com/spf13/pflag"
lua "github.com/yuin/gopher-lua"
"rpn/cmd"
)
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
-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 T.v.Dein`
func main() {
calc := NewCalc()
showversion := false
showhelp := false
showmanual := false
enabledebug := false
configfile := ""
flag.BoolVarP(&calc.batch, "batchmode", "b", false, "batch mode")
flag.BoolVarP(&calc.showstack, "show-stack", "s", false, "show stack")
flag.BoolVarP(&calc.intermediate, "showin-termediate", "i", false,
"show intermediate results")
flag.BoolVarP(&enabledebug, "debug", "d", false, "debug mode")
flag.BoolVarP(&showversion, "version", "v", false, "show version")
flag.BoolVarP(&showhelp, "help", "h", false, "show usage")
flag.BoolVarP(&showmanual, "manual", "m", false, "show manual")
flag.StringVarP(&configfile, "config", "c",
os.Getenv("HOME")+"/.rpn.lua", "config file (lua format)")
flag.Parse()
if showversion {
fmt.Printf("This is rpn version %s\n", VERSION)
return
}
if showhelp {
fmt.Println(Usage)
return
}
if enabledebug {
calc.ToggleDebug()
}
if showmanual {
man()
os.Exit(0)
}
// the lua state object is global, instanciate it early
L = lua.NewState(lua.Options{SkipOpenLibs: true})
defer L.Close()
// our config file is interpreted as lua code, only functions can
// be defined, init() will be called by InitLua().
if _, err := os.Stat(configfile); err == nil {
luarunner := NewInterpreter(configfile, enabledebug)
luarunner.InitLua()
calc.SetInt(luarunner)
}
if len(flag.Args()) > 1 {
// commandline calc operation, no readline etc needed
// called like rpn 2 2 +
calc.stdin = true
calc.Eval(strings.Join(flag.Args(), " "))
return
}
// interactive mode, need readline
rl, err := readline.NewEx(&readline.Config{
Prompt: calc.Prompt(),
HistoryFile: os.Getenv("HOME") + "/.rpn-history",
HistoryLimit: 500,
AutoComplete: calc.completer,
InterruptPrompt: "^C",
EOFPrompt: "exit",
HistorySearchFold: true,
})
if err != nil {
panic(err)
}
defer rl.Close()
rl.CaptureExitSignal()
if inputIsStdin() {
// commands are coming on stdin, however we will still enter
// the same loop since readline just reads fine from stdin
calc.ToggleStdin()
}
for {
// primary program repl
line, err := rl.Readline()
if err != nil {
break
}
calc.Eval(line)
rl.SetPrompt(calc.Prompt())
}
if len(flag.Args()) > 0 {
// called like this:
// echo 1 2 3 4 | rpn +
// batch mode enabled automatically
calc.batch = true
calc.Eval(flag.Args()[0])
}
}
func inputIsStdin() bool {
stat, _ := os.Stdin.Stat()
return (stat.Mode() & os.ModeCharDevice) == 0
}
func man() {
man := exec.Command("less", "-")
var b bytes.Buffer
b.Write([]byte(manpage))
man.Stdout = os.Stdout
man.Stdin = &b
man.Stderr = os.Stderr
err := man.Run()
if err != nil {
log.Fatal(err)
}
os.Exit(cmd.Main())
}

19
main_test.go Normal file
View File

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

View File

@@ -1,65 +0,0 @@
#!/bin/bash
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# get list with: go tool dist list
DIST="darwin/amd64
freebsd/amd64
linux/amd64
netbsd/amd64
openbsd/amd64
windows/amd64"
tool="$1"
version="$2"
if test -z "$version"; then
echo "Usage: $0 <tool name> <release version>"
exit 1
fi
rm -rf releases
mkdir -p releases
for D in $DIST; do
os=${D/\/*/}
arch=${D/*\//}
binfile="releases/${tool}-${os}-${arch}-${version}"
tardir="${tool}-${os}-${arch}-${version}"
tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz"
set -x
GOOS=${os} GOARCH=${arch} go build -tags osusergo,netgo -ldflags "-extldflags=-static" -o ${binfile}
mkdir -p ${tardir}
cp ${binfile} README.md LICENSE ${tardir}/
echo 'tool = rpn
PREFIX = /usr/local
UID = root
GID = 0
install:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/' > ${tardir}/Makefile
tar cpzf ${tarfile} ${tardir}
sha256sum ${binfile} | cut -d' ' -f1 > ${binfile}.sha256
sha256sum ${tarfile} | cut -d' ' -f1 > ${tarfile}.sha256
rm -rf ${tardir}
set +x
done

35
rpn.pod
View File

@@ -12,6 +12,8 @@ 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
@@ -110,7 +112,8 @@ If the first parameter to rpn is a math operator or function, batch
mode is enabled automatically, see last example.
You can enter integers, floating point numbers (positive or negative)
or hex numbers (prefixed with 0x).
or hex numbers (prefixed with 0x). Time values in hh::mm format are
possible as well.
=head2 STACK MANIPULATION
@@ -134,7 +137,7 @@ stack.
Basic operators:
+ add
- substract
- subtract
/ divide
x multiply (alias: *)
^ power
@@ -150,7 +153,7 @@ Bitwise operators:
Percent functions:
% percent
%- substract percent
%- subtract percent
%+ add percent
Batch functions:
@@ -170,14 +173,10 @@ Math functions:
Conversion functions:
cm-to-inch
inch-to-cm
gallons-to-liters
liters-to-gallons
yards-to-meters
meters-to-yards
miles-to-kilometers
kilometers-to-miles
cm-to-inch yards-to-meters bytes-to-kilobytes
inch-to-cm meters-to-yards bytes-to-megabytes
gallons-to-liters miles-to-kilometers bytes-to-gigabytes
liters-to-gallons kilometers-to-miles bytes-to-terabytes
Configuration Commands:
@@ -342,6 +341,16 @@ 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
@@ -358,13 +367,13 @@ tarball, there will also be a manual page you can read using C<man rpn>.
In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github:
L<https://github.com/TLINDEN/rpnc/issues>.
L<https://codeberg.org/scip/rpnc/issues>.
=head1 LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.
Copyright (c) 2023 by Thomas von Dein
Copyright (c) 2023-2024 by Thomas von Dein
This software uses the following GO modules:

BIN
rpnc.mp4 Normal file

Binary file not shown.

2
t/cmdline-command.txtar Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

16
t/cmdlinecalc-lua.txtar Normal file
View File

@@ -0,0 +1,16 @@
exec rpn -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

2
t/cmdlinecalc-time.txtar Normal file
View File

@@ -0,0 +1,2 @@
exec rpn 09:55 4:15 -
stdout '5.67\n'

2
t/cmdlinecalc.txtar Normal file
View File

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

2
t/getusage.txtar Normal file
View File

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

2
t/getversion.txtar Normal file
View File

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

4
t/stdin-batch-cmd.txtar Normal file
View File

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

4
t/stdin-batch.txtar Normal file
View File

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

4
t/stdin-calc.txtar Normal file
View File

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

13
t/stdin-use-vars.txtar Normal file
View File

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

View File

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

13
t/test.lua Normal file
View File

@@ -0,0 +1,13 @@
-- 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