スペースを含むファイルのリストを反復処理します


201

ファイルのリストを反復処理したい。このリストはfindコマンドの結果なので、私は思いつきました:

getlist() {
  for f in $(find . -iname "foo*")
  do
    echo "File found: $f"
    # do something useful
  done
}

ファイルの名前にスペースが含まれている場合を除き、問題ありません。

$ ls
foo_bar_baz.txt
foo bar baz.txt

$ getlist
File found: foo_bar_baz.txt
File found: foo
File found: bar
File found: baz.txt

スペースの分割を回避するにはどうすればよいですか?


回答:


253

単語ベースの反復を行ベースの反復に置き換えることができます。

find . -iname "foo*" | while read f
do
    # ... loop body
done

31
これは非常にきれいです。そして、forループと組み合わせてIFSを変更するよりも気分が良い
Derrick

15
これにより、\ nを含む単一のファイルパスが分割されます。OK、それらは存在すべきではありませんが、作成することができます:touch "$(printf "foo\nbar")"
Ollie Saunders

4
入力の解釈(バックスラッシュ、先頭と末尾の空白)を防ぐには、IFS= while read -r f代わりにを使用します。
mklement0

2
この回答は、findwhileループのより安全な組み合わせを示しています。
moi

5
明白なことを指摘しているように見えますが、ほとんどすべての単純なケースで-execは、明示的なループよりもクリーンになりますfind . -iname "foo*" -exec echo "File found: {}" \;。さらに、多くの場合、あなたはその最後置き換えることができます\;+1コマンドでファイルの多くを置くことを。
naught101 '09 / 09/27

152

これを実行するには、いくつかの実行可能な方法があります。

元のバージョンに固執したい場合は、次の方法で行うことができます。

getlist() {
        IFS=$'\n'
        for file in $(find . -iname 'foo*') ; do
                printf 'File found: %s\n' "$file"
        done
}

これは、ファイル名にリテラルの改行が含まれている場合でも失敗しますが、スペースによって改行されることはありません。

ただし、IFSをいじる必要はありません。これがこれを行うための私の好ましい方法です:

getlist() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: %s\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

もし発見した場合< <(command)について、あなたは読むべき不慣れな構文プロセス置換を。これに対する利点はfor file in $(find ...)、スペース、改行、その他の文字を含むファイルが正しく処理されることです。これは、findwith -print0null(別名\0)を各ファイル名のターミネーターとして使用し、改行とは異なり、nullはファイル名の正当な文字ではないため機能します。

ほぼ同等のバージョンに対するこれの利点

getlist() {
        find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
                printf 'File found: %s\n' "$file"
        done
}

それは、whileループの本体での変数割り当てが保持されるということですか。つまり、while上記のようにパイプすると、の本体はwhileサブシェルにあるため、希望どおりにならない可能性があります。

プロセス置換バージョンの利点find ... -print0 | xargs -0はごくわずかです。1 xargs行を印刷するか、ファイルに対して1つの操作を実行するだけでよい場合はバージョンで問題ありませんが、複数のステップを実行する必要がある場合は、ループバージョンの方が簡単です。

編集:これは素晴らしいテストスクリプトですので、この問題を解決するさまざまな試みの違いを理解することができます

#!/usr/bin/env bash

dir=/tmp/getlist.test/
mkdir -p "$dir"
cd "$dir"

touch       'file not starting foo' foo foobar barfoo 'foo with spaces'\
    'foo with'$'\n'newline 'foo with trailing whitespace      '

