LoginSignup
0
0

More than 1 year has passed since last update.

printfコマンドが\xHHに対応してないなら自力で対応させれば良い

Last updated at Posted at 2022-09-24

はじめに

多くのシェルでビルトインコマンドのになっている printf コマンドは、シェルによって対応しているフォーマット指定子が異なり、16 進数で文字を表現する \xHH は dash (ash 系のシェル)では対応してないということをご存知でしょうか? 手元の bash で動作確認して、いざ本番環境で動かしてみたら表示されない、といったことをしばしば見かけます。移植性がないため POSIX (ls)でも標準化できていないフォーマット指定子です。

8 進数で表現する \NNN であれば、どのシェルでも対応しています。したがって移植性がある方法は \xHH ではなく慣れない\NNN を使う方法です。せっかく書いたコードを書き直さなければなりません。……というのが多くの人の発想ではないかと思いますが、シェルスクリプトプログラミングを学んだ人の発想は違います。この記事では、この問題をシェルスクリプトプログラミングを使ってどのように解決するのかを紹介します。

ちなみに、なぜ \xHH に対応していないのかは、きちんと調べていませんが、昔は 8 進数の方がよく使われていた(16 進数表記でA〜Fを使うという発想は誕生してないか広く使われていなかった)からだと思われます。いい加減対応しろって感じですよね。POSIX が生まれたせいで、標準化されてないものには対応しなくても良いという風潮が生まれているように思えます。

状況確認

どのような問題かは想像できると思いますが、念の為に状況を確認しておきましょう。何事も問題を正しく把握することは重要なことです。

printf.sh
#!/bin/sh

printf '[\x41]\n'

上記のようなシェルスクリプトを、macOS で実行すると以下のように出力されます。なお macOS の /bin/sh は(POSIX 準拠モードの)bash 3.2.57です。

$ ./printf.sh
[A]

一方、このシェルスクリプトを Ubuntu で実行するとこのようになります。なお Ubuntu の /bin/sh は dash です。

$ ./printf.sh
[\x41]

さらに、FreeBSD では以下のように表示されます。FreeBSD の /bin/sh は ash です。

$ ./printf.sh
[x41]

同じ \xHH に対応していないという状況であっても出力に差があることがわかると思います。

外部コマンドの printf を使う

簡単な対応策は、外部コマンドの printf を使うことです。シェルスクリプトから外部コマンドの printf を使うには env printf で実行するのが簡単です。

printf.sh
#!/bin/sh

env printf '[\x41]\n'

上記のコードは正しく動作します。しかし全ての printf コマンドの前に env をつけるのは面倒でしょう。これを回避する方法は簡単です。以下のようにするだけです。

printf.sh
#!/bin/sh

printf() {
  env printf "$@"
}

printf '[\x41]\n'

シェルビルトインコマンドの printf コマンドを、ユーザー定義の printf 関数で置き換え、外部コマンド版の printf コマンドを呼び出す。ただこれだけです。

ただし、この方法は欠点があります。FreeBSD では外部コマンド版の printf\xHH に対応していません。macOS も /bin/sh\xHH に対応しているのですが、外部コマンド版の printf も同様に対応していません。ただし GNU 版の Coreutils をインストールするだけで gprintf というコマンド名で使えるようになるので、あとは適当に条件分岐して呼び出すコマンドを変更するだけです。

また外部コマンドの printf コマンドを呼び出しは、シェルビルトイン版のコマンドよりも遅いという問題もあります。

別解 自力で \xHH をデコードする

GNU 版 Coreutils をインストールすれば解決できるとは言え、これだけのためにインストールするのは面倒だと思うかもしれません。もっとも移植性が高いは自力で \xHH をデコードすることです。自力でデコードすることも可能ですが少し面倒なので、以下の例では \xHH\NNN の 8 進数表記に変更することで対応しています。

printf.sh
#!/bin/sh

printf() {
    decode_format decoded_format "$1"
    shift
    set -- "$decoded_format" "$@"
    unset decoded_format
    command printf "$@"
}

decode_format() {
    set -- "$1" "${2#*\\}\\" "${2%%\\*}"
    while [ "$2" ]; do
        set -- "$1" "${2#*\\}" "$3" "${2%%\\*}"
        case $4 in
            x[0-9a-fA-F][0-9a-fA-F]*)
                set -- "$1" "$2" "$3" "0${4%"${4#???}"}" "${4#???}"
                set -- "$1" "$2" "$3\\$(($4>>6 & 0x3))$(($4>>3 & 0x7))$(($4 & 0x7))$5"
                ;;
            x[0-9a-fA-F]*)
                set -- "$1" "$2" "$3" "0${4%"${4#??}"}" "${4#??}"
                set -- "$1" "$2" "$3\\$(($4>>3 & 0x1))$(($4 & 0x7))$5"
                ;;
            *) set -- "$1" "$2" "$3\\$4" ;;
        esac

    done
    eval "$1=\$3"
}

printf '[\x41]\n' # => [A]

コードはごちゃごちゃいていますが、大したことはしていません。単に \xHH の HH 部分を 16 進数から 8 進数に変換しているだけ、単純なビット演算で事足ります。説明が必要なのは command printf "$@" の部分でしょう。command コマンドを使うとユーザー定義のシェル関数を無視してコマンド(外部コマンド or ビルトインコマンド)を呼び出します。したがってこのコードは printf 関数を再帰呼び出しすることはなく、シェルビルトインの printf コマンドが呼び出されます。このテクニックは完全に POSIX シェルの言語仕様の範囲だけで実現されています。

なお、この記事はこのようなテクニックを紹介するのが目的なので、FreeBSD やその他の環境で動くかのテストはしていません。あしからず。(動くはずですが)

さいごに

たまにシェルスクリプトはプログラミング言語なのか?などと言う人がいますが、それは言っている人がプログラミング言語の定義を知らず、またシェルスクリプトのプログラミング能力を知らないからです。シェルスクリプトのプログラミングを知れば、このようにシェルスクリプト自身でシェルスクリプトの機能を拡張することが出来ます。

このようなテクニックを利用してシェルの互換性問題を解決しつつ、より高度な機能を提供するライブラリが、シェルスクリプトの世界にもあればよいのですが、残念ながらそのようなよく知られたライブラリは(おそらくまだ)存在していません。シェルスクリプトプログラミング技術が広まれば「printf コマンドは 16 進数表記は移植性がないから 8 進数表記を使わなければいけない」と言った無駄なバッドノウハウを減らしていくことが出来るでしょう。「8 進数で書かないとだめなんだよ」というバッドノウハウはドヤ顔で語るようなありがたい知識ではありません。なくしていくべきものです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0