web-dev-qa-db-ja.com

可変IFSと、ループ付きのリストファイルによる異なる結果

現在のディレクトリとそのサブディレクトリにあるファイルのリストを取得したい(ワンライナースクリプトを使用したい):

IFS=$(echo -en "\n\b");
for FILE in $(find -type f); do echo "$FILE"; done

通常、それは期待どおりに機能しますが、最近、私のファイルリストで次のようになりました。

file_.doc
file_0.doc
file_ [2006_02_25] .doc
file_ [2016_06_16] .odt
file_ [2016_06_16] .pdf
file_ [16-6-2006] .doc
file_.pdf
file_ 4-4-2006.doc

出力は次のとおりです。

./file_.doc                                                                                                                                                 
./file_0.doc                                                                                                                                                
./file_0.doc
./file_[2016_06_16].odt
./file_[2016_06_16].pdf
./file_0.doc
./file_.pdf
./file_ 4-4-2006.doc

変数IFSを次のように変更した場合:

IFS=$(echo -en "\n");

次に、出力は(修正されます):

./file_.doc
./file_0.doc
./file_[2006_02_25].doc
./file_[2016_06_16].odt
./file_[2016_06_16].pdf
./file_[16-6-2006].doc
./file_.pdf
./file_ 4-4-2006.doc

'\b'neccesary であることを読み、 printfの代わりにechoを使用 という解決策を見つけました。

私の質問は次のとおりです。

1)これらの出力が異なる理由を説明してください。

2)上記のprintfを使用したソリューションは、echo -en "\n\b"の代わりになる可能性がありますか?

5
duqu

コマンド置換の出力は、Word分割(IFSを設定することで処理しました)、およびファイル名グロブの対象になります。 [abc]構文は、通常どおり「任意の文字abcに一致」であり、[2006_02_25].doc0.docに一致します。


Bash/ksh/zshでは、ダブルスターを使用して、ディレクトリツリー内のすべてのファイルを取得できます(現在のディレクトリだけでなく、再帰的に)。これにより、例と同じファイルが見つかります。

shopt -s globstar      # in Bash
# set -o globstar      # in ksh
for file in **/* ; do
    [[ -f $file ]] || continue     # check it's a regular file, like find -type f
    ...
done

もちろん、findis強力なので、条件がたくさんある場合は、それを使用する方が簡単な場合があります。その場合は、IFSの修正に加えて、set -fでファイル名のグロブを無効にする必要があります。

set -f
IFS=$'\n'
for file in $(find -type f -some -other -conditions) ; do
    ...
done

または、代わりにプロセス置換を使用してwhile readループを使用します。

while IFS= read -r file ; do 
    ...
done < <(find -type f -some -other -conditions)

(上記はfind ... | while ...に似ていますが、パイプラインの最後の部分がサブシェルで実行されるという問題を回避します。)

これらは両方とも、findの出力の区切り文字として使用されるため、ファイル名に改行が含まれていないことを前提としています。 (そして$(..)が最後の改行を食べるからです。)

少なくともBashには、ファイル名の改行を機能させる方法が実際にあります。 read区切り文字を空の文字列に設定すると、NULバイトを区切り文字として効果的に使用できます。そう:

while IFS= read -d '' -r file ; do 
    ...
done < <(find -type f -some -other -conditions -print0)

それは書くのが面倒になり始めていますが、入力を操作せずに機能させるには、readに対するこれらすべてのオプションが必要です。


IFS...については、IFS=$(echo -en "\n");を設定すると、IFSが空の文字列に設定され(コマンド置換によって末尾の改行が消費されるため)、結果としてno =分割。この場合の出力は、行ごとではなく、一度にfindのすべての出力を取得するため、正しいように見えます。これにより、ファイル名のグロブに関する問題も隠されます。これは、完全な複数行の文字列がどのファイル名とも一致せず、そのまま渡されるためです。

ループ値を出力する以外のことを行うと、違いがわかります。いくつかのセパレーターを追加してみてください。

IFS=$(echo -en "\n")       # same as IFS=
for FILE in $(find -type f); do echo "<$FILE>"; done

最も単純な標準シェル以外でIFSを改行に設定する最も簡単な方法は、IFS=$'\n'です。

4
ilkkachu

$( find ... )を実行しないでください。ファイル名の生成(グロブ)が呼び出され、一部のファイル名は他のファイル名と一致するグロブパターンとして解釈されます。たとえば、パターンfile_[2006_02_25].docfile_[16-6-2006].docfile_0.docと一致します。そのため、これら2つのパターンの代わりにこのファイル名が使用されます。

さらに、コマンド置換のfindコマンドがすべてのパス名を生成するまで、ループは反復を開始しません。これは、一般的な場合、かなりの量のメモリを消費する可能性があり、実際にはそうではありませんエレガント

代わりに、findを使用してください(IFSは変更しないでください)。

find . type -f -print

これらのファイルで他のことをしたい場合は、-execで行うことができます。

find . -type f -exec sh -c 'printf "Found the file %s\n" "$@"' sh {} +

現在のディレクトリ内のファイルを処理したいだけの場合は、単に

for name in *; do
    printf 'Found the name %s\n' "$name"
done

関連:

4
Kusalananda