#!/usr/bin/env bash :<<'__comments__' Name: concur Usage: $0 Synopsis: CONvert CURrency from to and print the following 2 lines to stdout: 1 = = Requires: bash 4.2+, bc, curl Bugs: This script relies on the free api provided by and the included convert_currency function returns 4 if the url or api are invalid. Notes: As root make this file executable and place it in the $PATH or as an an ordinary user make it executable and place it in $HOME/bin; tested working OK on RHEL/CentOS 7+. Version 1.0.0 replaced our xe utility script which ceased working after xe.com disabled web page scraping of in August 2018. Version 2.0.0 implements the api key required to use the free version of currencyconverterapi.com effective February 2019. Version 2.1.0 modifies the value of $baseUrl in convert_currency() to match the change in URL from version 6 to version 7 of the API. The included convert_currency function will operate as an independent module and is suitable for use in other scripts. To obtain your own (free) api key (which is required to use this script or the convert_currency function), visit . To use this script you must set the global constant or export the environment variable CC_API_KEY="your api key"; if using the convert_currency function as an indpendent module, you may alternatively set the local variable ccApiKey="your api key". Updated 2022-10-03: Free keys now expire monthly; new key is emailed to the address you provided when you registered for the free key. Author: G. D. LaBossiere, Xview Solutions Inc. Version: 2.1.0 Created: 2018-08-30 (version 1.0.0) Modified: 2020-11-11 License: GNU GPL v3 __comments__ #------------------------------------------------------------------------------ # constants #------------------------------------------------------------------------------ # set the value of CC_API_KEY to your free api key obtainable from #+ or comment out the following line #+ and export CC_API_KEY="your api key" into the environment CC_API_KEY="" #------------------------------------------------------------------------------ # error codes #------------------------------------------------------------------------------ ERROR_10="Requires bash 4.2+" ERROR_15="Missing required program(s)" ERROR_16="Missing required function(s)" ERROR_18="Bad or missing required parameter(s)" #------------------------------------------------------------------------------ # variables #------------------------------------------------------------------------------ argList="()" conversionRate="" convertedValue="" errFuncName="" #------------------------------------------------------------------------------ # functions #------------------------------------------------------------------------------ :<<'__comments__' Name: convert_currency Synopsis: convert $1 from $2 to $3 using latest conversion rate pulled from Arguments: (required) $1="amount", $2="ISO code", $3="ISO code" Requires: bc, curl; global $CC_API_KEY or local $ccApiKey must expand to the value of your (free) api key obtainable from Returns: globals $conversionRate, $convertedValue and 0 on success; global $errFuncName and 2, 3, 4, 5, 6, 7, 8, exit 10 or exit 15 on error Calls: --- __comments__ convert_currency() { errFuncName="$FUNCNAME" # set the API key value here or search for $CC_API_KEY in the environment local ccApiKey="" [[ $ccApiKey ]] || ccApiKey="${CC_API_KEY}" # the key may still be invalid, this just sanitizes the input [[ $ccApiKey =~ ^[[:alnum:]]{8,128}$ ]] || { printf "%s\n" "${errFuncName}: missing or malformed API Key" return 2 } # needs bash 4.2+ [[ ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -ge 2 ]] || \ [[ ${BASH_VERSINFO[0]} -gt 4 ]] || { printf "%s %s\n" "${errFuncName}: ${ERROR_10}:" \ "current version: ${BASH_VERSION}" exit 10 } # needs 'bc' and 'curl' local eachOne missing=() required=(bc curl) thisOne for thisOne in ${required[@]}; do type -p ${thisOne} &>/dev/null || missing=(${missing[@]} ${thisOne}) done [[ ${missing[@]} ]] && { printf "%s" "${errFuncName}: ${ERROR_15}:" for eachOne in ${!missing[@]}; do printf "%s" "${missing[$eachOne]}" done printf "\n" exit 15 } [[ $# -ge 3 ]] || { printf "%s %s %s\n" "${errFuncName}:" \ "missing one or more parameters." "Need: " return 3 } # remove all commas in amount local ccQuan="${1//,/}" # convert ISO codes to upper case local ccFrom="${2^^}" local ccTo="${3^^}" [[ $ccQuan =~ ^[0-9]*\.[0-9]{1,}$ || $ccQuan =~ ^[0-9]{1,}\.*$ ]] || { printf "%s\n" "${errFuncName}: invalid amount: $1" return 3 } [[ $ccFrom =~ ^[A-Z]{3}$ ]] || { printf "%s\n" "${errFuncName}: invalid currency: $2" return 3 } [[ $ccTo =~ ^[A-Z]{3}$ ]] || { printf "%s\n" "${errFuncName}: invalid currency: $3" return 3 } local curlOpts="--connect-timeout 5 --insecure --max-time 15 --silent" local baseUrl="https://free.currconv.com/api/v7/" local queryStr="convert?q=${ccFrom}_${ccTo}&compact=ultra&apiKey=${ccApiKey}" local ccReply curlExit ccReply="$(curl ${curlOpts} ${baseUrl}${queryStr})" curlExit="$?" [[ $curlExit -eq 0 ]] || { printf "%s\n" "${errFuncName}: curl exited with error code ${curlExit}" return 4 } # valid JSON reply must be within curly brackets, e.g.: {"USD_CAD":1.301695} [[ $ccReply =~ ^\{.*\}$ ]] || { printf "%s\n%s\n%s\n" "${errFuncName}: curl received a bad reply" \ "requested URL ===> ${baseUrl}${queryStr}" \ "reply received ==> ${ccReply}" return 5 } [[ ${ccReply,,} =~ 'api key' ]] && { printf "%s %s\n" "${errFuncName}: your API Key is not valid" return 2 } [[ $ccReply =~ $ccFrom && $ccReply =~ $ccTo ]] || { printf "%s %s\n" "${errFuncName}: convert ${ccFrom} to ${ccTo}:" \ "bad or invalid source or target currency" return 6 } # strip closing curly brackets ccReply="${ccReply//\}/}" # strip all up to colon conversionRate="${ccReply#*:}" # should contain only digits and decimal [[ $conversionRate =~ ^[0-9]{1,}\.[0-9]{1,}$ ]] || { printf "%s\n" \ "${errFuncName}: ${ccFrom} to ${ccTo}: bad or missing conversion rate" return 7 } # to return arbitrary decimal precision ("scale") with bc, first get the #+ product then divide the result by 1, e.g. for 2 decimal precision, set num=2 #+ convertedValue="$(bc <<< "scale=${num}; (${ccQuan}*${conversionRate})/1")" convertedValue="$(bc <<< "${ccQuan}*${conversionRate}")" [[ $convertedValue =~ ^[0-9]*\.[0-9]{1,}$ ]] || { printf "%s\n" \ "${errFuncName}: ${ccFrom} to ${ccTo}: bad or missing converted value" return 8 } [[ $convertedValue =~ ^\.[0-9]{1,}$ ]] && convertedValue="0${convertedValue}" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: render_output Synopsis: print formatted globals to stdout Arguments: --- Requires: globals $argList, $conversionRate, $convertedValue Returns: globals ${argList[0]}, $convertedValue (printed in currency format), globals ${argList[1]}, ${argList[2]} and $conversionRate (printed as strings) and 0 on success; $errFuncName and exit 18 on error Calls: --- __comments__ render_output() { errFuncName="$FUNCNAME" [[ ${#argList[@]} -ge 3 && $conversionRate && $convertedValue ]] || { printf "%s\n" "${errFuncName}: ${ERROR_18}" exit 18 } # remove all commas in amount argList[0]="${argList[0]//,/}" printf "%s\n" "1 ${argList[1]^^} = ${conversionRate} ${argList[2]^^}" # print in currency format printf "%'0.2f" "${argList[0]}" printf " %s " "${argList[1]^^} =" # print in currency format printf "%'0.2f" "${convertedValue}" printf "%s\n" " ${argList[2]^^}" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: show_usage Synopsis: show required command line arguments (if running in a terminal) and exit with value of $1 (default 0) Arguments: (optional) $1="integer value from 0 to 255" Requires: --- Returns: exit $1 (or 0 if missing or invalid $1) Calls: --- __comments__ show_usage() { [[ -t 1 ]] && { printf "%b" " Usage: ${0##*/} Arguments: must be an integer or decimal value of maximum 10 digits with optional comma as 000 separator must be a valid 3-digit ISO code (e.g. USD) must be a valid 3-digit ISO code (e.g. EUR) Additional arguments are ignored \n" } [[ $1 =~ ^[0-9]{1,3}$ && $1 -ge 0 && $1 -le 255 ]] || exit 0 exit $1 } #------------------------------------------------------------------------------ :<<'__comments__' Name: validate_input Synopsis: test each element of global $argList for validity; call show_usage() and pass appropriate exit value if any test fails Arguments: --- Requires: global $argList Returns: 0 on success; $errFuncName and exit 16 on error Calls: show_usage __comments__ validate_input() { errFuncName="$FUNCNAME" local numDigits declare -F show_usage &>/dev/null || { printf "%s\n" "${errFuncName}: ${ERROR_16}: show_usage" exit 16 } [[ ${#argList[@]} -ge 3 ]] || show_usage 0 # amount must be valid decimal value [[ ${argList[0]} =~ ^\.[0-9]{1,}$ || \ ${argList[0]} =~ ^([0-9]+\,*[0-9]*)(\.*[0-9]*)$ ]] || show_usage 18 # count digits in amount numDigits="${argList[0]//,/}"; numDigits="${numDigits//./}" [[ ${#numDigits} -le 10 ]] || show_usage 18 [[ ${argList[1]} =~ ^[a-zA-Z]{3}$ && \ ${argList[2]} =~ ^[a-zA-Z]{3}$ ]] || show_usage 18 return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: main Synopsis: Contains all required functions and commands; must be the last (and only) command in this script Arguments: (required) quoted "$@" Requires: global $argList Returns: exit 0 on success; exit 16 or return value of convert_currency on error Calls: convert_currency, render_output, validate_input __comments__ main() { errFuncName="$FUNCNAME" argList=("$@") local eachOne missing=() retVal thisOne # report and quit if missing any required function local required=(convert_currency render_output validate_input) for thisOne in ${required[@]}; do declare -F ${thisOne} &>/dev/null || missing=(${missing[@]} ${thisOne}) done [[ ${missing[@]} ]] && { printf "%s" "${errFuncName}: ${ERROR_16}:" for eachOne in ${!missing[@]}; do printf "%s" "${missing[$eachOne]}" done printf "\n" exit 16 } validate_input convert_currency ${argList[@]} retVal="$?" [[ $retVal -eq 0 ]] && render_output exit $retVal } #------------------------------------------------------------------------------ # main program #------------------------------------------------------------------------------ main "$@"