一般的なBashプログラミングエラー

システムの自動化と最適化に使用されるスクリプトの品質は、その安定性と寿命の鍵であり、このシステムの管理者の時間と神経も節約します。 プログラミング言語としてのbashの見た目の原始性にもか​​かわらず、開発者と管理者の両方の気分を著しく損なう可能性のある落とし穴と巧妙な傾向に満ちています。

利用可能なマニュアルのほとんどは、書く方法に専念しています。 書く方法は必要ありません:-)

このテキストは、2008年12月13日現在のBashの落とし穴wikiの無料翻訳です。 ソースのウィキの性質により、この翻訳はオリジナルと異なる場合があります。 テキストのボリュームが大きすぎて全体を公開できないため、部分的に公開されます。


1. `ls * .mp3`のfor i


bashスクリプトで最も一般的なエラーの1つは、次のようなループです。

  for i in `ls * .mp3`;  #Falseを行う!
    いくつかのコマンド$ i#False!
やった 


いずれかのファイルの名前にスペースがある場合、これは機能しません。 ls *.mp3を置換した結果は、単語が壊れています。 現在のディレクトリにファイル01 - Don't Eat the Yellow Snow.mp3があるとします01 - Don't Eat the Yellow Snow.mp3forループはファイル名の各単語を処理し、 $iは値"01""-""Don't""Eat""the""Yellow""Snow.mp3"ます。

コマンド全体を引用することもできません:

  for i in "` ls * .mp3` ";  #Falseを行う!
     ... 


出力全体が1つの単語と見なされるようになり、リスト内の各ファイルを通過する代わりに、サイクルは1回だけ実行されますが、 iは値を取得します。これは、スペースを介したすべてのファイル名の連結です。

実際、 ls使用ls完全に冗長です。これは、この場合は単に必要のない外部コマンドです。 それではどうですか? など:

  for i in * .mp3;  #はるかに良くなりますが、...
    いくつかのコマンド "$ i"#... catch#2を参照
やった 


bashでファイル名を置き換えます。 このような置換は、文字列を単語に分割することにはなりません。 *.mp3パターンに一致する各ファイル名は1つの単語と見なされ、各ファイル名を1回循環します。

詳細については、Bash FAQの段落20を参照してください

注意深い読者は、上記の例の2行目に引用符があることに気付くはずです。 これにより、スムーズにキャッチ番号2に到達します。

2. cp $ file $ターゲット


このチームの何が問題になっていますか? 変数$file$targetスペースやワイルドカードが含まれていないことを絶対に知っている場合、特別なことはないように思えます。

しかし、どんな種類のファイルに出会うかわからない場合、またはあなたが偏執的である場合、または単に良いbashプログラミングスタイルに従うことを試みている場合は、単語に入れないように変数の名前を引用符で囲みます。

  cp "$ file" "$ target" 


