Sometimes I need to run some math operations. I know I can use bc or echo $(( 6/2 )). I have created my own function for bc to read input. But sometimes it takes a long time to type: _bc "6/2". So I have this question:

Is there way to teach zsh/bash how to run math operation for numbers in command line? One example is more than thousands words.

$ 6/2
$ 3.0

It means that zsh/bash must recognize numbers and call i.e. bc.

  • 4
    This reminds me of this question (which isn’t a duplicate). – Stephen Kitt Dec 6 at 10:04
  • 1
    While I've never seen a script called, for example, 2+2, it could conceivably exist, in which case you'd never be able to run it. – Jeff Schaller Dec 6 at 13:47
  • @JeffSchaller Or a bit more plausibly maybe 2/3. You could still run it by typing something like \2+2, if 2+2 was parsed as an alias for echo $((2+2)). – Gilles Dec 6 at 18:17
  • 1
    Wouldn't c 1+2+3 be a good alternative? (Calculate and then you can put anything you want, no spaces needed, same commands as bc if you want) – Fabby Dec 6 at 18:56
  • in powershell 6/2 and simply works – phuclv Dec 7 at 1:22
up vote 16 down vote accepted

Shortcut Alt-c (bash)

With bash, using the readline utility, we can define a key sequence to place the word calc at the start and enclose the text written so far into double quotes:

 bind '"\ec": "\C-acalc \"\e[F\""'

Having executed that, you type 23 + 46 * 89 for example, then Alt-c to get:

 calc "23 + 46 * 89"

Just press enter and the math will be executed by the function defined as calc, which could be as simple as, or a lot more complex:

 calc () { <<<"$*" bc -l; }

a (+) Alias

We can define an alias:

alias +='calc #'

Which will comment the whole command line typed so far. You type:

 + (56 * 23 + 26) / 17

