Yakstは、海外の役立つブログ記事などを人力で翻訳して公開するプロジェクトです。
8年以上前投稿 修正あり

Bashのよくある間違い

Bashのよくある間違い

原文
BashPitfalls - Greg's Wiki (English)
翻訳依頼者
D98ee74ffe0fafbdc83b23907dda3665 B5aa4f809000b9147289650532e83932 Ffe3a3509f42e6160e029aff8a545428
翻訳者
Ffe3a3509f42e6160e029aff8a545428 yutah-3
翻訳レビュアー
D98ee74ffe0fafbdc83b23907dda3665 doublemarket B5aa4f809000b9147289650532e83932 taka-h
原著者への翻訳報告
未報告


原文は2015年8月22日時点のものを利用しており、それ以降に追記、更新されている可能性があります。 本翻訳は原作者の許可を得て公開されています。 Thanks for GreyCat!


このページはBashプログラマーが陥りがちなよくあるエラーについてまとめました。以下の例は全てなんらかの欠陥があります。 クオートをいつも使い、どんな理由があっても単語分割を使わなければ、多くの落とし穴からあなた自身を守ることができます!単語分割はクオート表現をしない場合にはデフォルトでオンになっている、Bourneシェルから継承された壊れたレガシーな設計ミスです。落とし穴の大半はクオートされていない展開になんらか関連し、単語分割しその結果をグロブします。

1. for i in $(ls *.mp3)

BASHプログラマーたちがループを書く際にもっとも犯しがちなよくあるミスは以下のような感じです。:

for i in $(ls *.mp3); do    # 間違いです!
    some command $i         # 間違いです!
done

for i in $(ls)              # 間違いです!
for i in `ls`               # 間違いです!

for i in $(find . -type f)  # 間違いです!
for i in `find . -type f`   # 間違いです!

files=($(find . -type f))   # 間違いです!
for i in ${files[@]}        # 間違いです!

コマンド置換 はどの種類のものもクオート無しで使ってはいけません!

ここには2つの主な問題があります。クオート無しでの展開の出力を引数としてsplitして用いること、そしてlsの出力をパースすること -- それらの使い方の出力は決してパースされていないはずです。

何故って?これはファイル名にスペースが含まれていた時に壊れるからです。もう1つの理由として、 $(ls *.mp3) コマンド置換の出力が単語の分割になるからです。01 - Don't Eat the Yellow Snow.mp3というファイルがカレントディレクトリにあると想定しましょう、forループはファイル名を区切った結果の各単語を01, -, Don't, Eat ... というようにイテレーションします。

ありうる更に悪いこととして、前の単語をsplitしていって得られた文字列ではパス名展開が発生します。例えば、ls*を含む文字出力を発生させ、それを含む文字がパターンとして認識され、それとマッチする全てのファイル名のリストと置換されます。

