原文は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
節が評価されるかを決定します。また別のelif
はthen
節の1つが実行されるまでに評価されます。もし評価されるthen
節がないなら、else
節が取られ、もしくはelse
が与えられていなければif
ブロックは完了し、if
コマンド全体で0 (true)を返します。
もしgrep
コマンドの出力にもとづき何かを判断したいなら、丸括弧や鍵括弧、バッククオートや他のどんなシンタックスにおいても囲う必要はありません!こんな感じに、ただif
のあとにgrep
をコマンドとして使うだけで良いのです。
if grep -q fooregex myfile; then
...
fi
もしgrep
がmyfile
の行でマッチするなら、終了コードは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.
このコマンドを使った際には、 su
は root
というユーザ名があるとみなします、しかしShellにコマンドを後で渡したい場合にはこれは失敗します。このケースにおいては、必ずユーザ名を提供する必要があります。
19. cd /foo; bar
もしcd
コマンドからのエラーを確認しない場合、bar
は間違った場所で実行されて終了するでしょう。例としてもしbar
がrm -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-Z
をa-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
もしくはpkill
をssh
する場合、それは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
しかし word
が match
であった場合には失敗します。
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]="")'