Bash에서 vs. 찾기


28

파일을 반복 할 때 두 가지 방법이 있습니다.

  1. - for루프를 사용하십시오 :

    for f in *; do
        echo "$f"
    done
  2. 사용 find:

    find * -prune | while read f; do 
        echo "$f"
    done

이 두 루프가 동일한 파일 목록을 찾을 것이라고 가정하면 성능 과 처리 에서 두 옵션의 차이점은 무엇 입니까?


1
왜? find찾은 파일을 열지 않습니다. 많은 파일과 관련하여 여기서 당신을 물고 볼 수있는 유일한 것은 ARG_MAX 입니다.
kojiro

1
read f읽을 때 파일 이름을 맹 글링 할 것임을 알려주는 답변과 설명을 참조하십시오 (예 : 공백 문자가있는 이름). 또한 find * -prune단순히 ls -1예 라고 말하는 매우 복잡한 방법 인 것 같습니다 .
Ian D. Allen

4
두 개의 루프가 동일한 파일 세트를 찾을 것이라고 가정 하지 마십시오 . 대부분의 경우 그렇지 않습니다. 또한 find .그렇지 않아야 find *합니다.
Alexis

1
@terdon 예, 파싱 ls -l은 나쁜 생각입니다. 그러나 파싱 ls -1(이 1아닙니다 l)은 파싱보다 나쁘지 않습니다 find * -prune. 둘 다 이름에 줄 바꿈이있는 파일에서 실패합니다.
Ian D. Allen

5
필자는 문제의 스크립트 수명 동안의 총 성능 차이보다이 질문과 답변을 읽는 데 더 많은 시간을 보냈다고 생각합니다.
mpez0

답변:


9

1.

첫번째:

for f in *; do
  echo "$f"
done

라는 파일에 실패 -n, -e그리고 같은 변종 -nene파일 이름은 백 슬래시를 포함하여, 일부 bash는 배포와 함께.

두번째:

find * -prune | while read f; do 
  echo "$f"
done

