Skip to content

Migrating from hledger

This guide helps you migrate from hledger to rustledger's beancount format.

Key Differences

hledger uses a format compatible with ledger-cli. Beancount has stricter syntax requirements.

Syntax Comparison

FeaturehledgerBeancount
File extension.journal.beancount
Account opensOptionalRequired
Payee formatAfter dateIn quotes
AmountsCan auto-balanceMust balance
Comments; or #;
Directivesaccount, commodityopen, commodity

Example Comparison

hledger:

journal
2024-01-15 Coffee Shop
    expenses:food:coffee     $5.00
    assets:cash

2024-01-15 balance assets:checking  = $1234.56

Beancount:

beancount
2020-01-01 open Assets:Cash              USD
2020-01-01 open Assets:Checking          USD
2020-01-01 open Expenses:Food:Coffee

2024-01-15 * "Coffee Shop"
  Expenses:Food:Coffee     5.00 USD
  Assets:Cash             -5.00 USD

2024-01-15 balance Assets:Checking  1234.56 USD

Migration Steps

1. Export from hledger

bash
hledger print -f main.journal > export.journal

2. Convert Account Names

Beancount accounts must be Title:Case:

expenses:food:coffee  →  Expenses:Food:Coffee
assets:checking       →  Assets:Checking

Script to help:

bash
# Convert account names to Title Case
sed -E 's/([a-z]+:)/\u\1/g' export.journal

3. Generate Account Opens

bash
hledger accounts -f main.journal | while read account; do
  # Convert to Title Case
  titled=$(echo "$account" | sed 's/\(^\|:\)\([a-z]\)/\1\u\2/g')
  echo "2020-01-01 open $titled"
done > accounts.beancount

4. Convert Transactions

Key changes:

  1. Add quotes around payees

    2024-01-15 Coffee Shop
    
    2024-01-15 * "Coffee Shop"
  2. Convert commodities

    $5.00  →  5.00 USD
    €10    →  10.00 EUR
  3. Add explicit balance

    expenses:food    $5.00
    assets:cash
    
    Expenses:Food     5.00 USD
    Assets:Cash      -5.00 USD
  4. Convert balance assertions

    2024-01-15 balance assets:checking  = $1234.56
    
    2024-01-15 balance Assets:Checking  1234.56 USD

5. Validate

bash
rledger check ledger.beancount

Command Mapping

hledgerrustledger
hledger balrledger report balances
hledger regrledger report journal
hledger printrledger format
hledger accountsrledger report accounts
hledger bsrledger report balsheet
hledger isrledger report income
hledger checkrledger check

Query Differences

hledger uses its own query syntax; rustledger uses BQL:

hledger:

bash
hledger reg expenses:food date:2024

rustledger (BQL):

bash
rledger query ledger.beancount \
  "SELECT date, narration, position
   WHERE account ~ 'Expenses:Food' AND year(date) = 2024"

Shell Aliases

Recreate hledger-style commands:

bash
# ~/.bashrc
export LEDGER_FILE="$HOME/finances/ledger.beancount"

alias bal='rledger report balances "$LEDGER_FILE"'
alias bs='rledger report balsheet "$LEDGER_FILE"'
alias is='rledger report income "$LEDGER_FILE"'

reg() {
  if [ -n "$1" ]; then
    rledger report journal -a "$1" "$LEDGER_FILE"
  else
    rledger report journal "$LEDGER_FILE"
  fi
}

hledger Features

Supported Differently

hledger FeatureBeancount Equivalent
account directiveopen directive
Lowercase accountsTitle Case required
Auto-balanceExplicit amounts
Balance assertionsbalance directive
includeinclude
TagsTags and links

Not Supported

FeatureAlternative
Virtual accountsUse regular accounts with tags
Periodic transactionsManual entry or scripts
alias directiveNone (use search-replace)
CSV rulesrledger extract with config

Beancount Extras

Features available in beancount but not hledger:

  • Booking methods: FIFO, LIFO, STRICT, etc. for investments
  • Cost basis tracking: Built-in capital gains handling
  • Plugins: Validation and transformation plugins
  • BQL: SQL-like query language

Conversion Script

Basic conversion script:

python
#!/usr/bin/env python3
"""Convert hledger journal to beancount format."""

import re
import sys

def title_case(account):
    """Convert account:name to Account:Name."""
    return ':'.join(word.title() for word in account.split(':'))

def convert_amount(amount):
    """Convert $5.00 to 5.00 USD."""
    match = re.match(r'\$([0-9.,]+)', amount)
    if match:
        return f"{match.group(1)} USD"
    match = re.match(r'([0-9.,]+)', amount)
    if match:
        return f"{match.group(1)} EUR"
    return amount

def convert_line(line):
    """Convert a single line."""
    # Skip comments
    if line.strip().startswith(';') or line.strip().startswith('#'):
        return line

    # Convert posting
    match = re.match(r'(\s+)([a-z:]+)\s+(.+)', line)
    if match:
        indent, account, amount = match.groups()
        return f"{indent}{title_case(account)}  {convert_amount(amount.strip())}\n"

    # Convert transaction header
    match = re.match(r'(\d{4}-\d{2}-\d{2})\s+(.+)', line)
    if match:
        date, payee = match.groups()
        return f'{date} * "{payee.strip()}"\n'

    return line

if __name__ == '__main__':
    for line in sys.stdin:
        print(convert_line(line), end='')

Usage:

bash
python3 convert.py < main.journal > ledger.beancount

Note: This is a starting point. Manual review and cleanup will be needed.

Troubleshooting

Account Name Errors

Ensure accounts are Title:Case:

Error: Invalid account name 'expenses:food'
Fix: Use 'Expenses:Food'

Missing Open Directives

Generate them:

bash
rledger doctor missing-open ledger.beancount

Balance Assertion Failures

Check date format and amount:

beancount
; hledger: balance assets:checking  = $1234.56
; beancount:
2024-01-15 balance Assets:Checking  1234.56 USD

See Also