また、ダブルクオート(")を置換することもできません:

for i in "$(ls *.mp3)"; do # Wrong!

これは ls の全体の出力を1語として扱うようにしてしまいます。それぞれのファイルの名前をイテレーションする代わりに、ループは全てのファイル名をくっつけた文字列を i として一回だけ実行されます。

上記に加え、 ls コマンドのこの使い方は必要でないことは明らかです。この外部コマンド(訳者注: ls コマンド)の出力は人間が読むことを特に想定しており、スクリプトによりパースすることを想定していません。さて、これを行う正しい方法はなんでしょうか?

for i in *.mp3; do    # Better! and...
    some command "$i" # ...always double-quote expansions!
done

BashのようなPOSIXシェルはグロブ機能がこの目的、つまりシェルがファイル名マッチングのリストにパターンを展開することをできるようにするために、特化して実装されています。外部ユーティリティの結果を解釈する必要はありません。グロビングは展開の最終ステップであり、 各 *.mp3 パターンマッチが正しく単語分割を展開し、クオートなしの展開の影響を受けません。もしファイルを再帰的に処理する必要があるなら、 find(1)を使う (原文:UsingFind) のページを参照してください。

ここで問題です。もし *.mp3-files がカレントディレクトリになかったなら何がが起きるでしょうか?その場合 i="*.mp3" というものが iに挿入された状態でfor ループは一回だけ実行されますが、これは期待された結果ではありません!マッチしたファイルのあるなしをテストすることがワークアラウンドです:

# POSIX
for i in *.mp3; do
    [ -e "$i" ] || continue
    some command "$i"
done

単純にクオートを使用し、単語分割 をどんな理由があっても使わなければ、これらのよくある間違いを防ぐことができるでしょう。単語分割はあなたがクオート展開をしない場合に、デフォルトで備わっている不便な、壊れている仕様です(Bourneシェルから引き継がれました。)。落とし穴のほぼ大半は、クオートなし展開と何らかの関連があり、続いて単語分割、そしてグロビングがあります。ほかのこのテーマにおけるバリエーションは単語分割の不正な利用とファイルの行を読み込む for ループです。これは間違っています!それらの行はファイル名で、2倍、(もしくは3倍かも)です。

上のloop本文の $i の周りのクオートに注目してください。これは2つめの落とし穴にあなたを導きます。

2. cp $file $target

この上に表示されているコマンドの何が間違いでしょうか?もしあなたが $file$target がスペースもしくはワイルドカードのどちらも持っていないことを先に知っていれば、何もないでしょう。しかし、展開の結果は単語分割パス名展開となります。ダブルクオートされたパラメータは必ず展開されます。

cp -- "$file" "$target"

ダブルクオートなしでは cp 01 - Don't Eat the Yellow Snow.mp3 /mnt/usb のようなコマンドで次のような結果がエラーとなり返るでしょう。

cannot stat `01': No such file or directory

$fileがその中にワイルドカード ( *, ?, [ )を含んでおり、そこにそれらにマッチするファイルがあった場合、それらは展開されます。 - から始まる $fileの中身がある場合だけは、ダブルクオートがあっても cp はあなたがコマンドラインオプションを与えたと考えてしまいます(下の落とし穴 #3 を見て下さい。)。

特に変数が複数のファイル名を含む場合には、パラメータ展開をクオートで囲うことは慣習的でベストプラクティスです。たとえどんな一般的でない状況でも変数の中身(が問題ないこと)を保証できます。経験を積んだスクリプト作成者は、コードの文脈上、明らかにパラメータの中身が安全なことを保証されている数少ない場合を除いて、いつもクオートを使います。上級者はタイトルの cp コマンドは間違っているとほぼ考えるでしょう。

3. ダッシュから始まるファイル名

ダッシュ ( - )から始まるファイル名は様々な問題を起こします。展開されたリストの中に *.mp3 のようなグロブがソートされ (現在のロケールに基づき)、そして ダッシュ( - )は多くのロケールで文字の前にソートされます。リストはその後いくつかのコマンドに渡されますが、-filenameというものをオプションとして誤った解釈される可能性があります。これに対しては、2つの主な解決方法があります。

1つめの解決法は -- をコマンド(cpのような)とその引数の間に入れることです。これはオプションとしてのスキャンを止めさせて、いい感じにしてくれます。

cp -- "$file" "$target"

この方法には潜在的な問題があります。オプションとして解釈され得るコンテキストの中で全てのパタメータの使用に際し、-- を挿入したことを確かめる必要があり、--はとても見失いやすく、非常にコマンドを冗長にしてしまう可能性があることです。

ほとんどのよく書かれるオプションをパースするライブラリはこれを理解し、そしてプログラムはこれらの機能を正しく扱い、無料でこの機能を継承しているはずです。しかしながら、このオプションの終わりを認識することは結局のところアプリケーション依存です。いくつかのプログラムはオプションを手動でパースし、もしくは誤って解釈し、もしくはこれを認識できないサードパーティーの貧弱なオプションを使っているでしょう。標準的なユーティリティについても、POSIXにより記述されたいくつかの例外があるはずです。echoがその1つの例です。

他の方法は相対パスもしくは絶対パスを用いて、ファイル名がディレクトリから始まっていることを確認する方法です。

for i in ./*.mp3; do
    cp "$i" /target
    ...
done

このケースでは、ファイル名が-から始まっていたとしても、グロブは変数の値が./-foo.mp3のようなものを含んでいるか確認します。これはcpコマンドに関わるところでは、完璧に安全でしょう。

最終的には、全ての結果が同じプリフィックスを持つことを確かめ、loop本文の中で数回変数を使うだけなら、シンプルに展開を用いてプリフィックスを結合することができます。これは、生成した各単語のためにいくつかの余分な文字を格納する際に、理論的に節約をすることが出来ます。

for i in *.mp3; do
    cp "./$i" /target
    ...
done

4. [ $foo = "bar" ]

これは落とし穴#2と非常に似ているケースです。しかし、非常に重要なことなので繰り返します。この上の例では、クオートが間違った場所にあります。Bashにおいて文字列をクオートする必要はありません(メタ文字もしくはパターン文字を含まない限り)。しかし、もしスペースやワイルドカードを含むことがあるかないか確かで無いときは、変数をクオートすべきでしょう。

この例はいくつかの理由で壊れえます:

もし[の中の変数が存在しなかった場合、もしくはブランクであった場合、[コマンドは次のように閉じられることになります。

[ = "bar" ] # 間違っています!

そして、次のエラーを発するでしょう。

unary operator expected. (The = operator is binary, not unary, so the [ command is rather shocked to see it there.)

もし変数が内部に空白を含むなら、次のように[コマンドがその中身を見る前に2つの単語に分割されます。

[ multiple words here = "bar" ]

これがあなたには大丈夫なようにみえるかもしれませんが、[が係る限りシンタックスエラーです。 正しい書き方は次の通りです:

# POSIX
[ "$foo" = bar ] # Right!

POSIXの[は渡された引数の数に応じてその作用を決定するので、$foo-で始まる場合でも、POSIX準拠の実装で正常に動作します。古いシェルのみはこれに伴う問題がありますが、新しいコードをを書くときには心配する必要はありません。(以下のx"$foo"ワークアラウンドを参照して下さい)

Bashや他のkshのようなシェルでは、[[を使うさらなる代替案があります。

# Bash / Ksh
[[ $foo == bar ]] # 正しいです!

グロビングや単語分割がないため、[[ ]]の中にある=の左辺をクオートする必要はありません、また空の変数も正しく扱われます。一方、クオートすることはなにも傷をつけることはありません。[testとは異なり、同様に==を利用することもできます。しかし[[を使った比較は、ただの文字列比較ではなく、右辺の文字列に対してパターンマッチングを行います。右辺を文字列とするには、パターンマッチのコンテキストの中で特殊な意味を持つ文字列が使われている時にはクオートをする必要があることを覚えておいて下さい。

# Bash / Ksh
match=b*r
[[ $foo == "$match" ]] # いい感じです!クオートしないことでb*rのパターンに対してもマッチします。

以下のようなコードを見たこともあるでしょう:

# POSIX / Bourne
[ x"$foo" = xbar ] # Ok, but usually unnecessary.

x"$foo" ハックはコードを[[がなくもっと原始的なものである[がある非常に古いシェルで実行するのに必要で、もし$foo-から始まっていると混乱を招きます。古いシステムに関していうと、[-ではじまる=の右辺の中身であろうがなかろうが気にしません。ただ文字列的に扱います。左辺だけさらなる注意が必要です。

このワークアラウンドが必要なシェルはPOSIXに従っていないものと覚えておいて下さい。Heirloom Bourneシェルでさえこれは必要ありません。(おそらくBourneシェルのPOSIXでないクローンは広くシステムシェルとして使われています。)このような過剰なポータビリティは非常にレアな要件であり、コードを見づらくするでしょう。(そして見難いでしょう。)

5. cd $(dirname "$f")

これもまた別のクオートエラーです。変数展開とともに、コマンド置換の結果が単語分割パス名展開となります。よって、コレに対しては次のようにクオートを行うべきです。

cd -P -- "$(dirname -- "$f")"

ここで明確でないのはどうクオートをネストするかです。Cプログラマーはこれを最初と2番目のダブルクオートをまとまったグループとして期待し、そして3つ目と4つ目も同様です。しかしBashにおいてその限りではありません。Bashはダブルクオートを1つのペアとしてコマンド置換の中で扱い、置換の外側のダブルクオートを別のペアとして扱います。

別の方法でこれを表現すると、parserはコマンド置換をネストレベルとして扱い、その内側のクオートは外側のクオートとは別のものとして隔離されます。

6. [ "$foo" = bar && "$bar" = foo ]

&&古い test (もしくは [ )コマンドの中で使うことはできません。Bashのバーサーは、[[ ]] もしくは (( )) の外側の && について、 && の前後においてコマンドを2つのコマンドに分割します。

[ bar = "$foo" ] && [ foo = "$bar" ] # Right! (POSIX)
[[ $foo = bar && $bar = foo ]]       # Also right! (Bash / Ksh)

(落とし穴#4で紹介した伝統的な理由により[の中の変数と定数を逆にしています。また、[[のケースも同様に逆にしていますが、パターンとして解釈することを防ぐために展開の際にはクオートが必要です。)同じことは||にも当てはまります。どちらの場合もかわりに、[[、もしくは2つの[コマンドを使って下さい。

これを避けるには次の通りにします:

[ bar = "$foo" -a foo = "$bar" ] # Not portable.

-a, -o, そして()(グルーピング)などのバイナリの演算子はPOSIX標準へのXSI拡張です。 これら全ては、POSIX-2008ではobusolute (廃止予定)としてマークされます。これらは新しいコードでは使用すべきではありません。[ A = B -a C = D ] もしくは -oの実践的な問題の1つはPOSIXが4つ以上の引数とtestもしくは[コマンドの結果を明示しないことです。これはおそらく、ほとんどのシェルで動作しますが、それを頼りにすることはできません。POSIXシェルのためにコードを書く必要がある場合、2つのtestまたは&&演算子で区切られた[コマンドを代わりに使用する必要があります。

7. [[ $foo > 7 ]]

ここでは複数の問題があります。まず、 [[コマンドは、算術式を評価するためにのみ使用すべきではありません。サポートされているテスト演算子のいずれかを含むテスト式のために使用されるべきです。技術的にはあなたが[[演算子のいくつかを使用して計算を行うことができますが、それだけでどこかの式で非数値演算テストのいずれかの演算子と一緒にこれを行うには意味があります。数値比較(または任意の他のシェルの算術演算)を行いたいだけである場合には、(( ))を使用することがはるかに優れています:

# Bash / Ksh
((foo > 7))     # Right!
[[ foo -gt 7 ]] # Works, but is pointless. Most will consider it wrong. Use ((...)) or let instead.

もしあなたが[[ ]]の内側で>演算子を使うなら、それは文字列比較として扱われ、(ロケールの順序で照合します)、整数型の比較ではありません。これは時には動きますが、あなたが少なくとも期待した時には失敗するでしょう。もし>演算子を[ ]のなかで使うケースはもっと悪いケースです。出力リダイレクトになります。ディレクトリ内部の7と名付けられたファイルを受け取り、テストは$fooが空でない限り成功してしまいます。

もし厳密なPOSIX準拠が必要で、((が使えないなら、古い方式の[を使うことが正しい代替案です。

# POSIX
[ "$foo" -gt 7 ]       # Also right!
[ $((foo > 7)) -ne 0 ] # POSIX-compatible equivalent to ((, for more general math operations.

test ... -gtコマンドは$foo整数型でないと興味深いことになりうるということを覚えておいて下さい。そのため、正しいクオートをするところがパフォーマンスと、いくつかのシェルであり得るあいまいな副作用の可能性を低減するために単一の単語に引数を閉じ込めること以外にそんなにありません。

もし算術式への入力(((もしくはletを含む)もしくは数値比較に係る[テスト表現が確認出来ないのであれば、必ずいつも評価表現の前に入力をバリデーションしなければなりません。

# POSIX
case $foo in
    *[^[:digit:]]*)
        printf '$foo expanded to a non-digit: %s\n' "$foo" >&2
        exit 1
        ;;
    *)
        [ $foo -gt 7 ]
esac

8. grep foo bar | while read -r; do ((count++)); done

上のコードはひと目見ただけではOKなように見えますよね?確かに、ただの grep -c の貧相な実装ですが、これはシンプルな例になることを意図されているからです。別のSubShellの中でそれぞれのコマンドが並列で実行されるため、 count を変えることはwhile ループの外に移譲されていません。この動作は様々な点でほぼすべてのBash初心者を驚かせます。

POSIXはsubshell内で評価された並列の最後の要素について指定をしません。

POSIXは、パイプラインの最後の要素はサブシェルで評価されているかどうかを指定しません。ksh バージョン93、または Bash バージョン4.2以上で shopt -s lastpipe を実行した場合、 while ループをこの例のようにオリジナルのシェルプロセスで実行し、何の副作用なしに有効となります。そのため、ポータブルなスクリプトは、いずれの動作に依存しないような方法で記述する必要があります。

この問題、もしくは類似の問題へのワークアラウンドは、Bash FAQ #24を参照して下さい。ここに書くには長すぎるので・・・。

9. if [grep foo myfile]

多くの初心者は[もしくは[[のすぐ前にifキーワードがある非常に一般的なパターンをみてifステートメントについて誤った直感をもつことでしょう。このことは[が、C言語のifステートメントで使われる丸括弧のように、あたかもifステートメントのシンタックスの一部であるかのように確信させます。

これはそのようなものではありません!ifはコマンドを取るのです。[はコマンドで、ifステートメントのシンタックスマーカーではありません。これは]が最後の引数で有る必要を除いて、testコマンドと等価です。

# POSIX
if [ false ]; then echo "HELP"; fi
if test false; then echo "HELP"; fi

上記の2つは等価です。どちらも引数 falseが空ではないことを確認します。どちらの場合も HELP はいつも表示され、シェルシンタックスについて他のプログラム言語から類推したプログラマは驚かされるでしょう。

ifステートメントのシンタックスは次の通りです:

if COMMANDS
then <COMMANDS>
elif <COMMANDS> # optional
then <COMMANDS>
else <COMMANDS> # optional
fi # required

繰り返しますが、 [ はコマンドです。他の通常のシンプルなコマンド同様引数を取ります。 if は他のコマンドを含んで構成されるコマンドです、 [ はこのシンタックスそのものではありません!

Bashがビルトインコマンドである [ をもっており、 [] と何も特別な関係は無いということを知っています。Bashはただ ][ の引数として渡し、スクリプトをより良く見せるためだけに作られた最後の引数 ] を必要とします。

ゼロ以上のオプションのelifのセクション、 1オプションのelse節があるかもしれません。

ifを構成するコマンドは2つもしくはそれ以上のコマンドのリストを含み、then, elif, もしくはelseによりそれぞれ区切られ、fiというキーワードにより終了されます。最初の節の最後のコマンドの終了ステータスとそれぞれの次のelif節がそれぞれどのthen節が評価されるかを決定します。また別のelifthen節の1つが実行されるまでに評価されます。もし評価されるthen節がないなら、else節が取られ、もしくはelseが与えられていなければifブロックは完了し、ifコマンド全体で0 (true)を返します。

もしgrepコマンドの出力にもとづき何かを判断したいなら、丸括弧や鍵括弧、バッククオートや他のどんなシンタックスにおいても囲う必要はありません!こんな感じに、ただifのあとにgrepをコマンドとして使うだけで良いのです。

if grep -q fooregex myfile; then
...
fi

もしgrepmyfileの行でマッチするなら、終了コードは0(true)になり、then節が実行されます。そうでない場合、つまりマッチするものがない場合には、grepは0でないコードを返し、ifコマンド全体は0となります。

以下も参照して下さい。

10. if [bar="$foo"]; then ...

[bar="$foo"]   # 間違いです
[ bar="$foo" ] # これも同様に間違いです!

前の例で説明したように、[はコマンドなのです(そのことはtype -t [もしくはwhence -v [でわかります)。 他のシンプルなコマンドのように、Bashはコマンドにスペースが続くこと、その後に1つめの引数、その後別のスペース・・・等を期待します。スペースを入れることなしにすべてのものを一緒に実行することはできません! ここに正しい例方法があります:

if [ bar = "$foo" ]; then ...

bar, =, "$foo" の展開結果、 そして ][ コマンドにとってそれぞれ別の引数です。それぞれの引数のペアの間にはスペースがなくてはならず、それによりShellはそれぞれの引数の始まりと終わりがどこなのか知ることができます。

11. if [ [ a = b ] && [ c = d ] ]; then ...

さぁ、またです。[はコマンドです。ifとC言語のような条件式の類の間に位置するシンタックスマーカーではありません。また、グルーピングでもありません。C言語のようなifコマンドをとることは出来ませんし、括弧を[]で置き換えるだけでそれをBashコマンドにすることもできません!

もし合成条件式を表現したいのであれば、次のようにしてください:

if [ a = b ] && [ c = d ]; then ...

if のあとに &&(論理的AND、短縮された評価式)演算子でつなげられた2つのコマンドがあることを覚えておいて下さい。これは次のものと全く同じです:

if test a = b && test c = d; then ...

もし最初の test コマンドが false を返すなら、 if ステートメントの本文は何も入力されません。 true を返すときには、2番目の test コマンドが実行されます。そしてtrueを返す場合にはifステートメントの本文は入力されます。(Cプログラマーは && に慣れているでしょう。Bashも同じ短縮された評価式を用います。同様に||OR演算の短縮された評価式です。)

[[キーワードは&&の利用を許可するので、次のように書くことも可能です:

if [[ a = b && c = d ]]; then ...

評価演算子と一緒に使われるtestについては落とし穴#6も参照して下さい。

12. read $foo

readコマンドでは変数名の前に$を使うことはできません。もしfooという変数にデータを入れたいなら、次のようにすべきです:

read foo

もしくは、より安全に以下のようにします:

IFS= read -r foo

read $foo は入力の行を読み込もうとし、それを$fooの中にある名前に代入しようとします。これはあなたがfooを他の変数に参照されるよう意図する場合には有用です。しかしながらほとんどのケースではこれは単純にバグです。

13. cat file | sed s/foo/bar/ > file

ファイルから読み込み、書き込みを同じパイプライン上で行うことは出来ません。そのパイプラインが何をするかに依存し、ファイルは上書きされ得ります (0バイトに、もしくはあなたのOSのパイプラインバッファのサイズと同じバイト数になります。)、もしくはそれが利用可能なディスク・スペースをいっぱいになるまで大きくなり得ますし、OSのファイルサイズの制限に達したり、クオータ制限に達する等するでしょう。

もし安全にファイルに現行を加えたいなら、ファイルの終わりに追記する以外に、テキストエディタを使って下さい。

printf %s\\n ',s/foo/bar/g' w q | ed -s file

もしテキストエディタではできない何かを行う場合には、どこかの時点で作られたテンポラリファイルが必要です。(*)例として、次の例は完全にポータブルです:

sed 's/foo/bar/g' file > tmpfile && mv tmpfile file

以下の例はGNU sed 4.X系でのみ動作します:

sed -i 's/foo/bar/g' file(s)

これもテンポラリファイルを作成し、トリッキーにリネームと同じことをします。透過的にそれを処理するだけです。

以下の代替コマンドは perl 5.X系を必要とします(もしかしたらGNU sed 4.X系でも使えるかもしれません。)

perl -pi -e 's/foo/bar/g' file(s)

ファイルの中身を置換することの詳細は、Bash FAQ #21を参照して下さい。

(*) : moreutilsのspongeコマンドでは、そのマニュアルの中で次の例を利用しています:

sed '...' file | grep '...' | sponge file

mv コマンドの不可分性に加えテンポラリファイルを用いるより、このバージョンは全てのデータをファイルを開き、書き込む前に”浸し”ます(実際のマニュアル内の表現です!)。処理時点においてオリジナルファイルのコピーがディスク上にないため、このバージョンはプログラムが書き込み演算中にクラッシュした場合にはデータロスを起こします。

テンポラリファイルとmvを用いることは、電源断やシステムクラッシュの際にもほんの少ないリスクのみがあります。電源断の際も新しいファイルもしくは古いファイルのどちらかが残るように100%確実にするには、 mv の前に sync する必要があります。

14. echo $foo

この比較的害がないように見えるコマンドはとてつもない混乱を生じさせます。なぜなら$fooクオートされておらず、単語分割を生じるだけでなく、ファイルのグロブも生じるからです。これはBashプログラマー達を彼らの変数が間違った値を含んでいたと勘違いさせます。たとえ実際は値に問題がなかったとしても。単語分割もしくはファイル名展開は何が起こっているかの見通しを混乱させます。

msg="Please enter a file name of the form *.zip"
echo $msg

このメッセージは2つの単語に分割され、どんなグロブも展開されます、例えば *.zipのようなものも。あなたのユーザがこのメッセージを見た時、どう思うでしょうか。

Please enter a file name of the form freenfss.zip lw35nfss.zip

デモをお見せします:

var="*.zip"   # var contains an asterisk, a period, and the word "zip"
echo "$var"   # writes *.zip
echo $var     # writes the list of files which end with .zip

事実、ここではechoコマンドは絶対的な安全性がある状態で使われることはできません。例としてもし変数が-nを含んでいる場合、echoはそれを表示されるデータとしてではなく、オプションとして解釈します。変数の値を表示する最も絶対的に確実な方法はprintfを用いることです。

printf "%s\n" "$foo"

15. $foo=bar

$を変数名の前につけて変数を代入することはできません。Perlではないのですから。

16. foo = bar

変数に値を代入する際にスペースを=の周りにいれることはできません。Cではないのです。 あなたがfoo = barと書いた時、shellはそれらを3つの単語として分割します。最初の単語fooはコマンド名として認識されます。2つ目と3つ目はそのコマンドへの引数になります。

同じように、次の例も間違っています:

foo= bar    # 間違いです!
foo =bar    #  間違いです!
$foo = bar; # 完全に間違っています!

foo=bar     # 正しいです。
foo="bar"   # もっと正しいです。

17. echo <<EOF

ここでの文書はスクリプトでテキストデータの大きなブロックを埋め込むための便利なツールです。これはスクリプト中のテキストの行をコマンドの標準出力へリダイレクトします。残念ながら、echoは標準入力より読み込むことができません。

# この例は間違っています:
echo <<EOF
Hello world
How's it going?
EOF

# これがあなたがしようとしたことです:
cat <<EOF
Hello world
How's it going?
EOF

# Or, use quotes which can span multiple lines (efficient, echo is built-in):
echo "Hello world
How's it going?"

クオートをこのように使うのは問題ありません、すべてのシェルできちんと動きます。しかし行のまとまりをスクリプトへ落とすことは出来ません。最初と最後の行にシンタックスのマークアップがあります。もしシェルシンタックスから触らない行がある場合には、もしくは cat コマンドをspawnしたくないなら、ここに代替案があります:

# 代替手段として printf を用いる(これも有効であり、printfはビルトインコマンドです)
printf %s "\
Hello world
How's it going?
"

printf の例の中で、最初の行の \ はテキストブロックの始まりに余計な改行を加えることを防ぎます。最終行には改行が入ります(最後のクオートは改行と一緒にあるためです。)。 printf フォーマットの引数の \n を使わないことは printf が最後に余計な行を追加することを防ぎます。 \トリックはシングルクオート内では動作しません。もしテキストのかたまりの周りでシングルクオートを使いたい/使う必要が有るときには、2つ選択肢があります。どちらも必要なシェルシンタックスをあなたのデータに紛れ込ますことです:

printf %s \
'Hello world
'

printf %s 'Hello world
'

18. su -c 'some command'

このシンタックスはほとんど正しいです。問題は、多くのプラットフォームにおいて su-c引数を取りますが、これは望んだ結果ではありません。OpenBSDの例を見てみましょう。

$ su -c 'echo hello'
su: only the superuser may specify a login class

-c 'some command' をシェルに渡すには、ユーザ名を -cの前に付ける必要があります。

su root -c 'some command' # Now it's right.

このコマンドを使った際には、 suroot というユーザ名があるとみなします、しかしShellにコマンドを後で渡したい場合にはこれは失敗します。このケースにおいては、必ずユーザ名を提供する必要があります。

19. cd /foo; bar

もしcdコマンドからのエラーを確認しない場合、barは間違った場所で実行されて終了するでしょう。例としてもしbarrm -f *であれば、これは大変な災害を引き起こしかねません。

cdコマンドからのエラーを必ずいつも確認することが必要です。最もシンプルなこれを実施する方法は次の通りです:

cd /foo && bar

もしcdの後に1つ以上コマンドがある場合には、これが望ましいでしょう:

cd /foo || exit 1
bar
baz
bat ... # Lots of commands.

cdはディレクトリのの変更の失敗を"bash: cd: /foo: No such file or directory"のような標準エラー出力メッセージと共に知らせてくれます。しかし、もし標準出力に任意のメッセージを追加したいなら、次のようにコマンドのグループ分けが使えます。

cd /net || { echo "Can't read /net. Make sure you've logged in to the Samba network, and try again."; exit 1; }
do_stuff
more_stuff

{echoの間にスペースが必要なこと、そして}で閉じる前に;が必要なことを覚えておいて下さい

0でないコードを返したいずれかのコマンドでスクリプトを途中停止するために"set -e"を有効にするのを好む人もいますが、これを正しく使うのは少しトリッキーです。(多くの有名なコマンドは致命的として扱ってほしくないであろうWarningに対し0でないコマンドを返すためです。)

ところで、多くのBashスクリプトの内部でディレクトリ変更をしているなら、Bashのpushd,popd,dirsに関するヘルプを必ず読んで下さい。おそらくあなたの書いたcdおよびpwdを管理する全てのコードは完全に必要ないもののはずです。

これを語るにはこれを見てから次と比較してみてください:

find ... -type d -print0 | while IFS= read -r -d '' subdir; do
 here=$PWD
 cd "$subdir" && whatever && ...
 cd "$here"
done

比較対象です:

find ... -type d -print0 | while IFS= read -r -d '' subdir; do
 (cd "$subdir" || exit; whatever; ...)
done

SubShellを強制することでcdはSubShellの中でだけ発生します。ループの次のイテレーションには、cdの成功失敗にかかわらず通常の場所に戻ります。手動で戻る必要はなく、また終わらない他の条件式の使用を防ぐ&&ロジックの繰り返しで詰まることもありません。Subshellバージョンはシンプルかつ綺麗です。(少しだけ遅いにもかかわらずです。)

20. [ bar == "$foo" ]

== 演算子は [コマンドで有効ではありません。代わりに、=もしくは[[を使って下さい。

[ bar = "$foo" ] && echo yes
[[ bar == $foo ]] && echo yes

21. for i in {1..10}; do ./something &; done

;& の後に使うことは出来ません。異例な ; を全体から取り除いてください。

for i in {1..10}; do ./something & done

もしくは次のようにします。

for i in {1..10}; do
 ./something &
done

& は、; のように、コマンドを終了させる役割として機能しています。これら2つを混在させることはできません。

一般的に、; は改行で置き換えられます、しかし全ての改行が ;で置き換えられるわけではありません。

22. cmd1 && cmd2 || cmd3

&&||if ... then ... else ... fiのショートカットシンタックスとして用いる人がいます。多くの場合において、これは完全に安全です:

[[ -s $errorlog ]] && echo "Uh oh, there were some errors." || echo "Successful."

しかしながら、一般的にこの構成は完全にif ... fiと等価なわけではありません。&&もまた終了ステータスを生成した後にコマンドが来るためです。もしその終了ステータスが "true" (0) でなかった場合には、||の後のコマンドも無効にされます。例としては次の通りです:

i=0
true && ((i++)) || ((i--))
echo $i # Prints 0

ここでは何が起きたでしょうか?iが1になるべきに見えますが、0で終了しています。何故でしょうか。それはi++i--が両方とも実行されたからです。((i++))コマンドは終了ステータスを持ち、そしてその終了ステータスは括弧の中のC言語のような評価の表現から、届けられます。この表現の値は0(iの初期値)で、C言語ではint型の0はfalseとして捉えられます。つまり、((i++)) (iが0のとき)終了ステータスは1となり(つまりfalse)、従って((i--))コマンドも同様に実行されるのです。

これは前置インクリメント演算子を用いた場合には、++iからの終了ステータスがtrueとなるため、発生しません。

i=0
true && (( ++i )) || (( --i ))
echo $i # Prints 1

しかしこれは例のポイントを見失っています。これは単なる偶然により動作し、そしてもしyが失敗する可能性があるなら、x && y || z if yに頼ることはできません!(この例ではもしiを0に替えて-1で初期化したなら失敗します。)

もし安全性が必要なら、もしくはこれがどう動くか確かでないのなら、もしくはどこかの節の何かが明確でないなら、シンプルにif ... fiシンタックスをプログラム内部で利用して下さい。

i=0
if true; then
 ((i++))
else
 ((i--))
fi
echo $i # Prints 1

この節はBourne Shellにも当てはまり、それを示すコードがこれになります:

true && { echo true; false; } || { echo false; true; }

出力は"single"という一行の代わりに、"true"と"false"の2行になります。

23. echo "Hello World!"

ここにおける問題は、対話型Bashシェルにおいて、次のようなエラーが発生することです。

bash: !": event not found

これは対話型シェルのデフォルト設定においてはBashはcsh-styleのコマンド履歴展開を ! を用いて行うために発生します。これはシェルスクリプトにおける問題ではなく、対話型シェルのみにおいて発生する問題です。

残念ながら、これが動かないことを"直す"方法は次の通りです:

$ echo "hi\!"
hi\!

最も簡単な解決方法はhistexpandオプションを設定しないことです。これは set +H もしくは set +o histexpandにて実施できます。

さて、ここで問題です。なぜhistexpandはシングルクオートより優先されるのでしょうか。 私は曲のファイルを操作するとき、個人的にこの問題に遭遇しました。

mp3info -t "Don't Let It Show" ...
mp3info -t "Ah! Leah!" ...

シングルクオートを使うことは、曲名にアポストロフィーを含むすべての曲のせいで非常に不便です。 ダブルクオートを使うとコマンド履歴展開の問題に当たることになります。(そしてファイルがアポストロフィーおよび!の両方を含んでいることを想像してみてください。クオートによる区切りは酷いことになります。)私は実際にコマンド履歴展開を使ったことがないので、個人的な好みは ~/.bashrc の中でコマンド履歴展開を無効化することです。

この場合以下のコマンドへ有効です:

echo 'Hello World!'

もしくは

set +H
echo "Hello World!"

もしくは

histchars=

多くの人達はシンプルに~/.bashrcの中で set +H もしくは set +o histexpand を設定しコマンド履歴展開を無効にすることを選択するでしょう。これは個人的な好みなので、どの方法があなたにあっているか選ぶべきでしょう。

他の解決方法としては以下もあります:

exmark='!'
echo "Hello, world$exmark"

24. for arg in $*

Bash(すべてのBourneシェル)は位置パラメータのリストを1つづつ参照するための特別なシンタックスをもっています、そして$*はそれではありません。また$@もそうではありません。どちらもスクリプトのパタメータの中で単語のリストへ展開し、別の単語としてそれぞれのパラメータへ展開しません。

正しいシンタックスは次の通りです:

for arg in "$@"

# Or simply:
for arg

位置パラメータでループすることがスクリプト中でつぎのようにすることが一般的です、for argはデフォルトでfor arg in "$@"のような感じです。 ダブルクオートされた"$@"はそれぞれの変数を1つの単語として扱う(もしくは1つのループイテレーションとして扱う)特殊な魔法です。これは少なくとも99%の場合使うべきでしょう。

ここに例があります:

     # Incorrect version
     for x in $*; do
       echo "parameter: '$x'"
     done
     $ ./myscript 'arg 1' arg2 arg3
     parameter: 'arg'
     parameter: '1'
     parameter: 'arg2'
     parameter: 'arg3'

これは以下のように書く必要があります:

# 正しいバージョンです
for x in "$@"; do
 echo "parameter: '$x'"
done

$ ./myscript 'arg 1' arg2 arg3
parameter: 'arg 1'
parameter: 'arg2'
parameter: 'arg3'

25. function foo()

これはいくつかのShellでは動作します、しかし一部ではそうではありません。関数を定義する際にキーワード関数を()と結合するべきではありません。

Bash(少なくともいくつかのバージョンにおいて)は2つを混合することを許容しています。ほとんどのShellはこれを受け入れません。(例としてzsh 4.x 系もしくはおそらくそれ以上のバージョンもです。) いくつかのShellは上のfoo関数を許容しますが、移植性の最大化のために、次のようなものを使うべきでしょう:

foo() {
  ...
}

26. echo "~"

チルダの展開は ~がクオートされていない時のみ有効です。この例においてはechoは ~ をユーザのホームディレクトリのパスではなく、~として標準出力へ書き出します

クォートされたパスのパラメータでユーザホームディレクトリの相対パスを表現するには~ではなく $HOME を 使うべきです。例えば $HOME/home/my photosの場合を例として考えてみましょう。

"~/dir with spaces" # "~/dir with spaces"として展開
~"/dir with spaces" # "~/dir with spaces"として展開
~/"dir with spaces" # "/home/my photos/dir with spaces"として展開
"$HOME/dir with spaces" # "/home/my photos/dir with spaces"として展開

27. local varname=$(command)

ローカル変数を関数のなかで宣言するとき、 local はコマンドとして自分の権限に基づき振る舞います。これは奇妙にも他の行と影響しあうことがあります。例えば、コマンド置換の終了ステータス ( $? )を取得したいとしても、できません。 local の終了ステータスがそれを置き換えてしまいます。

次のように、別のコマンドを使うのがベストです。

     local varname
     varname=$(command)
     rc=$?

次の落とし穴では、シンタックスに関する別の問題を説明します。

28. export foo=~/bar

チルダの展開(ユーザ名と一緒に、もしくはユーザ名なしで)はチルダが文字列の最初、もしくはスラッシュの後、もしくはチルダのみの時だけ発生することが保証されています。また、 = による変数への代入の直後にチルダが現れるときにも保証されています。

しかしながら、 export もしくは localコマンドは代入を構成しません。つまり、いくつかのシェル(Bashのような)は export foo=~/bar はチルダの展開が行われます、いくつかのシェル(dash等)では行われません。

     foo=~/bar; export foo    # Right!
     export foo="$HOME/bar"   # Right!

29. sed 's/$foo/good bye/'

シングルクオートの中では、 $fooのようなbashパラメータ展開は展開されません。それこそがシングルクオートの目的で、 $ のような文字をシェルから保護するためにあります。

クオートをダブルクオートに変更してみましょう:

     foo="hello"; sed "s/$foo/good bye/"

しかしダブルクオートを使う場合更にエスケープが必要になる場合があることを覚えておいて下さい。詳細はクオートのページを参照してください。

30. tr [A-Z] [a-z]

ここには(少なくとも)3つ間違っている点があります。最初の問題は [A-Z][a-z]がシェルにはグロブのようにみえることです。もしカレントディレクトリに一文字のファイル名のファイルがないなら、このコマンドはあっているようにみえるでしょう。しかしやってみると、実行結果は失敗します。おそらく週末午前3時に。(訳者注:週末の午前3時にやってしまう、というぐらいケアレスミスということです)

2つ目の問題はtrの必ずしも正しい書き方では無いということです。これは実際には[[に変換します。A-Za-zに変換し、]]にも変換します。つまり、ブラケット([])は必要なく、最初の問題も発生しないのです。

3つめの問題はロケール依存で、A-Zもしくはa-zは期待した通りの26文字ASCII文字列を与えないことがあることです。実際、いくつかのロケールでは、zはアルファベットの中間の文字です!この解決法は何をしたいかに依存します:

     # 26文字ラテン配列を変更したい場合にはこれを使って下さい
     LC_COLLATE=C tr A-Z a-z

     # ユーザが期待しているものに近いロケール依存の変換をしたい場合にはこれを使って下さい
     tr '[:upper:]' '[:lower:]'

2番目のコマンドにおいてはグロブ防ぐためにクオートが必要です。

31. ps ax | grep gedit

ここでの根本的な問題は動作しているプロセスの名前は本質的に信頼できないということです。複数の正当なgeditのプロセスがあるかもしれません。何か他のものがgeditのとしての偽装をしているかもしれません。(実行したコマンドの報告されている名前の変更は簡単です。)これに対する本当の答えは、プロセス管理を参照してください。

つぎのものは素早く直せますが、汚い方法です。

(例として)geditのPIDを検索し、多くの人達は次のようにします。

$ ps ax | grep gedit
10530 ?        S      6:23 gedit
32118 pts/0    R+     0:00 grep gedit

これは、競合条件によっては、結果として多くの場合grep自身を結果とします。grepを取り除くには、次のようにします。

ps ax | grep -v grep | grep gedit   # will work, but ugly

別の方法として、これを使います。

ps ax | grep '[g]edit'              # quote to avoid shell GLOB

これはgrepが一度評価されたgeditを探し、[g]editをプロセステーブルから無視します。

GNU/Linuxにおいては、-Cパラメータがコマンド名でフィルタする代わりに使用できます。

$ ps -C gedit
  PID TTY          TIME CMD
10530 ?        00:06:23 gedit

しかしpgrepを使うときにはこの悩ましさはありません。

$ pgrep gedit
10530

2段階目においては、PIDは多くの場合awkもしくはcutにより抽出されます。

$ ps -C gedit | awk '{print $1}' | tail -n1

しかしpsの山ほどあるパラメータの一部として扱う事ができます。

$ ps -C gedit -opid=
10530

もしあなたが1992年で止まっており、pgrepを使わないなら、代わりにすでに古代のものであり、破棄された、非推奨のpidofを代わりに使うことができます。

$ pidof gedit
10530

そしてもしプロセスをkillするのにPIDが必要なら、pkillはあなたにとって興味深いものでしょう。しかし覚えておいて下さい。例えばpgrepもしくはpkillsshする場合、それはsshdという名前のプロセスをみつけ、それを終了したくないことでしょう。

残念ながら幾つかのプログラムはその名前で始まらないものもあり、例えばfirefoxは多くの場合firefox-binで始まりますが、ps ax | grep firefoxで探す必要があるでしょう。もしくは、pgrepにいくつかパラメータを足すことで関連付けることも可能です。

$ pgrep -fl firefox
3128 /usr/lib/firefox/firefox
7120 /usr/lib/firefox/plugin-container /usr/lib/flashplugin-installer/libflashplayer.so -greomni /usr/lib/firefox/omni.ja 3128 true plugin

プロセス管理も真剣に読んで下さい。

32. printf "$foo"

これはクオートの誤りではありませんが、文字列形式のエクスプロイトが原因です。もし$fooがあなたの確実に制御出来る範囲でなければ、変数の中の\もしくは%文字が望まない動きをするでしょう。

必ずフォーマット文字列を与えて下さい:

printf %s "$foo"
printf '%s\n' "$foo"

33. for i in {1..$n}

Bashのパーサー括弧の展開を他のどんな展開や置換のよりも前に行います。したがって、括弧の展開のコードは$nのような表記(数値ではない)をみて、数字の配列へ中括弧を展開しないのです。これはこれは括弧の展開を実行時にのみわかる大きさのリストを作ることに使うことが出来ないことにほぼ等しいです。

代わりにこうしてください:

for ((i=1; i<=n; i++)); do
...
done

この整数を通してのシンプルなイテレーションの場合、forループの演算は、ほとんどの場合、括弧の展開が遅くなる得ることがあったり、またすべての引数を事前に展開され、不必要にメモリを消費するので、そもそもブレース展開よりも優先されます。

34. if [ $foo = $bar ]

[[ (http://mywiki.wooledge.org/BashFAQ/031) の中の=演算子の右辺がクオートされていない場合、Bashは文字列としてそれを扱う代わりに、パターンマッチを行います。そして、上のコードの中では、bar*を含んでいた場合、結果はいつでもtrueになります。文字列が等価であることを評価したいのであれば、右辺はクオートされるべきです。

if [[ $foo = "$bar" ]]

もしパターンマッチを行いたい場合には、パターンを含む右辺であるとわかるような変数名にしたほうが懸命でしょう。もしくはコメントを使うかです。

これは同様に=~の右辺をクオートした場合には正規表現マッチングよりも単純な文字列比較となることを強制し、指摘する価値があります。

35. if [[ $foo =~ 'some RE' ]]

=~ 演算子の右辺のクオートは正規表現ではなく、文字列となってしまうことがあります。もし複雑だったり長い正規表現を使いたい、そして \ エスケープだらけになることを避けたいなら、変数に格納してしまいましょう。

re='some RE'
if [[ $foo =~ $re ]]

これは Bashバージョンの違いによる =~ の仕様差分のワークアラウンドとしても動作します。変数を使うことは、微妙かつ面倒ないくつかの問題を避ける事にもなるのです。

同じ問題は [[ の中のパターンマッチングでも発生します。

[[ $foo = "*.glob" ]]      # Wrong! *.glob is treated as a literal string.
[[ $foo = *.glob ]]        # Correct. *.glob is treated as a glob-style pattern.

36. [ -n $foo ] or [ -z $foo ]

[ コマンドを使うとき、それぞれの置換を必ずクオートする必要があります。しない場合には、 $foo は1語ではなく0語もしくは42語、もしくはいくつかの語として展開され得て、シンタックスを破壊します。

[ -n "$foo" ]
[ -z "$foo" ]
[ -n "$(some command with a "$file" in it)" ]

# [[ doesn't perform word-splitting or glob expansion, so you could also use:
[[ -n $foo ]]
[[ -z $foo ]]

37. [[ -e "$broken_symlink" ]] が壊れたシンボリックリンクがあっても1を返す

testはシンボリックリンクをたどり、シンボリックリンクが壊れていた場合(例えば存在しないファイルを指している、等)には test -e の結果はシンボリックリンクが存在していたとしても1を返します。

このワークアラウンド(もしくは事前に防ぐには)次のようにすべきでしょう。

[[ -e "$broken_symlink" || -L "$broken_symlink" ]]

38. ed file <<<"g/d{0,3}/s//e/g" fails

ここでの問題は ed\{0,3\} に対し0を受け入れない事により生じます。 下のコードは動くことを確認できるはずです。

ed file <<<"g/d\{1,3\}/s//e/g"

POSIXが0を連続数の最低値として受け入れるはずのBRE( ed により使われる正規表現)で書いていてもこれが発生することは覚えておいて下さい。(5節を参照して下さい)

39. expr sub-string fails for "match"

多くの場合これはこれはとても合理的に動きます。

word=abcde
expr "$word" : ".\(.*\)"
bcde

しかし wordmatchであった場合には失敗します。

word=match
expr "$word" : ".\(.*\)"

問題は"match"がキーワードであることです。(GNUだけですが)解決方は+を接頭語にすることです。

word=match
expr + "$word" : ".\(.*\)"
atch

もしくは、あなたも知る通り、exprを使うことをやめることです。パラメーター展開を使えばexprでできることは何でもできます。そこまでしたやろうとしていることは何ですか?単語の最初の文字を削除しますか?それはPOSIXシェルPEを使用することで、または拡張サブストリングで行うことができます。

$ word=match
$ echo "${word#?}"    # PE
atch
$ echo "${word:1}"    # SE
atch

真剣に、SolarisのPOSIX準拠でない/bin/shを使っていない限り`exprを使う言い訳はありません。それは外部プロセスであり、内部文字列操作より遅いのです。そして誰も使っていないので、誰も何をしているのか解りませんし、あなたのコードは難読化しメンテナンスすることが難しくなるでしょう。

40. UTF-8環境におけるスクリプト、そしてByte-Order Marks (BOM)

一般的に、UnixのUTF-8テキストはBOMを使いません。プレーンテキストのエンコードはロケール、MIMEタイプ、もしくは他のメタデータにより決定されます。BOMの存在は人間に読めるUTF-8にダメージを与えません、

通常BOMの存在は人間だけによる読み取りのためのUTF-8ドキュメントを傷つけませんが、スクリプト、ソースコード、設定ファイルなどの自動化プロセスによって解釈されるテキストファイルにおいては、問題となります。(多くの場合、シンタックス的には規約違反です。)BOMで始まるファイルは、MS-DOSの改行を持つものと同等に外のものとして考慮すべきです。

シェルスクリプトにおいては、次のページでは以下のように示されています。 "UTF-8は8ビット環境で透過的に使用される場合、先頭に特定のASCII文字を含むとき、例えばUnixのシェルスクリプトの先頭に#!の使用などがある、どんなファイルフォーマットもしくはプロトコルであってもBOMの使用はそれに干渉するでしょう。"

引用元: http://unicode.org/faq/utf_bom.html#bom5

41. content=$(<file)

この表現には何も間違っているところはありません、しかしコマンド置換が最後の改行を削除することに気づくはずです。(..., $(...), $(<file), `<file`, ${ ...; の全てです。)これは多くの場合、重要ではないかあっても望ましいものですが、行末にあるすべての存在し得る改行文字を含むリテラルの出力を保持しなければならない場合は、出力が改行文字を含むものを含んでいたか、またいくつ含んでいたのかについて知る方法はなく、トリッキーです。一つの醜いながらも使用可能な回避策は、コマンド置換の内部に接尾辞を追加して、外側でそれを削除することです:

absolute_dir_path_x=$(readlink -fn -- "$dir_path"; printf x)
absolute_dir_path=${absolute_dir_path_x%x}

さらにポータブルでないですが、見やすい方法としては、readを空のデリミタと共に使用することです。

# Ksh (or bash 4.2+ with lastpipe enabled)
readlink -fn -- "$dir_path" | IFS= read -rd '' absolute_dir_path

この方法のデメリットは、読み込まれつストリームの一部だけがコマンド出力がNULバイトであってもreadがいつもfalseを返すことです。コマンドの終了ステータスを取得する唯一の方法はPIPESTATUSを通すことだけです。readがtrueを返すように強制するよう、pipefailを利用し、内部的にNULバイトを出力することもできます。

set -o pipefail
{ readlink -fn -- "$dir_path"; printf '\0x'; } | IFS= read -rd '' absolute_dir_path

これは、ポータビリティがない方法で、bashがpipefailとPIPESTATUSの両方をサポートしており、ksh93はpipefailだけをサポートし、最近のバージョンのmkshだけがpipefailをサポートして、以前のバージョンではPIPESTATUSのみをサポートしています。 加えて、NULバイト文字でreadが止まるようにksh93の最新版が必要です。

42. for file in ./* ; do if [[ $file != . ]]

プログラムがファイル名解釈することを防ぐ方法はパス名を利用することです。(落とし穴#3を参照して下さい。) カレントディレクトリ配下のファイルには、ファイル名は相対的パス名として./接頭辞がついているでしょう。

しかしながら*/*のようなパターンの場合、./filenameという形の文字列にマッチしてしまうため、問題が発生します。シンプルな場合、直接グロブを使い望ましいマッチを生成できるでしょう。しかしながらもしパターンマッチのステップが必要な場合には (例えば、結果がすでに処理され配列に保持されており、フィルタされる必要がある等) パターンを考慮し接頭辞を取る、つまり [[ $file != ./*.* ]] もしくはマッチよりパターンを削除することで解決できるでしょう。解決できます。

# Bash
shopt -s nullglob
for path in ./*; do
    [[ ${path##*/} != *.* ]] && rm "$path"
done
# Or even better
for file in *; do
    [[ $file != *.* ]] && rm "./${file}"
done

# Or better still
for file in *.*; do
    rm "./${file}"
done

他の可能性は--引数と共にオプションを終了することです。(繰り返しになりますが、落とし穴#3で言及されています。)

shopt -s nullglob
for file in *; do
    [[ $file != *.* ]] && rm -- "$file"
done

43. somecmd 2>&1 >>logfile

これはリダイレクションの係る通常のミスからは程遠いですが、ターミナルに標準エラーがいまだ表示されることを理解せず標準出力と標準エラーをファイルもしくはパイプに渡したい人により行われがちです。もしこれに困惑するなら、おそらくあなたはどのようにしてリダイレクションするのかもしくはファイルディスクリプタがそのためにどのように動くのかわかっていないのでしょう。リダイレクションはコマンドが実行される前に左辺で評価されます。この意味的に不正なコードは、本質的には次を意味します 「まず、標準出力が現在ポイントしているところ(TTY )に、標準エラーをリダイレクトし、その後に標準出力をログファイルにリダイレクトする。」これはすでに過去のことです。標準エラーは、すでにttyに渡されています。代わりに以下を使用してください。

somecmd >>logfile 2>&1

もっと詳しい説明については、コピーデスクリプタの説明BashGuideのredirectionを参照して下さい。

44. cmd; (( ! $? )) || die

$? は前のコマンドのステータス詳細を取得する必要があるときにのみ必要です。もし成功もしくは失敗(もしくは返り値が0でないステータス)をチェックしたいだけなら、 単純に test コマンドを使いましょう。

if cmd; then
    ...
fi

選択肢のリストに対して終了時のステータスをチェックすると、次のようなパターンになるでしょう。

cmd
status=$?
case $status in
    0)
        echo success >&2
        ;;
    1)
        echo 'Must supply a parameter, exiting.' >&2
        exit 1
        ;;
    *)
        echo "Unknown error $status, exiting." >&2
        exit "$status"