二重引用符がないと、スクリプトはコマンドcp 01 - Don't Eat the Yellow Snow.mp3 /mnt/usb cp: cannot stat `01': No such file or directory cp 01 - Don't Eat the Yellow Snow.mp3 /mnt/usb cp: cannot stat `01': No such file or directoryような多くのエラーcp: cannot stat `01': No such file or directory 。 変数$fileまたは$targetの値にワイルドカードテンプレートで使用される文字*、?、[..]、または(..)が含まれている場合、パターンに一致するファイルがある場合、変数の値が変換されますこれらのファイルの名前に。 "$file"がハイフンで始まっていない限り、二重引用符でこの問題を解決します。この場合、 cpは別のコマンドラインオプションを指定しようとしていると考えます。

回避策の1つは、 cpコマンドとその引数の間に二重ハイフン( -- )を挿入することです。 二重ハイフンは、 cpにオプションの検索を停止するように指示します。

  cp-"$ファイル" "$ターゲット" 


ただし、このようなトリックが機能しないシステムの1つに遭遇する可能性があります。 または、実行しようとしているコマンドが--オプションをサポートしていません。 この場合、読み進めてください。

別の方法は、ファイル名が常にディレクトリ名で始まるようにすることです(現在のディレクトリ名の./を含む)。 例:

  for i in ./*.mp3; する
     cp "$ i" /ターゲット
     ... 


名前が「-」で始まるファイルがある場合でも、テンプレート置換メカニズムにより、変数に./-foo.mp3などが含まれることが保証されcp 。これはcp使用しても絶対に安全です。

3. [$ foo = "bar"]



この例では、引用符は正しく配置されていません。bashでは、文字列リテラルを引用符で囲む必要はありません。 ただし、スペースやワイルドカードが含まれていないことが確かでない場合は、必ず変数を引用する必要があります。

このコードは、次の2つの理由で誤っています。

1.条件[使用される変数が存在しないか空の場合、行

  [$ foo = "bar"] 


として認識されます

  [= "バー"] 


「単項演算子」エラーが発生します。 (演算子「=」は単項ではなくバイナリであるため、コマンド[はこの構文によってショックを受けます)
2.変数にスペースが含まれている場合、変数は[で処理される前に異なる単語に分割されます。
  [ここに複数の単語=「バー」] 

これが正常であると個人的に考えても、そのような構文は誤りです。

次のように修正されます。

  ["$ foo" = bar]#近い! 


ただし、$ fooが-始まる場合、このオプションは機能しません。

bashでは、この問題を解決するために、古いtestコマンド( [も呼ばれる)を含めて大幅に拡張するキーワード[[使用できます。

  [[$ foo = bar]]#正しい! 


[[ and ]]内では、変数が単語に分割されなくなり、空の変数も正しく処理されるため、変数の名前を引用符で囲む必要がなくなりました。 一方、もう一度引用符で囲んだとしても、何も害はありません。

次のようなコードを見たことがあるかもしれません。

  [x "$ foo" = xbar]#正しい! 


コードにはハックx"$foo"が必要です。これは、 [[サポートしないシェルで動作するはずです。 $foo-で始まる場合、コマンド[は混乱します。

式の一部の1つが定数である場合、これを行うことができます。
  [bar = "$ foo"]#とても正しい! 


コマンド[は、「=」記号の右側の式が-始まることを気にしません。 彼女はこの式を文字列として使用しています。 そのような細心の注意が必要なのは左側だけです。

4. cd `dirname" $ f "`



これまでのところ、基本的に同じことについて話している。 変数の値の展開と同様に、コマンド置換の結果は、単語とファイル名の展開(パス名展開)に分割されます。 したがって、コマンドを引用符で囲む必要があります。

  cd "` dirname "$ f" `" 


ここで完全に明らかではないのは、引用符のシーケンスです。 Cプログラマーは、1番目と2番目の引用符と3番目と4番目の引用符をグループ化することを提案する場合があります。 ただし、この場合はそうではありません。 Bashは、コマンド内の二重引用符を最初のペアとして扱い、外側の引用符を2番目のペアとして扱います。

つまり、パーサーは逆引用符( ` )をネストのレベルとして扱い、その内部の引用符は外側の引用符から分離されます。

同じ効果は、より好ましい$()構文を使用して達成できます。

  cd "$(dirname" $ f ")" 


$()内の引用符はグループ化されています。

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



