web-dev-qa-db-ja.com

名前にスペースが含まれるファイルをループしますか?

私は次のスクリプトを作成して、2つのディレクターの出力をすべて同じファイルで比較しました。

#!/bin/bash

for file in `find . -name "*.csv"`  
do
     echo "file = $file";
     diff $file /some/other/path/$file;
     read char;
done

これを達成する方法は他にもあります。不思議なことに、ファイルにスペースが含まれていると、このスクリプトは失敗します。どうすればこれに対処できますか?

Findの出力例:

./zQuery - abc - Do Not Prompt for Date.csv
160
Amir Afghani

短い答え(あなたの答えに最も近いが、スペースを扱います)

_OIFS="$IFS"
IFS=$'\n'
for file in `find . -type f -name "*.csv"`  
do
     echo "file = $file"
     diff "$file" "/some/other/path/$file"
     read line
done
IFS="$OIFS"
_

より良い回答(ファイル名のワイルドカードと改行も処理します)

_find . -type f -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done
_

ベストアンサー( ギレスの回答)に基づいて

_find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' {} ';'
_

または、ファイルごとに1つのshを実行しないようにするには、次のようにします。

_find . -type f -name '*.csv' -exec sh -c '
  for file do
    echo "$file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
  done
' sh {} +
_

長い答え

次の3つの問題があります。

  1. デフォルトでは、シェルはコマンドの出力をスペース、タブ、改行に分割します
  2. ファイル名には、展開されるワイルドカード文字を含めることができます
  3. 名前が_*.csv_で終わるディレクトリがある場合はどうなりますか?

1。改行のみで分割

fileに何を設定するかを理解するには、シェルはfindの出力を取得して何らかの方法で解釈する必要があります。そうでない場合、filefindの出力全体になります。

シェルはIFS変数を読み取ります。この変数はデフォルトで_<space><tab><newline>_に設定されています。

次に、findの出力の各文字を調べます。 IFSに含まれる文字が見つかるとすぐに、ファイル名の終わりをマークしていると考えられるため、fileを今まで見た文字に設定し、ループを実行します。次に、中断したところから開始して次のファイル名を取得し、出力の終わりに達するまで次のループなどを実行します。

だから、これは効果的にこれを行っています:

_for file in "zquery" "-" "abc" ...
_

入力を改行でのみ分割するように指示するには、次のようにする必要があります。

_IFS=$'\n'
_

_for ... find_コマンドの前。

これはIFSを単一の改行に設定するため、改行でのみ分割され、スペースやタブも分割されません。

_ksh93_、shまたはdashの代わりにbashまたはzshを使用している場合は、代わりに次のように_IFS=$'\n'_を記述する必要があります。

_IFS='
'
_

スクリプトを機能させるにはおそらくこれで十分ですが、他のコーナーケースを適切に処理したい場合は、以下をお読みください...

2。ワイルドカードなしで_$file_を展開する

あなたがするループの内側

_diff $file /some/other/path/$file
_

シェルは_$file_を拡張しようとします(これも!)。

スペースを含めることもできますが、上記でIFSをすでに設定しているため、ここでは問題になりません。

ただし、_*_や_?_などのワイルドカード文字を含めることもできます。これにより、予期しない動作が発生します。 (これを指摘してくれたGillesに感謝します。)

シェルにワイルドカード文字を展開しないように指示するには、変数を二重引用符で囲みます。

_diff "$file" "/some/other/path/$file"
_

同じ問題が私たちを噛む可能性もあります

_for file in `find . -name "*.csv"`
_

たとえば、これら3つのファイルがある場合

_file1.csv
file2.csv
*.csv
_

(ほとんどありませんが、それでも可能です)

走ったかのように

_for file in file1.csv file2.csv *.csv
_

に拡大されます

_for file in file1.csv file2.csv *.csv file1.csv file2.csv
_

_file1.csv_および_file2.csv_が2回処理される。

代わりに、

_find . -name "*.csv" -print | while IFS= read -r file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done
_

readは、標準入力から行を読み取り、IFSに従って行を単語に分割し、指定した変数名に格納します。

ここでは、行を単語に分割せず、_$file_に格納するように指示しています。

_read line_が_read line </dev/tty_に変更されたことにも注意してください。

これは、ループ内では、標準入力がfindからパイプライン経由で送信されるためです。

readを実行しただけでは、ファイル名の一部またはすべてが消費され、一部のファイルがスキップされます。