# while with process substitution, null terminated, empty IFS
getlist0() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# while with process substitution, null terminated, default IFS
getlist1() {
    while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# pipe to while, newline terminated
getlist2() {
    find . -iname 'foo*' | while read -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# pipe to while, null terminated
getlist3() {
    find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, default IFS
getlist4() {
    for file in "$(find . -iname 'foo*')" ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, newline IFS
getlist5() {
    IFS=$'\n'
    for file in $(find . -iname 'foo*') ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}


# see how they run
for n in {0..5} ; do
    printf '\n\ngetlist%d:\n' $n
    eval getlist$n
done

rm -rf "$dir"

1
あなたの答えを受け入れました:最も完全で興味深い-私は$IFS< <(cmd)構文については知りませんでした。なぜ、まだ一つのことは、私には曖昧なまま$$'\0'?どうもありがとう。
gregseth 2011

2
+1、ただしwhile IFS= read...を追加して、空白で開始または終了するファイルを処理する必要があります。
Gordon Davisson、2011

1
プロセス置換ソリューションには1つの注意点があります。ループ内にプロンプ​​トがある場合(または他の方法でSTDINから読み取っている場合)、入力はループに入力したもので埋められます。(おそらくこれを回答に追加する必要がありますか?)
andsens 12/12/13

2
@uvsmtid:この質問にはタグが付けられたbashので、bash固有の機能を使用しても安全だと感じました。プロセスの置換は他のシェルに移植できません(sh自体は、このような重要な更新を受け取ることはほとんどありません)。
sorpigal 2015年

2
と組み合わせるIFS=$'\n'for、行内部の単語分割が防止されますが、結果の行はグロビングの対象になります。そのため、このアプローチは完全に堅牢ではありません(最初にグロビングをオフにしない限り)。一方でread -d $'\0'作品それはあなたが使用できることを示唆しているという点で、それは少し誤解を招くされ$'\0'NULsを作成するために-あなたがすることはできません。\0ANSI C-引用符で囲まれた文字列は、効果的に終了ように、文字列を-d $'\0'効果的に同じです-d ''
mklement0 2016

29

非常に単純な解決策もあります:bash globbingに依存する

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"
$ ls
stupid   file 3  stupid file1     stupid file2
$ for file in *; do echo "file: '${file}'"; done
file: 'stupid   file 3'
file: 'stupid file1'
file: 'stupid file2'

この動作がデフォルトの動作であるかどうかはわかりませんが、私のショップには特別な設定が表示されないので、「安全」(osxとubuntuでテスト済み)であると言います。


13
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"

6
補足として、これはコマンドを実行する場合にのみ機能します。組み込みシェルはこの方法では機能しません。
アレックス

11
find . -name "fo*" -print0 | xargs -0 ls -l

を参照してくださいman xargs


6

で他のタイプのフィルタリングを行っていないためfindbash4.0以降では以下を使用できます。

shopt -s globstar
getlist() {
    for f in **/foo*
    do
        echo "File found: $f"
        # do something useful
    done
}

**/完全なパターンが一致するので、ゼロ個以上のディレクトリーに一致します。foo*現在のディレクトリまたは任意のサブディレクトリに。


3

私はforループと配列反復が本当に好きなので、この答えをミックスに追加すると思います...

私はマーチェリングの愚かなファイルの例も好きでした。:)

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"

テストディレクトリ内:

readarray -t arr <<< "`ls -A1`"

これにより、各ファイルリスト行が、arr末尾の改行が削除された名前のbash配列に追加されます。

これらのファイルにより良い名前を付けたいとしましょう...

for i in ${!arr[@]}
do 
    newname=`echo "${arr[$i]}" | sed 's/stupid/smarter/; s/  */_/g'`; 
    mv "${arr[$i]}" "$newname"
done

$ {!arr [@]}は0 1 2に展開されるため、「$ {arr [$ i]}」は配列のi 番目の要素です。変数を囲む引用符は、スペースを保持するために重要です。

その結果、3つの名前が変更されたファイルになります。

$ ls -1
smarter_file1
smarter_file2
smarter_file_3

2

find-exec検索結果をループして任意のコマンドを実行する引数があります。例えば:

find . -iname "foo*" -exec echo "File found: {}" \;

ここで{}は、見つかったファイルを表し、それをラップする""ことで、結果のシェルコマンドがファイル名のスペースを処理できるようになります。

多くの場合、最後の\;(新しいコマンドを開始する)をで置き換えることができ\+ます。これにより、1つのコマンドに複数のファイルが配置されます(ただし、一度にすべてのファイルを必ずしもそうとは限りませんman find。詳細については、を参照してください)。


0

場合によっては、ここでファイルのリストをコピーまたは移動する必要があるだけであれば、そのリストをawkにパイプすることもできます。フィールド
\"" "\"周りを重要視します$0(簡単に言うと、ファイルの1行リスト= 1ファイル)。

find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'

0

OK-スタックオーバーフローに関する私の最初の投稿!

これに関する私の問題は常にcshにありましたが、私が提示する解決策はbshではなく、両方で機能します。問題は、 "ls"リターンのシェルの解釈にあります。*ワイルドカードのシェル展開を使用するだけで、問題から「ls」を削除できます。ただし、現在の(または指定したフォルダー)にファイルがない場合は、「一致なし」エラーが発生します。これを回避するには、単にしたがって、ドットファイルを含めるための拡張:* .*-これにより、ファイル以降の結果が常に得られます そして..常に存在します。cshでは、この構成を使用できます...

foreach file (* .*)
   echo $file
end

標準のドットファイルを除外したい場合、それは十分簡単です...

foreach file (* .*)
   if ("$file" == .) continue
   if ("file" == ..) continue
   echo $file
end

このスレッドの最初の投稿のコードは次のように記述されます:-

getlist() {
  for f in $(* .*)
  do
    echo "File found: $f"
    # do something useful
  done
}

お役に立てれば!


0

仕事のための別の解決策...

目標は:

  • ディレクトリでファイル名を再帰的に選択/フィルタリングする
  • それぞれの名前を処理します(パスのどのスペースでも...)
#!/bin/bash  -e
## @Trick in order handle File with space in their path...
OLD_IFS=${IFS}
IFS=$'\n'
files=($(find ${INPUT_DIR} -type f -name "*.md"))
for filename in ${files[*]}
do
      # do your stuff
      #  ....
done
IFS=${OLD_IFS}


建設的な発言のためのThx、しかし:1-これは実際の問題です、2-シェルは時間の中で進化したかもしれません... 3-上記の答えはどれも、問題を変更したり解体したりせずに、鉛の直接的な解決を満たすことはできません:-)
Vince B
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.