Dynamic functions in ZSH

published on 22 Feb 2014, tagged with zsh jshon

Inspired by the command not found hook that you'll find in many GNU/Linux distributions I wanted to use a similar approach in order to create dynamic functions. Let's write a simple currency converter! First we're going to need a source to get exchange rates from. A quick search led me to http://rate-exchange.appspot.com/ (if you plan to keep using the API you can help out the host by donating).

The reason I chose this particular site is because it doesn't require registration and it will return the data in json which is awesome for two reasons. It's a simpler (less fat) format than xml and it gives me an excuse to show off jshon. The program can be found at http://kmkeen.com/jshon/ and hopefully also in your distributions repositories. If you scroll to the bottom of the page you'll see a REAL EXAMPLE and some additional notes on using jshon in the shell pipeline.

Let's take jshon for a spin. Running the following command will pretty-print the data to make it more readable.

% wget -qO- "http://rate-exchange.appspot.com/currency?from=eur&to=usd" | jshon
 "to": "usd",
 "rate": 1.3737299999999999,
 "from": "eur"

To extract a value we will pass the -e parameter to jshon followed by the index rate that will return to us the value of 1.3737299999999999.

Let's skip ahead to the function.

function currconv()
  if [ $# != 3 ]; then
    echo "usage: $0 <from> <to> <amount>"
    return 0

  local from="$1" to="$2" amount="$3"

  local reply=$(wget -qO- "http://rate-exchange.appspot.com/currency?from=$from&to=$to" | jshon -QC -erate)

  if [ $reply = "null" ]; then
    return 127
    printf "%.2f\n" $(( $amount * $reply ))
  • declare a function that expects exactly 3 arguments, else display help
  • assign variables local to the function
  • run wget against the API with the parameters supplied to the script, pipe to jshon that will disable error reporting (-Q), continue on errors (-C) and output the value of rate
  • if the response is null it means we supplied a non supported rate conversion (or something else could've gone wrong, error handling could be improved). A return code of 127 implies an error and prints command not found if called from command_not_found_handler()
  • if we got a valid response, perform the arithmetic evaluation and return the value with 2 decimal places
% currconv eur usd 5

Next step is to get this function in command_not_found_handler()

function command_not_found_handler()
  if [[ $1 =~ "[a-zA-Z]{3}2[a-zA-Z]{3}" ]]; then
    local from=${1%2*} to=${1#*2} amount=$2
    currconv $from $to $amount
    return 127
  • match the command that was not found against a regex pattern
    • 3 characters matching a..z upper and lower case
    • the digit 2
    • same as first
  • if match
    • create local variables from and to by splitting the command
      • say we call the command eur2usd from the shell
      • from = eur
      • to = usd
    • since the command (eur2usd) was the first argument, the amount will be stored in the second ($2)
    • call the function
  • no match
    • return 127 (command not found)

The result

% eur2usd 5
% usd2wut 10
zsh: command not found: usd2wut