· 5 years ago · Apr 13, 2020, 09:26 PM
1#!/bin/bash
2# bcalc.sh -- simple bash bc wrapper
3# v0.6.9 feb/2020
4
5#defaults
6
7#use of record file (enabled=1/disabled=0 or unset)
8BCREC=1
9
10#special variable that holds last entry in the record file
11HOLD=res #ans
12
13#record file path
14RECFILE="${HOME}/.bcalc_record"
15
16#extension file path
17EXTFILE="${HOME}/.bcalc_extensions"
18
19#don't change these
20
21#length of result line (newer bash accepts '0' to disable multiline)
22export BC_LINE_LENGTH=1000
23
24#make sure numeric locale is set correctly
25export LC_NUMERIC=en_US.UTF-8
26
27#man page
28HELP_LINES="NAME
29 bcalc.sh -- simple bash bc wrapper
30
31
32SYNOPSIS
33 bcalc.sh [-cf] [-s'NUM'|-NUM] 'EXPRESSION'
34
35 bcalc.sh -t [-s'NUM'|-NUM] [-f] 'EXPRESSION'
36
37 bcalc.sh -n 'SHORT NOTE'
38
39 bcalc.sh [-cchrv]
40
41
42DESCRIPTION
43 Bcalc.sh uses the powerful Bash Calculator (Bc) with its math library
44 and adds some useful features described below.
45
46 A record file (a history) is created at '${RECFILE}'
47 To disable using a record file set option '-f' or unset BCREC in the
48 script head code, section 'defaults'. If a record file is available, the
49 special variable '${HOLD}' can be used in EXPRESSION and holds the last
50 entry in the record file. This special variable can be user-defined in
51 the script head source code, section defaults.
52
53 If no EXPRESSION is given, wait for for Stdin (from pipe) or user input.
54 Press 'Ctr+D' to send the EOF signal, as in Bc. If no user EXPRESSION
55 was given so far, prints last entry in the record file.
56
57 EXPRESSIONS containing specials chars interpreted by your shell may need
58 escaping.
59
60 The number of decimal plates (scale) of output floating point numbers is
61 dependent on user input for all operations except division, as in pure
62 Bc. Mathlib scale defaults to 20 plus one uncertainty digit and the sci-
63 entific extensions defaults to 100. If the scale option '-s' is not set
64 explicitly, trailing zeroes will be trimmed by a special Bc function be-
65 fore being printed to screen.
66
67 Remember that the decimal separator must be a dot '.'. Results with a
68 thousands separator can be obtained with option '-t', in which case a
69 comma ',' is used as thousands delimiter.
70
71
72BC MATH LIBRARY
73 Bcalc.sh uses Bc with the math library. Useful functions from Bc man:
74
75 The math library defines the following functions:
76
77 s (x) The sine of x, x is in radians.
78
79 c (x) The cosine of x, x is in radians.
80
81 a (x) The arctangent of x, arctangent returns radians.
82
83 l (x) The natural logarithm of x.
84
85 e (x) The exponential function of raising e to the value x.
86
87 j (n,x)
88 The Bessel function of integer order n of x.
89
90
91SCIENTIFIC EXTENSION
92
93 The scientific option will try to download a copy of a table of scien-
94 tific constants and extra math functions such as 'ln' and 'log'. Once
95 downloaded, it will be kept for future use. Download of extensions re-
96 quires Wget or cURL and will be placed at '${EXTFILE}'
97
98 Extensions from:
99
100 <http://x-bc.sourceforge.net/scientific_constants.bc>
101
102 <http://x-bc.sourceforge.net/extensions.bc>
103
104
105BASH ALIASES
106 Consider creating a bash alias. Add to your ~/.bashrc:
107
108 alias c='/path/to/bcalc.sh'
109
110
111 There are two interesting functions for using pure Bc interactively:
112
113 c() { echo \"\${*}\" | bc -l;}
114
115 alias c=\"bc -l <<<'\"
116
117
118 In the latter, user must type a \"'\" sign after the expression to end
119 quoting.
120
121
122WARRANTY
123 Made and tested with Bash 5. This programme is distributed without
124 support or bug corrections. Licensed under GPLv3 and above.
125
126 If useful, consider giving me a nickle! =)
127
128 bc1qlxm5dfjl58whg6tvtszg5pfna9mn2cr2nulnjr
129
130
131BUGS
132 When option '-t' is used, decimal precision will become limited and de-
133 teriorated if the resulting number is longer than 20 digits total. That
134 is printf limitation as it uses bc with the mathlib internally.
135
136 Multiline input will skip format settings defined by script options.
137
138 Bash Bc can only use point as decimal separator, independently of user
139 locale.
140
141
142USAGE EXAMPLES
143 Below are shown some ways to escaping specials chars to avoid their being
144 interpreted by the shell and also some examples of this script options.
145 Chars '-' and '()' need escaping when they start the expression, while '*'
146 needs escaping every time.
147
148 (I) Escaping
149 $ bcalc.sh '(-20-20)/2'
150
151 $ bcalc.sh \\(20+20\\)/2
152
153 $ bcalc.sh -- -3+30
154
155 $ bcalc.sh 'a=4;dog=1;cat=dog; a/(cat+dog)'
156
157 Z-shell users need extra escaping
158 % bcalc.sh '10*10*10'
159
160 % bcalc.sh 10\\*10\\*10
161
162 % bcalc.sh '2^2+(30)'
163
164
165 (II) Setting scale and thousands separator
166 $ bcalc.sh -s2 1/3
167
168 $ bcalc.sh -2 1/3
169
170 $ bcalc.sh 'scale=2;1/3'
171
172 $ echo '0.333333333' | bcalc.sh -2
173 result: 0.33
174
175 $ bcalc.sh -t 100000000
176 result: 100,000,000.00
177
178
179 (III) Scientific extensions
180 $ bcalc.sh -c 'ln(0.3)' #natural log function
181
182 $ bcalc.sh -c 0.234*na #'na' is Avogadro's constant
183
184
185 (IV) Adding notes
186 $ bcalc.sh -n This is my note.
187
188 $ bcalc.sh -n '<This; is my w||*ird not& & >'
189
190
191OPTIONS
192 -NUM Shortcut for scale setting, same as '-sNUM'.
193
194 -c Use scientific extensions; pass twice to print extensions.
195
196 -f Do not use a record file.
197
198 -h Show this help.
199
200 -n Add note to last entry in the record file.
201
202 -r Print record file.
203
204 -s Set scale (decimal plates).
205
206 -t Thousands separator.
207
208 -v Print this script version."
209
210
211#functions
212
213#-n add note function
214notef() {
215 if [[ -n "${*}" ]]; then
216 sed -i "$ i\>> ${*}" "${RECFILE}"
217 exit 0
218 else
219 printf 'Note is empty.\n' 1>&2
220 exit 1
221 fi
222}
223#https://superuser.com/questions/781558/sed-insert-file-before-last-line
224#http://www.yourownlinux.com/2015/04/sed-command-in-linux-append-and-insert-lines-to-file.html
225
226#-c scientific extension function
227setcf() {
228 #test if extensions file exists, if not download it from the internet
229 if ! [[ -f "${EXTFILE}" ]]; then
230 #test for cURL or Wget
231 if command -v curl &>/dev/null; then
232 YOURAPP='curl -L'
233 elif command -v wget &>/dev/null; then
234 YOURAPP='wget -O-'
235 else
236 printf 'cURL or Wget is required.\n' 1>&2
237 exit 1
238 fi
239
240 #download extensions
241 {
242 ${YOURAPP} 'http://x-bc.sourceforge.net/scientific_constants.bc'
243 printf '\n'
244 ${YOURAPP} 'http://x-bc.sourceforge.net/extensions.bc'
245 printf '\n'
246 } > "${EXTFILE}"
247 fi
248
249 #print extension file?
250 if [[ "${CIENTIFIC}" -eq 2 ]]; then
251 cat "${EXTFILE}"
252 exit
253 fi
254
255 #set extensions for use with Bc
256 #scientific extensions defaults scale=100
257 EXT="$(<"${EXTFILE}")"
258}
259
260
261#parse options
262while getopts ':0123456789cfhnrs:tv' opt; do
263 case ${opt} in
264 ( [0-9] ) #scale, same as '-sNUM'
265 SCL="${SCL}${opt}"
266 ;;
267 ( c ) #load cientific extensions
268 #twice to print cientific extensions
269 [[ -z "${CIENTIFIC}" ]] && CIENTIFIC=1 || CIENTIFIC=2
270 ;;
271 ( f ) #no record file
272 unset BCREC
273 ;;
274 ( h ) #show this help
275 printf '%s\n' "${HELP_LINES}"
276 exit
277 ;;
278 ( n ) #disable record file
279 NOTEOPT=1
280 ;;
281 ( r ) #print record
282 if [[ -f "${RECFILE}" ]]; then
283 cat "${RECFILE}"
284 exit 0
285 else
286 printf 'No record file.\n' 1>&2
287 exit 1
288 fi
289 ;;
290 ( s ) #scale (decimal plates)
291 SCL="${OPTARG}"
292 ;;
293 ( t ) #thousands separator
294 TOPT=1
295 ;;
296 ( v ) #show this script version
297 grep -m1 '^# v' "${0}"
298 exit 0
299 ;;
300 ( \? )
301 printf 'Invalid option: -%s\n' "${OPTARG}" 1>&2
302 #check if last arg starts with a negative sign
303 [[ "${@: -1}" = -* ]] && printf "First char in EXPRESSION is '-', try escaping: '(%s)'\n" "${@: -1}" 1>&2
304 exit 1
305 ;;
306 esac
307done
308shift $((OPTIND -1))
309
310#unset 'file record'?
311[[ "${BCREC}" != '1' ]] && unset BCREC
312
313#process expression
314EQ="${*:-$(</dev/stdin)}"
315EQ="${EQ//,}"
316EQ="${EQ%;}"
317
318#check if a 'record file' can be available
319#otherwise, create and initialise one
320if [[ -n "${BCREC}" ]]; then
321 #init record file if none
322 if [[ ! -f "${RECFILE}" ]]; then
323 printf '## Bcalc.sh Record\n\n' >> "${RECFILE}"
324 printf 'File initialised: %s\n' "${RECFILE}" 1>&2
325 fi
326
327 #add note to record
328 if [[ -n "${NOTEOPT}" ]]; then
329 notef "${*}"
330 fi
331
332 #swap '$HOLD' by last entry in history, or use last entry if no input
333 if [[ "${EQ}" =~ "${HOLD}" ]] || [[ -z "${EQ}" ]]; then
334 LASTRES=$(tail -1 "${RECFILE}")
335 EQ="${EQ//${HOLD}/${LASTRES}}"
336 EQ="${EQ:-${LASTRES}}"
337 fi
338#some error handling
339elif [[ -n "${NOTEOPT}" ]]; then
340 printf 'A record file is required for adding notes.\n' 1>&2
341 exit 1
342fi
343
344#load cientific extensions?
345[[ -n "${CIENTIFIC}" ]] && setcf
346
347#calc new result and check expression syntax
348if RES="$(bc -l <<<"${EXT};${EQ}")"; then
349 [[ -z "${RES}" ]] && exit 1
350else
351 exit 1
352fi
353
354#print to record file?
355if [[ -n "${BCREC}" ]]; then
356 #grep last history entry
357 LASTRES="$(tail -1 "${RECFILE}")"
358
359 #check for duplicate entries
360 if [[ "${RES}" != 0 ]] && [[ "${RES}" != "${LASTRES}" ]]; then
361 {
362 #print timestamp
363 printf '## %s\n## { %s }\n' "$(date "+%FT%T%Z")" "${EQ}"
364
365 #print new result
366 printf '%s\n' "${RES}"
367 } >> "${RECFILE}"
368 fi
369fi
370
371#format result
372
373#don't format multiline inputs
374if [[ "$(wc -l <<<"${RES}")" -gt 1 ]]; then
375 [[ -n "${SCL}${TOPT}" ]] && printf 'Multiline skips formatting options.\n' 1>&2
376#thousands separator opt
377elif [[ -n "${TOPT}" ]]; then
378 printf "%'.${SCL:-2}f\n" "${RES}"
379 exit
380#user-set scale
381elif [[ -n "${SCL}" ]] &&
382 #make bc result with user scale
383 RESS="$(bc -l <<<"${EXT};scale=${SCL};${EQ}/1" 2>/dev/null)"; then
384 #check result
385 { [[ -n "${RESS}" ]] && [[ "${RESS}" != '0' ]];} || unset RESS
386#trim trailing noughts; set a big enough scale
387elif REST="$(bc -l <<< "define trunc(x){auto os;scale=${SCL:-200};os=scale;for(scale=0;scale<=os;scale++)if(x==x/1){x/=1;scale=os;return x}}; trunc(${RES})" 2>/dev/null)"; then
388 #check result
389 { [[ -n "${REST}" ]] && [[ "${REST}" != '0' ]];} || unset REST
390fi
391
392#print result
393printf '%s\n' "${REST:-${RESS:-${RES}}}"