私は次のスクリプトを作成して、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
短い答え(あなたの答えに最も近いが、スペースを扱います)
_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つの問題があります。
*.csv
_で終わるディレクトリがある場合はどうなりますか?1。改行のみで分割
file
に何を設定するかを理解するには、シェルはfind
の出力を取得して何らかの方法で解釈する必要があります。そうでない場合、file
はfind
の出力全体になります。
シェルは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 ''
_を使用して、read
がfind
と同じ行区切り文字を使用するようにします。ちなみに、_-d $'\0'
_も同様に機能します。これは、bash
がNULバイトをサポートしないため、空の文字列として扱われるためです。
正確にするために、_-r
_も追加しています。これは、ファイル名のバックスラッシュを特別に処理しないことを示しています。たとえば、_-r
_を使用しない場合、_\<newline>
_は削除され、_\n
_はn
に変換されます。
bash
やzsh
を必要としない、または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
_に変更しました。
このスクリプトは、ファイル名にスペースまたはシェルグロビング文字_\[?*
_が含まれていると失敗します。 find
コマンドは、1行に1つのファイル名を出力します。次に、コマンド置換_`find …`
_がシェルによって次のように評価されます。
find
コマンドを実行して、その出力を取得します。find
出力を個別の単語に分割します。空白文字はすべて単語の区切り文字です。たとえば、現在のディレクトリに_`foo* bar.csv
_、_foo 1.txt
_、_foo 2.txt
_という3つのファイルがあるとします。
find
コマンドは_./foo* bar.csv
_を返します。./foo*
_と_bar.csv
_を生成します。./foo*
_にはグロビングメタ文字が含まれているため、一致するファイルのリストに展開されます:_./foo 1.txt
_および_./foo 2.txt
_。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でこれを行うための正規の方法であるべきです。
完全に安全な検索 でファイル(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
Afaik findには必要なものがすべて揃っています。
find . -okdir diff {} /some/other/path/{} ";"
findは、プログラムをsavelyに呼び出すことに注意を払っています。 -okdirは、diffの前にプロンプトを表示します(本当にyes/noですか)。
シェルは関与せず、グロビング、ジョーカー、pi、pa、poはありません。
補足として:findをfor/while/do/xargsと組み合わせると、ほとんどの場合、それは間違っています。 :)
ここで明白な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
スペースが含まれているファイル名は、引用符で囲まないと、コマンドラインで複数の名前のように見えます。ファイルの名前が「Hello World.txt」の場合、diff行は次のように展開されます。
diff Hello World.txt /some/other/path/Hello World.txt
これは4つのファイル名のように見えます。引数を引用符で囲みます:
diff "$file" "/some/other/path/$file"
二重引用符はあなたの友達です。
diff "$file" "/some/other/path/$file"
それ以外の場合、変数の内容は単語分割されます。
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