#!/usr/bin/env bash :<<'__comments__' Name: list-transactions-for Usage: see show_usage() Synopsis: Extract, sort, format and print a list of postings from $LEDGER_FILE. Notes: See style guide at https://google.github.io/styleguide/shell.xml Author: G. D. LaBossiere, Xview Solutions Inc. Version: 1.0.0 Created: 2019-04-07 (version 1.0.0) Modified: 2019-05-01 License: GNU GPL v3 __comments__ #------------------------------------------------------------------------------ # constants #------------------------------------------------------------------------------ # export ledger environment vars to set options which must otherwise be set on #+ the command line; see 'man 1 ledger' export LEDGER_DATE_FORMAT="%d %b %Y" export LEDGER_FILE="" # ledger account regexes to search for query_customer() and query_supplier(); #+ ledger regex searches are case-insensitive; modify the values of these #+ constants to match the naming convention used in your chart of accounts QUERY_CUSTOMER_ACCOUNTS="(receivable or deferred)" QUERY_SUPPLIER_ACCOUNTS="(Assets or Expense) and not (Bank or Taxes)" #------------------------------------------------------------------------------ # error codes #------------------------------------------------------------------------------ ERROR_0="Program terminated normally" 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)" ERROR_22="Required file(s) not found" #------------------------------------------------------------------------------ # variables #------------------------------------------------------------------------------- argList="" beginDate="" endDate="" errFuncName="" exitMessage="" ledgerOpts="" pattern1="" pattern2="" queryType="" #------------------------------------------------------------------------------ # functions #------------------------------------------------------------------------------ :<<'__comments__' Name: check_bash Synopsis: Check for supported bash versions Arguments: --- Requires: bash 4.2+ Returns: 0 on success; $errFuncName and exit 10 on error. Calls: --- __comments__ check_bash() { errFuncName="$FUNCNAME" [[ ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -ge 2 ]] || \ [[ ${BASH_VERSINFO[0]} -gt 4 ]] || exit 10 return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: check_dates Synopsis: If $beginDate and $endDate are set verify the first is earlier. Arguments: --- Requires: --- Returns: 0 on success; $errFuncName, $exitMessage and exit 15 or 18 on error. Calls: --- __comments__ check_dates() { errFuncName="${FUNCNAME}" type -p date &>/dev/null || exit 15 [[ $beginDate ]] && { date -d "$beginDate" &>/dev/null || { exitMessage="'--begin' is not followed by a valid date" exit 18 } } [[ $endDate ]] && { date -d "$endDate" &>/dev/null || { exitMessage="'--end' is not followed by a valid date" exit 18 } } [[ $beginDate && $endDate ]] && { [[ $(date -d "$beginDate" +%s) -le $(date -d "$endDate" +%s) ]] || { exitMessage="'--begin' date must be earlier than '--end' date" exit 18 } } return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: check_params Synopsis: If $1 contains 'acc[ount]' or 'cus[tomer]' or 'sup[plier]' assign "$1" to $queryType; if $queryType is set and does not have value 'acc[ount]' assign "$@" to $pattern1; if $queryType has value 'acc[ount]' and $pattern1 is not set assign "$@" to $pattern1; if $queryType has value 'acc[ount]' and $pattern1 is set assign "$@" to $pattern2. Arguments: (required) $1=TYPE, $2=PATTERN; (optional) $3=PATTERN Requires: ledger Returns: 0 on success; $errFuncName, $exitMessage and exit 15 or 18 on error. Calls: --- __comments__ check_params() { errFuncName="$FUNCNAME" type -p ledger &>/dev/null || exit 15 [[ $queryType ]] || { # need minimum 3 alphanum chars to match a valid $queryType [[ ${1,,} =~ ^acc[ount]*$|^cus[tomer]*$|^sup[plier]*$ ]] || { exitMessage="Valid TYPE values are: 'acc[ount]' or 'cus[tomer]' or" exitMessage="${exitMessage} sup[plier]'" exit 18 } queryType="${1,,}" return 0 } # the value for $pattern1 or $pattern2 may be a string (e.g. 'abcdefg') or a #+ regular expression (e.g. '^abcdefg$' or '/abcdefg/') [[ $queryType && $pattern1 ]] || { pattern1="$@" return 0 } [[ $queryType && $pattern1 && $pattern2 ]] || pattern2="$@" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: check_required Synopsis: Verify all required tools/utilities are available on this host. Arguments: --- Requires: sed, sort, tr Returns: 0 on success; $errFuncName, $exitMessage and exit 15 or 22 on error. Calls: --- __comments__ check_required() { errFuncName="$FUNCNAME" type -p sed sort tr &>/dev/null || exit 15 local each missing required # explanation of sed commands: #+ sed -e "s/#.*$//" -e "/^$/d" ==> exclude commented or blank lines #+ sed -ne "/ type /p" ==> select only lines containing ' type ' #+ ${0} ==> this file #+ sed -e "s/.*type \-*p*P*\(.*\).\+>.*/\1/" ==> extract strings between #+ 'type -p' and '?>' (i.e. save the arguments passed to 'type' but exclude #+ everything from and after the first char before a '>' redirection symbol) #+ sed -e "s/\$.\+ //g" ==> delete any space-terminated strings that start with #+ a '$' dollar symbol (i.e. unexpanded vars) #+ sed -e "s/[^0-9a-zA-Z_ -]//g" ==> delete chars not allowed in program names required=($(sed -e "s/#.*$//" -e "/^$/d" -ne "/ type /p" "${0}" | \ sed -e "s/.*type \-*p*P*\(.*\).\+>.*/\1/" -e "s/\$.\+ //g" \ -e "s/[^0-9a-zA-Z_ -]//g")) # explanation of commands: #+ printf "%s\n" "${required[@]}" ==> output each array element #+ sort -u ==> remove duplicate names #+ tr '\n' ' ' ==> replace newlines with spaces (i.e. convert to a string) #+ tr -s ' ' ==> squeeze spaces (i.e. leave a single space between names) required="$(printf "%s\n" "${required[@]}" | sort -u | tr '\n' ' ' | \ tr -s ' ' )" for each in ${required}; do type -p "${each}" &>/dev/null || missing="${missing} ${each}" done exitMessage="Required program(s) not found: ${missing}" [[ $missing ]] && exit 15 exitMessage="Cannot read LEDGER_FILE '$LEDGER_FILE' or the file is undefined" [[ -r $LEDGER_FILE ]] || exit 22 return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: exit_message Synopsis: If attached to a tty and $? is non-zero, display a message; exit with value $?; the first statement to appear in main() must be: trap exit_message EXIT Arguments: --- Requires: --- Returns: exit $? Calls: --- __comments__ exit_message() { local errNum="$?" local errTxt="ERROR_${errNum}" local errMsg="Error ${errNum}: No description available" [[ ${!errTxt} ]] && errMsg="Error ${errNum}: ${!errTxt}" [[ ${errFuncName} ]] && errMsg="${errFuncName}: ${errMsg}" errMsg="${0##*/}: ${errMsg}" [[ -t 1 && $errNum -ne 0 ]] && { [[ $exitMessage ]] && printf "%b\n" "${exitMessage}" printf "%s\n" "${errMsg}" } } #------------------------------------------------------------------------------ :<<'__comments__' Name: parse_command_line Synopsis: Parse the command line arguments. Arguments: $queryType $pattern1 [$pattern2] [--begin DATE] [--end DATE] [--effective] [--basis|--cost] Requires: --- Returns: $beginDate, $endDate, $ledgerOpts; 0 in all cases. Calls: check_dates, check_params, show_usage __comments__ parse_command_line() { errFuncName="$FUNCNAME" local arg=0 args=("${argList[@]}") while [[ $arg -lt ${#args[@]} ]]; do case "${args[$arg]}" in --basis|--cost) [[ $ledgerOpts =~ '--basis'|'--cost' ]] || \ ledgerOpts="${ledgerOpts} ${args[$arg]}" ((++arg)) ;; --effective) [[ $ledgerOpts =~ '--effective' ]] || \ ledgerOpts="${ledgerOpts} ${args[$arg]}" ((++arg)) ;; --begin) [[ $beginDate ]] || { beginDate="${args[$((${arg}+1))]}" check_dates ledgerOpts="${ledgerOpts} --begin ${beginDate}" ((++arg)) } ((++arg)) ;; --end) [[ $endDate ]] || { endDate="${args[$((${arg}+1))]}" check_dates ledgerOpts="${ledgerOpts} --end ${endDate}" ((++arg)) } ((++arg)) ;; -*) show_usage ;; *) # do NOT quote the argument; check_params() will parse the command line check_params ${args[$arg]} ((++arg)) ;; esac done return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: query_account Synopsis: Create the search patterns and output formats associated with the $queryType value 'acc[ount]'; search $LEDGER_FILE; generate and print the results. Arguments: --- Requires: cut, ledger, sed, sort, tr Returns: 0 on success; $errFuncName, $exitMessage and exit 15 or 18 on error. Calls: --- __comments__ query_account() { [[ ${queryType,,} =~ ^acc[ount]*$ ]] || return 0 errFuncName="$FUNCNAME" type -p cut ledger sed sort tr &>/dev/null || exit 15 local account accounts dashes date format header search date="Date" [[ $ledgerOpts =~ '--effective' ]] || date="Actual date" header="Account names containing '${pattern1^^}'" [[ $pattern1 =~ [0-9a-zA-Z] ]] || header="All accounts" printf "\n" [[ $ledgerOpts =~ '--effective' ]] || printf "%13s" printf "%13s%s" "" "${header}" [[ $pattern2 ]] && { printf " %s" "with transactions containing '${pattern2^^}'" search="and ( %(payee($pattern2)) or %(note($pattern2))" search="${search} or %(code($pattern2)) )" } [[ $beginDate ]] && printf " %s" "starting at ${beginDate}" [[ $endDate ]] && printf " %s" "up to ${endDate}" printf "\n\n" format="%A\n" # each $account in ${accounts[@]} matches $pattern1 readarray -t accounts < <(ledger -F "${format}" reg ${pattern1} | sort -u) # list all transactions in $account if $pattern2 is not set, else list only #+ transactions matching $pattern2; skip $account if no transactions match for account in "${accounts[@]}"; do # must replace spaces in $account with '.' because when bash expands the value #+ of $account ledger will not escape the spaces so regex parsing is broken [[ $(ledger ${ledgerOpts} register ${account// /.} ${search}) ]] && { printf -v dashes "%154s" [[ $ledgerOpts =~ '--effective' ]] || printf -v dashes "%167s" dashes="${dashes// /-}" printf "%s\n\n\n" "${dashes}" [[ $ledgerOpts =~ '--effective' ]] || printf "%-11s%2s" "Posted date" "" printf "%-11s%2s%-107s%2s%15s%2s%15s\n" "${date}" "" \ "${account//:/ ==> }" "" "Amount" "" "Balance" printf -v dashes "%107s" dashes="${dashes// /-}" [[ $ledgerOpts =~ '--effective' ]] || printf "%s%2s" "-----------" "" printf "%s%2s%s%2s%s%2s%s\n" "-----------" "" "${dashes}" "" \ "---------------" "" "---------------" format="%d %-20.20C %-30.30P %-55.55N %15.15t %15T\r" [[ $ledgerOpts =~ '--effective' ]] || { ledger ${ledgerOpts} -F "${format}" -S 'date' register \ ${account// /.} ${search} | tr '\n' '\v' | tr '\r' '\n' | \ sed -e "s/=/ /g; s/\xB/ /g; s/[()]/ /g;" } [[ $ledgerOpts =~ '--effective' ]] && { ledger ${ledgerOpts} -F "${format}" -S 'date' register \ ${account// /.} ${search} | tr '\n' '\v' | tr '\r' '\n' | \ cut -d'=' -f2 | sed -e "s/\xB/ /g; s/[()]/ /g;" } } done printf -v dashes "%154s" [[ $ledgerOpts =~ '--effective' ]] || printf -v dashes "%167s" dashes="${dashes// /-}" printf "%s\n" "${dashes}" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: query_customer Synopsis: Create the search patterns and output formats associated with the $queryType value 'cus[tomer]'; search $LEDGER_FILE; generate and print the results. Arguments: --- Requires: cut, ledger, sed, tr Returns: 0 on success; $errFuncName, $exitMessage and exit 15 or 18 on error. Calls: --- __comments__ query_customer() { [[ ${queryType,,} =~ ^cus[tomer]*$ ]] || return 0 errFuncName="$FUNCNAME" type -p cut ledger sed tr &>/dev/null || exit 15 local dashes date format date="Date" [[ $ledgerOpts =~ '--effective' ]] || date="Actual date" printf -v dashes "%65s" dashes="${dashes// /-}" printf "\n" [[ $ledgerOpts =~ '--effective' ]] || printf "%-11s%2s" "Posted date" "" printf "%-11s%2s%-65s%2s%15s%2s%15s\n" "${date}" "" \ "Transactions for ${pattern1^^}" "" "Amount" "" "Balance" [[ $ledgerOpts =~ '--effective' ]] || printf "%s%2s" "-----------" "" printf "%s%2s%s%2s%s%2s%s\n" "-----------" "" "${dashes}" "" \ "---------------" "" "---------------" format="%d %-20.20C %-45.45N %15.15t %15T\r" [[ $ledgerOpts =~ '--effective' ]] || { ledger ${ledgerOpts} -F "${format}" -S 'date' register \ ${QUERY_CUSTOMER_ACCOUNTS} and @${pattern1} | tr '\n' '\v' | \ tr '\r' '\n' | sed -e "s/=/ /g; s/\xB/ /g; s/[()]/ /g;" return 0 } ledger ${ledgerOpts} -F "${format}" -S 'date' register \ ${QUERY_CUSTOMER_ACCOUNTS} and @${pattern1} | tr '\n' '\v' | \ tr '\r' '\n' | cut -d'=' -f2 | sed -e "s/\xB/ /g; s/[()]/ /g;" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: query_supplier Synopsis: Create the search patterns and output formats associated with the $queryType value 'sup[plier]'; search $LEDGER_FILE; generate and print the results. Arguments: --- Requires: cut, ledger, sed, tr Returns: 0 on success; $errFuncName, $exitMessage and exit 15 or 18 on error. Calls: --- __comments__ query_supplier() { [[ ${queryType,,} =~ ^sup[plier]*$ ]] || return 0 errFuncName="$FUNCNAME" type -p cut ledger sed tr &>/dev/null || exit 15 local dashes date format date="Date" [[ $ledgerOpts =~ '--effective' ]] || date="Actual date" printf -v dashes "%107s" dashes="${dashes// /-}" printf "\n" [[ $ledgerOpts =~ '--effective' ]] || printf "%-11s%2s" "Posted date" "" printf "%-11s%2s%-107s%2s%15s%2s%15s\n" "${date}" "" \ "Transactions for ${pattern1^^}" "" "Amount" "" "Balance" [[ $ledgerOpts =~ '--effective' ]] || printf "%s%2s" "-----------" "" printf "%s%2s%s%2s%s%2s%s\n" "-----------" "" "${dashes}" "" \ "---------------" "" "---------------" format="%d %-20.20C %-30.30P %-55.55N %15.15t %15T\r" [[ $ledgerOpts =~ '--effective' ]] || { ledger ${ledgerOpts} -F "${format}" -S 'payee,date' register \ ${QUERY_SUPPLIER_ACCOUNTS} and @${pattern1} | tr '\n' '\v' | \ tr '\r' '\n' | sed -e "s/=/ /g; s/\xB/ /g; s/[()]/ /g;" return 0 } ledger ${ledgerOpts} -F "${format}" -S 'payee,date' register \ ${QUERY_SUPPLIER_ACCOUNTS} and @${pattern1} | tr '\n' '\v' | \ tr '\r' '\n' | cut -d'=' -f2 | sed -e "s/\xB/ /g; s/[()]/ /g;" return 0 } #------------------------------------------------------------------------------ :<<'__comments__' Name: show_usage Synopsis: Display program usage if attached to a tty. Arguments: --- Requires: --- Returns: Exit 0 in all cases. Calls: --- __comments__ show_usage() { [[ -t 1 ]] || exit 0 printf "%b" " ------------------------------------------------------------------------------- Usage: ${0##*/} TYPE 'PATTERN 1' ['PATTERN 2'] [OPTIONS] Extract, sort, format and display records from \$LEDGER_FILE where TYPE must be one of: acc[ount], cus[tomer] or sup[plier] and both 'PATTERN 1' and 'PATTERN 2' are quoted strings or regular expressions. All of them are case-insensitive. If TYPE is acc[ount] then 'PATTERN 1' must specify the ledger accounts to list. Use '.' to list all accounts. The records listed may be optionally limited to those containing 'PATTERN 2'. Use a period '.' not a space ' ' in a quoted string. If TYPE is cus[tomer] or sup[plier] 'PATTERN 2' is ignored. Options: --begin DATE Exclude records before DATE. --end DATE Exclude records after DATE. --effective Exclude records by their effective date not their posting date. --basis, --cost Display all monetary amounts converted to base currency. Unrecognized options cause this message to be displayed. ------------------------------------------------------------------------------- " exit 0 } #------------------------------------------------------------------------------ # main #------------------------------------------------------------------------------ main() { trap exit_message EXIT argList=("$@") declare -F check_bash check_required check_dates check_params exit_message \ parse_command_line query_account query_customer query_supplier show_usage \ &>/dev/null || exit 16 check_bash check_required parse_command_line query_account query_customer query_supplier exit 0 } #------------------------------------------------------------------------------ # run program #------------------------------------------------------------------------------ main "$@"