This post is about a DIY way to split bills with friends. It sets itself apart from a smartphone app solution in the following aspects:

  • It combines small programs that are readily available on UNIX-based operating systems (Linux, MacOS, etc);
  • Its ledgers are stored in plain-text files;
  • It operates on the command-line;
  • It uses shell scripts to process data and produce html reports.

Essentially, a way to split bills UNIX style.

Double entry bookkeeping

This is the standard bookkeeping method in accounting. I’m by no means an accounting professional but here is how I make sense of it for this simple bill-splitting use case: Essentially, every transaction should involve at least two accounts and at least one positive and one negative records, summing up to zero. Say Alice pays $90 for the lunch of the group (Alice, Bob and Carol). The two accounts involved here are Alice’s income account and the expense account of the rest of the group - so actually four of them:

  • Alice’s income;
  • Alice’s expense;
  • Bob’s expense;
  • Carol’s expense.

When Alice pays, the convention is that this incurs a negative entry in her income account (I guess that makes sense). On the opposite side, consuming on something results in a positive entry in the expense account. So the lunch transaction can be booked with the following entries:

Negative Positive
Alice’s income: -$90 Alice’s expense: +$30
Bob’s expense: +$30
Carol’s expense: +$30

All values balance out, which is very important (satisfying the accounting equation). Now let’s say the place where the group goes for dinner only accepts cash and the bill is $150. None of them has enough cash so they combine what they have:

Negative Positive
Alice’s income: -$10 Alice’s expense: +$50
Bob’s income: -$120 Bob’s expense: +$50
Carol’s income: -$20 Carol’s expense: +$50

With every shared bill booked this way, finding how much a person owes (or is owed) becomes a simple matter of adding all the entries from that person’s accounts:

Settling-up
Alice -$20 (= -$90-$10+$30+$50)
Bob -$40 (= -$120+$30+$50)
Carol +$60 (= -$20+$30+$50)

A negative value means the amount one is owed. In this case, Carol needs to pay Alice $20 and Bob $40.

Two “read more” links on double entry bookkeeping: wikipedia’s article, beancount’s article.

Beancount, plain-text accounting

The free software I use that does the bookkeeping and the math above is Beancount. Its killer feature, for me, is the plain-text ledger file (in contrast with GnuCash, for example). The ledger file (with .beancount extension) for the example above would look like this (note that ;; starts a comment line):

;; Opening accounts

2024-05-01 open Income:Alice
2024-05-01 open Income:Bob
2024-05-01 open Income:Carol
2024-05-01 open Expenses:Lunch:Alice
2024-05-01 open Expenses:Lunch:Bob
2024-05-01 open Expenses:Lunch:Carol
2024-05-01 open Expenses:Dinner:Alice
2024-05-01 open Expenses:Dinner:Bob
2024-05-01 open Expenses:Dinner:Carol

;; Ledger starts

2024-05-02 * "Restaurant A" "Lunch"
  Income:Alice                                -90.00 CAD
  Expenses:Lunch:Alice                         30.00 CAD
  Expenses:Lunch:Bob                           30.00 CAD
  Expenses:Lunch:Carol                         30.00 CAD
2024-05-02 * "Restaurant B" "Dinner"
  Income:Alice                                -10.00 CAD
  Income:Bob                                 -120.00 CAD
  Income:Carol                                -20.00 CAD
  Expenses:Dinner:Alice                        50.00 CAD
  Expenses:Dinner:Bob                          50.00 CAD
  Expenses:Dinner:Carol                        50.00 CAD

And here is a greatly simplified version thanks to beancount’s smart plugins:

;; This plugin splits expense equally automatically
plugin "beancount.plugins.split_expenses" "Alice Bob Carol"

;; This plugin opens accounts automatically
plugin "beancount.plugins.auto_accounts"

2024-05-01 * "Restaurant A" "Lunch"
  Income:Alice                                -90.00 CAD
  Expenses:Lunch
2024-05-01 * "Restaurant B" "Dinner"
  Income:Alice                                -10.00 CAD
  Income:Bob                                 -120.00 CAD
  Income:Carol                                -20.00 CAD
  Expenses:Dinner

