Linux コマンドの多言語対応 ~ tar の「空のアーカイブ作成はご容赦願います」の謎に迫る~

多くの Linux コマンドは、環境変数によってメッセージを切り替えます。
参考 【Linux入門】ローカライゼーションのためのロケール情報設定方法

]# LANG=C tar -c
tar: Cowardly refusing to create an empty archive
Try 'tar --help' or 'tar --usage' for more information.

]# LANG=ja_JP.utf8 tar -c
tar: 空のアーカイブ作成はご容赦願います
より詳しい情報は 'tar --help' または 'tar --usage' で.

各コマンドは C、Python、Go、Bash といった色々な言語で実装されてるわけですが、「 各言語ごとに多言語対応してたら大変じゃないのかな」という疑問を持ちました。

そこで「もしかして Linux 上で多言語に対応する共通的な仕組みがあるんじゃないか。なんかこう…bash が stdout をごにょごにょして変換したりして…!」と思って調べました。結論からいうとそんな仕組みはありません。言語ごとの仕組みを利用しているだけです。まあ、そりゃそうですよね。

これで終わりだと寂しいので、今回は気になる日本語メッセージの代表格(自分調べ)である tar の実装を調べてみました。

tar といえば、めちゃくちゃフランクなメッセージだったり

]# tar -x
tar: 端末からアーカイブの内容を読み取るのを拒否 (-f オプションの指定忘れ?)
tar: Error is not recoverable: exiting now

急に丁寧なメッセージだったりすることでおなじみ(?)ですよね。

]# tar -c
tar: 空のアーカイブ作成はご容赦願います
より詳しい情報は 'tar --help' または 'tar --usage' で.

パッケージのソースコードリーディング方法は省略しますが、

www.kimullaa.com

gdb でデバッグしていくと、 _() マクロの部分で変換されてることがわかります。

f:id:kimulla:20200125171303p:plain

メッセージから、__GI___dcgettext で具体的な処理をしているようです。

このシンボル名(__GI_)は glibc の呼び出しでよく見ますね。おそらくglibc の呼び出しがあって plt の解決が行われたあとなんじゃないでしょうか。なので、再度実行して layout asm に切り替え、 plt のジャンプ前をみてみます。

f:id:kimulla:20200125181648p:plain

すると、dcgettext が呼び出されているようです。

dcgettext/usr/include/libintl.h に定義されています。 このファイルの先頭を見ると、次のようなメッセージが記載されていました。どうやら GNU gettext が関係ありそうです。

