5. 核心语言语法精讲

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 的账户名是结构化的字符串,遵循特定的命名约定。

账户命名规则

一个有效的账户名由一个或多个由冒号(:)分隔的组件组成。每个组件必须以大写字母或数字开头,后面可以跟字母、数字或短横线(-)。

最重要的是,账户名的第一个组件必须是以下五种基本账户类型之一:

  1. Assets (资产)
  2. Liabilities (负债)
  3. Equity (权益)
  4. Income (收入)
  5. Expenses (支出)

这五种类型是复式记账法的基石,它们定义了账户的默认余额方向(例如,资产和支出类账户的增加通常记为正数,而负债、收入和权益类账户的增加通常记为负数)。

一些有效的账户名示例:

  • Assets:US:Bank:Checking
  • Liabilities:US:CreditCard:Amex
  • Expenses:Food:Groceries
  • Income: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 账户可能只允许 USDAAPL 进入。这是一个非常有用的防错机制,可以防止你意外地将错误的货币记入该账户。
  • ["BookingMethod"] (可选): 指定该账户的库存记账方法(如 "STRICT""NONE"),我们将在后续章节深入探讨。

Close 指令

当一个现实世界的账户被关闭后,你应该在账本中使用 Close 指令。

语法:

YYYY-MM-DD close <AccountName>

示例:

2023-12-31 close Assets:US:Bank:OldChecking

作用:

  1. 防止误用: 在关闭日期之后,任何试图向该账户记账的操作都会引发错误。
  2. 报表优化: 在生成报表时,可以轻松地过滤掉已关闭的账户,使报表更整洁。

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>: 商品代码。通常使用全大写字母,如 USDEURAAPLGOOG。可以包含数字和一些特殊字符(如 _-),但不能包含空格。
  • 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

结构详解:

  1. 日期 (YYYY-MM-DD): 交易发生的日期。
  2. 标志 ([txn|*|!]):
    • *: 表示已确认、清晰的交易(最常用)。
    • !: 表示未确认、需要核查的交易。
    • txn: 显式关键字,效果等同于 *,但很少使用。
  3. ["Payee"] (可选): 收款方或交易对手方,例如 "Whole Foods"
  4. ["Narration"] (可选): 交易的描述,例如 "Groceries"
  5. [metadata]... (可选): 可以在交易行直接附加元数据。
  6. 换行和缩进: 交易的条目(Postings)必须在下一行,并以空格或制表符缩进。
  7. 条目 (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): 可以是字符串(用引号括起来)、数字、账户名、商品代码、日期等。
  • 位置: 可以附加在任何指令(如 TransactionOpenCommodity)或其下的任何 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

pushtagpoptag 之间的所有交易都会自动被加上 #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 的一个核心设计哲学是,所有影响数据语义的配置都应该在输入文件中通过 optionplugin 指令明确指定,而不是通过命令行参数。这确保了加载文件的结果是确定的,与使用哪个报告工具无关。


至此,我们已经掌握了 Beancount 的核心语法元素。从指令的基本结构,到账户和商品的声明,再到最复杂的交易指令,以及如何通过元数据、标签、链接来丰富数据,最后是如何组织和配置你的账本。这些知识构成了你使用 Beancount 进行记账的基础。在下一章,我们将深入探讨交易管理的细节,特别是如何处理成本基础和库存,这是 Beancount 强大功能的关键所在。