Now you can run bean-query on your ledger file, which is Beancount’s own SQL-like query client. For example, if Alice wants to see her part of the ledger:

$ bean-query file.beancount 'SELECT date,account,payee,position WHERE account ~ "Alice"'

   date           account           payee      position
---------- --------------------- ------------ ----------
2024-05-01 Income:Alice          Restaurant A -90.00 CAD
2024-05-01 Expenses:Lunch:Alice  Restaurant A  30.00 CAD
2024-05-01 Income:Alice          Restaurant B -10.00 CAD
2024-05-01 Expenses:Dinner:Alice Restaurant B  50.00 CAD

And if Alice asks for the net amount:

$ bean-query file.beancount 'SELECT sum(position) WHERE account ~ "Alice"'

sum_positi
----------
-20.00 CAD

So she is being owed 20 bucks. Check out here for more supported queries.

Editing-wise, Vim users should check out the beancount plugin. Emacs support is also available and probably comes with more goodies since Beancount’s author uses Emacs and contributes to the plugin.

Of course, this toy bill-splitting example does very little justice to the power of Beancount. For more possibilities, see the cookbooks & examples section in its documentation.

Processing and reporting with a shell script

After editting the ledger file, I use a shell script to process the data and generate an html report. There is a css stylesheet that beautifies the report but I tried to make it minimalistic. See here for a demo of a ledger splitting the bills during a group trip. Another example would be sharing house utility bills with your roommates. See here for an example.

The small set of scripts (including the two example beancount files in the previous paragrah) is downloadable here. And here is what you are downloading:

.
├── fold.awk
├── house.beancount
├── ledger.css
├── ledger_update
└── trip.beancount

1 directory, 5 files

The main script ledger_update takes two arguments: (1) the main beancount file and (2) the user-specified title of the html report. For the house utility ledger, you may run it like this

./ledger_update house.beancount "Sharing utilities with roommates"

This will produce the ledger page as a ledger.html.

Remark: the fold.awk script called in ledger_update is some awk magic that folds the ledger to de-clutter the html page.

Finally, the full script

#!/bin/sh

date=$(date '+%D')
doc=$1
title=$2

echo "<!DOCTYPE html>" > ledger.html

echo "<html lang='en'>" >> ledger.html
echo "<head>" >> ledger.html
echo "<title>${title}</title>" >> ledger.html
echo "<meta charset=\"utf-8\"/>" >> ledger.html
echo "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">" >> ledger.html
echo "<link rel=\"stylesheet\" type=\"text/css\" href=\"ledger.css\" />" >> ledger.html
echo "</head>" >> ledger.html
echo "<body>" >> ledger.html
echo "<h1>" >> ledger.html
echo "${title} <span style=\"font-size:60%;\">powered by <a href=\"https://github.com/beancount/beancount\">Beancount</a></span>" >> ledger.html
echo "</h1>" >> ledger.html
echo "<hr>" >> ledger.html
echo "<p>Go straight to the <a href=\"#your-owing\">Balances</a> section for: <ul><li> the amount of money you are owed or owing;</li><li>your personal ledger.</li></ul></p>" >> ledger.html

#==========================================
# our ledger
echo "<h2>" >> ledger.html
echo "Our ledger" >> ledger.html
echo "</h2>" >> ledger.html

#------------------------------------------
## stuff shared by all
echo "<h3>" >> ledger.html
echo "Stuff shared by all" >> ledger.html
echo "</h3>" >> ledger.html

echo "<pre>" >> ledger.html
echo "<label for=\"unfold1\">Read unfolded ledger by checking here -> </label><input id=\"unfold1\" type=\"checkbox\">" >> ledger.html
echo "<div class=\"pre-box\">" >> ledger.html
echo "<div>" >> ledger.html
echo "$ bean-query ${doc} 'SELECT date,account,payee,position WHERE narration ~ \"for all\"'" >> ledger.html
echo "</div>" >> ledger.html
echo "<div class=\"ledger\" id=\"full1\">" >> ledger.html
bean-query ${doc} 'SELECT date,account,payee,position WHERE narration ~ "for all"' >> ledger.html
echo "</div>" >> ledger.html
echo "<div class=\"ledger\" id=\"brief1\">" >> ledger.html
bean-query ${doc} 'SELECT date,account,payee,position WHERE narration ~ "for all"' | awk -f fold.awk >> ledger.html
echo "</div>" >> ledger.html
echo "</div>" >> ledger.html
echo "</pre>" >> ledger.html