esac

45. y=$(( array[$x] ))

算術展開のPOSIXシンタックス(パラメータ展開の後のコマンド置換の展開を呼び出します)により、算術展開のSubscript内部の配列の展開はコードインジェクションとエクスプロイトが発生し得ます。

非常に紛らわしい単語ですね。ここにどう壊れるかの例があります:

$ x='$(date >&2)'        # リダイレクションは全てに対し発生するように見えます
$ y=$((array[$x]))       # 配列にも存在している必要はありません
Mon Jun  2 10:49:08 EDT 2014

同様に$xをクオートすることは助けになりません:

$ y=$((array["$x"]))
Mon Jun  2 10:51:03 EDT 2014

これを動かす2つの技があります:

# 1. $xを事前に展開されないようにエスケープします
$ y=$((array[\$x]))
# 2. 完全な ${array[$x]} シンタックスを利用します
$ y=$((${array[$x]}))

46. read num; echo $((num+1))

算術式としてコードを挿入できるように、 num の前に入力バリデーションをしてください。 (詳細は BashFAQ/054 を参照して下さい。)

$ echo 'a[$(echo injection >&2)]' | bash -c 'read num; echo $((num+1))'
injection
1

47. IFS=, read -ra fields <<< "$csv_line"

見てわかるかもしれませんが信じられないことにPOSIXはIFSを区切り文字として扱うことが必要です。これの意味するところは、この例では入力文字のサイドに空のフィールドがあった場合、それは破棄されるということです。