_/dev/tty_は、ユーザーがスクリプトを実行している端末です。スクリプトがcron経由で実行された場合、これによりエラーが発生することに注意してください。ただし、この場合、これは重要ではないと思います。

次に、ファイル名に改行が含まれている場合はどうなりますか?

_-print_を_-print0_に変更し、パイプラインの最後で_read -d ''_を使用することで、これを処理できます。

_find . -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read char </dev/tty
done
_

これにより、findは各ファイル名の最後にnullバイトを置きます。 nullバイトはファイル名に使用できない唯一の文字であるため、どのように奇妙であっても、これはすべての可能なファイル名を処理する必要があります。

反対側のファイル名を取得するには、_IFS= read -r -d ''_を使用します。

上記でreadを使用した場合、改行のデフォルトの行区切り文字を使用しましたが、現在、findはnullを行区切り文字として使用しています。 bashでは、引数にNUL文字を(組み込みのものであっても)コマンドに渡すことはできませんが、bashは_-d ''_をNUL区切りとして解釈します。したがって、_-d ''_を使用して、readfindと同じ行区切り文字を使用するようにします。ちなみに、_-d $'\0'_も同様に機能します。これは、bashがNULバイトをサポートしないため、空の文字列として扱われるためです。

正確にするために、_-r_も追加しています。これは、ファイル名のバックスラッシュを特別に処理しないことを示しています。たとえば、_-r_を使用しない場合、_\<newline>_は削除され、_\n_はnに変換されます。

bashzshを必要としない、またはnullバイトに関する上記のすべてのルールを思い出す必要のない、これを書くためのより移植性の高い方法(これもGillesのおかげです):

_find . -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read char </dev/tty
' {} ';'
_

3。名前が* .csvで終わるディレクトリをスキップする

_find . -name "*.csv"
_

_something.csv_と呼ばれるディレクトリにも一致します。

これを避けるには、_-type f_をfindコマンドに追加します。

_find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' {} ';'
_

glenn jackman が指摘するように、これらの例の両方で、各ファイルに対して実行するコマンドはサブシェルで実行されているため、ループ内の変数を変更すると、それらは忘れられます。

変数を設定し、ループの最後でも変数を設定する必要がある場合は、次のようなプロセス置換を使用するように変数を書き直すことができます。

_i=0
while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
    i=$((i+1))
done < <(find . -type f -name '*.csv' -print0)
echo "$i files processed"
_

コマンドラインでこれをコピーして貼り付けようとすると、_read line_が_echo "$i files processed"_を消費するため、そのコマンドは実行されません。

これを回避するには、_read line </dev/tty_を削除して、lessなどのポケットベルに結果を送信します。


