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

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

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

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

答え2

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

  1. findコマンドを実行して出力を取得します。
  2. 出力をfind別の単語に分割します。空白文字は単語区切り記号です。
  3. 各単語に対してワイルドカードパターンの場合は、一致するファイルのリストに展開します。

たとえば、現在のディレクトリに`foo* bar.csvfoo 1.txtおよびという3つのファイルがあるとしますfoo 2.txt

  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

単語の分離を減らし、ワイルドカードをオフにすると、この段階でほとんどの問題を回避できます。単語分離効果を弱めるには、IFS変数を単一の改行に設定します。これにより、出力はfind改行でのみ分割され、空白が維持されます。ワイルドカードをオフにするには、次の手順を実行しますset -f。コードのこの部分は、ファイル名に改行文字が含まれていない限り機能します。

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

(これはあなたの質問の一部ではありませんが、$(…)overを使用することをお勧めします`…`。意味は同じですが、バックティックバージョンには奇妙な引用規則があります。)

diff $file /some/other/path/$file以下に別の質問があります。

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

それ以外の場合、値は$file単語に分割され、その単語は上記のコマンド置換と同様にグローバルパターンとして扱われます。シェルプログラミングについて覚えておくべきことが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

答え3

私はその言及を見なかったことに驚きました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でこれを行う標準的な方法です。

答え4

Afaik findには必要なものがすべてあります。

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

find は呼び出しプログラムを保存する役割を担います。 -okdirはdiffの前にメッセージを表示します(はい/いいえと確信しています)。

シェルは含まれておらず、ワイルドカード、ピエロ、パイ、波、砲はありません。

ちなみに、 find を for/while/do/xargs と組み合わせると、ほとんどの場合は間違って実行されます。 :)

関連情報