はじめに
POSIX に準拠しているシステムなら tr -d '0-9'
は意図したとおりに動作します。動作しないのは「POSIX 準拠モード」を有効にしていないからです。UNIX の認証を取得しているシステムであれば必ず POSIX に準拠しています。
もし「POSIX 準拠モード」を知らずに「互換モード」を使っていたら、ただでさえ移植性が低いシェルスクリプトで、本来必要のない互換性問題に対応しなければならなくなってしまい、無駄に開発コストを上げることになってしまいます。(注意「POSIX 準拠モード」や「互換モード」は私が便宜上につけた名前です)
せっかくベンダーが UNIX の認証を取得して POSIX に準拠してるというのに、それを使用する側が POSIX 準拠モードを使わずに POSIX 以前の古いコードを書いていたとしたら POSIX の意味がありません。
注意事項(前提条件)
毎回前書きや注意事項が長くて面倒かもしれませんが、これを書いてないと前提としてない例外的なシステムの話をされるので申し訳ありません。
まず前提として私は POSIX に準拠したシステムを最低ラインとして考えています。古い時代、POSIX 策定以前(1988 年より前)には POSIX に準拠してないシステムがあったことを知ってはいますが、古いシステムはサポートが終了しているはずですし、使われてない(使うべきではない)ので考慮していません。Solaris、AIX、HP-UX など現在サポートが行われており POSIX に準拠しているシステムのみを対象としています。もちろん POSIX に完全準拠しているシステムだけではなく POSIX にほぼ準拠している Linux や BSD 等もサポートが終了していないのであれば対象です。
POSIX に準拠していない環境(例えば AT&T 本家の オリジナルの UNIX System V)まで考慮すべきだと言われても、対応すべき範囲が際限なく広がってしまうので現実的ではありません。また POSIX に準拠していないのであれば、どう動くかは実際に該当の環境を使う以外に知りようがないのでそれらを想定して書くのは不可能です(POSIX が登場したのは、それ以前のシステムに移植性がないからです)。もしそれらの環境を実際に使うことがあるならばその環境に応じた回避策を組み込むというだけのことです。
POSIX に準拠した書き方
まず最初に POSIX に準拠した書き方を紹介しておきます。要するに []
で囲う必要はありません。
$ echo "abc123" | tr 'a-z' 'A-Z'
ABC123
$ echo "abc123" | tr -d '0-9'
abc
注意 この記事の内容はロケールに依存するので注意してください。
昔は両方の環境で動く書き方をする必要があった
tr
コマンドの範囲指定の書き方は 0-9
のように []
を使わないものと [0-9]
のように []
を使うものの二通りがあります。POSIX に準拠したシステムでは []
を付ける必要はありません。開始文字と終了文字を -
で区切るだけです。-d
オプション(フラグとも言う)は指定された文字を削除するという意味ですので tr -d '0-9'
を実行すると標準入力で与えられた文字列から 0
から 9
までの数字を削除します。この書式はもともと BSD 版の tr
コマンドで採用されていた書式です。そして BSD 版の書式は System V 版の tr
コマンドでは動きません。
BSD 版の書式は System V 版で使うと、エラーで落ちてしまいます。
だから、[]
(カッコ)で囲う必要があったんですね。
# BSD 版の書式を System V 版で使うとエラーで落ちる
$ echo "[a012b]" | tr -d '0-9' # [] で囲う必要がある
[a12b]
# System V 版の書式を BSD 版で使うと [] まで除去の対象となる
$ echo "[a012b]" | tr -d '[0-9]' # [] で囲ってはいけない
ab
なんで?なんで?なんで?落ちてないじゃーん?このコマンド!-d
フラグまでイカレてるんじゃないだろうな!・・・まさか・・・そうかぁぁぁ! 0-9
は 0
, -
, 9
の意味なのかぁぁぁ!あああああ!ふーーざーーk(略)
はい、本当にすみませんでした。えーと、訂正です。**上のコードは忘れてください。**BSD 版の書式 0-9
は、System V 系で使うと範囲指定の意味ではなく 0
、-
、9
という 3 文字として解釈されるため、それぞれの文字が削除されてしまいます。
# BSD 版の書式を System V 版で使うと範囲指定にならない【訂正】
$ echo "[a-0123456789b]" | tr -d '0-9' # [] で囲う必要がある
[a12345678b]
# System V 版の書式を BSD 版で使うと [] まで除去の対象となる
$ echo "[a012b]" | tr -d '[0-9]' # [] で囲ってはいけない
ab
# BSD 版でも System V 版でもどちらでも使える書き方
$ echo "[a012b]" | tr -d '0123456789'
ab
シェルスクリプトを BSD 版と System V 版の両方に対応したい場合もあると思います。
だから、どちらでも動く書き方を、昔はする必要があったんですね。
動かないのは「POSIX 準拠モード」にしてないから
Solaris、AIX、HP-UX など UNIX に認証を取得したシステムは POSIX に準拠しているため、System V ベースであっても System V の書き方をする必要はありません。POSIX (BSD) の書き方で動かないとしたら、それは「POSIX 準拠モード」になっていないからです。
実はこの記事は「POSIX 準拠モード」のことを解説した「POSIX準拠モードに変更して互換性を上げる方法」の関連記事です。「POSIX 準拠モード」を有効にする具体的なやり方はこの記事を参照してください。と言っても難しいものではなく、単に環境変数を設定するだけです。例えば Solaris の場合は環境の設定として PATH="$(getconf PATH):$PATH"
を実行しておくだけです。ただしベンダーによってやり方は異なるので注意が必要です。
他にも POSIX に準拠してないコマンドはいくつもあります。Solaris では /usr/xpg4/bin/
や /usr/xpg6/bin
に POSIX に準拠したコマンドが置かれています。これらのコマンドがいつからあるのか調べた所 「autoconf のドキュメント」から Solaris 2.0 (1992) の頃にはあったことがわかります。1992 年というと POSIX.2 (シェルとユーティリティ)の標準化が行われた年でもあります。つまり**今からおよそ 30 年前の POSIX で標準化されたときから「POSIX 準拠モード」は使えたことになります。**そして製品のサポート期間(Solaris 1.x のサポート終了が 2003 年)を考慮するとおよそ 20 年近く前には System V の構文は使わなくて良くなっていただろうと思われます。これは Solaris の話ですが他の 商用 Unix でも同様でしょう。
商用 Unix の多くは POSIX が標準化される以前から UNIX システムを販売して商売をしています。POSIX は POSIX の仕様の範囲で互換性を保っていれば十分でしょうがベンダーにとっては十分ではありません。いくら POSIX で標準化されたからと言って、古くからの顧客のために過去のシステムとの互換性を安易に切り捨てるわけには行きません。ベンダーにとっての優先順位は「POSIX 以前との互換性 > POSIX 準拠」です。そこで採用したのがデフォルトを「互換モード」にしておき、設定によって「POSIX 準拠モード」にするという方法なのです。「互換モード」はあくまでも古いソフトウェアを壊さないために実装されているものです。POSIX に準拠しているコマンドは新しく作るソフトウェアが使うために用意されています。
System V を切り捨てて BSD の動作を採用した理由
さて少し話がそれますが、知識としてどうして POSIX は BSD の動作を採用したかという話をしたいと思います。その前に一つ勘違いしている人がいそうなので補足しておくと POSIX は Unix の OS としてのインターフェースを定義したものですが Unix を開発した AT&T が作った標準規格ではありません。実は AT&T は POSIX とは別に SVID (System V Interface Definition) と呼ばれる UNIX の標準規格を別に作っています(参考)。これはその名の通り System V のインターフェースの定義をしたものです。一方 POSIX は System V だけではなく BSD を視野に入れた、別の OS との移植性のための標準規格です。そのため System V をひいきしてはいません。BSD の仕様の方が優れていると判断すれば BSD の仕様を採用します。
本題に戻って POSIX が System V を切り捨てて BSD の動作を採用した理由ですが、これは POSIX の tr
の RATIONALE に書かれています。ただ少し分かりづらい気もします。要約すると「一番被害が小さいから」です。まず tr
は「translate characters」の略で文字集合を変換するものです。ここまでの説明では話が簡単なため -d
という"オプション"をつけて説明していましたが、一般論としてオプションは省略可能なものです。ということで -d
をつけない場合の話をします。(余談ですが一部のコマンドにはコマンドラインオプションに省略不可能な「必須オプション」を備えたものがありますが、これはオプションの意味に照らし合わせると矛盾しており実にナンセンスなものです。)
tr
を使って小文字を大文字に「変換したい場合以下のように書きます。
echo "a123b" | tr 'a-z' 'A-Z' # BSD の書き方(POSIX 準拠)
echo "a123b" | tr '[a-z]' '[A-Z]' # System V の書き方
この 2 つの書き方を、それぞれの環境で実行した結果がこれです。
# Solaris の書き方([] 必須)を POSIX で採用していたとしたら
$ echo "a123b" | tr 'a-z' 'A-Z' # × BSD の書き方は動かない
$ echo "a123b" | tr '[a-z]' '[A-Z]' # ○ System V の書き方だけ動く
# BSD の書き方([] なし)を POSIX で採用したので
$ echo "a123b" | tr 'a-z' 'A-Z' # ○ BSD の書き方は動く
$ echo "a123b" | tr '[a-z]' '[A-Z]' # ○ System V の書き方も動く
このように仮に System V 版の構文を採用していたとしたら BSD で使われていたスクリプトは壊れてしまいます。一方、BSD 版の構文を採用したため、BSD でも System V も壊れることはありませんでした。BSD 版の書き方は [
, ]
がそれぞれ同じ文字である [
, ]
に変換されるため無害なのです。
-d
の場合は問題あるじゃないか?というのはその通りなのですが -d
はあくまでオプションですし POSIX 流に「標準化しない」という手段だって取れます。また削除は sed
で代用することができます。しかし tr
の標準的な使い方である文字集合の変換は sed
では代用できません(\U
や \L
を使った大文字小文字変換は GNU 拡張です)。awk
でなら代用できますが長くなりますし、POSIX に準拠していない環境だと最小限の機能しか持たない歴史的な awk の可能性があるため別の注意が必要になってしまいます。(実際に Solaris 10/11 の /usr/bin/awk
は歴史的な awk であるため toupper
/ tolower
関数が使えません。)
ということで BSD の仕様を採用するのは、明らかなメリットがあったということです。これが POSIX が言っている「this convention is used here to avoid breaking large numbers of BSD scripts
」(多くのBSD スクリプトを壊さないために)という言葉の意味なのでしょう。
余談ですが GNU の tr の man ページにはこのように書かれています。
tr [OPTION]... SET1 [SET2]
CHAR1-CHAR2
CHAR1 から CHAR2 までを昇順に展開した文字列[CHAR1-CHAR2]
SET1 と SET2 の両方で指定した場合には CHAR1-CHAR2 と同じ
一瞬 []
を使った書式に対応しているのかと思いきや「SET1 と SET2 の両方で指定した場合」という条件が付いています。つまり置換の場合は無害で -d
の場合には使えない(正確にはどう動くのか書いてない)というだけの事です。この書き方はちょっとひどいですね。
ShellCheck おすすめ
シェルスクリプトを書く時にはおすすめの ShellCheck はこの問題も指摘してくれます。
$ cat sample.sh
#!/bin/sh
echo a123b | tr -d '[0-9]'
$ shellcheck sample.sh
In sample.sh line 2:
echo a123b | tr -d '[0-9]'
^-----^ SC2021: Don't use [] around classes in tr,
it replaces literal square brackets.
For more information:
https://www.shellcheck.net/wiki/SC2021 -- Don't use [] around classes in tr...
リンク先のページ、SC2021 ではこの警告の Rationale (理論的根拠)が示されており、これが古い System V の tr
の実装で必要だったものであり、POSIX、GNU、macOS、 BSD では必要ないということが書かれています。
Rationale:
Ancient System V tr required brackets around operands, but modern implementations including > POSIX, GNU, OS X and *BSD instead treat them as literals.Unless you want to operate on literal square brackets, don't include them.
tr コマンドだけを POSIX に準拠させる方法
「POSIX 準拠モード」で POSIX に準拠させることが出ることはわかったが、すでに大量の「互換モード」用のシェルスクリプトがある。リスクが大きいため一気に変更することはできない。というような場合に、参考として tr
コマンドだけを POSIX に準拠させるテクニックを紹介します。やり方は細かい方針の違いでいくつか考えられますが、外部コマンド版とシェル関数版の 2 つを紹介します。なお使用しているシェルも POSIX シェルではなく Bourne シェルの可能性が高いのでそれも考慮しています。
外部コマンド版
一番簡単な例です。この方法を使うことで、tr
コマンドだけを POSIX に準拠させることができます。ひとまず他のコマンドの対応は置いておいて、tr
コマンドだけ対応したいという場合に使用することができます。
#!/bin/sh
# HP-UX のように POSIX 準拠モードにするために別の環境変数を
# 設定する必要がある場合はここ設定して export する
/usr/xpg4/bin/tr ${1+"$@"} # パスは適切なパスに書き換えること
#!/bin/sh
echo "[a012b]" | tr -d '0-9'
# PATH 環境変数を変えて tr コマンドを実行した時に /opt/bin/tr が起動するようにする
PATH="/opt/bin:$PATH"
./srcript1.sh
もし呼び出すスクリプトごとに tr
を POSIX に準拠させるかどうかを切り替えたい場合は、例えば環境変数 POSIX_TR
のようなものを作って起動するコマンドを切り替えるようにします。(その他の手段として呼び出し元のコマンド名で切り替える方法も考えられます。)
Solaris のように POSIX に準拠したコマンドが別のパスに用意されている場合
#!/bin/sh
if [ "$POSIX_TR" ]; then
/usr/xpg4/bin/tr ${1+"$@"}
else
/usr/bin/tr ${1+"$@"}
fi
HP-UX のように POSIX 準拠モードにするために別の環境変数を設定する必要がある場合
if [ "$POSIX_TR" ]; then
export UNIX_STD
UNIX_STD=2003
fi
/usr/bin/tr ${1+"$@"}
これで環境変数 POSIX_TR
が設定されている場合だけ tr
コマンドだけが POSIX に準拠するようになります。スクリプト(script1.sh
)の中で POSIX_TR
を設定しても構いません。
PATH="/opt/bin:$PATH"
POSIX_TR=1 ./srcript1.sh
シェル関数版
書いてしまったので一応紹介しますが、今回の例では外部コマンド版の方が便利そうです。
tr() {
(
# HP-UX のように POSIX 準拠モードにするために別の環境変数を
# 設定する必要がある場合はここ設定して export する
/usr/xpg4/bin/tr ${1+"$@"}
)
}
# もしパスを getconf で取得したい場合の例
# POSIX_PATH=`getconf PATH`
# tr() {
# (
# export PATH
# PATH="$POSIX_PATH:$PATH"
# env tr ${1+"$@"}
# )
# }
あとはこのように tr
コマンドを POSIX に準拠させたいスクリプトの頭で上記のスクリプトを読み込むだけです。
#!/bin/sh
. ./tr.sh
echo "[a012b]" | tr -d '0-9' # => [ab]
もし bash スクリプトであれば、以下のように環境変数 BASH_ENV
でスクリプト実行前に読み込むスクリプトを指定することで、スクリプトを書き換えることなく tr
コマンドだけを POSIX に準拠させることもできます。
#!/bin/bash
echo "[a012b]" | tr -d '0-9'
BASH_ENV=./tr.sh ./script2.sh
このようにシェルスクリプトのプログラミングテクニックを利用することで、元のシェルスクリプトを僅かな修正、または全く修正せずに、段階的に POSIX に準拠させていくというようなこともできます。必ず使用すべきテクニックというものではありませんが、スクリプトのコード量が多くて一度に修正するのが大変な場合に使うことが出来るテクニックです。
また、このテクニックを応用すると System V 用の古い構文が使われている場合に警告を出力したりすることも可能になります。いろいろと応用の幅が広いテクニックなので覚えておくと良いと思います。ただし使いすぎると逆に修正に時間がかかってしまうこともあるので使い所には注意が必要です。
さいごに - アジャイル「変化ヲ抱擁セヨ」
この話も私が危惧している「シェルスクリプトの知識や技術って全然更新されないよね」という問題の一つなのだと思いますが、さらにこの話は POSIX だけに詳しくても知ることはできません。POSIX には「POSIX に準拠してないシステムを POSIX に準拠させる方法についての仕様」がないからです。「POSIX 準拠モード」にする方法はベンダーごとに違うので、ベンダーが用意したマニュアルを読まなければ知りようがありません。
ただし、このような問題があるのは、古くからある商用 Unix が主です。そしてそれらの商用 Unix の多くは UNIX の認証を取得しています。UNIX の認証を取得しているならば POSIX に準拠している、もしくは準拠させる方法があるはずだという推測をすることはできます。POSIX に加えて Unix と UNIX の認証の意味を正しく理解することでもこの問題を回避することはできるでしょう。
多くの人は POSIX 準拠以降の新しい世代(私を含む☺)でしょうから、普通に tr
コマンドの使い方を調べて使っていれば古い書き方をすることはないと思います。しかしシェルスクリプト関係は技術書は古いものばかりだったり、ネット上の記事も古いものが多いのでそれらを参考にしていると間違って学んでしまう可能性があります。例えば 2000 年(原書は 1993 年)に出版されたオライリーの「入門 Korn シェル」では P133 で大文字から小文字への変換で tr [A-Z] [a-z]
を使用しています。Korn シェルは主に商用 Unix の世界で使われていたシェルです。ですので商用 Unix を使うときに、この本で Korn シェルを勉強してしまうと間違って覚えてしまう可能性があります。しかしこれ以外に Korn シェルを専門に扱った日本語の本はありません。
また、実際の開発現場で実践経験豊富な上司や先輩から「口伝でのみ」技術が伝わっている場合も新しい知識に更新されない原因となります。実戦経験が長いと言えば聞こえはいいですが、同じことを何年繰り返しても知識が自動的に更新されることはありませんし技術力があがったりもしません。シェルスクリプトでも何年も経てばその常識は変わっていきます。「今までこのやり方でやってきたから、このやり方が一番正しい」は通用しないのです。
新しいシステムを導入しても、そこで使うソフトウェアの技術が古ければ、古いシステムを導入したのとなにも変わりません。新しいシステムは優れている(例えば POSIX に準拠している)はずなのに、全くそれが活かせていないことになります。新しいものを導入したらそれによって何が改善されたかを公式のマニュアルを読んで理解し、それを取り入れてシステム開発全体の効率化・生産性の向上を行う必要があります。
もうずいぶん古い言葉になった気もしますが、つまりアジャイル開発で言われていた「変化ヲ抱擁セヨ」(変化を嫌うのではなく 変化が起こることを自然なこととして受け入れようという基本姿勢)を実践しようということです。
ソフトウェア開発には素早い変化が求められます。
シェルスクリプトだって変化を求められます。
だから、みんなアジャイルを実践するわけですね。