シェルスクリプトでBase64を使う方法のまとめ + POSIXコマンドのみを使った独自実装

## はじめに

シェルスクリプトで Base64 エンコーダーとデコーダーを実装しました。[sh-base64 プロジェクト](https://github.com/ko1nksm/sh-base64)はこちらです。POSIX コマンド以外のコマンドは使用しておらず、おそらく POSIX シェルが動くすべての環境でそのまま動きます。実装しましたという話だけではなんなんで、シェルスクリプトで Base64 を使う一般的な方法についてもまとめました。

## なぜ作ったのか？

特に必要だったから作ったのではないのですが、「[HMAC-SHA1のシェルスクリプト実装 〜 openssl不要・安全なOAuth 1.0認証の実装用に](https://qiita.com/ko1nksm/items/8c4908fdabaf7a3731d8)」で Base64 エンコードで出力する処理を書いたので頭に Base64 のアルゴリズムがあるうちに、ちょっとした楽しみで作ってみただけです。とはいっても移植性やパフォーマンスには気をつけており、Debian、macOS、FreeBSD、Solaris といった環境で動くことをちゃんと確認しています。

またどの環境でも同じように動く `base64` 関数が手に入るので、移植性が高いシェルスクリプトを書くときに使うと便利でしょう。シェル関数なのでコードが小さく再利用しやすくなっています。

## シェルスクリプトでBase64を使う方法

前提知識として Base64 エンコードにはいくつかの種類が RFC で提案されており、使用する文字、一行の長さ、パディングの有無などの仕様が異なります。唯一の Base64 エンコード規則というものはないので注意してください。詳細は[英語版の Wikipedia](https://en.wikipedia.org/wiki/Base64#Implementations_and_history) を参照してください。 日本語版 Wikipedia は情報が古く正確ではないようです。

### base64コマンド

シェルスクリプトで Base64 を扱う場合、`base64` コマンドを使うのが多いのではないかと思います。まずはこの話をしましょう。`base64` コマンドは POSIX では標準化されていませんが、いくつかの環境で実装されています。ただしその仕様はそれぞれで異なっています。それぞれの違いは以下のようになります。ちなみに FreeBSD 13.2 までは Ports からインストールした John Walker 版で、Solaris では GNU 版が使われ、OpenBSD と DragonFlyBSD では実装されていないことに注意してください。

**エンコード**

| GNU              | エンコード | 一行の文字数 | 一行の文字数の変更 | 補足           |
| ---------------- | ------- | ---------- | -------------- | -------------- |
| GNU              |         | 76 文字     | -w, --wrap     |                |
| macOS 13 以降     |         | 制限なし     | -b, --break    |                |
| FreeBSD 13.2 以前 | -e, --encode | 76 文字     |                | John Walker 版 |
| FreeBSD 14.0 以降 |         | 76 文字     | -w, --wrap     |                |
| NetBSD 9.0 以降   |         | 76 文字     | -w, --wrap     |                |

**デコード**

| GNU              | デコード           | 不正な文字の無視    | 補足                |
| ---------------- | ---------------- | ---------------- | ------------------ |
| GNU      　        | -d, --decode     | -i               |                    |
| macOS 13 以降     | -d, -D, --decode |                  | -i は入力ファイルの指定 |
| FreeBSD 13.2 以前 | -d, --decode     | -n, --noerrcheck | なぜか -u はヘルプ    |
| FreeBSD 14.0 以降 | -d, --decode     |                  |                    | 
| NetBSD 9.0 以降   | -d               | -i               |                    |

**macOS の `-d` オプションは以前はデバッグ用のオプション**で、デコードには `-D` または `--decode` を使う例がよく紹介されています。現在は他の実装との互換性のためにデコード用のオプションに変更したようです。現在の macOS 版はシェルスクリプトで実装されており、内部で `openbsd enc -base64` を呼び出しています。また base64url でもデコードできるようになっています。

```console:macOS 13 の場合
$ echo '>>>???' | base64
Pj4+Pz8/Cg==

$ echo 'Pj4+Pz8/Cg==' | base64 -d
>>>???

$ echo 'Pj4-Pz8_Cg==' | base64 -d # base64url でも動く
>>>???
```

### basencコマンド

GNU coreutils には Base64 / Base32 とその亜種に一つのコマンドで対応している `basenc` コマンドも実装されています。2018 年に開発が開始された新しいコマンドのようです（参考 [baseN: new program suggestion (various 'base' encoding)](https://lists.gnu.org/r/coreutils/2018-11/msg00014.html)）。ちなみに GNU coreutils には `base32` コマンドもあります。

ヘルプより以下の形式とオプションに対応しています。

```txt
$ basenc --help
Usage: basenc [OPTION]... [FILE]
basenc encode or decode FILE, or standard input, to standard output.

With no FILE, or when FILE is -, read standard input.

Mandatory arguments to long options are mandatory for short options too.
      --base64          same as 'base64' program (RFC4648 section 4)
      --base64url       file- and url-safe base64 (RFC4648 section 5)
      --base32          same as 'base32' program (RFC4648 section 6)
      --base32hex       extended hex alphabet base32 (RFC4648 section 7)
      --base16          hex encoding (RFC4648 section 8)
      --base2msbf       bit string with most significant bit (msb) first
      --base2lsbf       bit string with least significant bit (lsb) first
  -d, --decode          decode data
  -i, --ignore-garbage  when decoding, ignore non-alphabet characters
  -w, --wrap=COLS       wrap encoded lines after COLS character (default 76).
                          Use 0 to disable line wrapping
      --z85             ascii85-like encoding (ZeroMQ spec:32/Z85);
                        when encoding, input length must be a multiple of 4;
                        when decoding, input length must be a multiple of 5
      --help        display this help and exit
      --version     output version information and exit
```

あまり使わなそうなものばかりですが、唯一使いそうなものは `--base64url` でしょう。base64url はファイル名や URL として使いやすく変更された Base64 で、本来の Base64 で使用する `+` と `/` の代わりに、`-` と `_` を使用します。ただし base64url は仕様上はパディングの `=` は省略可能なはずですが `basenc` では必須となっているようです。

### opensslコマンドのBase64対応

`openssl` コマンドを使った Base64 エンコードまたはデコードもよく行われています。`openssl` コマンドは 64 文字で折り返します。この折返しを抑制するには `-A` オプションを使用します。

**エンコード**

```console
$ echo abc | openssl base64
YWJjCg==

$ echo abc | openssl enc -e -base64
YWJjCg==
```

**デコード**

```console
$ echo 'YWJjCg=='| openssl base64 -d
abc

$ echo 'YWJjCg=='| openssl enc -base64 -d
abc
```

### uuencodeコマンドのBase64対応

`uuencode` は POSIX で標準化されている「UU エンコードを行うコマンド」ですが、実は `-m` オプションで Base64 エンコードにも対応しています。実は **Base64 に対応した POSIX コマンドはある**のです。

```console
$ echo abc | uuencode filename # デフォルトの UU 形式
begin 644 filename
$86)C"@``
`
end

$ echo abc | uuencode -m filename # Base64 形式
begin-base64 644 filename
YWJjCg==
====
```

前後に特殊な行（`begin 644 filename` と `end`、`begin-base64 644 filename` と `====`）がついていますが、これは元々 uuencode がメールの一部としてファイルを埋め込むためのものだからです。パーミッションおよびファイル名が形式の一部として含まれており終了文字列で終わりが示されています。なお一行は 60 文字のようです。

`uuencode` コマンドを `base64` コマンドのように使うには次のようにします。また Base64 エンコードをデコードするには `uudecode` を使用します。

```sh
# sed コマンドで前後の一行を取り除く
$ echo abc | uuencode -m filename | sed '1d;$d'
YWJjCg==

# 開始行と終了行を追加する
$ printf '%s\n' 'begin-base64 644 -' 'YWJjCg==' '====' | uudecode -o /dev/stdout
abc
```

**この使い方は完全に POSIX で標準化された範囲での使い方**です。`/dev/stdout` という標準出力を示す特殊なファイルは POSIX で標準化されていませんが、`uuencode` と `uudecode` に限っては「標準出力を意味する特別な文字列」として解釈するように規定されています。`/dev/stdout` ではなく `-` でもおそらく動作すると思いますが、`-` は以前は標準入力を示す意味としてのみ使用されていたため、POSIX では標準出力の意味で `-` を使うことを避けたかったそうです。そして本来 POSIX では標準化されていない `/dev/stdout` を特別な意味として解釈するという抜け道のような方法で規定されています（[参考](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/uudecode.html)）。

`uuencode` と `uudecode` は POSIX で標準化されているものの、Issue 6 までは UP （ユーザーポータビリティ）オプションでした。それもあって **`uuencode` と `uudecode` は標準構成であってもインストールされていることが少ないコマンド**です。POSIX コマンドであっても必ずしもインストールされているわけではなく、これらのコマンドを使っても環境によってはそのまま動作するわけではないことに注意が必要です。

#### b64encodeコマンドについて

FreeBSD と OpenBSD では `uuencode -m` の別名として `b64encode` コマンドがあります。出力形式は `uuencode` と同じです。同様に `b64decode` もあります。

### 「echo -n ... | base64」よりもprintf推奨

`echo ... | base64` は `echo` コマンド出力の末尾に追加される改行まで Base64 エンコードを行うので注意してください。

```console
$ echo abc | base64 | base64 -d | od -tx1
0000000 61 62 63 0a     ← 末尾に 0a（改行）が含まれている
0000004
```

これを防ぐために `-n` オプションがよく使われています。

```console
$ echo -n abc | base64 | base64 -d | od -tx1
0000000 61 62 63     ← 末尾の 0a（改行）がない
0000003
```

しかし `-n` オプションには移植性がなく、例えば macOS の `/bin/sh` では `-n` という文字として出力されてしまいます。

```console
$ /bin/sh
$ echo -n abc | base64 | base64 -d | od -tx1
0000000    2d  6e  20  61  62  63  0a  ← 冒頭に「-n 」の文字がある
0000007
```

この問題はシェルスクリプトの一行目に `#!/bin/sh` と書いているときにも発生します。理解した上で行っているのであれば `echo -n` を使ってもよいのですが `printf "%s"` を使ったほうが移植性は向上します。

```console
$ printf "%s" abc | base64 | base64 -d | od -tx1
0000000 61 62 63
0000003
```

## sh-base64 - POSIXコマンドによる独自実装

さて既存のコマンドで Base64 を扱えるコマンドはあります。しかし標準構成で動くとは限りません。コマンドをインストールすればよいだけではあるのですが、シェルスクリプトファイル一つをコピーするだけで動けば便利でしょう。

**sh-base64 は小さなシェル関数として実装**されており、あなたのシェルスクリプトに含めて配布することが可能になっています。この関数を使うと別のコマンドをインストールする必要はなく、シェルスクリプトファイル一つをコピーするだけで動くようにすることができます。

### 移植性

私は最大限の移植性を実現するために、外部コマンドに依存せずにシェル言語のみで作ることが多いのですが、今回はいくつかの POSIX コマンドを使っています。使用している外部コマンドは POSIX コマンドのうちどの環境でもインストールされていることを確認した `od`, `tr`, `fold`, `xargs`, `awk` のみなのでほぼすべての環境で問題なく動作するはずです。

### パフォーマンス

外部コマンドを使用した理由はパフォーマンスです。小さなデータであれば外部コマンドを呼び出すコストのほうが大きくなるため、シェル言語で実装するほうがパフォーマンスは高いのですが、Base64 エンコードまたはデコードするデータはある程度の大きさになるためシェル言語だけで作ると十分なパフォーマンスが出ません。もちろんネイティブコマンドよりは圧倒的に遅いのですが、それでも数 MB までのデータなら実用的な速度になったと考えています。

その他にパフォーマンスを上げるために、外部コマンド版の `printf` コマンドの呼び出し回数を減らしたり、Base64 エンコードの 2文字を一組として扱って変換するという工夫をしています。

### シェル関数による実装

Base64 エンコーダーとデコーダーはシェル関数で実装しています。「本当にやるべきこと」のみを実装しているため、エンコーダーとデコーダーのそれぞれのシェル関数は 40 行程度と小さなものになっています。どちらか片方だけで使うことができ、必要なものを「あなたのシェルスクリプト」に含めて使うことを想定しています。シェルスクリプトで使う場合にはコア機能だけで十分なことがほとんどでしょう。再利用しやすい形にできたのではないかと思います。

### 使用方法

`base64encode` と `base64decode` の 2 つのシェル関数があります。引数を一つ渡すことができ、Base64 で使用する 62番目の文字と63 番目の文字の 2文字、もしくはパディングに使用する文字を加えた 3 文字を指定することができます。これにより base64url などにも対応可能になっています。76 文字での折り返しの機能はありません。

```sh
# 「.」で読み込まずに中身をそのままコピーしても良い
. ./base64.sh 

# Base64 encode
base64encode       # Basic usage (base64 standard)
base64encode "+/"  # Use + and / for the 62nd and 63rd characters, no padding
base64encode "-_"  # Use - and _ for the 62nd and 63rd characters, no padding (base64url)
base64encode "+/=" # Use + and / for the 62nd and 63rd characters and = for padding

# Base64 decode
base64decode       # Basic usage (base64 standard)
base64decode "+/"  # Use + and / for the 62nd and 63rd characters
base64decode "-_"  # Use - and _ for the 62nd and 63rd characters (base64url)
```

引数の仕様が分かりづらいと思う方はこれをラップした便利関数を作成してください。シェル関数として実装しているのは内部で使うものであり、内部で使う場合にはこれだけで十分と考えているからです。それによりコードを短くしてあなたのシェルスクリプトに埋め込みやすくしています。

提供する機能はコアのみで、必要なら欲しい人が追加すれば良いという考え方です。対話シェルから利用しやすくしたりさまざまな機能を追加するとコードが膨れ上がってしまいます。折返しの機能がないのも必要な人が `fold` コマンドと組み合わせれば十分だからです。「一つのことだけをする」という Unix 哲学の考え方を実践するためにシェル関数にしています。

### ライセンス

ライセンスはパブリックドメイン相当の 0BSD を使用しています。ご自由にお使いください。

## さいごに

ということで、シェルスクリプトで Base64 したい人のためのまとめと、移植性があるシェルスクリプトを書きたい人のための、POSIXコマンドだけを使った独自実装の話でした。

sh-base64 のコードの実装についてはもう少し解説したい（引数全体や一つの引数のサイズ制限の回避方法など）ことがあるのですが、Base64 とはあまり関係ない話が多く、少々長くなるので別記事で解説したいと考えています。~~ここからリンクを貼る予定です。~~ 記事を書きました。→ [シェルスクリプトでバイナリは扱えるが、うまく扱うのは大変だ！](https://qiita.com/ko1nksm/items/2c22e700b547ff99ecc3)