両方のディレクトリにあるすべての同じファイルの出力を比較するために、次のスクリプトを作成しました。
#!/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つの質問があります。
- デフォルトでは、シェルはコマンドの出力をスペース、タブ、および改行に分割します。
- ファイル名には、拡張されるワイルドカード文字を含めることができます。
- 名前で終わるディレクトリがある場合はどうなりますか
*.csv
?
1.改行文字のみに分割
何を設定するかを調べるために、file
シェルは出力を取得してfind
何らかの方法で解釈する必要があります。それ以外の場合はfile
フル出力になりますfind
。
シェルはIFS
デフォルトで設定された変数を読み込みます。<space><tab><newline>
次に、出力の各文字を調べますfind
。で文字が見つかるとすぐにIFS
ファイル名の終わりが表示されていると思って、file
これまで見た文字に設定してループを実行します。次に、最後に停止した場所から次のファイル名を取得し、出力の終わりに達するまで次のループを実行します。
したがって、効果的に次のことを行います。
for file in "zquery" "-" "abc" ...
改行でのみ入力を分割するように指示するには、次の手順を実行する必要があります。
IFS=$'\n'
あなたの命令の前にfor ... find
。
IFS
これは単一の改行に設定されるため、スペースとタブではなく改行にのみ分割されます。
または代わりにorsh
を使用する場合は、次のように書く必要があります。dash
ksh93
bash
zsh
IFS=$'\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.csv
、file2.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も機能します。read
find
-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 …`
次のようにコマンド置換を評価します。
find
コマンドを実行して出力を取得します。- 出力を
find
別の単語に分割します。空白文字は単語区切り記号です。 - 各単語に対してワイルドカードパターンの場合は、一致するファイルのリストに展開します。
たとえば、現在のディレクトリに`foo* bar.csv
、foo 1.txt
およびという3つのファイルがあるとしますfoo 2.txt
。
- コマンド
find
はを返します./foo* bar.csv
。 - シェルは文字列をスペースに分割して2つの単語と
./foo*
を生成しますbar.csv
。 ./foo*
ワイルドカードメタ文字(./foo 1.txt
および)が含まれているため、一致するファイルのリストに展開されます./foo 2.txt
。- したがって、
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 と組み合わせると、ほとんどの場合は間違って実行されます。 :)