[〜#〜]メモ[〜#〜]

ループ内のセミコロン(_;_)を削除しました。必要に応じて元に戻すことができますが、必要ありません。

最近では、$(command)は_`command`_よりも一般的です。これは主に、_`command1 \`command2\``_より$(command1 $(command2))の方が記述しやすいためです。

_read char_は実際には文字を読みません。行全体を読み取るため、_read line_に変更しました。

218
Mikel

このスクリプトは、ファイル名にスペースまたはシェルグロビング文字_\[?*_が含まれていると失敗します。 findコマンドは、1行に1つのファイル名を出力します。次に、コマンド置換_`find …`_がシェルによって次のように評価されます。

  1. findコマンドを実行して、その出力を取得します。
  2. find出力を個別の単語に分割します。空白文字はすべて単語の区切り文字です。
  3. Wordごとに、それがグロビングパターンである場合は、一致するファイルのリストに展開します。

たとえば、現在のディレクトリに_`foo* bar.csv_、_foo 1.txt_、_foo 2.txt_という3つのファイルがあるとします。

  1. findコマンドは_./foo* bar.csv_を返します。
  2. シェルはこの文字列をスペースで分割し、2つの単語_./foo*_と_bar.csv_を生成します。
  3. _./foo*_にはグロビングメタ文字が含まれているため、一致するファイルのリストに展開されます:_./foo 1.txt_および_./foo 2.txt_。
  4. したがって、forループは、_./foo 1.txt_、_./foo 2.txt_および_bar.csv_を使用して連続して実行されます。

この段階でほとんどの問題を回避するには、Wordの分割を弱め、グロビングをオフにします。 Word分割をトーンダウンするには、IFS変数を単一の改行文字に設定します。これにより、findの出力は改行でのみ分割され、スペースが残ります。グロビングをオフにするには、_set -f_を実行します。次に、ファイル名に改行文字が含まれていない限り、コードのこの部分は機能します。

_IFS='
'
set -f
for file in $(find . -name "*.csv"); do …
_

(これは問題の一部ではありませんが、_`…`_ではなく$(…)を使用することをお勧めします。これらは同じ意味ですが、逆引用バージョンには奇妙な引用規則があります。)

以下に別の問題があります:_diff $file /some/other/path/$file_は

_diff "$file" "/some/other/path/$file"
_

それ以外の場合、_$file_の値は単語に分割され、単語は上記のコマンドsubstitutioと同様にグロブパターンとして扱われます。シェルプログラミングについて1つ覚えておく必要がある場合は、次の点に注意してください。変数の展開(_$foo_)とコマンドの置換($(bar)、分割したい場合を除きます。 (上記では、findの出力を行に分割したいと思っていました。)

findを呼び出す確実な方法は、見つかった各ファイルに対してコマンドを実行するように指示することです。

_find . -name '*.csv' -exec sh -c '
  echo "$0"
  diff "$0" "/some/other/path/$0"
' {} ';'
_

この場合、別のアプローチは2つのディレクトリを比較することですが、すべての「退屈な」ファイルを明示的に除外する必要があります。

_diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path
_

readarrayが記載されていないことに驚いています。 <<<演算子と組み合わせて使用​​すると、これが非常に簡単になります。

$ touch oneword "two words"

$ readarray -t files <<<"$(ls)"

$ for file in "${files[@]}"; do echo "|$file|"; done
|oneword|
|two words|

<<<"$expansion"構文を使用すると、次のように、改行を含む変数を配列に分割することもできます。

$ string=$(dmesg)
$ readarray -t lines <<<"$string"
$ echo "${lines[0]}"
[    0.000000] Initializing cgroup subsys cpuset

readarrayは長年Bashに存在しているので、これはおそらくBashでこれを行うための正規の方法であるべきです。

6
blujay

完全に安全な検索 でファイル(any特殊文字を含む)をループします(ドキュメントのリンクを参照):

exec 9< <( find "$absolute_dir_path" -type f -print0 )
while IFS= read -r -d '' -u 9
do
    file_path="$(readlink -fn -- "$REPLY"; echo x)"
    file_path="${file_path%x}"
    echo "START${file_path}END"
done
6
l0b0

Afaik findには必要なものがすべて揃っています。

find . -okdir diff {} /some/other/path/{} ";"

findは、プログラムをsavelyに呼び出すことに注意を払っています。 -okdirは、diffの前にプロンプ​​トを表示します(本当にyes/noですか)。

シェルは関与せず、グロビング、ジョーカー、pi、pa、poはありません。

補足として:findをfor/while/do/xargsと組み合わせると、ほとんどの場合、それは間違っています。 :)

4
user unknown

ここで明白なzshソリューションについて誰も言及していないことに驚いています。

for file (**/*.csv(ND.)) {
  do-something-with $file
}

(D)隠しファイルも含めるには、(N)一致しない場合のエラーを回避するには、(.)regularファイルに制限します。)

bash4.3以上では、部分的にもサポートしています。

shopt -s globstar nullglob dotglob
for file in **/*.csv; do
  [ -f "$file" ] || continue
  [ -L "$file" ] && continue
  do-something-with "$file"
done
4

スペースが含まれているファイル名は、引用符で囲まないと、コマンドラインで複数の名前のように見えます。ファイルの名前が「Hello World.txt」の場合、diff行は次のように展開されます。

diff Hello World.txt /some/other/path/Hello World.txt

これは4つのファイル名のように見えます。引数を引用符で囲みます:

diff "$file" "/some/other/path/$file"
2
Ross Smith

二重引用符はあなたの友達です。

diff "$file" "/some/other/path/$file"

それ以外の場合、変数の内容は単語分割されます。

1
geekosaur

Bash4では、組み込みのmapfile関数を使用して、各行を含む配列を設定し、この配列を反復処理することもできます。

$ tree 
.
├── a
│   ├── a 1
│   └── a 2
├── b
│   ├── b 1
│   └── b 2
└── c
    ├── c 1
    └── c 2

3 directories, 6 files
$ mapfile -t files < <(find -type f)
$ for file in "${files[@]}"; do
> echo "file: $file"
> done
file: ./a/a 2
file: ./a/a 1
file: ./b/b 2
file: ./b/b 1
file: ./c/c 2
file: ./c/c 1
1
kitekat75