textunpack: テキストベースのアーカイブユーティリティ

textunpack について

textunpack はテキストベースのフォーマットを使ったアーカイブユーティリティです。textunpack を使うと、複数のテキストファイルを一つのテキストファイルで管理できます。

textunpack は本サイト (https://inzkyk.xyz) の作成で使われています。書籍の翻訳を一つの巨大なファイルで行い、そこから章ごとの Markdown ファイルを切り出すといった使い方です。

この記事では textunpack を簡単に紹介します。textunpack のソースコードとバイナリは https://github.com/inzkyk/textunpack で公開されています。

使用例

次に示すのは、アーカイブ sample.txt に含まれる二つのエントリ foo.txt, bar.txttextunpack で展開する例です:

$ ls
sample.txt
$ cat sample.txt
#+EXPORT_BEGIN foo.txt
This is foo.

#+EXPORT_END foo.txt

#+EXPORT_BEGIN bar.txt
This is bar.

#+EXPORT_END bar.txt
$ textunpack sample.txt
$ ls
bar.txt  foo.txt  sample.txt
$ cat foo.txt
This is foo.
$ cat bar.txt
This is bar.

--list オプションを付けると、アーカイブに含まれるエントリのファイル名を確認できます:

$ cat sample.txt
#+EXPORT_BEGIN foo.txt
This is foo.

#+EXPORT_END foo.txt

#+EXPORT_BEGIN bar.txt
This is bar.

#+EXPORT_END bar.txt
$ textunpack --list sample.txt
foo.txt
bar.txt

解説

エントリの構文

textunpack が扱うアーカイブフォーマットでは、各エントリが次の形をしたテキストで表されます:

#+EXPORT_BEGIN <FILENAME>
...
#+EXPORT_END <FILENAME>

ここで <FILENAME> はエントリのファイル名です。ファイル名は絶対パスでも相対パスでも構いません。

エントリ以外のテキスト

#+EXPORT_BEGIN <FILENAME> ... #+EXPORT_END <FILENAME> 以外のテキストは無視されます。そのため「ファイルの特定の部分をファイルとして書き出す」という使い方ができます。

アーカイブの展開

textunpack FILE を実行すると FILE に含まれる全てのエントリが展開されます。エントリのファイル名が相対パスの場合は FILE があるディレクトリが相対パスの起点として使われます。

textunpack --stdin DIR を実行すると、標準入力から読み取ったデータに含まれる全てのエントリが展開されます。そのとき、DIR が相対パスの起点として使われます。

このため、次の二つのコマンドは同じ処理をします:

# 同じ処理をする二つのコマンド
$ textunpack ./foo/bar.txt
$ cat ./foo/bar.txt | textunpack --stdin ./foo/

アーカイブの作成

textunpack --pack FILE_0 FILE_1... を実行すると、FILE_0, FILE_1, ... をエントリに持つアーカイブが作成されます:

$ cat foo.txt
This is foo.
$ cat bar.txt
This is bar.
$ textunpack --pack foo.txt bar.txt
#+EXPORT_BEGIN foo.txt
This is foo.

#+EXPORT_END foo.txt
#+EXPORT_BEGIN bar.txt
This is bar.

#+EXPORT_END bar.txt

-o FILE オプションを付ければファイル FILE にアーカイブが作成されます。

次のコマンドは noop です:

# 意味のないコマンド
$ textunpack --pack foo.txt bar.txt | textunpack --stdin ./

応用例

textunpackgokurai と一緒に使うことを前提として開発されました。例えば次のように使います:

#+MACRO chapter-filename-1 ./1-preface.md
#+MACRO chapter-filename-2 ./2-introduction.md
#+MACRO chapter-filename-3 ./3-background.md
#+MACRO chapter #+EXPORT_BEGIN ^[[[chapter-filename-$1]]]
#+MACRO /chapter #+EXPORT_END ^[[[chapter-filename-$1]]]

[[[chapter(1)]]]
# 第一章 ...
[[[/chapter(1)]]]

[[[chapter(2)]]]
# 第二章 ...
[[[/chapter(2)]]]

[[[chapter(3)]]]
# 第三章 ...
[[[/chapter(3)]]]

textunpack は最低限の機能しか持ちませんが、gokurai と一緒に使えばたいていの処理は実現できます。

その他の話題

思い出話とか。

作った経緯

翻訳で必要になったので作りました。

長い文章を書いていると「数百ページ前に使った表現を修正する」「各章の構成を統一する」「特定の単語を全て置換する」といった「文章中の遠く離れた箇所を参照・編集する」操作がよく必要になります。文章を一つのファイルにまとめると、こういった操作がテキストエディタの機能を通して簡単に実行できます。

一方で、文章をウェブサイトとして公開するには文書を複数のファイル (HTML や Markdown) に分割する必要があります。さらに、複数のフォーマット (PDF, EPUB, Jupyter Notebook) をサポートするにはフォーマットごとに異なるファイルを作成しなければなりません。フォーマットによってファイルの分割方法が異なる場合もあります。

こういった問題を解決するために textunpack は作成されました。textunpack を使うと、文章全体を一つのファイルに収めつつ、必要な場合は特定の部分を個別のファイルとして切り出すことが可能になります。

また、Windows のシェル cmd/powershell でパイプとリダイレクトの振る舞いが「独特」なことも textunpack を作った理由の一つです。ファイル全体を処理して別のファイルに書き出す cat text.txt | work | another_work > result.txt という単純なコマンドでさえ、意識しなければならない落とし穴が多くあります。textunpack はファイルの内容をそのまま書き出すようにしてあるので、扱いが簡単です。

textunpack は最初 Go 言語で実装されました。その後 ix ライブラリが充実してきたので C++ で書き直されました。

ix ライブラリ

ix は自作の C++ ライブラリです。gokurai の作成でも利用しましたが、その後も改善を続けて textunpack でも利用しました。gokurai で使用した ix と比べると変化が分かります。

gokurai を公開してから ix に加えた主な変更は次の通りです:

ix ではコンパイルの速さを優先してコードを書いてきましたが、その利点がジワジワと効いてきていると感じました。例えば、テスト用のマクロが定義される ix_test.hpp を編集すると、そのたびにテストを含む全ての .cpp ファイルが再コンパイルされます。しかしコンパイルが速いおかげで、このファイルの編集を躊躇せずに済みました。コンパイルを高速化する細々した最適化も実装できたので、「コンパイルが速いとコンパイルの高速化がしやすくなる」という雪だるま効果が実感できました。

ファイル I/O の高速化

textunpack はファイル I/O をそれなりに多く実行します。いい機会なのでスレッドプールを使って高速化しました。ファイル I/O 高速化の選択肢としては他にノンブロッキング I/O や epoll/IOCP がありますが、「クロスプラットフォームで実装するのが大変」「スレッドプールを実装すれば他の場面でも使えそう」「展開するアーカイブは人間が手で書く程度の規模なので、少しくらい遅くても問題ない」といった理由からスレッドプールが選択されました。

スレッドプール ix_ThreadPool は lock-free な work-stealing deque を実装します。Job System 2.0: Lock-Free Work Stealing, Dynamic Circular Work-Stealing Deque, Correct and Efficient Work-Stealing for Weak Memory Models などを参考にしました。ストレステストもして一応正しく動いているようですが、バグが無いとは信じられません。特に ARM アーキテクチャの CPU で実行したことがないので、実行すればバグが見つかるでしょう。

Amalgamation Build

Amalgamation Build とは全てのソースコードを一つの .cpp ファイル (翻訳単位) にまとめ、そのファイルをコンパイルすることで最終的な実行形式またはライブラリを生成するビルド手法のことです。Jumbo Build や Unity Build とも呼ばれます。SQLite, Chromium, bgfx などで使われており、Unreal Ungine でも使われているそうです。

textunpack では Amalgamation Build を整備しました。amalgamator.py がソースツリーから amalgam.cpp を生成します。amalgam.cpp を生成する処理は書くのが大変なのではないかと思いましたが、すぐ書けました。C/C++ の #include が持つ単純な意味論がここでは役立ちました。テスト関連のコードを除去するロジックを入れられたのでよかったです。生成される amalgam.cpp には textunpack からは使われないコードも多く含まれますが、正しくコンパイルできるのでそれ以上の改善は止めました。C++ コード用の tree-shaking ツールがあれば使ったでしょうが、存在しないようです。

Amalgamation Build の利点としてコンパイルの高速化や生成されるコードの高速化がよく挙げられます。textunpack でもそういった利点はあったと思いますが、最も大きかったのはビルドの簡素化です。レポジトリをクローンしてコンパイラを一度呼び出すだけで最終的な実行形式が作成されるので、CI を整備するときに「Ninja が無い」「CMake のバージョンが違う」「CMake がエラーを出した」「生成物が指定されたパスに存在しない」といったエラーを見ることはありませんでした。fix ci というコミットメッセージを持つコミットは大きく減ったと思います。

GitHub Actions

textunpackGitHub レポジトリには GitHub Actions を使った CI がセットアップされています。コミットされた amalgam.cpp をコンパイルしてリリースするだけの単純なものです。textunpack を他人が使う可能性は限りなくゼロに近いですが、GitHub Actions を使ってみたかったので使いました。

アーカイブフォーマットの名前

アーカイブを展開するプログラムの名前が textunpack なので、このプログラムが扱うアーカイブフォーマットには textpack という名前を付けようと思っていました。ただ、textpack と呼ばれるフォーマットやプログラムがいくつかあるようなので止めました。

「エントリ」の構文

エントリは #+EXPORT_BEGIN ... #+EXPORT_END で表されます。なぜ #+ENTRY_BEGIN ... #+ENTRY_END でないかというと、以前に私が原稿を Emacs の Org Mode で書いていたためです。Org Mode には #+BEGIN_EXPORT html ... #+END_EXPORT#+BEGIN_EXPORT latex ... #+END_EXPORT という構文があり、本サイトの初期の翻訳でも使っていました。しばらくして Org Mode の利用をやめたときに作られたのが textunpack (の Go 実装) であり、そのとき Org Mode と同様の構文 #+BEGIN_EXPORT ... #+END_EXPORT がエントリ用に採用されました。その後 gokurai の構文と一貫させるために BEGIN/ENDEXPORT が入れ替わって今に至ります。

amalgam.cpp の行数

現在 amalgam.cpp の行数は 11,274 行です。こんなものか、と思って少しいじってから保存したら、デフォルト状態の clang-format が実行されて行数が 9453 行まで減って驚きました。行数がかさむスタイルを使っている自覚はありましたが、想像以上に減りました。

広告