(라는 파일보다 경우에 실패 !, -H, -name, (... 시작이나 끝 공백이나 개행 문자가 포함 된 파일 이름)

그것은 확장하는 쉘이며 *, find인수로받은 파일을 인쇄하는 것 외에는 아무것도하지 않습니다. 당신은뿐만 아니라 사용할 수도 printf '%s\n'로하는 대신 printf내장도 피할 것입니다 너무 많은 인수를 잠재적 인 오류가 발생했습니다.

2.

의 확장 *은 정렬되어 있으므로 정렬이 필요하지 않으면 조금 더 빨라질 수 있습니다. 에서 zsh:

for f (*(oN)) printf '%s\n' $f

또는 간단히 :

printf '%s\n' *(oN)

bash내가 말할 수있는 한 그에 상응하는 것이 없기 때문에에 의지해야 find합니다.

삼.

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(GNU / BSD -print0비표준 확장 사용).

여전히 find 명령을 생성하고 느린 while read루프를 사용하므로 for파일 목록이 크지 않으면 루프를 사용하는 것보다 느릴 것 입니다.

4.

또한 쉘 와일드 카드 확장과 달리 각 파일에 대해 시스템 호출을 find수행 lstat하므로 정렬되지 않은 것이이를 보완하지는 않습니다.

GNU / BSD find에서는 -maxdepth확장을 사용하여 피할 수 있습니다 lstat.

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

findstdio 출력 버퍼링을 제외하고 파일 이름을 찾 자마자 출력 하기 시작하므로 루프에서 수행하는 작업에 시간이 많이 걸리고 파일 이름 목록이 stdio 버퍼 이상인 경우 더 빠를 수 있습니다 (4 / 8 kB). 이 경우 루프 내 처리 find가 모든 파일 찾기를 마치기 전에 시작됩니다 . GNU 및 FreeBSD 시스템에서는 stdbuf더 빨리 발생 하도록 사용할 수 있습니다 (stdio 버퍼링 사용 안함).

5.

각 파일에 대해 명령을 실행하는 POSIX / standard / portable 방법 find-exec술어 를 사용하는 것입니다 .

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

그러나 echo쉘에는 내장 버전이 echo있지만 find새로운 프로세스를 생성하고 /bin/echo각 파일마다 실행해야 하므로 쉘에서 루핑을 수행하는 것보다 덜 효율적 입니다.

여러 명령을 실행해야하는 경우 다음을 수행 할 수 있습니다.

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

그러나 성공한 cmd2경우에만 실행됩니다 cmd1.

6.

각 파일에 대해 복잡한 명령을 실행하는 정식 방법은 다음을 사용하여 쉘을 호출하는 것입니다 -exec ... {} +.

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

그 시간, 우리 효율적인있는 방법입니다 다시 echo우리가 사용하고 이후 sh의 내장의 하나는 -exec +몇 가지로 버전 급부상 sh가능합니다.

7.

에서 200.000 파일과 디렉토리에 내 테스트 에서 ext4에 짧은 이름의 zsh하나 (제 2)가 첫 번째 간단한에 이어, 지금까지 가장 빠른 것입니다 for i in *루프 (평소와 같이, 비록 bash많은 느린 다른 조개에 비해 그입니다).


!find 명령에서 수행하는 작업은 무엇 입니까?
rubo77

@ rubo77 !은 부정입니다. ! -name . -prune more...할 것입니다 -prune(그리고 more...이후 -prune항상 true를 반환) 모든 파일하지만 위해 .. 따라서의 more...모든 파일을 처리 .하지만의 .하위 디렉토리를 제외 하거나 제외 하지는 않습니다 .. 따라서 GNU와 동등한 표준 -mindepth 1 -maxdepth 1입니다.
Stéphane Chazelas

18

2259 항목이있는 디렉토리에서 이것을 시도하고 time명령을 사용했습니다 .

time for f in *; do echo "$f"; done파일을 뺀 결과 는 다음과 같습니다.

real    0m0.062s
user    0m0.036s
sys     0m0.012s

time find * -prune | while read f; do echo "$f"; done파일을 뺀 결과 는 다음과 같습니다.

real    0m0.131s
user    0m0.056s
sys     0m0.060s

캐시 누락을 제거하기 위해 각 명령을 여러 번 실행했습니다. 이것에 유지 제안 bash빨리 사용하는 것보다 (... I에 대해) find와 (출력을 배관 bash)

완벽 find을 기하기 위해 예제에서 파이프 가 완전히 중복되었으므로 파이프를에서 삭제 했습니다. 의 결과 find * -prune는 다음과 같습니다.

real    0m0.053s
user    0m0.016s
sys     0m0.024s

또한 time echo *(출력은 줄 바꿈으로 구분되지 않습니다) :

real    0m0.009s
user    0m0.008s
sys     0m0.000s

이 시점에서 나는 그 이유 echo *가 더 빠르다고 생각합니다. 줄 바꿈이 너무 많지 않아서 출력이 많이 스크롤되지 않기 때문입니다. 테스트합시다 ...

time find * -prune | while read f; do echo "$f"; done > /dev/null

수율 :

real    0m0.109s
user    0m0.076s
sys     0m0.032s

반면 time find * -prune > /dev/null수익률 :

real    0m0.027s
user    0m0.008s
sys     0m0.012s

그리고 time for f in *; do echo "$f"; done > /dev/null수확량 :

real    0m0.040s
user    0m0.036s
sys     0m0.004s

그리고 마지막으로 : time echo * > /dev/null수확량 :

real    0m0.011s
user    0m0.012s
sys     0m0.000s

변형의 일부는 임의의 요인으로 설명 할 수 있지만 다음과 같이 분명해 보입니다.

  • 출력이 느리다
  • 배관 비용이 약간
  • for f in *; do ...find * -prune자체 보다 속도가 느리지 만 파이프가 포함 된 위의 구조에서는 속도가 더 빠릅니다.

또한 두 가지 접근 방식은 공백이있는 이름을 잘 처리하는 것으로 보입니다.

편집하다:

find . -maxdepth 1 > /dev/null대 타이밍 find * -prune > /dev/null:

time find . -maxdepth 1 > /dev/null:

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null:

real    0m0.031s
user    0m0.020s
sys     0m0.008s

따라서 추가 결론 :

  • find * -prunefind . -maxdepth 1전자 보다 속도가 느리면 쉘은 glob를 처리 한 다음에 (대형) 명령 줄을 작성 find합니다. NB : find . -prune그냥 반환합니다 ..

추가 테스트 : time find . -maxdepth 1 -exec echo {} \; >/dev/null:

real    0m3.389s
user    0m0.040s
sys     0m0.412s

결론:

  • 지금까지 가장 느린 방법입니다. 이 접근법이 제안 된 답변에 대한 의견에서 지적했듯이 각 인수는 쉘을 생성합니다.

어떤 파이프가 중복되어 있습니까? 파이프없이 사용한 라인을 보여줄 수 있습니까?
rubo77

2
@ rubo77 find * -prune | while read f; do echo "$f"; done에는 이중화 파이프가 있습니다. 파이프가 수행하는 모든 작업은 자체 출력을 정확하게 출력하는 것 find입니다. 파이프가 없으면 간단합니다 find * -prune . 파이프의 반대쪽에있는 것이 stdin을 stdout (대부분)에 복사하기 때문에 파이프는 특히 중복됩니다. 비싼 노업입니다. 다시 출력하는 것 외에 find의 출력으로 물건을 만들고 싶다면 그것은 다릅니다.
Phil

아마도 시간이 많이 걸리는 것은 *입니다. 으로 BitsOfNix가 말했다 : 나는 아직도 강하게 사용하지 않는 것이 좋습니다 *.위해 find대신.
rubo77

@ rubo77은 그렇게 보입니다. 나는 그것을 간과했다고 생각합니다. 내 시스템에 대한 결과를 추가했습니다. 나는 디렉토리 엔트리를 그대로 읽을 find . -prune것이기 때문에 find쉘 이 더 빠르다고 생각 하지만 쉘은 glob와 일치 할 가능성이 있고 (을 위해 최적화 할 수 있음 *) 큰 명령 줄을 빌드합니다 find.
Phil

1
find . -prune.내 시스템 에서만 인쇄 합니다. 거의 작동하지 않습니다. find * -prune현재 디렉토리의 모든 이름을 표시 하는 것과 전혀 다릅니다 . 맨손 read f은 맨 앞에 공백이있는 파일 이름을 엉망으로 만듭니다.
Ian D. Allen

10

찾기를 다음과 같이 바꾸더라도 찾기와 함께 확실히 갈 것입니다.

find . -maxdepth 1 -exec echo {} \;

현명한 성능 find은 물론 필요에 따라 훨씬 빠릅니다. 현재 가지고 for있는 것은 현재 디렉토리의 파일 / 디렉토리 만 표시하지만 디렉토리 내용은 표시하지 않습니다. find를 사용하면 하위 디렉토리의 내용도 표시됩니다.

나는 당신 for*의지가 먼저 확장되어야 하기 때문에 찾기가 더 낫다고 말하고 파일의 양이 많은 디렉토리를 가지고 있다면 오류 인수 목록이 너무 길 것을 두려워합니다 . 동일find *

예를 들어, 현재 사용중인 시스템 중 하나에는 2 백만 개가 넘는 파일이있는 두 개의 디렉토리가 있습니다 (각 <100k).

find *
-bash: /usr/bin/find: Argument list too long

내가 추가 한 -prune두 예제가 더 비슷하게 만들 수 있습니다. 그리고 파이프를 선호하므로 루프에 더 많은 명령을 적용하는 것이 더 쉽습니다
rubo77


하드 한계를 변경하는 것은 내 POV의 적절한 해결 방법이 아닙니다. 특히 2 백만 개가 넘는 파일에 대해 이야기 할 때. 질문에서 벗어나지 않으면 간단한 수준의 경우 하나의 수준 디렉토리로 더 빠르지 만 파일 / 디렉토리 구조를 변경하면 마이그레이션하기가 더 어려워집니다. 찾기 옵션을 사용하는 동안 방대한 양의 옵션을 사용하면 더 잘 준비 할 수 있습니다. 여전히 나는 여전히 *와를 사용하지 말 것을 강력히 권합니다. 대신 찾으십시오. 하드
리미트

4
파일 당 하나의 에코 프로세스를 생성하고 (쉘 for 루프에서는 추가 프로세스를 수행하지 않고 사용되는 에코 내장) 디렉토리로 내려가므로 속도 가 훨씬 느려집니다 . 또한 도트 파일이 포함됩니다.
Stéphane Chazelas

당신은 옳습니다, maxdepth 1을 추가하여 현재 레벨에만 고정되도록했습니다.
BitsOfNix

7
find * -prune | while read f; do 
    echo "$f"
done

쓸모없는 사용 find-당신이 말하는 것은 효과적으로 "디렉토리 ( *) 의 각 파일에 대해 어떤 파일도 찾지 못합니다. 또한 여러 가지 이유로 안전하지 않습니다 :

  • 경로의 백 슬래시는 -r옵션 없이 특별히 처리 됩니다 read. 이것은 for루프 문제가 아닙니다 .
  • 경로의 줄 바꿈은 루프 내부의 사소한 기능을 손상시킵니다. 이것은 for루프 문제가 아닙니다 .

로 파일 이름을 처리하는 find것은 어렵 기 때문에 for가능할 때마다 루프 옵션을 사용해야합니다 . 또한 같은 외부 프로그램을 실행하면 find일반적으로 같은 내부 루프 명령을 실행하는 것보다 느립니다 for.


@ I0b0 find -path './*'-prune 또는 find -path './[^.]*'-숨겨진 파일과 디렉토리를 피하기 위해 -prune은 더 나은 구문으로-전체 형식 : find -path ' ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs

1
어느 쪽 find-print0도는 xargs' -0POSIX 호환되지 않습니다, 당신은에서 임의의 명령을 넣을 수 없습니다 sh -c ' ... '꽤 그렇게 간단하지 그래서, (작은 따옴표 작은 따옴표 안에 탈출 할 수 없음).
l0b0

4

그러나 우리는 성능 문제에 대한 빨판입니다! 이 실험 요청은별로 유효하지 않은 가정을 적어도 두 번 만듭니다.

A. 동일한 파일을 찾는다고 가정합니다.

글쎄, 그들은 처음에 같은 파일을 찾을 것입니다 . 왜냐하면 둘 다 같은 글 로프를 반복하기 때문 *입니다. 그러나 find * -prune | while read f몇 가지 결함으로 인해 예상되는 모든 파일을 찾을 수는 없습니다.

  1. POSIX find는 둘 이상의 경로 인수를 허용하지 않습니다. 대부분의 find구현은 여전히 ​​그렇지만 당신은 그것에 의존해서는 안됩니다.
  2. find *때 때 깰 수 있습니다 ARG_MAX. 내장 for f in *이 아닌에 ARG_MAX적용 되기 때문 exec입니다.
  3. while read f공백으로 시작하고 끝나는 파일 이름으로 중단 될 수 있습니다. while read기본 매개 변수로 이를 극복 할 수는 REPLY있지만 줄 바꿈이있는 파일 이름에 대해서는 여전히 도움이되지 않습니다.

B echo.. 아무도 파일 이름을 에코하기 위해이 작업을 수행하지 않습니다. 원하는 경우 다음 중 하나를 수행하십시오.

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

while루프 의 파이프 는 루프가 끝날 때 닫히는 암시 적 서브 쉘을 생성하는데, 이는 직관적이지 않을 수 있습니다.

질문에 대답하기 위해 여기에 184 개의 파일과 디렉토리가있는 내 디렉토리의 결과가 있습니다.

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s

나는 문을 서브 쉘을 While 루프의 급부상을 동의하지 않는 - 최악의 경우에, 새 스레드 : 다음은 전후 보여주기 위해 노력하고, 가난한 형식에 대한 사과$ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20811 pts/1 R+ 0:00 grep bash $ while true; do while true; do while true; do while true; do while true; do sleep 100; done; done; done; done; done ^Z [1]+ Stopped sleep 100 $ bg [1]+ sleep 100 & $ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20924 pts/1 S+ 0:00 grep bash

기술적으로 내가 틀린 것 : 파이프 는 while 루프가 아닌 암시 적 서브 쉘을 유발합니다. 편집하겠습니다.
kojiro

2

find **경로가 아닌 술어처럼 보이는 토큰을 생성하면 올바르게 작동하지 않습니다 .

옵션의 끝을 나타내며 find의 옵션이 경로 앞에 --오므로 일반적인 인수를 사용하여이 문제를 해결할 수 없습니다 --.

이 문제를 해결하려면 find ./*대신 사용할 수 있습니다 . 그러나 정확히 같은 문자열을 생성하지는 않습니다 for x in *.

find ./* -prune | while read f ..사실의 스캔 기능을 사용하지 않습니다 find. ./*실제로 디렉토리를 탐색하고 이름을 생성하는 것은 글 로빙 구문 입니다. 그런 다음 find프로그램은 stat해당 이름 각각에 대해 최소한 확인 을 수행해야 합니다. 프로그램을 시작하고 이러한 파일에 액세스 한 다음 I / O를 수행하여 출력을 읽는 오버 헤드가 있습니다.

그것이보다 효율적일 수있는 방법을 상상하기는 어렵습니다 for x in ./* ....


1

우선 forBash에 내장 된 쉘 키워드 find는 별개의 실행 파일입니다.

$ type -a for
for is a shell keyword

$ type -a find
find is /usr/bin/find

for이 확장 할 때 루프 만 발견 된 어떤 디렉토리에 재귀하지 않습니다는 globstar 문자에서 파일을 찾을 수 있습니다.

반면에 찾기에는 globstar가 확장 한 목록이 제공되지만이 확장 목록 아래의 모든 파일과 디렉토리를 재귀 적으로 찾아서 각각 while루프로 파이프 합니다.

이 두 가지 접근 방식은 공백이 포함 된 경로 나 파일 이름을 처리하지 않는다는 점에서 위험한 것으로 간주 될 수 있습니다.

이것이이 두 가지 접근법에 대해 언급 할 가치가있는 모든 것입니다.


find 명령에 -prune을 추가 했으므로 더 비슷합니다.
rubo77

0

find로 반환 된 모든 파일을 단일 명령으로 처리 할 수 ​​있다면 (위의 에코 예제에는 해당되지 않음) xargs를 사용할 수 있습니다.

find * |xargs some-command

0

몇 년 동안 나는 이것을 사용했다 :-

find . -name 'filename'|xargs grep 'pattern'|more

grep이 찾을 수있는 패턴이 포함 된 특정 파일 (예 : * .txt)을 찾아 화면에 스크롤되지 않도록 더 파이프로 연결합니다. 때로는 >> 파이프를 사용하여 나중에 볼 수있는 다른 파일에 결과를 씁니다.

결과 샘플은 다음과 같습니다.

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <448E53556A3F442ABC58203D6281923E@hypermax>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2011-April.txt:URL: http://mylist.net/private/rodgersorganusers/attachments/20110420/3f
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.