공백이있는 파일 목록을 반복합니다.


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을 포함하는 단일 파일 경로를 분할합니다. 좋아, 그것들은 주변에 있으면 touch "$(printf "foo\nbar")"
안되지만

4
입력 (백 슬래시, 선행 및 후행 공백)의 해석을 방지하려면 IFS= while read -r f대신 사용하십시오.
mklement0

2
답변 은보다 안전한 조합 find과 while 루프를 보여줍니다 .
moi

5
명백한 점을 지적하는 것처럼 보이지만 거의 모든 경우 -exec에는 명시 적 루프보다 깨끗 find . -iname "foo*" -exec echo "File found: {}" \;합니다. 또한 많은 경우에 당신은 마지막 교체 할 수 있습니다 \;+하나의 명령 파일을 많이 넣어합니다.
naught101

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 ...)공백, 줄 바꿈 및 기타 문자가 포함 된 파일이 올바르게 처리된다는 것입니다. 때문에이 작품 find-print0용도 것 null(일명를 \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은 최소입니다. xargs파일을 한 줄로 인쇄하거나 단일 작업을 수행하는 것만으로도 버전이 양호하지만 여러 단계를 수행해야하는 경우 루프 버전이 더 쉽습니다.

편집 : 여기 에이 문제를 해결하기위한 다른 시도의 차이점에 대한 아이디어를 얻을 수있는 좋은 테스트 스크립트가 있습니다.

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

2
+1이지만 while IFS= read공백으로 시작하거나 끝나는 파일을 처리하려면 ... ...를 추가해야합니다 .
Gordon Davisson

1
프로세스 대체 솔루션에는 하나의 경고가 있습니다. 루프 내부에 프롬프트가 있거나 다른 방법으로 STDIN에서 읽는 경우 루프에 입력 한 내용으로 입력이 채워집니다. (아마도 이것이 답변에 추가되어야합니까?)
andsens

2
@uvsmtid :이 질문은 태그되었습니다. bash나는 bash 고유의 기능을 사용하여 안전하다고 느꼈습니다. 프로세스 대체는 다른 쉘로 이식 할 수 없습니다 (그 자체로는 그렇게 중요한 업데이트를받지 못할 것입니다).
sorpigal

2
IFS=$'\n'와 결합 for하면 줄 내부 단어 분리 를 방지 할 수 있지만 결과 줄이 계속 글 로빙 될 수 있으므로이 방법은 완전히 강력하지 않습니다 (먼저 글 로빙을 먼저 끄지 않는 한). read -d $'\0'작동 하는 동안 $'\0'NUL을 만드는 데 사용할 수 있음을 제안한다는 점에서 약간 오도 \0합니다. ANSI C 인용 문자열의 a 는 문자열을 효과적으로 종료 하므로 실제로 는와-d $'\0' 동일합니다 -d ''.
mklement0

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
참고로, 이것은 명령을 실행하려는 경우에만 작동합니다. 내장 쉘은 이런 방식으로 작동하지 않습니다.
Alex

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

참조하십시오 man xargs.


6

로 다른 유형의 필터링을 수행하지 않기 때문에 4.0 find에서 다음을 사용할 수 있습니다 bash.

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

**/전체 패턴이 일치하므로, 0 개 이상의 디렉토리에 일치합니다 foo*현재 디렉토리 또는 하위 디렉토리에.


3

나는 for 루프와 배열 반복을 정말로 좋아하므로 믹스 에이 답변을 추가 할 것이라고 생각합니다 ...

나는 marchelbling의 바보 같은 파일 예제를 좋아했습니다. :)

$ 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: {}" \;

여기 {}에서 찾은 파일을 나타내며이를 줄 바꿈 ""하면 결과 쉘 명령이 파일 이름의 공백을 처리 할 수 ​​있습니다.

대부분의 경우 마지막 \;명령 (새 명령을 시작)을로 대체 \+하여 한 명령에 여러 파일을 넣을 수 있습니다 (한 번에 모든 파일을 한꺼번에 다룰 필요는 없습니다 man find. 자세한 내용 참조).


0

경우에 따라 파일 목록을 복사하거나 이동해야하는 경우 해당 목록을 awk로 파이프 할 수도 있습니다. 필드 주변에서
중요합니다 (즉, 파일 한 줄, 하나의 행 목록 = 하나의 파일).\"" "\"$0

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

0

Ok-스택 오버플로에 대한 첫 번째 게시물!

이것에 대한 나의 문제는 항상 csh에 있었지만 내가 제시 할 솔루션을 강타하지는 않지만 두 가지 모두에서 작동합니다. 문제는 쉘의 "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- 위의 답변이 문제를 변경하거나 논문을 제출하지 않고 pb의 직접 해상도를 만족시킬 수 없음 :-)
Vince B
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.