#------------------------------------------
## stuff shared by some
echo "<h3>" >> ledger.html
echo "Stuff shared by some" >> ledger.html
echo "</h3>" >> ledger.html

echo "<pre>" >> ledger.html
echo "<label for=\"unfold2\">Read unfolded ledger by checking here -> </label><input id=\"unfold2\" type=\"checkbox\">" >> ledger.html
echo "<div class=\"pre-box\">" >> ledger.html
echo "<div>" >> ledger.html
echo "$ bean-query ${doc} 'SELECT date,account,payee,position WHERE narration ~ \"&\"'" >> ledger.html
echo "</div>" >> ledger.html
echo "<div class=\"ledger\" id=\"full2\">" >> ledger.html
bean-query ${doc} 'SELECT date,account,payee,position WHERE narration ~ "&"' >> ledger.html
echo "</div>" >> ledger.html
echo "<div class=\"ledger\" id=\"brief2\">" >> ledger.html
bean-query ${doc} 'SELECT date,account,payee,position WHERE narration ~ "&"' | awk -f fold.awk >> ledger.html
echo "</div>" >> ledger.html
echo "</div>" >> ledger.html
echo "</pre>" >> ledger.html

#==========================================
# balances
echo "<h2 id="your-owing">" >> ledger.html
echo "Balances" >> ledger.html
echo "</h2>" >> ledger.html
echo "<p>A negative number means you are being owed!</p>" >> ledger.html

bean-query ${doc} 'SELECT DISTINCT account' | tail -n +3 | rev | cut -d':' -f 1 | sed 's/^[[:space:]]*//g' | rev | sort | uniq -d | {
	while read p; do
		echo "<h3>" >> ledger.html
		echo $p | sed 's/\([a-zA-Z]\)\([1-9]\)/\1 \2/g' >> ledger.html
		echo "</h3>" >> ledger.html

		echo "<pre>" >> ledger.html
		echo "<div class=\"pre-box\">" >> ledger.html
		echo "<div>" >> ledger.html
		echo "$ bean-query ${doc} 'SELECT sum(position) WHERE account ~ \":${p}$\"'" >> ledger.html
		echo "</div>" >> ledger.html
		echo "<div class=\"ledger\">" >> ledger.html
		bean-query ${doc} "SELECT date,account,payee,position WHERE account ~ \":${p}$\"" >> ledger.html
		echo "</div>" >> ledger.html
		echo "<div>" >> ledger.html
		bean-query ${doc} "SELECT sum(position) WHERE account ~ \":${p}$\"" | sed 's/sum.*/ Balance/g' >> ledger.html
		echo "</div>" >> ledger.html
		echo "</div>" >> ledger.html
		echo "</pre>" >> ledger.html
	done
}

#==========================================
# Beancount sourcefile

echo "<h2>" >> ledger.html
echo "Beancount source file" >> ledger.html
echo "</h2>" >> ledger.html
echo "<pre>" >> ledger.html
echo "<label for=\"source\">Read full source file by checking here -> </label><input id=\"source\" type=\"checkbox\">" >> ledger.html
echo "<div class=\"pre-box\">" >> ledger.html
echo "<div class=\"ledger\" id=\"verbatim\">" >> ledger.html
cat $doc >> ledger.html
echo "</div>" >> ledger.html
echo "<div class=\"ledger\" id=\"dotdotdot\">" >> ledger.html
echo "..." >> ledger.html
echo "</div>" >> ledger.html
echo "</div>" >> ledger.html
echo "</pre>" >> ledger.html

#==========================================
# Last updated

echo "<hr>" >> ledger.html
echo "Last updated: ${date}" >> ledger.html
echo "</body>" >> ledger.html