名前にスペースを含むファイルを繰り返しますか? [コピー]

名前にスペースを含むファイルを繰り返しますか? [コピー]

両方のディレクトリにあるすべての同じファイルの出力を比較するために、次のスクリプトを作成しました。

#!/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

ベストアンサー1

単答型(回答に最も近いが空白処理)

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
' exec-sh {} ';'

shまたは、ファイルごとに1つずつ実行しないことをお勧めします。

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

長い答え

3つの質問があります。

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

1.改行文字のみに分割

何を設定するかを調べるために、fileシェルは出力を取得してfind何らかの方法で解釈する必要があります。それ以外の場合はfileフル出力になりますfind

シェルはIFSデフォルトで設定された変数を読み込みます。<space><tab><newline>

次に、出力の各文字を調べますfind。で文字が見つかるとすぐにIFSファイル名の終わりが表示されていると思って、fileこれまで見た文字に設定してループを実行します。次に、最後に停止した場所から次のファイル名を取得し、出力の終わりに達するまで次のループを実行します。

したがって、効果的に次のことを行います。

for file in "zquery" "-" "abc" ...

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

IFS=$'\n'

あなたの命令の前にfor ... find

IFSこれは単一の改行に設定されるため、スペースとタブではなく改行にのみ分割されます。

または代わりにorshを使用する場合は、次のように書く必要があります。dashksh93bashzshIFS=$'\n'

IFS='
'

これはスクリプトを操作するのに十分かもしれませんが、他の特別なケースを適切に処理することに興味がある場合は、読んでください。

2.$fileワイルドカード拡張を使用しないでください。

ループ内部

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

シェルは$file(再び!)拡張を試みます。

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

ただし、予測不能な動作を引き起こす可能性がある、*または同じワイルドカード文字が含まれる可能性があります。?(この点を指摘してくれたGilesに感謝します。)

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

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

2回発生しfile1.csvfile2.csv処理されました。

代わりに、私たちはしなければなりません

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各ファイル名の末尾にヌルバイトが追加されます。ヌルバイトはファイル名には許可されない唯一の文字なので、どんなに奇妙な場合でも、可能なすべてのファイル名を処理する必要があります。

相手のファイル名を取得するにはIFS= read -r -d ''

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

-r正確性のために、ファイル名のバックスラッシュを特に処理しないことを追加しました。たとえば、noは削除-rされ\<newline>\nに変換されますn

ヌルバイトに対する上記のすべての規則を要求しbashたり覚えていないより移植性のある作成方法です(Gillesにもう一度感謝します)。zsh

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

*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
' exec-sh {} ';'

〜のようにグレンジャックマンどちらの例でも、各ファイルに対して実行されるコマンドはサブシェルで実行されるため、ループ内の変数が変更されると忘れてしまうことに注意してください。

変数を設定し、ループの終わりに引き続き設定する必要がある場合は、次のようにプロセス置換を使用するように変数をオーバーライドできます。

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

おすすめ記事