「古い」 testコマンドまたはそれに相当する[中で&&を使用することはできません。 bashパーサーは、括弧の外側で&&認識し、コマンドを&&前後に2つに分割します。 次のオプションのいずれかを使用してください。

  [bar = "$ foo" -a foo = "$ bar"]#正しい!
 [bar = "$ foo"] && [foo = "$ bar"]#そうです!
 [[$ foo = bar && $ bar = foo]]#正しい! 


前の段落で説明した理由により、定数と変数を[ -内で交換したことに注意してください。

同じことが||も当てはまります 。 [[ 、または-o 、または2つのコマンド[ます。

6. [[$ foo> 7]]


[[ ]]内で>演算子が使用されている場合、数字ではなく文字列を比較するための演算子と見なされます。 場合によっては、これが機能する場合と機能しない場合があります(これは、予想以上の場合に発生します)。 >[ ]内にある場合、さらに悪いことです。この場合、指定された番号でファイル記述子からの出力をリダイレクトしています。 7という名前の空のファイルが現在のディレクトリに表示され、 $foo変数が空でない限り、 testコマンドは成功します。

したがって、operator>および<は、 [ .. ]または[[ .. ]]内の数値の比較には使用できません。

2つの数値を比較する場合は、 (( ))使用します。

  ((foo> 7))#そうです! 


BashではなくBourne Shell(sh)向けに記述している場合、正しい方法は次のとおりです。

  [$ foo -gt 7]#そうです! 


test ... -gt ...は、引数の少なくとも1つが整数でない場合にエラーをtest ... -gt ...ことに注意してください。 したがって、引用符が正しく配置されているかどうかは問題ではありません。変数が空であるか、スペースが含まれているか、値が整数でない場合、いずれの場合でもエラーが発生します。 testコマンドで変数を使用する前に、変数の値を注意深く確認してください。

二重角括弧もこの構文をサポートしています。

  [[$ foo -gt 7]]#そうです! 


7.カウント= 0; grep foo bar | 行を読みながら; do((count ++)); 完了; echo "行数:$カウント"


一見、このコードは問題ないように見えます。 しかし、実際には、ループを終了した後も$count変数は変更されないままであるため、bash開発者は驚いたことになります。 なぜこれが起こっているのですか?

パイプラインの各コマンドは個別のサブシェルで実行され、サブシェル内の変数の変更は、親シェルインスタンス(つまり、このコードを呼び出したスクリプト)のこの変数の値に影響しません。

この場合、 forループはパイプラインの一部であり、変数$countコピーを持つ別のサブシェルで実行され、親シェルの変数$count値で初期化されます: "0"。 ループが終了すると、ループで使用された$countのコピーは破棄され、 echoコマンドは変更されていない初期値の$count ( "0")を表示します。

これにはいくつかの方法があります。

サブシェルでループを実行できます(少し曲がっていますが、より簡単で理解しやすく、shで動作します)。

  #POSIX互換
カウント= 0
猫/ etc / passwd |  (
    行を読みながら; する
        カウント= $((カウント+ 1))
    やった
     echo "行の総数:$ count"
 ) 


サブシェルの作成を完全に回避するには、リダイレクトを使用します(Bourne shell(sh)では、リダイレクト用のサブシェルも作成されるため、このトリックはbashでのみ機能します)。

  #bashのみ!
カウント= 0
行を読みながら; する
    カウント= $(($カウント+ 1))
 </ etc / passwd
 echo "行の総数:$ count" 


前の方法はファイルに対してのみ機能しますが、コマンドラインの出力を1行ずつ処理する必要がある場合はどうなりますか? プロセス置換を使用する:
  LINEを読みながら; する
     echo "-> $ LINE"
 done <<(grep PATH / etc / profile) 


サブシェルの問題を解決するためのさらに興味深い方法は、 Bash FAQ#24で説明されています。

8. if [grep foo myfile]



多くの人はif後に角括弧を置く習慣に戸惑い、初心者はしばしば、条件付きC言語構造の括弧のように、条件付き構文の一部であるという誤った印象を得る。

しかし、そのような意見は間違いです! 開始角括弧( [ )は構文の一部ではありませんが、コマンドの終了引数が最後の引数でなければならないことを除いて、 testコマンドと同等のコマンドです。

構文のif

 コマンドの場合
それから
    コマンド
 elifコマンド#オプション
それから
    コマンド
 else#オプション
    コマンド
 fi 


ご覧のとおり、 [または[[はありません!

繰り返しますが、 [は引数を取り、戻りコードを返すコマンドです。 すべての通常のコマンドと同様に、エラーメッセージを表示できますが、原則として、STDOUTには何も生成しません。

コマンドの最初のセットを実行し、このセットの最後のコマンドのリターンコードに応じて、「then」セクションのコマンドブロックを実行するか、スクリプトの実行を継続するかを決定します。

grep出力に応じて決定する必要がある場合は、括弧、角括弧、中括弧、バックティック、またはその他の構文要素で囲む必要はありません。 if後にコマンドとしてgrepを書くだけif

  if grep foo myfile> / dev / null; それから
     ...
 fi 


標準のgrep出力を破棄することに注意してください。検索結果は必要ありません。ファイルに行が存在するかどうかを知りたいだけです。 grepが文字列を見つけると、0を返し、条件が満たされます。 それ以外の場合(ファイルに行がありません)、 grepは0以外の値を返します。GNUgrepでは、リダイレクト>/dev/null-qオプションに置き換えることができます。

9. if [bar = "$ foo"]



前の段落で説明したように、 [はコマンドです。 他のコマンドと同様に、bashはコマンドの後にスペースが続き、最初の引数が続き、再びスペースが続くと想定します。 したがって、スペースなしですべてを書くことはできません! それはまさにこのようなものです:

  if [bar = "$ foo"] 


bar="$foo" (置換後、ただし単語分割なし)および][コマンドの引数であるため、シェルが引数の開始位置と終了位置を判別できるように、引数の各ペアの間にスペースが必要です。

10. if [[a = b] && [c = d]]



再び同じ間違い。 [コマンドであり、 ifと条件の間の構文要素ではなく、さらにグループ化の手段でもありません。 C構文を使用して、括弧を単に正方形に置き換えるだけではbash構文に変換できません。

難しい条件を実装したい場合、正しい方法は次のとおりです。

  if [a = b] && [c = d] 


ここでは、&&演算子によって結合されたifの後に2つのコマンドがあることに注意してください。 このコードは、次のようなコマンドと同等です。

 テストa = b &&テストc = dの場合 


最初のテストコマンドがfalse (ゼロ以外の数値)を返す場合、条件の本文はスキップされます。 true返すtrue 、2番目の条件はtrueです。 true返すtrue 、条件本体が実行されます。

継続する。

この翻訳の最初の公開は私のブログのページで行われました

Source: https://habr.com/ru/post/J47706/


All Articles