Skip to content

Instantly share code, notes, and snippets.

@gnidan
Last active December 3, 2025 21:57
Show Gist options
  • Select an option

  • Save gnidan/ae7e0774342d3104dd753b5b405dcc6e to your computer and use it in GitHub Desktop.

Select an option

Save gnidan/ae7e0774342d3104dd753b5b405dcc6e to your computer and use it in GitHub Desktop.
`hledger balancesheetequity` with revenues+expenses retained
#!/bin/bash
# hledger-bsrx - balance sheet with retained earnings
# Place in PATH as 'hledger-bsrx' to use as 'hledger bsrx'
set -e
usage() {
cat <<EOF
hledger-bsrx - balance sheet with automatic retained earnings
Usage: hledger bsrx [OPTIONS] [QUERY...]
Generates a balance sheet that automatically closes revenue and expense
accounts to a retained earnings account.
Custom flags:
--retain-acct=ACCT set retained earnings account
(default: equity:retained earnings)
--commodity-format=FMT set commodity format for internal calculations
(default: \$1,000.000000000)
Standard hledger flags are supported:
Query flags (-b, -e, -p, account patterns) filter both the closing
entries and the final report.
Report flags (--layout, -O, -t, -l, -B, -V, etc.) apply only to the
final balance sheet output.
Examples:
hledger bsrx
hledger bsrx --retain-acct='equity:earnings' ^mycompany
hledger bsrx -e 2024-12-31 --tree --layout wide
EOF
exit 0
}
log() {
echo "$@" >&2
}
# Defaults
COMMODITY_FORMAT='$1,000.000000000'
RETAINED_EARNINGS_ACCOUNT='equity:retained earnings'
# Argument arrays
query_args=() # dates, account patterns -> both close_entries and bse
report_args=() # display/valuation flags -> only bse
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
;;
# Custom flags
--retain-acct=*)
RETAINED_EARNINGS_ACCOUNT="${1#*=}"
shift
;;
--retain-acct)
RETAINED_EARNINGS_ACCOUNT="$2"
shift 2
;;
--commodity-format=*)
COMMODITY_FORMAT="${1#*=}"
shift
;;
--commodity-format)
COMMODITY_FORMAT="$2"
shift 2
;;
# Query/filter flags -> both
-b|--begin|-e|--end|-p|--period|-f|--file)
query_args+=("$1" "$2")
shift 2
;;
-b=*|--begin=*|-e=*|--end=*|-p=*|--period=*|-f=*|--file=*)
query_args+=("$1")
shift
;;
# Report display flags -> only bse
--layout|--drop|--format|-O|--output-format|-o|--output-file)
if [[ "$1" == *=* ]]; then
report_args+=("$1")
shift
else
report_args+=("$1" "$2")
shift 2
fi
;;
--layout=*|--drop=*|--format=*|-O=*|--output-format=*|-o=*|--output-file=*)
report_args+=("$1")
shift
;;
-t|--tree|-l|--flat|-N|--no-total|-S|--sort-amount|-A|--average|-T|--row-total)
report_args+=("$1")
shift
;;
--no-elide|--declared|-%|--percent|--summary-only|--transpose|--invert)
report_args+=("$1")
shift
;;
# Valuation flags -> only bse
-B|--cost|-V|--market|--value|--value=*)
report_args+=("$1")
shift
;;
-X|--exchange)
report_args+=("$1" "$2")
shift 2
;;
-X=*|--exchange=*)
report_args+=("$1")
shift
;;
# Accumulation modes -> only bse (close_entries always uses implicit behavior)
-H|--historical|--cumulative|--change)
report_args+=("$1")
shift
;;
# Calculation modes -> only bse
--sum|--valuechange|--gain|--count)
report_args+=("$1")
shift
;;
# Everything else is a query pattern -> both
*)
query_args+=("$1")
shift
;;
esac
done
close_entries() {
local balances
log "Fetching balances..."
balances=$(hledger bal "${query_args[@]}" type:RX -O tsv -c "$COMMODITY_FORMAT" --layout bare --no-total -N 2>/dev/null | tail -n +2)
[[ -z "$balances" ]] && return
local comms
comms=$(echo "$balances" | cut -f2 | sort -u)
echo "$(date +%Y-%m-%d) retain earnings"
local tmpdir
tmpdir=$(mktemp -d)
trap "rm -rf '$tmpdir'" EXIT
local pids=()
local i=0
for comm in $comms; do
log "Spawning job for commodity: $comm"
(
local cur_filter
if [[ "$comm" == '$' ]]; then
cur_filter='cur:\$'
else
cur_filter="cur:$comm"
fi
local cost_data
cost_data=$(hledger bal "${query_args[@]}" type:RX "$cur_filter" -c "$COMMODITY_FORMAT" -B -O tsv --layout bare --no-total -N 2>/dev/null | tail -n +2)
local base_currency
base_currency=$(echo "$cost_data" | head -n 1 | cut -f2)
paste \
<(echo "$balances" | awk -F'\t' -v c="$comm" '$2 == c') \
<(echo "$cost_data" | cut -f3) \
| while IFS=$'\t' read -r account _ amount cost; do
[[ -z "$account" || -z "$amount" ]] && continue
abs_cost="${cost#-}"
if [[ "$amount" == -* ]]; then
neg_amount="${amount#-}"
else
neg_amount="-$amount"
fi
if [[ "$comm" == "$base_currency" ]]; then
printf " %-60s %s%s\n" "$account" "$base_currency" "$neg_amount"
printf " %-60s %s%s\n" "$RETAINED_EARNINGS_ACCOUNT" "$base_currency" "$amount"
else
printf " %-60s %s %s @@ %s%s\n" "$account" "$neg_amount" "$comm" "$base_currency" "$abs_cost"
printf " %-60s %s %s @@ %s%s\n" "$RETAINED_EARNINGS_ACCOUNT" "$amount" "$comm" "$base_currency" "$abs_cost"
fi
done
) > "$tmpdir/$i" &
pids+=($!)
((i++))
done
wait "${pids[@]}"
cat "$tmpdir"/*
}
# Main execution
hledger -f "${LEDGER_FILE:-$HOME/.hledger.journal}" \
-f <(close_entries) \
bse "${query_args[@]}" "${report_args[@]}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment