[Pandoc 2.8 新機能] 長いオプションをファイルにまとめるDefault file


この記事は Pandoc Advent Calendar 2019 の9日目です。
現在登録されている次回は、12/12の高見知英さんです。

(カレンダーの前後に空きがあります。今後の追加登録はもちろん、空いてる過去の日へも後追い参加も歓迎します!)


文書変換ツール Pandoc のバージョン 2.8 が2019年11月22日にリリースされました。

(2019年12月9日時点のPandoc最新バージョンは2.8.1です)

Pandoc 2.8 では、Defailt files (-d / --defaults) という機能が追加されました。
今回はこの機能について説明します。

Pandocの地味なつらさ:オプションが長い

Pandocを“まともに”使うと、pandocコマンドに与えるオプションがだんだん長くなってきます。

たとえば、日本語で書かれたMarkdown文書をPDFに変換したいとき(LaTeXエンジンを使用)を考えます。
デフォルトのPandocでは、そのままでは和文にあたる文字をうまく出力してくれません。

そこで次のようにコマンドラインオプションでLaTeXエンジンとその各種設定を指定してやると、
日本語で書かれたPDFがうまく出力されます。
(詳細: メモ: Pandoc+LaTeXで気軽に日本語PDFを出力する

$ pandoc sample.md -o sample.pdf --pdf-engine=lualatex -V documentclass=bxjsarticle -V classoption=pandoc
  • sample.md: 入力ファイル (Pandoc's Markdown)
  • -o sample.pdf: 出力ファイル (PDF)
  • --pdf-engine=lualatex: PDFの生成にLuaLaTeXを指定
  • -V: テンプレート変数
    • -V documentclass=bxjsarticle: LaTeX文書クラスに bxjsarticle (BXjscls) を指定
      • \documentclass{bxjsarticle} に相当
    • -V classoption=pandoc: LaTeX文書クラスのオプションに pandoc を指定
      • \documentclass[pandoc]{bxjsarticle} に相当

これはPandocでLaTeXエンジンを用いて“まともに”日本語PDFを出力する場合の、ほぼ最低限のオプションです。(うーん、もう既に長いですね……)

この長さが最低限レベルなので、たとえば本を書いてPDFに変換しようと思うと

  • 目次を入れたい (--toc)
  • LaTeXヘッダをカスタマイズしたい (-H)
  • 見出しに番号を付けて (-N)、章から始めるようにしたい (--top-level-division=chapter)
  • ……

のように、どんどんオプションが増えていくことが多いです。

従来の解決策

本の執筆などでPandocを“まともに”使おうと思ったときに、上記のような長いオプションをいちいち打ったり、シェルの履歴に頼って呼びだすのは辛いです。
特にDockerを使って執筆環境を丸ごと固定したい場合には、ファイルに設定を書くことは必須です。

従来は「Pandocの設定をファイルに保存する」ための方法が公式にはありませんでした。
そこで次のような方法がとられます。

Makefileを使ってコマンドラインオプションを指定する場合、たとえば下記のようになります。
(この例では make でビルドできます)

# Pandocのオプション
pandoc_options := -s
pandoc_options += -N
pandoc_options += -f markdown+raw_tex+east_asian_line_breaks
pandoc_options += --top-level-division=chapter
pandoc_options += --pdf-engine=lualatex

# ルール
book.pdf: book.md
    pandoc $(pandoc_options) book.md -o book.pdf

Makefileでは変数が使えます。そして := で代入1+= で既存の変数(文字列)の末尾に追加ができます。
追加の際にはタブ文字(半角スペースと同等の効果)が間に挟まるため、上記の pandoc_options のように1オプション1行で書けば、pandoc コマンドは都合良く解釈してくれます。

これでもよいですが、もうちょっとPandoc側のソリューションがないかと個人的には思っていました。
その中で満を持して(?)出てきたのが、Default fileです。

Default file とは

Default file は、pandoc のコマンドラインオプションをYAMLファイルから指定する仕組みです。
Pandoc 2.8以降から追加された新機能です。

Pandoc User's Guideでは、次のようなオプションが追加されました

-d FILE, --defaults=FILE

Specify a set of default option settings. FILE is a YAML file whose fields correspond to command-line option settings. All options for document conversion, including input and output files, can be set using a defaults file.

つまり、YAMLファイルをオプション -d/--defaults に与えることで、
コマンドラインオプションに対応する設定をまとめて与えられる機能がDefault fileです。

たとえば、最初に例として示した

$ pandoc sample.md -o sample.pdf --pdf-engine=lualatex -V documentclass=bxjsarticle -V classoption=pandoc

は、次のように書き換えられます。

コマンド:

$ pandoc -d my-defaults.yaml

Default File:

my-defaults.yaml
# 入力ファイル (配列形式で指定)
input-files:
- src/sample.md

# 出力ファイル (単一アイテムで指定)
output-file: sample.pdf

# --pdf-engine オプション
pdf-engine: lualatex

# テンプレート変数
variables:
  documentclass: bxjsarticle
  classoption: pandoc

ディレクトリ構成:

.
├── sample.pdf        # 出力ファイル
├── my-defaults.yaml  # Defaults file
├── src
│   └── sample.md    # 入力ファイル

my-defaults.yaml はYAMLファイルです。拡張子はyamlでもymlでも構いません2
my-defaults という名前は、各自で適宜変更してかまいません。

文法はYAMLに従います。配列、連想配列(ハッシュ/key-value)、複数行文字列なども利用可能です。
詳細は プログラマーのための YAML 入門 (初級編) が参考になるでしょう。

Default Fileで利用できるキーおよび値の例

Default Files の節にあるDefault fileの例をそのまま引用します。

defaults.yaml
from: markdown+emoji
# reader: may be used instead of from:
to: html5
# writer: may be used instead of to:

# leave blank for output to stdout:
output-file:
# leave blank for input from stdin, use [] for no input:
input-files:
- preface.md
- content.md
# or you may use input-file: with a single value

template: letter
standalone: true
self-contained: false

# note that structured variables may be specified:
variables:
  documentclass: book
  classoption:
    - twosides
    - draft

# metadata values specified here are parsed as literal
# string text, not markdown:
metadata:
  author:
  - Sam Smith
  - Julie Liu
metadata-files:
- boilerplate.yaml
# or you may use metadata-file: with a single value

# Note that these take files, not their contents:
include-before-body: []
include-after-body: []
include-in-header: []
resource-path: ["."]

# filters will be assumed to be lua filters if they have
# the .lua extension, and json filters otherwise.  But
# the filter type can also be specified explicitly, as shown:
filters:
- pandoc-citeproc
- wordcount.lua
- type: json
  path: foo.lua

file-scope: false

data-dir:

# ERROR, WARNING, or INFO
verbosity: INFO
log-file: log.json

# citeproc, natbib, or biblatex
cite-method: citeproc
# part, chapter, section, or default:
top-level-division: chapter
abbreviations:

pdf-engine: pdflatex
pdf-engine-opts:
- "-shell-escape"
# you may also use pdf-engine-opt: with a single option
# pdf-engine-opt: "-shell-escape"

# auto, preserve, or none
wrap: auto
columns: 78
dpi: 72

extract-media: mediadir

table-of-contents: true
toc-depth: 2
number-sections: false
# a list of offsets at each heading level
number-offset: [0,0,0,0,0,0]
# toc: may also be used instead of table-of-contents:
shift-heading-level-by: 1
section-divs: true
identifier-prefix: foo
title-prefix: ""
strip-empty-paragraphs: true
# lf, crlf, or native
eol: lf
strip-comments: false
indented-code-classes: []
ascii: true
default-image-extension: ".jpg"

# either a style name of a style definition file:
highlight-style: pygments
syntax-definitions:
- c.xml
# or you may use syntax-definition: with a single value
listings: false

reference-doc: myref.docx

# method is plain, webtex, gladtex, mathml, mathjax, katex
# you may specify a url with webtex, mathjax, katex
html-math-method:
  method: mathjax
  url: "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
# none, references, or javascript
email-obfuscation: javascript

tab-stop: 8
preserve-tabs: true

incremental: false
slide-level: 2

epub-subdirectory: EPUB
epub-metadata: meta.xml
epub-fonts:
- foobar.otf
epub-chapter-level: 1
epub-cover-image: cover.jpg

reference-links: true
# block, section, or document
reference-location: block
atx-headers: false

# accept, reject, or all
track-changes: accept

html-q-tags: false
css:
- site.css

# none, all, or best
ipynb-output: best

# A list of two-element lists
request-headers:
- ["User-Agent", "Mozilla/5.0"]

fail-if-warnings: false
dump-args: false
ignore-args: false
trace: false

推測されるDefault Fileのルール

現在のところコマンドラインオプション名とDefault file上のキーとの対応は、User's Guideには明示されていません。
そのため下記は推測になりますが、おおよそDefault fileは次のルールがあると思われます。

  • 長い形式のオプション --foo-bar は、Default fileのキー foo-bar に変換される
  • オプション引数を複数指定できる場合は、YAMLの配列形式で書く (例: input-files)
    • 1つしかなくても、必ず配列形式で書く
  • オプション引数にkey-value形式で指定する場合は、YAMLの連想配列形式で書く (例: variables)

ただし、例外もいくつかあります。

  • 入力ファイル
    • オプション: 無名の引数 / Default file: input-files
  • テンプレート変数(単数・複数が違う)
    • オプション: --variable / Default file: variables
  • ログ (--log オプション) 関連は、Default file上だとだいぶ違います(エラーメッセージのレベルも指定できる)

注意点

実際に使ってみるといくつか落とし穴があったので、補足します。

YAMLの単一アイテム/配列の区別に注意

たとえば、先ほどの my-defaults.yaml の1行目を

my-defaults.yaml
input-files: src/sample.md

にしてしまうと、次のようなエラーが発生します。

$ pandoc -d src/my-defaults.yaml
Error parsing src/my-defaults.yaml line 1 column 13:
expected !!seq instead of !!str

input-files の値は配列形式とする必要があるようです3

my-defaults.yaml
input-files:
- src/sample.md

Default fileを書く際は、User's Guideの例を注意深く確認してください。

補足2019-12-12: 脚注 3 の訂正

Default Fileの例(コメント部分)をよく読んだら「or you may use input-file: with a single value」と書いてました。
つまり input-file は単一の値(入力ファイルパス)を指定する必要があり、input-files は配列形式の値(入力ファイルパス)を指定する必要があるようです。

入力ファイルを子ディレクトリに分けている場合

たとえば次のような構成を考えます。

.
├── src
│   ├── my-defaults.yaml
│   └── sample.md

このとき、Default file (my-defaults.yaml) の input-files の値は

  • 正: - src/sample.md
  • 誤: - sample.md

です。つまり、入力ファイルのルートは

  • 正: pandocコマンドを実行したときのカレントディレクトリ (./)
  • 誤: my-defaults.yaml のディレクトリ (src/)

ということです。

複雑なディレクトリ構造を想定している場合は、Default fileのみで頑張るのではなく、
バッチファイルやMakefileなどで別途ディレクトリを指定してあげるほうがよさそうです。
(この場合は、入力・出力ファイル名をDefaults fileではなく直接コマンドラインオプションで与える方がいいかも?)

既存のコマンドラインオプションとの組み合わせ

Defailt files の説明では、最後に次のように注意があります。

Note that, where command-line arguments may be repeated (--metadata-file, --css, --include-in-header, --include-before-body, --include-after-body, --variable, --metadata, --syntax-definition), the values specified on the command line will combine with values specified in the defaults file, rather than replacing them.

つまり、上記のオプションが --defaults と同時に指定された場合には、値が(上書きではなく)追記されるようです。

Default Files ができないこと

その他にも、Default Files にもできないことがあります。

  • pandoc 以外の外部コマンドを実行したり、シェルの機能(パイプ・リダイレクトなど)を使ったりすること
    • 例(sedで置換): $ cat hoge.md | sed -e 's/foo/bar/g' | pandoc -f markdown -o hoge.html
  • ファイルの依存関係を記述すること

下記に比較表を示します。

Default Files YAMLメタデータファイル バッチファイル
シェルスクリプト
Makefile (GNU Make)
形式 (拡張子) YAMLファイル (.yaml/.yml) YAMLファイル (.yaml/.yml) Win: バッチファイル (.bat)
Unix: シェルスクリプト (.sh)
Makefile
pandocオプション ○ (Default Filesの形式) × ○ (コマンドラインオプション) ○ (コマンドラインオプション)
メタデータ ○ (オプションで指定) ○ (オプションで指定)
テンプレート変数 △ (メタデータ扱い) ○ (オプションで指定) ○ (オプションで指定)
フィルタの指定 × ○ (オプションで指定) ○ (オプションで指定)
外部コマンド
シェル機能
× ×
ファイルの依存関係 × × ×

設定ファイルの使い分け案

上記の比較表のように、いくつかの手段で記述できる内容が重複しています。

そこで、大まかには次のように使い分けることを提案してみます(あくまでも一案です)。

  • YAMLメタデータファイル
    • 文書に付随する(本来の意味の)メタデータ(タイトル、日付、概要など)
  • Default file
    • 文書に付随しない(Pandoc上の便宜的な)メタデータ
    • ユーザが指定するテンプレート変数
    • その他、Pandocに直接与えるオプション(Pandocを制御するためのもの)
  • Makefile
    • プロジェクトにおけるビルドの設定
    • ディレクトリの指定、依存関係、外部コマンド・シェル機能の併用

Default fileによって上記のようにPandocにまつわる設定を目的別にファイル分けして、設定ファイルの見通しをよくできるかもしれません。たとえば

  • Makefileから、Pandocのオプションを削る
  • YAMLメタデータファイルから、テンプレート変数(as メタデータ)を削除
    • あるいは、YAMLメタデータファイルを使わずDefault fileにすべてマージする
  • 上記をDefault fileにまとめる

だけでも、だいぶすっきりする気がします。この点で既存プロジェクトにDefault fileを導入する意義はあるかと思います。

ただし上記はあくまでも一案です。もう少し良いプロジェクト構成がありそうな気がします。

ぜひ「ぼくのかんがえたさいきょうのPandocプロジェクト」をPandoc Advent Calendar 2019に投稿してください。

サンプルファイル

サンプルファイルをGitHubに上げました。参照してみてください。

以上です。


  1. = もありますが、遅延評価の代入なので単純な設定記述にはおすすめしません。:= は即時評価なのでわかりやすいです。 

  2. -d/--defaults で指定したファイル名に拡張子がない場合は、自動的に .yaml が補完されるようです。たとえば --defaults letter とすれば letter.yaml が呼びだされます。またDefault fileの探索パスは、カレントディレクトリに加えて ユーザデータディレクトリ直下の defaults ディレクトリも含まれます。 

  3. 余談ですが、なぜか input-file (単数形) は input-files (複数形) のエイリアスとして機能するらしく、input-file に配列値を与えてもvalidという謎の仕様があります……。