When you press enter, the line will be converted to calc #(56 * 23 + 26) / 17 and the command calc will be called. If calc is this function:

 calc(){ s=$(HISTTIMEFORMAT='' history 1);   # recover last command line.
         s=${s#*[ ]};                        # remove initial spaces.
         s=${s#*[0-9]};                      # remove history line number.
         s=${s#*[ ]+};                       # remove more spaces.
         eval 'bc -l <<<"'"$s"'"';           # calculate the line.
       }

 calc(){ s=$(history -1 |                          # last command(s)
             sed '$!d;s/^[ \t]*[0-9]*[ \t]*+ //'); # clean it up 
                                                   # (assume one line commads)
         eval 'bc -l <<<"'"$s"'"';                 # Do the math.
       }

zsh doesn't allow neither a + alias nor a # character.

The value will be printed as:

 $ + (56 * 23 + 26) / 17
 77.29411764705882352941

Only a + is required, String is quoted (no globs), shell variables accepted:

 $ a=23
 $ + (56 * 23 + $a) / 17
 77.11764705882352941176

a (+) Function

With some limitations, this is the closest I got to your request with a function (in bash):

+() { bc -l <<< "$*"; }

Which will work like this:

$ + 25+68+8/24
93.33333333333333333333

The problem is that the shell parsing isn't avoided and a * (for example) could get expanded to the list of files in the pwd.

If you write the command line without (white) spaces you will probably be ok.

Beware of writing things like $(...) because they will get expanded.

The safe solution is to quote the string to be evaluated:

$ + '45 + (58+3 * l(23))/7'
54.62949752111249272462

$ + '4 * a(1) * 2'
6.28318530717958647688

Which is only two characters shorter that your _bc "6/2", but a + seems more intuitive to me.

  • @Issac Thank you for your answer. Yes, it's shorter - but it's about name of function which you choose. In my own name convention has every function underscore at the beginning of name. – waldauf Dec 7 at 9:40
  • @waldauf Added a readline solution (no + needed, only one keyboard chord). Require the calc function. Also an alias solution, a bit more complex. – Isaac Dec 7 at 10:24
  • @waldauf The name of the function could perfectly be named _calc or anything else. – Isaac Dec 7 at 10:28
  • @Isaac Solution with keyboard shortcut looks great! I'll try to do it for bindkey in ZSH. :] – waldauf Dec 7 at 11:48
  • @waldauf It seems a bit strange to both pick a convention that has an underscore at the beginning of all names (may require the shift key!) and complain about _bc taking a long time to type :-) – ShreevatsaR Dec 7 at 18:20

I use a variant of bash's magic alias hack:

asis() { bc <<< "$(history 1 | perl -pe 's/^ *[0-9]+ +[^ ]+ //')"; }
alias c='asis #'

Then:

$ c 1+1
2
$ c -10 + 20 / 5
-6
$ c (-10 + 20) / 5
2
$ c 2^8 / 13
19
$ c scale=5; 2^8 / 13
19.69230

The magic is the fact that alias expansion happens before the usual command line processing, which allows us to create a command whose remaining arguments follow a comment character, that the implementing function finds with the history command.

This magic allows me to type *, (, and other characters literally. But that also means I can't use shell variables because $ is also literal:

$ x=5.0
$ y=-1.2
$ z=4.7
$ c ($x + $y) > $z
(standard_in) 1: illegal character: $
(standard_in) 1: illegal character: $
(standard_in) 1: illegal character: $

I get around this by a bit of bootstrapping:

$ echo "x=$x; y=$y; z=$z"
x=5.0; y=-1.2; z=4.7
$ c x=5.0; y=-1.2; z=4.7; (x + y) > z
0

You might just be better off typing: bc Enter 1 + 1 Enter Control+D


As a side note, I have my default bc settings (like scale) in $HOME/.bc and I use bc -l in the alias. Your use may not require these modifications.

  • why are you saying that you can't use shell variables? you can! alias c='calc #'; calc(){ eval "$(history -a /dev/stdout | sed 's/c /expr /;s/[()*/+-]/ \\& /g')"; }; then x=33; c ($x/3)*10. This has a great potential, thanks for the trick! – mosvy Dec 7 at 0:33
  • @mosvy Well, can't as written in my answer. But certainly eval raises this hack to all-new heights! – bishop Dec 7 at 0:45
  • 1
    +1 Thanks for your idea, I implemented an alternative in my answer. Note that the value of HISTTIMEFORMAT needs to be cleared to avoid the time tag printed by history. Perl seems like too much for this little edition use. – Isaac Dec 7 at 10:52

In zsh, you could do something like:

autoload zcalc
accept-line() {
  if [[ $BUFFER =~ '^[ (]*[+-]? *(0[xX]|.)?[[:digit:]]+[^[:alnum:]]' ]]; then
    echo
    zcalc -e $BUFFER
    print -rs -- $BUFFER
    BUFFER=
  fi
  zle .$WIDGET
}
zle -N accept-line

It redefines the accept-line widget (mapped on Enter) to a user-defined widget that checks if the current line starts with a number (decimal or hexadecimal) optionally prefixed with any number of (s, looking for a non-alnum character after that to avoid false positives for commands like 7zip or 411toppm.

If that matches then we pass it to zcalc (more useful than bc in that it can use shell variables and all of zsh math functions and number styles, but does not support arbitrary precision), add the line to history and accept an empty command.

Note that it can cause confusion if you enter a line with digits in things like:

cat << EOF
213 whatever
EOF

Or:

var=(
  123 456
)

Inspired by Stéphane's zsh answer, and longer than Isaac's bash answer in total but shorter in operation:

trap '[[ $_ =~ [[:digit:]] ]] && bc -l <<< "$_"' ERR

This also has the side effect of showing a "No such file or directory" error each time:

$ foozle
-bash: foozle: command not found
$ 1+2+3
-bash: 1+2+3: command not found
6
$ 6/3
-bash: 6/3: No such file or directory
2.00000000000000000000

The regex could be tightened, depending on the operations you expect to perform.

This (ab)uses the bash behavior of calling the ERR trap when a given command does not exist. If the last command (in $_) contains a digit, then it executes bc on that "command".


Thanks to a hint from Stéphane, here's a slightly cleaner way to achieve the result (requires bash 4.0 or later, which introduced the functionality):

if ! declare -F command_not_found_handle > /dev/null
then
  command_not_found_handle() { 
    if [[ "$@" =~ [[:digit:]] ]]; then 
      bc <<< "$@"; 
    else
      printf 'bash: %s: command not found\n' "$1" >&2
      return 127
    fi
  }
else
  echo Unable to set up the handler function, sorry
fi

The function is called any time a command isn't found. If that command contains a digit, we throw it through bc; otherwise, we emit a message similar to bash's stock message and return a 127 exit code.

  • Caveat - the ERR trap is not inherited to subshells, so (1+2) would need to be '(1+2)' – Jeff Schaller Dec 6 at 18:55
  • 2
    bash and zsh have a command-not-found-handler hook (mispelled handle in bash) that you could also use here. – Stéphane Chazelas Dec 6 at 19:27
  • Thanks, Stéphane! I've incorporated the idea. – Jeff Schaller Dec 6 at 19:49
  • 2
    command_not_found_handle doesn't work on something like 123/456, though. Bash doesn't seem to bother calling the hook on commands that look like pathnames. With the ERR trap, I think you could also use $BASH_COMMAND to get the full command line, and not just the last word. That would make expressions with spaces work. – ilkkachu Dec 6 at 20:23
  • 1
    A workaround using readline is possible and seems simpler/easier. – Isaac Dec 7 at 10:49

The following command lines are rather simple to type,

<<< 5+4 bc
<<< 6/3 bc
<<< 7*2 bc

and slightly more complicated with parentheses (must be quoted or escaped),

<<< "(5+4)*2/3" bc
<<< \(5+4\)*2/3 bc

Another imperfect way of doing that in Bash would be to use the DEBUG trap, which runs on every command. With extdebug set, the trap handler can prevent the main command from running, so you don't get the "command not found" errors.

$ cat bash_calc.sh
shopt -s extdebug
debug_calc() {
    local re='^[ (]*-?[0-9]'
    if [[ $BASH_COMMAND =~ $re ]]; then
        echo "$BASH_COMMAND" | bc -l
        return 1
    fi
}
trap debug_calc DEBUG
$ . ./bash_calc.sh
$ 123 * 456
56088
$ 123/456
.26973684210526315789

The trap gets the full command line before expanding variables or filename patterns, so an unquoted * works. (But using shell variables in the calculation doesn't work.)

However, unquoted parenthesis still cause a syntax error, so this is not perfect either.

(I nicked the regex above from Stéphane's answer.)

  • Running with extdebug active will make the shell quite slower, but it is a clever idea, thanks. – Isaac Dec 7 at 10:55
  • @Isaac, hmm, that's not good. I'll admit I'd never tested it for speed, though I wonder if it matters here if this is for interactive use. Anyway, as mentioned, this is just another imperfect idea. I originally wasn't going to post it at all, but there are other somewhat hackish Bash solutions here, too, so I think it can stay. – ilkkachu Dec 7 at 16:18

What about expr?

$ expr 6 / 2
3

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.