ワイルドカードに一致するゼロ個のファイルだけでなく、それ以上のファイルも処理する sh 構文はありますか?

ワイルドカードに一致するゼロ個のファイルだけでなく、それ以上のファイルも処理する sh 構文はありますか?

ワイルドカードに一致するファイルを処理するシェル スクリプトを作成したいと考えています/bin/sh。一致するファイルが 1 つ以上ある場合の処理​​は簡単です。ただし、一致するファイルが 0 個の場合の処理​​は面倒です。

明らかな構成は次のとおりです。

#!/bin/sh
for f in *.ext; do
  handle "$f"
done

ここで、 は*.ext、シェルがファイル パスと比較する 1 つ以上の式であり、 は、handle既存のファイルへのパスが指定された場合は正しく実行されるシェル コマンドですが、ファイルにマップされていないパスが指定された場合は失敗します。 (この質問を引き起こす場合、それらは*.flacとですffmpegが、それは問題ではないと思います。)

一致するファイルがある場合foo.extbar.extこのスクリプトは

handle "foo.ext"
handle "bar.ext"

予想通りです。しかし、ない一致するファイルがない場合、このスクリプトは次のようなエラーメッセージを表示します。

handle: *.ext: No such file or directory

なぜこのようなことが起こるのかは分かっていると思う。バッシュマニュアルページ(これは にも当てはまると思います/bin/sh) は、「 に続く単語のリストinが展開され、項目のリストが生成されます。… に続く項目の展開の結果が空のリストになった場合、コマンドは実行されず、戻りステータスは 0 になります。」と述べています。どうやら、*.ext「 が展開されると」、結果のリスト*.extには空のリストではなく が含まれるようです。しかし、それが起こらないようにする方法は説明されていません。

(更新:sh (1)Heirloom プロジェクトのマニュアルページこれは Bourne Shell について記述したものshで、後の Bourne Again Shell について記述したものではありませんbashファイル名の生成では、「パターンに一致するファイル名が見つからない場合は、単語は変更されません」と明確に書かれています。リストにパターンshが残る理由が説明されています。*.ext

一致するファイルがない場合にはエラーなしで 0 回実行され、一致するファイルごとに 1 回実行されるように、このループを記述するコンパクトで慣用的な方法は何ですか? さらに良いのは、次のような複数のパターンで機能する方法です。

for f in *.ext1 special.ext1 *.ext2; do ...

移植性のために、と互換性のある構文を使用することを好みます/bin/sh。たまたま Mac OS X 10.11.6 を使用していますが、Unix 系 OS であればどれでも動作する構文を期待しています。

私は、このようなループを書くための、不器用で非慣用的な方法をいくつか思いつきました。すぐに良い回答が得られない場合は、記録のために、回答として投稿します。

答え1

これを行う最も簡単な移植可能な方法は、展開によって実際に存在するものが生成されない場合にループをスキップすることです。

for f in *.ext1 *.ext2; do
  [ -e "$f" ] || continue
  handle "$f"
done

説明: .ext2 ファイルはいくつかあるが、.ext1 ファイルはないとします。その場合、ワイルドカードは に展開されます*.ext1 filea.ext2 fileb.ext2 filec.ext2。したがって は[ -e "*.ext1" ]失敗し、 が実行されcontinue、ループのその反復の残りがスキップされます。その後、[ -e "filea.ext2" ]etc は成功し、ループのそれらの反復は正常に実行されます。

ちなみに、これを変更して、たとえば[ -f "$f" ] || continueプレーン ファイル以外のものをスキップするようにすることができます (ディレクトリなどをスキップしたい場合)。

もちろん、bash で実行している場合は、shopt -s nullglob最初に を実行して、リストに一致しないパターンが残らないようにすることができます。その後shopt -u nullglob、予期しない副作用を防ぐために を実行します (たとえば、が設定されているgrep pattern *.nonexistent場合は stdin から読み取ろうとしますnullglob)。

答え2

/bin/bashBourne Shell ( ) からBourne Again Shell ( ) に切り替えると/bin/sh、シンプルなソリューションが可能になります。

bash(1)マニュアルページ言及しているヌルグロブオプション:

設定されている場合、bashはどのファイルにもマッチしないパターンを許可します(パス名の拡張上記の文字列は、それ自体ではなく null 文字列に展開されます。

パス名の拡張セクションには次のように書かれています:

単語分割後、…bashは各単語をスキャンして文字を探します*?、 そして[これらの文字のいずれかが出現した場合、その単語はパターンとみなされ、パターンに一致するファイル名のアルファベット順リストに置き換えられます。一致するファイル名が見つからず、シェルオプションヌルグロブが有効になっていない場合、単語は変更されません。ヌルグロブオプションが設定されていて、一致するものが見つからない場合、その単語は削除されます。…

そこで、ヌルグロブオプションはパターンに対して望ましい動作を与えます。パターンに一致するファイルがない場合、ループは実行されません。

#!/bin/bash
shopt -s nullglob
for f in *.ext; do
  handle "$f"
done

しかしヌルグロブオプションは他のコマンドの望ましい動作を妨げる可能性があります。そのため、既存の設定を保存しておくのが賢明でしょう。ヌルグロブ、そして後でそれを復元します。幸い、shopt -p組み込みコマンドは入力として再利用できる形式で出力を出力します。しかし、それはすべてのオプションに対して出力を出力するのでgrepnullオブジェクト設定。以下のログを参照してください ( $bash コマンド プロンプトを示します)。

$ shopt -p | grep nullglob
shopt -u nullglob
$ shopt -s nullglob
$ shopt -p | grep nullglob
shopt -s nullglob

したがって、ワイルドカード パターンに一致する 0 個以上のファイルを処理する最終的な bash 構文は次のようになります。

#!/bin/bash
SAVED_NULLGLOB=$(shopt -p | grep nullglob)
shopt -s nullglob
for f in *.ext; do
  handle "$f"
done
eval "$SAVED_NULLGLOB"

また、質問では次のような複数のパターンが言及されていました。

for f in *.ext1 special.ext1 *.ext2; do ...

ヌルグロブオプションはリスト内の「パターン」ではない単語には影響しないので、special.ext1ループに渡されてしまいます。これに対する唯一の解決策は@ゴードン・デイヴィソンcontinue表現、[ -e "$f" ] || continue

謝辞: 両方@イグナシオ・バスケス・エイブラムスそして@ゴードン・デイヴィソン言及bash(1)とそのヌルグロブオプション。ありがとうございます。彼らはすぐには完全な例を添えた回答を寄せてくれなかったので、私自身がそうすることにします。

関連情報