Skip to content

Instantly share code, notes, and snippets.

@Max-E
Last active April 6, 2020 00:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Max-E/5298687 to your computer and use it in GitHub Desktop.
Save Max-E/5298687 to your computer and use it in GitHub Desktop.
concalc_wrap.sh - Turns Rainer Strobel's batch-mode concalc(1) utility into a useful interactive scientific and programming calculator.
#! /usr/bin/env bash
# DESCRIPTION
# concalc_wrap.sh - Turns Rainer Strobel's batch-mode concalc(1) utility into
# a useful interactive scientific and programming calculator.
# The script is also its own documentation.
# LICENSE
# Copyright (c) 2013 Max Eliaser
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and/or associated documentation files (the
# "Materials"), to deal in the Materials without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Materials, and to
# permit persons to whom the Materials are furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Materials.
#
# THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
# INTRODUCTION
# I've been a loyal gcalctool user for many years now, even to the point of
# manually installing obsolete versions in order to keep the scientific mode.
# But recently I came to the conclusion that having a buttons for each number
# is completely stupid when I'm going to use the keyboard anyway, and that
# there's no reason I shouldn't get a multi-line display and command history.
# I set out looking for another calculator app that would allow me to use
# pretty much the same keystrokes and features I'd gotten used to from
# gcalctool, while taking up much less screen space and showing multiple lines
# and command history. bc was close, but wouldn't automatically remember
# previous answers and had some other minor differences I didn't want to get
# used to. I briefly considered forking gcalctool before I came to my senses.
# Finally, I found concalc, which was *almost* what I needed. And all its
# shortcomings were easily addressed with a little bit of shell-script
# plumbing.
# FEATURES
# * Permanently stored command line history and command line editing, provided
# by rlwrap(1)
# * Permanent storage for concalc's settings (mode, base, angle, complex,
# etc.)
# * Commands to set those settings
# * Automatically prepends "ans" if the first operand of a binary operator is
# missing (NOTE: use parentheses or a space at the beginning to input a
# literal -3 instead of ans-3)
# * Automatically juggles concalc's variables file so that multiple instances
# of this script may be run at once without interfering with each other; the
# last to exit will have its command history, variables, and settings stored
# permanently
# * Additional commands for features not provided by Concalc itself (currently
# just "factor.")
# * Commands for running a single line in a different number system (for
# example, "hexrun F+F" will always display 1E as a result, regardless of
# current mode.)
# * Commands for inputting a single line in a different number system (for
# example, "hexin A+3" will display 13 as a result when in decimal mode.)
# * Commands for displaying the result of a single line in a different number
# system (for example, "hexout 5+5" will display A when in decimal mode.)
# * User-friendly customization system (by editing this script)
# OTHER NOTES
# I recommend you create an XDG launcher (i.e. a .desktop file) for this
# script using whatever utility your desktop environment provides, and have it
# launch in a graphical terminal emulator window. No need to invoke it
# manually in a shell.
# DEPENDENCIES
# * GNU BASH
# * rlwrap(1)
# * concalc(1)
# * factor(1)
# * fold(1)
# * seq(1)
# * terminal emulator
# CHANGELOG
# * 1.00 - 04/02/13, 9:35 PM GMT-8 - Initial version
# * 1.01 - 04/02/13, 9:45 PM GMT-8 - Fix some comments
# * 1.02 - 04/06/13, 1:54 PM GMT-8 - Add a "factor" command
# * 1.03 - 04/06/13, 3:18 PM GMT-8 - Added "showargs" command to show current
# mode.
# - Unbreak angle setting.
# * 1.04 - 04/06/13, 3:35 PM GMT-8 - Have this script invoke itself, one
# process for rlwrap and one for
# everything else. I think that'll fix all
# these timing issues.
# - Retroactively rewrite the changelog to
# get rid of all the failed past fixes,
# since none of them are in the code now.
# * 1.05 - 04/07/13, 1:16 PM GMT-8 - Add binrun octrun, decrun, and hexrun.
# - Add binin, octin, decin, and hexin.
# - Add binout, octout, decout, and hexout.
# - Validate user input
# - Cleanup code and comments.
# CUSTOMIZABLE CONSTANTS
# Window size in characters
CALC_COLUMNS=30
CALC_LINES=10
# These two lines might mess things up if not run in a graphical terminal
# emulator. You should comment them out entirely if you intend to run them in
# a non-resizable terminal (such as a text-mode VT.)
resize -s $CALC_LINES $CALC_COLUMNS 2>/dev/null 1>/dev/null # Set window size
echo -n $'\e'"]0;calculator"$'\a' # Set tindow title
# Text output gets piped through this
TEXT_FORMAT_CMD="fold -s -w $CALC_COLUMNS"
# Files used by this script
histfile=~/.concalc_wrap_history # Command history
cfgfile=~/.concalc_wrap_config # Configuration
# If the user inputs a command that starts with one of these characters, add
# ans before it
BINARY_OPERATORS='-+*/^%&|'
# This sed script is applied to every command before it is passed to concalc.
# It substitutes "ans" as the first argument if there is a binary operator
# with no first argument specified
SEDSCRIPT='s/^['$BINARY_OPERATORS']/ans&/'
# END customizable constants
#Settings -- they correspond to concalc's command line options
OUTPUT="18"
OUTPUT_VALID_VALS="`seq 1 18`"
MODE="std"
MODE_VALID_VALS="std base"
BASE="dec"
BASE_VALID_VALS="bin oct dec hex"
ANGLE="rad"
ANGLE_VALID_VALS="deg rad gra"
COMPLEX="off"
COMPLEX_VALID_VALS="off on"
# This is where concalc keeps its variables. Don't change this.
varfile=~/.concalcvariables
# This is the line number in $varfile where concalc keeps the "ans" variable
CONCALC_ANS_LINE=53
# This script invokes itself, creating two processes, which is how rlwrap
# likes to work. If you pipe rlwrap's output through another filter, that
# seems to confuse it and lines come out in the wrong order. Also, give rlwrap
# a bit longer than default before it tries to output a prompt.
if [ "x$1" = "x" ] ; then
exec rlwrap -w 100 rlwrap -S "> " -H $histfile $0 innerloop
fi
# "INNER" PROCESS
# Create temporary files
tmpvarfile=`mktemp`
tmpcfgfile=`mktemp`
# Would be cleaner to write the cfg file directly in the trap without using a
# temporary file, but that doesn't seem to work. Despite every reference I've
# found suggesting that as long as I avoid quotes, the trap command should
# pick up on changed global variables, it doesn't actually seem to do so.
ARGS=""
function gen_concalc_args {
# write to the temporary config file
echo output $OUTPUT > $tmpcfgfile
echo mode $MODE >> $tmpcfgfile
echo base $BASE >> $tmpcfgfile
echo angle $ANGLE >> $tmpcfgfile
echo complex $COMPLEX >> $tmpcfgfile
# build concalc's arguments
ARGS="-m $MODE -o $OUTPUT -a $ANGLE"
if [ "$MODE" = "base" ] ; then
ARGS=$ARGS" -b $BASE"
fi
if [ "$complex" = "on" ] ; then
ARGS=$ARGS" -c"
fi
}
# Run a command temporarily in another number system
function tmp_base_cmd {
old_mode=$MODE
old_base=$BASE
MODE=base
BASE=$1
gen_concalc_args
eval "$2"
BASE=$old_base
MODE=$old_mode
gen_concalc_args
}
# For validating whether a setting can be set to a certain value.
function validate_setting {
new_value=$1
valid_values="$2"
found_value=`echo $valid_values | grep -w $new_value`
if [ "$found_value" ] ; then
return 0
fi
return 1
}
# For controlling one of concalc's settings.
function set_concalc_setting {
setting_name=$1
setting_var_name=`echo $1 | tr [a-z] [A-Z]`
setting_cur_val=`eval "echo $"$setting_var_name`
setting_valid_vals="`eval "echo $"$setting_var_name"_VALID_VALS"`"
if [ "x$2" = "x" ] ; then
echo "Current $setting_name is $setting_cur_val" | $TEXT_FORMAT_CMD
elif validate_setting $2 "$setting_valid_vals" ; then
eval "$setting_var_name=$2"
gen_concalc_args
else
valid_vals_fmt="`echo $setting_valid_vals | sed "s/ /, /g"`"
echo "Invalid $setting_name $2! Valid values are: $valid_vals_fmt." |
$TEXT_FORMAT_CMD
echo "Current $setting_name is $setting_cur_val" | $TEXT_FORMAT_CMD
fi
}
# In addition to running concalc commands, this adds a few extra commands to
# control settings.
function run_command {
case "x$1" in
xoutput | xmode | xbase | xangle | xcomplex)
set_concalc_setting $@
;;
xshowargs)
echo $ARGS
;;
xbinrun | xoctrun | xdecrun | xhexrun )
# Run the specified command temporarily in another number system
tmp_base=`echo $1 | sed "s/run$//"`
shift
tmp_base_cmd $tmp_base "run_command $@"
;;
xbinin | xoctin | xdecin | xhexin )
# Run a command temporarily in another number system, but display
# its result in the currently selected number system
tmp_base=`echo $1 | sed "s/in$//"`
shift
tmp_base_cmd $tmp_base "run_command $@ 2>/dev/null >/dev/null"
run_command
;;
xbinout | xoctout | xdecout | xhexout )
# Run a command in the currently selected number system, but
# display its result another number system
tmp_base=`echo $1 | sed "s/out$//"`
shift
run_command $@ 2>/dev/null >/dev/null
tmp_base_cmd $tmp_base "run_command"
;;
xfactor)
# not provided by concalc but still handy
if [ "$2" = "ans" ] ; then
factor `sed -n "$CONCALC_ANS_LINE"p $varfile`
elif [ "x$2" = "x" ] ; then
factor `sed -n "$CONCALC_ANS_LINE"p $varfile`
else
factor $2
fi
;;
x)
concalc $ARGS ans
;;
*)
concalc $ARGS "$@"
;;
esac
}
# runs all commands piped to it
function loop {
while read -r line; do
cp $tmpvarfile $varfile #in case another instance is running
run_command $line
cp $varfile $tmpvarfile #save any changes that might have occurred
done
}
# saves the variable file and config file; the history file is saved already
# by rlwrap
function cleanup {
mv $tmpvarfile $varfile
mv $tmpcfgfile $cfgfile
}
trap cleanup EXIT INT TERM
touch $varfile #in case concalc has never been run before
cp $varfile $tmpvarfile #save copy of concalc's variable file
touch $cfgfile #in case this script has never been run before
loop < $cfgfile # read commands from the config file
gen_concalc_args # generate initial arguments string for concalc
sed -u "$SEDSCRIPT" | loop # read commands from stdin
@ailaire
Copy link

