Here's an EBNF approximation of hledger's journal format:
(* 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, [ cost-expr ] ;
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" ;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
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.