$ IFS=, read -ra fields <<< "a,b,"
$ declare -p fields
declare -a fields='([0]="a" [1]="b")'

空のフィールドはどこにいったのでしょうか。歴史的な理由により"食べられて"しまったのです。(なぜからいつもそのようにしてきたから、です。)この動作はBashだけのユニークなものではありません。全てのPOSIX準拠のシェルがこうします。空でないフィールドはおそらくスキャンされるでしょう。

$ IFS=, read -ra fields <<< "a,b,c"
$ declare -p fields
declare -a fields='([0]="a" [1]="b" [2]="c")'

さぁ、そのようにこのナンセンスなものを回避しましょう。すでにわかったように、入力文字列の最後に、IFSの文字を追加することでスキャンが動作するように強制できます。末尾に空のフィールドがあった場合、それがスキャンされるように、余分なIFSの文字は、それを「削除」します。後続の空でないフィールドがあった場合は、IFSの文字が削除された新しい空のフィールドを作成します。

$ input="a,b,"
$ IFS=, read -ra fields <<< "$input,"
$ declare -p fields
declare -a fields='([0]="a" [1]="b" [2]="")'

次の記事
Telegrafを使ってInfluxDBとKafkaにメトリクスを送信する(InfluxDB Blogより)
前の記事
InfluxDB 0.9.4のリリース, TOP関数, 時間の降順のorder byなど(InfluxDB Blogより)

Feed small 記事フィード

新着記事Twitterアカウント