ailaire commented Apr 29, 2019

Hello,
The line 171 seems to pose problem. What is innerloopintended for ?

@Max-E
Copy link
Author

Max-E commented Apr 6, 2020

Sorry for the late reply, I didn't log into github for a while.

innerloop is for the purpose of working around a limitation of rlwrap.

rlwrap likes to invoke whatever program is being wrapped, and has no ability to wrap a program that is already running. It would have been possible to write two scripts, a main script with all the functionality, and a second script to invoke rlwrap with the main script. But in order to keep this all in one file, I instead had this script start rlwrap and have rlwrap start the script again. In BASH, $0 is a way for a script to refer to itself, so rlwrap $0 means "have rlwrap run the current script."

I used the first argument $1 to distinguish between when concalc_wrap.sh is first started by the end-user, and when it is started again by rlwrap. if [ "x$1" = "x" ] means "if I was run with no arguments" and $0 innerloop means "run myself with the argument innerloop".

If the concalc_wrap.sh is failing on line 171, there could be a few things causing that:

  • rlwrap is not installed on your system
  • rlwrap is unable to run concalc_wrap.sh for whatever reason, for instance because the execute permissions are not enabled on it
  • concalc_wrap.sh is failing, but only when run with an argument. You can force it to run in inner-loop mode without rlwrap by manually running concalc_wrap.sh innerloop from the command line, and this may help you do some further troubleshooting on what might be going wrong.

Hope that helps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment