Create a gist now

Instantly share code, notes, and snippets.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment