44950


POSIX shell (あるいは Dash) のシェルスクリプトにおいて `.txt`
という拡張子を末尾に持つファイルたち全てに対して何らかの処理がしたいとき、どのように書けますか？
たとえば全てのファイル名を出力するにはどうすれば良いですか？

ただし、以下を全て満たしたいです。

  * ファイル名に空白が入っていても良い。
  * ファイル名に改行が入っていても良い。
  * ディレクトリは除きたい。それ以外はシンボリックリンクも含め全てそのまま扱いたい。
  * 条件に合うファイルが無い場合、何もしない。


CC BY-SA 4.0
2018-06-21T08:00:24.510
44949
2018-06-22T11:19:58.323
2018-06-22T11:19:58.323
19110
19110
question
shellscript sh dash
ある拡張子を持つファイルたち全てについてループするシェルスクリプト


glob を使ってファイルたちを見つけ出し、for 文を回せば良いです。

[code]

    for file in *.txt ; do
        [ -f "$file" ] || continue
        # 実行したいコマンド
        echo "$file"
    done
    
[/code]

ディレクトリも含めたい場合は、[`-f` でなく `-e`
を使ってください。](http://tech.nikkeibp.co.jp/it/article/COLUMN/20060227/230901/)

以下、注意事項です。

  * この方法では `.piyo.txt` のようなファイルは捕捉されません。ドットから始まるファイルも捕捉したい場合、`.*.txt` というパターンを使うとマッチすることができます。
  * `*.txt` にマッチするファイルが無かった場合、`$file` には `*.txt` という文字列そのものが代入されてしまいます。しかし `[ -f "$file" ]` でそのファイルが存在しているかどうか確認しているので問題ありません。また Bash の場合は、`shopt -s nullglob` を使うことで[この問題を回避できます。](https://stackoverflow.com/a/14505622/5989200)
  * この方法はファイルたちを一旦列挙してから 1 つずつ処理するため、`.txt` ファイルがあまりにも多い場合はメモリ容量に関するエラーが出るかもしれません。そうならないような方法については [774RR さんの回答](https://ja.stackoverflow.com/a/44960/19110)をご覧ください。
  * `*.txt` の代わりに `$(ls *.txt)` を使ってしまうと、空白が含まれたファイル名を正しく処理できません。


CC BY-SA 4.0
2018-06-21T08:00:24.510
44950
2018-06-22T03:29:27.103
2018-06-22T03:29:27.103
19110
19110
44949
answer


書き込み依頼があったのでフォローアップなど

shell glob はコマンド起動前に展開されるので、一致ファイル名が shell の制約以上にある場合には argument too long
エラーとなって起動しなくなることがあります。 shell glob によらずにファイル名を展開するツールには `find`
がありますのでこちらを使う手を紹介。  
# Windows 環境では MS-DOS の頃からの伝統で `grep` 相当の機能である `find` が入っているため、要注意。 Windows
上で試すなら cygwin なり WSL なりが必要となります。

基本的使い方：ファイル名にスペースが入っていない場合限定

[code]

    $ find . -name '*.txt' -exec rm -f {} \;
    
[/code]

で `rm -f filename` をファイル１つにつき１回起動します。

覚えるコツ

  * シングルクォートで括らずに `*.txt` と書くと、これを glob が展開してしまいます。ファイル名展開は `find` にやらせたいので glob を回避するためにはシングルクォートが必要。
  * `find` は `{}` のところにファイル名を格納して exec/fork します
  * オプション `-exec` と次のオプションを区切るためには「 `find` のコマンドラインとして」のセミコロンが必要です。これもシェルによって解釈されないようにエスケープ（この例ではバックスラッシュ）が必要です。エスケープしないとシェルによって複数コマンドの起動のための区切りとみなされエラーになってしまいます。

ディレクトリは除外ファイル限定なら `-type f` をつけるとかの指定もできます。

`find -exec` を正しく使うためには注意事項が多くて意外に難しかったりします。 shell glob + for
でよければそっちのほうが圧倒的に簡単だったりします・・・

この形式は POSIX 標準なので非 GNU な POSIX (要するに `UNIX` TM) でも使うことができます (HPUX とか) が
`xargs` またはシェルがスペースでファイル名を区切ってしまう関係で、ファイル名にスペース等が入っていない場合限定です。

なおファイル１つにつきコマンド１回が起動されるので、ファイルが１０００個あればコマンドも１０００回起動され、効率はあまりよくありません。たいていのコマンドはファイル名を複数個受け付けるので、そういうコマンドの場合は
`xargs` を使って

[code]

    $ find . -name '*.txt' -print | xargs rm -f
    
[/code]

とすると `rm -f filename0 filename1 filename2 ...`
のように、コマンドラインの制約いっぱいまで自動的にファイル名をくっつけてコマンドを複数回起動してくれます。（コマンドライン中のファイル数を制約したり、複数プロセス同時起動したりすることもできますが解説略）こうすることでコマンドの起動回数を激減させることができ、性能が向上します。

応用例：ファイル名にスペースが含まれるような場合

ファイル名がシェルまたは `xargs` によってスペースで区切られないようにしないとファイル名が途中で切れてしまいます。異ファイルの区切り記号を
`'\0'` つまり `NUL` 文字とすることで誤動作を防ぐことができますが、この拡張機能を使うためには `xargs -0` あるいは `xargs
--null` を使う必要があります。同時にパラメータを渡す側、つまり `find` もオプション `-print0` に書き換える必要が生じ

[code]

    $ find . -name '*.txt' -print0 | xargs -0 command
    
[/code]

この形式を使うのが Linux では最もお勧めです。 `find -print0` と `xargs -0`
をペアで使う、と単純にイディオムとして覚えるだけでコマンドラインワンライナーのレベルが上がります。  
# 同時に正しいシングルクォートの使い方を覚えることができてウマー

この機能は POSIX にはない GNU 拡張なので Linux では使えますが、素の HPUX では使えなかったりします。まあ今時商用 UNIX
は壊滅に近い状況にありますが・・・


CC BY-SA 4.0
2018-06-21T12:14:29.503
44960
2018-06-21T12:14:29.503
8589
44949
answer