5.核心语言语法精讲
Beancount 指令类型
在 Beancount 的世界里,一切皆指令(Directive)。你编写的每一行有效文本,本质上都是一个指令,告诉 Beancount 在特定的时间点发生了什么。这些指令构成了你财务数据的基石。理解这些指令的类型和结构,是掌握 Beancount 的第一步。
Beancount 的指令有一个统一的格式:以日期开头,后跟指令类型和参数。这种设计使得整个语言看起来非常整洁且可预测。主要的指令类型包括:
- Open: 开启一个账户。
- Close: 关闭一个账户。
- Commodity: 声明一种商品(或货币)。
- Transaction: 记录一笔交易,这是最核心的指令。
- Balance: 余额断言,用于核对账户余额。
- Pad: 自动填充,用于在余额断言失败时生成调整交易。
- Note: 附注,为账户添加一条带日期的注释。
- Document: 关联外部文件(如 PDF 对账单)。
- Price: 记录商品的价格。
- Event: 记录生活中的事件。
- Query: 在账本中嵌入一个查询。
- Custom: 自定义指令,供插件使用。
除此之外,还有一些不以日期开头的“元指令”(Meta-directives),它们影响文件的解析方式或全局配置:
- Option: 设置全局选项。
- Plugin: 加载并启用一个插件。
- Include: 包含另一个文件的内容。
- Pushtag/Poptag: 操作标签栈。
在本章中,我们将深入探讨其中最核心、最常用的几种指令的语法和用法。
Account 指令 syntax
在 Beancount 中,账户(Account)是组织财务数据的核心。与许多其他系统不同,Beancount 的账户名是结构化的字符串,遵循特定的命名约定。
账户命名规则
一个有效的账户名由一个或多个由冒号(:)分隔的组件组成。每个组件必须以大写字母或数字开头,后面可以跟字母、数字或短横线(-)。
最重要的是,账户名的第一个组件必须是以下五种基本账户类型之一:
Assets(资产)Liabilities(负债)Equity(权益)Income(收入)Expenses(支出)
这五种类型是复式记账法的基石,它们定义了账户的默认余额方向(例如,资产和支出类账户的增加通常记为正数,而负债、收入和权益类账户的增加通常记为负数)。
一些有效的账户名示例:
Assets:US:Bank:CheckingLiabilities:US:CreditCard:AmexExpenses:Food:GroceriesIncome:US:Acme:Salary
Open 指令
在向一个账户记录任何交易之前,你必须先使用 Open 指令“开启”它。这不仅是为了声明账户的存在,也是为了设置一些约束条件。
语法:
YYYY-MM-DD open <AccountName> [ConstraintCurrency, ...] ["BookingMethod"]
示例:
2023-01-01 open Assets:US:Bank:Checking
2023-01-01 open Assets:Investment:Brokerage USD, AAPL
2023-01-01 open Expenses:Food:Groceries
详解:
- 日期: 账户的开启日期。在此日期之前(严格来说是此日期的开始),该账户不能有任何余额。
open: 指令关键字。<AccountName>: 要开启的账户名。[ConstraintCurrency, ...](可选): 货币/商品约束。你可以指定一个或多个允许进入该账户的商品。例如,Assets:Investment:Brokerage账户可能只允许USD和AAPL进入。这是一个非常有用的防错机制,可以防止你意外地将错误的货币记入该账户。["BookingMethod"](可选): 指定该账户的库存记账方法(如"STRICT"或"NONE"),我们将在后续章节深入探讨。
Close 指令
当一个现实世界的账户被关闭后,你应该在账本中使用 Close 指令。
语法:
YYYY-MM-DD close <AccountName>
示例:
2023-12-31 close Assets:US:Bank:OldChecking
作用:
- 防止误用: 在关闭日期之后,任何试图向该账户记账的操作都会引发错误。
- 报表优化: 在生成报表时,可以轻松地过滤掉已关闭的账户,使报表更整洁。
Commodity 指令 syntax
Commodity 指令用于声明一种商品(货币、股票、基金等)。虽然 Beancount 会在你首次使用一种新商品时自动识别它,但使用 Commodity 指令是最佳实践。
语法:
YYYY-MM-DD commodity <CommoditySymbol>
metadata...
示例:
1998-07-22 commodity AAPL
name: "Apple Inc."
asset-class: "stock"
2010-01-01 commodity USD
name: "United States Dollar"
详解:
<CommoditySymbol>: 商品代码。通常使用全大写字母,如USD、EUR、AAPL、GOOG。可以包含数字和一些特殊字符(如_、-),但不能包含空格。metadata...(可选): 这是Commodity指令的主要价值所在。你可以附加任意的元数据(键值对)来描述该商品。这些元数据可以被插件或报告工具使用。例如,你可以添加name(全名)、asset-class(资产类别)、isin(国际证券识别编码)等。
这个指令本身不执行任何计算,它只是一个声明,为商品提供额外的上下文信息。
Transaction 指令 structure
Transaction 是 Beancount 中最核心、最复杂的指令,用于记录所有资金流动和事件。
基本结构:
YYYY-MM-DD [txn|*|!] ["Payee"] ["Narration"] [metadata]...
[flag] <Account> <Amount> [{cost}] [@ price] [metadata]...
[flag] <Account> <Amount> [{cost}] [@ price] [metadata]...
...
示例:
2023-10-26 * "Whole Foods" "Groceries"
Assets:US:Bank:Checking -87.45 USD
Expenses:Food:Groceries 87.45 USD
2023-10-27 ! "Vanguard" "Buy ETF"
Assets:Investment:Brokerage 10 VTI {123.45 USD}
Assets:Investment:Brokerage -1234.50 USD
结构详解:
- 日期 (
YYYY-MM-DD): 交易发生的日期。 - 标志 (
[txn|*|!]):*: 表示已确认、清晰的交易(最常用)。!: 表示未确认、需要核查的交易。txn: 显式关键字,效果等同于*,但很少使用。
["Payee"](可选): 收款方或交易对手方,例如"Whole Foods"。["Narration"](可选): 交易的描述,例如"Groceries"。[metadata]...(可选): 可以在交易行直接附加元数据。- 换行和缩进: 交易的条目(Postings)必须在下一行,并以空格或制表符缩进。
- 条目 (
Posting):[flag](可选): 可以为单个条目设置标志(如*或!)。<Account>: 条目所属的账户。<Amount>: 数额。可以是正数或负数。如果留空,则由 Beancount 自动计算(见下文)。[{cost}](可选): 成本基础,用于跟踪库存和计算损益。例如{123.45 USD}。[@ price](可选): 价格,用于货币转换。[metadata]...(可选): 可以在条目行附加元数据。
核心规则:
- 一个交易必须包含至少两个条目。
- 所有条目的加权和必须为零。这是复式记账的核心,也是 Beancount 严格强制执行的规则。
Metadata 和 tag syntax
元数据(Metadata)和标签(Tag)是为交易和条目添加额外信息的强大方式,极大地增强了账本的可查询性和灵活性。
Metadata 语法
元数据是以键值对(Key-Value)的形式附加在指令上的信息。
语法:
<Directive>
key: "value"
another_key: 123.45
...
示例:
2023-10-26 * "Whole Foods" "Groceries" ; <- 交易行的元数据
invoice: "INV-2023-1026" ; <- 条目行的元数据
Assets:US:Bank:Checking -87.45 USD
Expenses:Food:Groceries 87.45 USD
category: "Essential" ; <- 条目行的元数据
规则:
- 键 (Key): 必须以小写字母开头,可以包含字母、数字、短横线和下划线。
- 值 (Value): 可以是字符串(用引号括起来)、数字、账户名、商品代码、日期等。
- 位置: 可以附加在任何指令(如
Transaction、Open、Commodity)或其下的任何Posting上。
元数据是用户自定义的,Beancount 核心不会使用它们,但插件和 bean-query 可以利用它们进行过滤和聚合。
Tag 语法
标签是一种特殊的元数据,用于快速标记一组相关的交易,方便后续筛选。
语法:
YYYY-MM-DD [txn|*|!] ["Payee"] ["Narration"] #tag1 #tag2
...
示例:
2023-07-15 * "Delta Airlines" "Flight to Berlin" #travel #vacation-2023
Assets:US:Bank:Checking -580.00 USD
Expenses:Travel:Airfare 580.00 USD
标签栈 (pushtag / poptag):
如果你有一系列连续的交易都属于同一个标签,可以使用标签栈来避免重复输入。
pushtag #vacation-2023
2023-07-15 * "Delta Airlines" "Flight to Berlin"
...
2023-07-16 * "Hotel Berlin" "Accommodation"
...
poptag #vacation-2023
在 pushtag 和 poptag 之间的所有交易都会自动被加上 #vacation-2023 标签。
Link 指令 usage
链接(Link)与标签类似,但其语义不同。标签用于“分类”(grouping),而链接用于“关联”(chaining),将多个相关的交易串联起来。
语法:
YYYY-MM-DD [txn|*|!] ["Payee"] ["Narration"] ^link1 ^link2
...
示例: 假设你有一笔发票和后续的付款,可以用链接将它们关联起来。
2023-09-01 * "Client ABC" "Invoice #123 for September" ^invoice-123
Assets:AccountsReceivable 5000.00 USD
Income:Consulting -5000.00 USD
2023-09-15 * "Client ABC" "Payment for Invoice #123" ^invoice-123
Assets:US:Bank:Checking 5000.00 USD
Assets:AccountsReceivable -5000.00 USD
作用:
- 追踪: 你可以轻松地找到所有与
^invoice-123相关的交易,无论它们发生在何时。 - Web 界面: Beancount 的 Web 界面会为每个链接提供一个专门的页面,显示所有关联的交易,非常方便核对。
Include 指令 syntax
随着账本的增长,将所有内容放在一个文件中会变得难以管理。Include 指令允许你将账本拆分到多个文件中。
语法:
include "path/to/your/file.beancount"
示例: 假设你的文件结构如下:
main.beancount
accounts.beancount
transactions_2023.beancount
transactions_2022.beancount
main.beancount 的内容可能是:
; -*- mode: beancount; coding: utf-8; -*-
option "title" "My Personal Ledger"
option "operating_currency" "USD"
include "accounts.beancount"
include "transactions_2023.beancount"
include "transactions_2022.beancount"
要点:
- 路径可以是绝对路径或相对路径。如果是相对路径,则是相对于当前文件的路径。
- Beancount 会读取所有被包含文件的内容,就好像它们是写在
main.beancount里一样。 - 由于 Beancount 的指令顺序无关紧要(见下文),你可以自由地组织这些文件。
Options 指令 configuration
Option 指令用于设置 Beancount 的全局行为。它必须写在文件的顶层(不能在交易内部),并且通常放在文件的开头。
语法:
option "option_name" "option_value"
常用选项:
title: 设置账本的标题,会在 Web 界面和某些报告中显示。option "title" "My Personal Ledger"operating_currency: 设置你的主要记账货币。可以多次出现以指定多个主要货币。这会影响报告的显示方式。option "operating_currency" "USD" option "operating_currency" "EUR"documents: 指定一个目录,Beancount 会自动扫描该目录下的文件,并根据目录结构和文件名(以YYYY-MM-DD开头)生成Document指令。这是一个非常强大的功能,用于将账本与你的电子对账单、发票等文件关联起来。option "documents" "/home/user/finances/documents"plugin: 虽然plugin是一个独立的指令,但其作用是配置性的。它用于加载一个 Python 插件来扩展 Beancount 的功能。plugin "beancount.plugins.auto_accounts"
重要原则:
Beancount 的一个核心设计哲学是,所有影响数据语义的配置都应该在输入文件中通过 option 或 plugin 指令明确指定,而不是通过命令行参数。这确保了加载文件的结果是确定的,与使用哪个报告工具无关。
至此,我们已经掌握了 Beancount 的核心语法元素。从指令的基本结构,到账户和商品的声明,再到最复杂的交易指令,以及如何通过元数据、标签、链接来丰富数据,最后是如何组织和配置你的账本。这些知识构成了你使用 Beancount 进行记账的基础。在下一章,我们将深入探讨交易管理的细节,特别是如何处理成本基础和库存,这是 Beancount 强大功能的关键所在。