journal format
Key features of hledger's journal syntax:
- Indentation matters: Postings must be indented (at least one space or tab)
- Double-space rule: In many contexts, a double space separates fields (like between account name and amount)
- Status marks: * for cleared, ! for pending
- Virtual postings: Accounts in parentheses (account) or brackets [account] denote virtual postings
- Comments: Lines starting with ;, #, *, %, or any indented line starting with ;
- Balance assertions: =, =, ==, == variants
- Costs: @ for unit cost, @@ for total cost, {} for lot costs
- Flexible amounts: Commodity symbols can appear before or after quantities; different number styles supported
Grammar
A rough approximation of hledger's journal format. For documentation only. May not be completely in sync with the implementation (JournalReader.hs, Common.hs).
This EBNF is simplified and doesn't capture all edge cases (like virtual postings, lot dates, multipliers in transaction modifiers, etc.) but covers the core syntax elements.
See also: hledger.m4.md > Journal > Journal cheatsheet
(* Journal Structure *)
journal = { journal-item } ;
journal-item = transaction
| periodic-transaction
| transaction-modifier
| directive
| market-price-directive
| comment-line
| blank-line ;
(* Comments *)
comment-line = ( ";" | "#" | "*" | "%" ), { any-char - newline }, newline ;
line-comment = ";", { any-char - newline } ;
blank-line = [ whitespace ], newline ;
(* Transactions *)
transaction = simple-date, [ "=", secondary-date ], [ whitespace ],
[ status ], [ code ], [ description ], [ line-comment ], newline,
[ transaction-comment ],
{ posting } ;
periodic-transaction = "~", [ whitespace ], period-expr, [ whitespace ],
[ status ], [ code ], [ description ], [ line-comment ], newline,
[ transaction-comment ],
{ posting } ;
transaction-modifier = "=", [ whitespace ], query-expr, [ line-comment ], newline,
[ transaction-comment ],
{ posting } ;
simple-date = date-year, date-sep, date-month, date-sep, date-day ;
secondary-date = simple-date ;
date-year = digit, digit, digit, digit ;
date-month = digit, [ digit ] ;
date-day = digit, [ digit ] ;
date-sep = "/" | "-" | "." ;
status = "*" (* cleared *)
| "!" (* pending *)
;
code = "(", { any-char - ")" }, ")" ;
description = { any-char - ";" - newline } ;
transaction-comment = { comment-line-indented } ;
comment-line-indented = whitespace-1, ";", { any-char - newline }, newline ;
(* Postings *)
posting = whitespace-1, [ status ], [ whitespace ],
account-name, [ whitespace ],
[ amount-expr ], [ whitespace ],
[ balance-assertion ], [ whitespace ],
[ line-comment ], newline,
[ posting-comment ] ;
posting-comment = { comment-line-indented } ;
account-name = account-name-component, { ":", account-name-component } ;
account-name-component = account-char, { account-char } ;
account-char = any-char - ";" - newline - ":" - " " (* two spaces *) ;
(* Amounts *)
amount-expr = amount-sum, [ cost-expr ] ;
(* An amount with arithmetic, in posting amounts only. Evaluated when parsed; only the result is kept. *)
amount-sum = amount-product, { [ whitespace ], ( "+" | "-" ), [ whitespace ], amount-product } ;
amount-product = amount-factor, { [ whitespace ], ( "*" | "/" ), [ whitespace ], amount-factor } ;
amount-factor = amount
| [ "-" | "+" ], "(", [ whitespace ], amount-sum, [ whitespace ], ")" ;
(* + and - need the same commodity on both sides, or a number without a commodity symbol on one side,
which takes the other side's commodity. * needs a number without a commodity symbol on one side,
/ needs one on the right. If the result has no commodity, the default commodity (D) is applied.
The result of * or / is exact while the entry is read; after its last posting, it is rounded
(banker's rounding) to the entry's balancing precision for its commodity: the most decimal places of
that commodity's posting amounts, as written (including amounts inside expressions and balance
assignment amounts, not costs).
It gets that precision, so what is stored is what print shows. In periodic transaction rules and
auto posting rules, it is rounded to its own precision. Commodity directives don't affect this.
In a posting amount, an unquoted commodity-symbol can't begin with "(" before the quantity,
or with "/" after it, and inside parentheses can't contain ")". *)
amount = [ "-" | "+" ], commodity-symbol, quantity-no-sep
| [ "-" | "+" ], quantity, [ whitespace ], commodity-symbol ;
quantity = { digit | digit-group-mark }, decimal-mark, { digit }
| { digit | digit-group-mark }
| decimal-mark, digit, { digit } ;
quantity-no-sep = { digit }, [ decimal-mark, { digit } ] ;
commodity-symbol = ( letter, { letter | digit | symbol-char } )
| ( symbol-char, { symbol-char } )
| ( '"', { any-char - '"' }, '"' ) ;
decimal-mark = "." | "," ;
digit-group-mark = "," | "." | " " ;
(* Cost notation *)
cost-expr = unit-cost | total-cost | lot-cost ;
unit-cost = "@", [ whitespace ], amount ;
total-cost = "@@", [ whitespace ], amount ;
lot-cost = "{", [ whitespace ], [ "=" ], [ whitespace ], amount, [ whitespace ], "}"
| "{{", [ whitespace ], [ "=" ], [ whitespace ], amount, [ whitespace ], "}}"
| "[", simple-date, "]" ;
(* Balance Assertions *)
balance-assertion = "=", [ "=" ], [ whitespace ], amount
| "=", [ "=" ], [ "=" ], [ whitespace ], amount (* == for subaccount inclusive *) ;
(* Directives *)
directive = [ "!" | "@" ], directive-keyword ;
directive-keyword = account-directive
| commodity-directive
| default-commodity-directive
| default-year-directive
| alias-directive
| end-aliases-directive
| payee-directive
| tag-directive
| apply-account-directive
| end-apply-account-directive
| include-directive
| decimal-mark-directive ;
account-directive = "account", whitespace-1, account-name,
[ line-comment ], newline,
{ subdirective } ;
commodity-directive = "commodity", whitespace-1,
( amount | commodity-symbol ),
[ line-comment ], newline,
{ subdirective } ;
subdirective = whitespace-1, ( "format", whitespace-1, amount
| any-text ), newline ;
default-commodity-directive = "D", whitespace-1, amount, newline ;
default-year-directive = ( "Y" | "year" | "apply year" ),
[ whitespace ], date-year, newline ;
alias-directive = "alias", whitespace-1, ( account-name, "=", account-name
| "/", regex, "/", "=", replacement ), newline ;
end-aliases-directive = "end", whitespace-1, "aliases", newline ;
payee-directive = "payee", whitespace-1, ( quoted-text | text ), [ line-comment ], newline ;
tag-directive = "tag", whitespace-1, tag-name, [ line-comment ], newline ;
apply-account-directive = "apply", whitespace-1, "account", whitespace-1, account-name, newline ;
end-apply-account-directive = "end", whitespace-1, "apply", whitespace-1, "account", newline ;
include-directive = "include", whitespace-1, file-path, [ line-comment ], newline ;
decimal-mark-directive = "decimal-mark", whitespace-1, ( "." | "," ), newline ;
(* Market prices *)
market-price-directive = "P", [ whitespace ], datetime,
whitespace-1, commodity-symbol,
whitespace-1, amount, newline ;
datetime = simple-date, [ whitespace-1, time ] ;
time = digit, digit, ":", digit, digit, [ ":", digit, digit ], [ timezone ] ;
timezone = ( "+" | "-" ), digit, digit, digit, digit ;
(* Period expressions for periodic transactions *)
period-expr = interval, [ whitespace-1, "from", whitespace-1, simple-date ],
[ whitespace-1, "to", whitespace-1, simple-date ]
| simple-date, [ whitespace-1, "to", whitespace-1, simple-date ]
| "every", whitespace-1, interval ;
interval = "daily" | "weekly" | "monthly" | "quarterly" | "yearly"
| "every", whitespace-1, number, whitespace-1, ( "days" | "weeks" | "months" | "quarters" | "years" )
| "every", whitespace-1, nth, whitespace-1, day-of-week,
[ whitespace-1, "of", whitespace-1, "month" ] ;
day-of-week = "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday" ;
nth = "1st" | "2nd" | "3rd" | digit, "th" ;
(* Common elements *)
query-expr = any-text ;
file-path = any-text ;
tag-name = letter, { letter | digit | "-" | "_" } ;
quoted-text = '"', { any-char - '"' }, '"' ;
text = { any-char - ";" - newline } ;
any-text = { any-char - newline } ;
number = digit, { digit } ;
whitespace = { " " | tab } ;
whitespace-1 = ( " " | tab ), { " " | tab } ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
letter = "a" | ... | "z" | "A" | ... | "Z" ;
symbol-char = "$" | "£" | "€" | "¥" | etc. ;
any-char = ? any character ? ;
newline = "\n" ;
tab = "\t" ;