/* Message catalogs for internationalization.
   Copyright (C) 1995-2018 Free Software Foundation, Inc.
   This file is part of the GNU C Library.
   This file is derived from the file libgettext.h in the GNU gettext package.
   ...

GNU gettext を調べたところ、言語ごとに翻訳ファイル(mo ファイル)を用意して実行時に読み込む仕組みだそうです。
参考 GUN gettext
参考 Wikipedia gettext

そこで tar パッケージのインストールファイルを確認したところ、 /usr/share/locale/ 配下に言語ごとの mo ファイルが存在しました。

]# rpm -ql tar
/usr/bin/gtar
/usr/bin/tar
/usr/lib/.build-id
/usr/lib/.build-id/e7
/usr/lib/.build-id/e7/a78cae1d85ad11ac18ae07efe26553a4d8ae5a
/usr/share/doc/tar
/usr/share/doc/tar/AUTHORS
/usr/share/doc/tar/ChangeLog
/usr/share/doc/tar/NEWS
/usr/share/doc/tar/README
/usr/share/doc/tar/THANKS
/usr/share/info/tar.info-1.gz
/usr/share/info/tar.info-2.gz
/usr/share/info/tar.info.gz
/usr/share/licenses/tar
/usr/share/licenses/tar/COPYING
/usr/share/locale/bg/LC_MESSAGES/tar.mo
/usr/share/locale/ca/LC_MESSAGES/tar.mo
/usr/share/locale/cs/LC_MESSAGES/tar.mo
/usr/share/locale/da/LC_MESSAGES/tar.mo
/usr/share/locale/de/LC_MESSAGES/tar.mo
/usr/share/locale/el/LC_MESSAGES/tar.mo
...

mo ファイルの中身を確認すると、エラーメッセージに出力されていた日本語メッセージが表示されました。

]# msgunfmt /usr/share/locale/ja/LC_MESSAGES/tar.mo -o tar.po

// msgid を msgstr で置き換える仕組みっぽい
]# nkf tar.po  | grep "Cowardly refusing to create an empty archive" -A 2
msgid "Cowardly refusing to create an empty archive"
msgstr "空のアーカイブ作成はご容赦願います"

試しに mo ファイルを編集すると、エラーメッセージが変更できました。

// メッセージを編集する
]# vim -c "e ++enc=euc-jp" tar.po
...
msgid "Cowardly refusing to create an empty archive"
msgstr "空のアーカイブ作成はご容赦願います"
-> 変更する
msgstr "空のアーカイブ作成はダメよ~ダメダメ"

// 編集したメッセージが表示されることを確認する
]# msgfmt tar.po  -o /usr/share/locale/ja/LC_MESSAGES/tar.mo
]# tar -c
tar: 空のアーカイブ作成はダメよ~ダメダメ
より詳しい情報は 'tar --help' または 'tar --usage' で.

ということで C 言語で開発されたツールは、 GNU gettext で多言語対応ができるようです。なお、この機能は glibc に含まれている標準的な仕組みです。Linux Base Standard にも Localization Functions として記載されていました。
参考 14.3.6.1. Interfaces for Localization Functions

そのため、 tar 以外にも、様々なコマンドの mo ファイルが /usr/share/locale に配置されています。

# ls /usr/share/locale/ja/LC_MESSAGES/
Linux-PAM.mo       cpplib.mo            gawk.mo             grub.mo                       libsecret.mo        popt.mo                    sudo.mo
NetworkManager.mo  cracklib.mo          gcc.mo              gsettings-desktop-schemas.mo  libsoup.mo          psmisc.mo                  sudoers.mo
PackageKit.mo      cryptsetup.mo        gdbm.mo             gtk20.mo                      libuser.mo          realmd.mo                  syspurpose.mo
atk10.mo           diffutils.mo         gdk-pixbuf.mo       initscripts.mo                man-db-gnulib.mo    rhn-client-tools.mo        systemd.mo
authselect.mo      dnf-plugins-core.mo  gettext-runtime.mo  json-glib-1.0.mo              man-db.mo           rpm.mo                     tar.mo
bash.mo            dnf.mo               gettext-tools.mo    ld.mo                         mlocate.mo          sed.mo                     texinfo.mo
...

実装してみよう

C 言語で多言語対応してみます。

まずは mo ファイルを読み込むソースコードを用意します。

#include <stdio.h>
#include <locale.h>
#include <libintl.h>

#define _(msgid) gettext(msgid)

int main()
{
    setlocale(LC_ALL, "");
    bindtextdomain("main", "/root/work/locale");
    textdomain("main");
    printf(_("hello world"));
}

次に mo ファイルを作ります。mo ファイルの作成に利用するコマンドは、 gettext パッケージで提供されています。

]# rpm -ql gettext | head
/usr/bin/envsubst
/usr/bin/gettext
/usr/bin/gettext.sh
/usr/bin/msgattrib
/usr/bin/msgcat
/usr/bin/msgcmp
/usr/bin/msgcomm
/usr/bin/msgconv
/usr/bin/msgen

mo ファイルは、次の流れで作成します。

ソースコード
↓
xgettext コマンド
↓
pot ファイル(po ファイルのひな型) 
↓
msginit コマンド
↓
po ファイル(翻訳した文言が入るファイル)
↓
msgfmt コマンド
↓
mo ファイル(po ファイルのバイナリ表現)
// ソースコードからひながたを作成する
]# xgettext -k"_" -L C -o main.pot main.c
]# cat main.pot
...
"Content-Type: text/plain; charset=utf-8\n" // encode を編集した
"Content-Transfer-Encoding: 8bit\n"

// 今回は日本語に対応する
]# msginit --locale=ja --input=main.pot -o main_ja.po
ユーザが翻訳に関するフィードバックをあなたに送ることができるように,
新しいメッセージカタログにはあなたの email アドレスを含めてください.
またこれは, 予期せぬ技術的な問題が発生した場合に管理者があなたに連絡が取れる
ようにするという目的もあります.

Is the following your email address?
  root@localhost
Please confirm by pressing Return, or enter your email address.

/bin/sh: /usr/share/gettext/projects/team-address: そのようなファイルやディレクトリはありません
msginit: /usr/share/gettext/projects/team-address サブプロセスが失敗しました
msginit: /usr/share/gettext/projects/team-address サブプロセス失敗, 終了コード 127

main_ja.po を生成.

// 翻訳ファイルに追記
]# cat main_ja.po
...
#: main.c:12
#, c-format
msgid "hello world"
msgstr "こんにちは世界" // ここを編集する

]# mkdir -p locale/ja/LC_MESSAGES/

// mo のファイル名は domain で指定した値と合わせる
]# msgfmt -o locale/ja/LC_MESSAGES/main.mo main_ja.po

mo ファイルが作成できたので、あとは実行するだけです。

]# gcc main.c

]# LC_ALL=C ./a.out
hello world
]# LC_ALL=ja_JP.utf8 ./a.out
こんにちは世界

そのほかの言語のサポートは?

言語ごとにサポート状況に差はありますが、おおむね似たような API で操作できるようです。Bash ですらライブラリが提供されています。
参考 GNU gettext 15.5 Individual Programming Languages

例えば Python は標準ライブラリに gettext に似た API が提供されています。

gettext モジュールでは、以下の GNU gettext API に非常に良く似た API を提供しています。
引用元: gettext --- 多言語対応に関する国際化サービス

次のように利用します。

import gettext
gettext.bindtextdomain('main', '/root/work/locale')
gettext.textdomain('main')
_ = gettext.gettext

// 多言語化するメッセージ
print(_('hello world'))
]# LANG=c python3 main.py
hello world

]# LANG=ja_JP.utf8 python3 main.py
こんにちは 世界

ただ gettext があらゆる言語における多言語対応の標準的な仕組みかというと、そんなわけはありません。別の仕組みが流行ってる言語もあります。(まあ glibc に組み込まれている機能なので、色々な言語の API の裏で最終的に呼ばれている可能性は否定できませんが、もはやそれは別の仕組みだと考えます)

最後に

今回は多言語対応の仕組みの1つである gettext を触ってみました。まあ正直、そうですかなるほど、という感じですがご容赦願います。