DockerでLibreOffice Basicマクロを実行する

これは [LibreOffice Advent Calendar 2020](https://adventar.org/calendars/5258) の25日目の記事です。

---

（追記 2022-12-11）
アップデート版を書きました。動かない場合はこちらも参照してください。

<!-- DockerでLibreOffice Basicマクロを実行する（2022年版） - Qiita -->
https://qiita.com/sonota88/items/395a32555d1187df2cc3

----

<!--
- mal で「このコマンドを実行するとターミナルでreplを試せますよ！」
  と持って行きたかった
  - ちょっと調べてみたが分からず時間切れ。あとまわし
- その後やり方が分かった
    - このやり方だけ切り出すと誰かの役に立つかも
- ぱっと思いつくところではテスト自動化に使えそう


そもそもは [LibreOffice BasicでLispインタプリタ（mal）を書いた](https://qiita.com/sonota88/items/501f0efa437bc4d53cc7) を書いたときに「Docker で REPL 試せますよ」というところまで書きたかったのですが、ちょっと試してダメで間に合わなそうだったのでいったん後回しにしていたのでした。
その後やり方が分かり、怪しいところがありつつも REPL が動かせるようになりました。

-->

そもそもは「[LibreOffice BasicでLispインタプリタ（mal）を書いた](https://qiita.com/sonota88/items/501f0efa437bc4d53cc7)」を書いたときに「Docker を使って REPL を試せますよ」というところまで用意したかったんですよね。ところがちょっと試して間に合わなそうだったのでいったん後回しにしていたのでした。

その後あらためて調べて、怪しいところがありつつもなんとか REPL が動かせるようになりました（記事の方にも追記しました）。

で、 Docker で Basic マクロを実行する部分だけ切り出して書いておくと誰かの役に立つかもということでこれを書いています。パッと思いつくところではテストの自動化なんかに使えそう。



<!-- ■■■■■■■ -->
# 最低限のサンプル

`Standard` ライブラリ + `Module1` モジュールだけを使った最低限の例です。

## 用意するファイル

Dockerfile

```dockerfile
FROM ubuntu:18.04

RUN apt-get update \
  && apt-get -y install --no-install-recommends \
    libreoffice-calc \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

# ~/.config/libreoffice 以下を生成
RUN libreoffice --headless --terminate_after_init

WORKDIR /root/work
```

sample.sh（コンテナ内で実行するスクリプト）

```sh
escape_xml() {
  cat \
    | sed -e 's/&/\&amp;/g' \
    | sed -e 's/"/\&quot;/g' \
    | sed -e "s/'/\&apos;/g" \
    | sed -e 's/</\&lt;/g' \
    | sed -e 's/>/\&gt;/g'
}

print_basic_src() {
  cat << BAS
Sub Main
  Dim fid As Integer
  fid = Freefile
  Open "/tmp/output" For Binary Access Write As fid
  Put(fid, 1, CStr(Time()) & Chr(10))
  Close fid
End Sub
BAS
}

user_bas_dir="${HOME}/.config/libreoffice/4/user/basic"
mod_name=Module1
mod_file="${user_bas_dir}/Standard/${mod_name}.xba"

cat << XML > ${mod_file}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
  <script:module
    xmlns:script="http://openoffice.org/2000/script"
    script:name="${mod_name}"
    script:language="StarBasic"
  >
    $(print_basic_src | escape_xml)
  </script:module>
XML

libreoffice \
  --headless --norestore --nologo --nofirststartwizard --nolockcheck \
  "vnd.sun.star.script:Standard.${mod_name}.Main?language=Basic&location=application"

cat /tmp/output
```

短いので説明するまでもないかもしれませんが、次のような処理内容です。

- `~/.config/libreoffice/4/user/basic/Standard/Module1.xba` に Basic のコードを格納
  - できればちゃんと XML を読み書きした方がよいと思いますが、最低限のサンプルということで `Module1.xba` をまるごと上書き
- `libreoffice` コマンドでマクロを実行
  - 現在の時刻が一時ファイルに書き込まれる
- 一時ファイルに出力された内容を表示



## 実行

```sh
# イメージをビルド
docker build -t libreoffice-basic:trial .
# 実行
docker run --rm -v "$(pwd):/root/work" libreoffice-basic:trial bash sample.sh
```

<!--
Basic マクロで現在の時刻を一時ファイルに出力し、マクロの実行の終了後にそのファイルを cat しています。

なので、うまく動いていれば docker run 〜 のたびに現在時刻がプリントされます（タイムゾーン設定をしていないので日本の時刻とはずれます）。
-->

うまく動いていれば `docker run 〜` を実行するたびに現在時刻がプリントされます（タイムゾーン設定をしていないので日本の時刻とはずれます）。


# メモ

## マクロのセキュリティレベルと CUI での実行

<!--
ファイル内のマクロを CLI で実行する場合困る点

- ファイル内にマクロを置いてコマンドラインから実行する場合、セキュリティレベルを「低」にする必要がある
- コンテナ内であればこの方法でもよいかもしれないが、少なくともコンテナ外ではやりたくない

というわけで別の方法を試すことに。

コンテナを使わない場合は user をいじるのは環境が汚れて嫌だが、
コンテナを使うのであれば積極的に使っていく方針でよさそう。

- ファイル内ではなく、アプリケーション管理下に置けば、セキュリティレベルを下げなくてもコマンドラインから実行できる
Docker コンテナ内であっても。
-->

最初は Basic のコードをファイルに入れて実行する方法を試していたのですが、この場合セキュリティレベルを「低」にしないと GUI なしでは実行できません。設定ファイルをごにょごにょすればセキュリティレベルの変更も GUI なしでできそう……と思いついて試してはみたものの、そういうのあんまりやりたくないですよね……。

というわけで、ファイルではなくユーザ管理のマクロ（マクロの管理ダイアログで「マイマクロ」と表示されるもの）として登録して実行する方法に落ち着きました。Docker を使うと既存の環境を汚さないかとか気にせず大胆にやれてよいですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/05fba23e-2fd3-26f8-82e2-4e69852b349b.png)


## 設定の初期化

<!--
- apt install libreoffice しただけの状態だと、 `~/.config/libreoffice/` ディレクトリが存在しない。
- 自分で作ってもいいが、初期化の方法が用意されているならそれを利用したい
- 一度起動すると作成されるが、その後 soffice.bin を kill しないといけない
- そこで `--terminate_after_init`
    - ちゃんと調べてないけど、名前から察するとたぶんこういう用途で使うもの
-->

ユーザ管理のマクロの置き場所は `~/.config/libreoffice/4/user/basic` 以下なのですが、apt で libreoffice をインストールしただけだとこのディレクトリや他の設定ファイルなどは生成されません。一度 LibreOffice を起動すると生成されるんですけど、普通に起動すると起動したままなので別途 kill したりしないといけない……。

どうしようかなと思ってコマンドラインオプション一覧を眺めていたら `--terminate_after_init` という、名前から察するにこういう用途のために用意されていると思しきオプションを見つけました。

```
Developer arguments:
   --terminate_after_init
                       Exit after initialization complete (no documents loaded).
```

Dockerfile 内で使っているのがこれです。ドキュメントで確認した訳ではないのでひょっとして目的外使用だったらごめんなさい。




## ライブラリを追加する場合

上の例ではすでに存在している `Standard` ライブラリを利用しているため不要ですが、別のライブラリを追加する場合は `~/.config/libreoffice/4/user/basic/script.xlc` に追記する必要があります。

下記は `Mylib` というライブラリを追加する例。

```diff
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE library:libraries PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "libraries.dtd">
 <library:libraries xmlns:library="http://openoffice.org/2000/library" xmlns:xlink="http://www.w3.org/1999/xlink">
   <library:library library:name="Standard" xlink:href="$(USER)/basic/Standard/script.xlb/" xlink:type="simple" library:link="false"/>
+  <library:library library:name="Mylib" xlink:href="$(USER)/basic/Mylib/script.xlb/" xlink:type="simple" library:link="false"/>
 </library:libraries>
```


# 環境

- Ubuntu Linux 18.04
- LibreOffice 6.0.7.3

※ コンテナの中も外も同じ

# 関連

- [LibreOffice Calcの入ったDockerイメージを作ってヘッドレスで動かす - memo88](https://memo88.hatenablog.com/entry/2019/11/03/090019)

# 他に LibreOffice 関連で書いたもの

https://memo88.hatenablog.com/archive/category/LibreOffice