POSIX 準拠 awk 用の JSON エスケープ文字列のデコード関数

## はじめに

[シェルスクリプトの実験のために作った POSIX 準拠 awk 実装の JSON パーサー (SAX風ストリーミング対応)](https://qiita.com/ko1nksm/items/35952abf8feb3ec1eae9) に関連して JSON データの中にあるエスケープされた文字列をデコードする必要があるだろうということで実装しました。コードのライセンスは CC0 としますので、コピペして使うなり、加工して自分のコードとして使うなり、ご自由にどうぞ。

## 実行例

```sh
$ printf '%s\n' '\\\u0009\u0022\u0061\u3042\u00c0\n\ud83c\udf40\uff05' | awk -b -f unescape.awk
\      "aあÀ
🍀％
```

**注意点** 環境によっては `echo` がエスケープシーケンスを解釈しておかしな結果になるので検証には `printf` を使ってください。また gawk の場合、ロケールを C (`LC_ALL=C`) にするか `-b` (`--characters-as-bytes`) オプションを付ける必要があります。

```sh
# 参考 シェルスクリプトの上にこんなコードを入れておけば、必要な場合に -b オプションを追加します
if ! awk 'BEGIN { exit (sprintf("%c", 128) != "\200") }'; then
  awk() { command awk -b "$@"; }
fi
```

## コード

Unicode エスケープシーケンス (`\uHHHH`) は UTF-8 で出力します。サロゲートペアにも対応しています。

```awk:unescape.awk
# JSON escape decoder
# License: Creative Commons Zero v1.0 Universal (CC0)

function initialize(  i) {
    for (i = 0; i <= 127; i++) {
        _hex2char[sprintf("%02x", i)] = sprintf("%c", i)
    }
    _esc2char["b"]  = _hex2char["08"]
    _esc2char["t"]  = _hex2char["09"]
    _esc2char["n"]  = _hex2char["0a"]
    _esc2char["f"]  = _hex2char["0c"]
    _esc2char["r"]  = _hex2char["0d"]
    _esc2char["\""] = _hex2char["22"]
    _esc2char["/"]  = _hex2char["2f"]
    for (i = 0; i <= 255; i++) {
        _hex2dec[sprintf("%02x", i)] = i
    }
}

function esc2char(ch) {
    return _esc2char[ch]
}

function hex2char(hex) {
    return _hex2char[tolower(hex)]
}

function hex2dec(hex) {
    hex = tolower(hex)
    return _hex2dec[substr(hex, 1, 2)] * 256 + _hex2dec[substr(hex, 3)]
}

function unescape(str,  ret, array, i, len, codepoint, c1, c2, c3, c4) {
    len = split(str, array, /\\/)
    ret = array[1]
    for (i = 2; i <= len; i++) {
        str = array[i]
        if (str == "") {
            ret = ret "\\" array[++i]
        } else if (match(str, /^[btnfr""\/]/)) {
            ret = ret esc2char(substr(str, 1, 1)) substr(str, 2)
        } else if (match(str, /^u00[0-7][0-9A-Fa-f]/)) {
            codepoint = substr(str, 4, 2)
            ret = ret hex2char(codepoint) substr(str, 6)
        } else if (match(str, /^u0[0-7][0-9A-Fa-f][0-9A-Fa-f]/)) {
            codepoint = hex2dec(substr(str, 2, 4))
            c1 = 192 + int(codepoint / 64); codepoint %= 64 # 0x40
            c2 = 128 + codepoint
            ret = ret sprintf("%c%c", c1, c2) substr(str, 6)
        } else if (match(str, /^u[Dd][89AaBb][0-9A-Fa-f][0-9A-Fa-f]$/)) {
            # Surrogate pair to Code point:
            #   ((High Surrogate - 0xD7C0) << 10) + (Low Surrogate - 0xDC00)
            codepoint = hex2dec(substr(str, 2, 4)) - 55232 # 0xD7C0
            codepoint *= 1024 # <<= 10
            continue
        } else if (match(str, /^u[Dd][C-Fc-f][0-9A-Fa-f][0-9A-Fa-f]/)) {
            codepoint += hex2dec(substr(str, 2, 4)) - 56320 # 0xDC00
            c1 = 240 + int(codepoint / 262144); codepoint %= 262144 # 0x40000
            c2 = 128 + int(codepoint / 4096); codepoint %= 4096 # 0x1000
            c3 = 128 + int(codepoint / 64); codepoint %= 64 # 0x40
            c4 = 128 + codepoint
            ret = ret sprintf("%c%c%c%c", c1, c2, c3, c4) substr(str, 6)
        } else if (match(str, /^u[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]/)) {
            codepoint = hex2dec(substr(str, 2, 4))
            c1 = 224 + int(codepoint / 4096); codepoint %= 4096 # 0x1000
            c2 = 128 + int(codepoint / 64); codepoint %= 64 # 0x40
            c3 = 128 + codepoint
            ret = ret sprintf("%c%c%c", c1, c2, c3) substr(str, 6)
        } else {
            ret = ret "\\" str
        }
        codepoint = 0
    }
    return ret
}

BEGIN { initialize() }

{ print unescape($0) }
```