Upgrading to hledger 2
hledger 2 (the 1.99.x previews, and 2.0 when released) is a major update, whose biggest new feature is lot tracking and capital gains reporting. It reads hledger 1 data, and most hledger 1 users can switch to it with no changes. This page helps you check that, and fix anything that needs it. It covers the changes since hledger 1.52 that can affect existing setups; the release notes list all changes.
hledger 1 (1.52.x) continues to receive occasional fixes, and both versions can be installed side by side.
Upgrade steps
-
Install hledger 2 (see Install), keeping hledger 1 available under another name, eg by renaming its executable to
hledger1. -
Make sure your data files are backed up, or committed to version control.
-
Check your data with hledger 2:
$ hledger check(Add any extra checks you normally use, eg
hledger check -s.) -
If you import CSV files, run your usual import command with
--dry-runadded, which shows what would be imported without changing anything. Eg:$ hledger import --dry-run *.rules -
Compare your usual reports with hledger 1’s. Eg:
$ diff <(hledger1 accounts) <(hledger accounts) $ diff <(hledger1 bs) <(hledger bs) $ diff <(hledger1 is) <(hledger is)(This works in bash or zsh. Elsewhere, save each report to a file and compare the files.)
-
Fix anything found, using the sections below. And update any scripts or config which use changed commands or options.
Errors you might see
These are errors in hledger 2, which hledger 1 accepted. You probably won’t see these.
Lot errors.
hledger 1 accepted cost basis annotations like {$50} but ignored them.
hledger 2 uses them to track lots, and checks them:
-
“no … lots available for disposal”: a sale (a negative amount with a
{...}annotation) takes from a lot that the journal never acquires, often because the journal starts partway through your investment history. Add the missing purchases, or an opening entry which acquires the lots, giving each lot’s original date and cost:2026-01-01 opening lots assets:stocks 10 AAPL {2024-03-15, $50} equity:opening/closing balances(A dated annotation like this needs hledger 2; hledger 1 can’t read it.)
-
“This acquire posting’s cost basis ($50) differs from its transacted cost ($55)”: a purchase like
10 AAPL {$50} @ $55. These must now agree. Usually@is what you paid; make the{}amount match it, or remove the{}. If the difference is real (eg a gift with a carried-over cost basis), record the cost basis as the cost, and the difference in another posting, as in this gift example. -
Account names ending in
{...}, likeassets:{savings}, are reserved for lot subaccounts, and are rejected unless the braces contain a valid lot name. Rename such accounts.
If you don’t want to deal with these now, you can run hledger with --ignore-lots or -I.
This skips lot tracking and lot error checking, giving results close to hledger 1’s.
(-I also skips balance assertions.)
Decimal marks.
When a decimal-mark directive (or CSV rule) is in effect,
a number using the other decimal mark (eg 1,000.00 when decimal-mark , is declared),
or using the declared mark more than once (eg 1,2,34),
is now an error. Fix the number, or the directive.
CSV rules comments.
In CSV rules files, a line beginning with * is no longer a comment, and is reported as an error.
Use # or ; to begin comment lines.
Report differences
These changes can cause reports to differ, without causing an error. Comparing reports from both versions, as in the upgrade steps above, will show them.
-
Config file default command. A config file can no longer choose the command to run. A bare word at the top of its general section, which hledger 1 used as the default command, is now read as a query argument, which quietly filters every report. Remove it, and give the command on the command line.
-
Realised gains. If your journal has sales with cost basis annotations, like
-5 AAPL {$50} @ $70, hledger 2 calculates the realised gain and posts it to a gain account (revenues:gainby default). So your income statement shows the gain, and your balance sheet totals change by the same amount. This is usually what you want;-Igives hledger 1’s results. -
Aliases and
apply account. Whenaliasdirectives andapply accountaffect the same entries, the aliases now apply first, to account names as written, and the parent account is added afterward (as in Ledger). hledger 1 did it the other way round. So aliases written with the parent account (alias business:checking = ...) no longer match, and aliases written without it (alias checking = ...) now do. Update the aliases if you need to. (--aliasoptions are unaffected; they still apply last.) -
CSV rules. When a directive like
date-formatorseparatoris written more than once, the last one now wins (except forskip), as documented. If you wrote one before anincludeto override the included file’s, move it after theinclude. -
CSV data directory. The
sourcerule now looks for bare file names and relative paths in adata/directory next to the main journal, then in~/Downloads. (Paths beginning with./or../are still relative to the rules file.) Thearchiverule now saves files indata/archive/there, instead of in adata/directory next to the rules file. -
print layout.
printnow aligns amounts by their decimal mark.--layout=hledger1gives the old layout. -
Decimal places. Amounts which hledger infers no longer affect a commodity’s display precision, so some reports show fewer decimal places.
-
accounts and payees. Query terms which only match transactions, like
date:ordesc:, now exclude declared accounts or payees which aren’t used. Andpayees payee:REGEXnow matches declared payees. -
any: and all: queries. These now also work in posting reports like
registerandbalance, not just inprint. -
commodities –used. This now shows only commodities used in transactions;
--pricedshows those used in P directives.
Changed commands and options
If your scripts, aliases or config files use these, update them:
--tldris now--examples.stats -1is nowstats --oneline.--verbose-tagsis now an option ofprintandrewriteonly.- The
demoandcommandscommands have been removed. (hledger help commandslists the commands.) - hledger-web, when listening on a non-local address (set with
--host), is now read-only by default. Add--allow=editto allow editing.
What’s new
Some of the most useful new features in hledger 2:
- Speed: 2-3x faster than hledger 1.52.
- Localisation: report headings and hledger-ui/hledger-web’s UI can be shown in other languages, with
--lang(currently en, de, zh). - Lot tracking and capital gains: a
lotstag tracks each purchase’s cost basis and calculates gains when you sell, with several cost basis methods (FIFO, LIFO, HIFO, AVERAGE..), and the new holdings report shows your investments, their value and gains. - Simpler importing: the new get command fetches new transactions and market prices,
and with a conventional
data/,rules/,prices/layout,hledger importneeds no arguments (see Finding the data). - CSV rules: merge combines several CSV records into one transaction, functions transform field values, and CSV or rules files can be included in a journal.
- Aliases: command aliases in your config file, and commodity aliases.
- Find anywhere:
::TEXTmatches text in any field, as you’d see it inprintoutput. - Compact transactions list: the new transactions (tx) command lists transactions one per line.
- hledger-ui updates automatically when your files change, by default.
- hledger-web gains balance reports, paging and a dark mode.
For all changes, see the release notes.
Turning on lot tracking
Once your data works with hledger 2, you can let it track your investment lots and calculate gains for you.
Usually you just add a lots tag to each investment commodity’s declaration:
commodity AAPL ; lots:
One thing to watch for: in hledger 1 a sale was often recorded at its original cost, with a written gain posting:
2026-02-01 sell
assets:stocks -5 AAPL @ $50 ; at original cost
assets:cash $350
revenues:gain $-100
With lot tracking, @ is read as the selling price, so this entry becomes unbalanced.
Write the selling price instead:
2026-02-01 sell
assets:stocks -5 AAPL @ $70
assets:cash $350
revenues:gain $-100
hledger then checks the written gain against its own calculation; or you can leave out the gain posting, and let hledger add it.
Switching back to hledger 1
Journals written for hledger 2 are mostly readable by hledger 1, with these caveats:
- hledger 1 ignores cost basis annotations.
An acquisition written as
10 AAA {$50}means10 AAAin hledger 1. So you may need to write the transacted cost too:10 AAA {$50} @ $50. - hledger 1 rejects
type: Uaccount declarations. (hledger 2 supports them but doesn’t use them.) - hledger 1 does not infer gain postings, and balances disposals at transacted cost,
so a disposal entry with an explicit gain posting (like
revenues:gain $-50) is unbalanced in hledger 1. Omit the gain posting (or its amount) instead. See Recording gains. - hledger 2’s
print --lotsoutput includes lot subaccount names likeassets:stocks:{2026-01-01, $50}; hledger 1 reads these as ordinary subaccounts. - The
lotstag on commodity and account declarations is an ordinary tag in hledger 1, and is ignored.
Keeping data usable with both
To keep the same journal working in both hledger 1 and hledger 2:
- write acquisitions with both cost basis and cost, with the same amount in each:
10 AAA {$50} @ $50 - write disposals without a gain posting (then hledger 2 infers it, and hledger 1 shows no gain)
- don’t declare
type: Uaccounts (hledger 1 rejects them, hledger 2 doesn’t need them) - avoid account names ending in
{...} - after changes, check the journal with both versions, eg
hledger checkandhledger1 check