find로 반환 된 파일 이름을 반복하는 방법은 무엇입니까?


223
x=$(find . -name "*.txt")
echo $x

Bash 쉘에서 위의 코드를 실행하면 목록이 아닌 공백으로 구분 된 여러 파일 이름이 포함 된 문자열이 나타납니다.

물론 목록을 얻기 위해 공백으로 더 분리 할 수는 있지만 더 좋은 방법이 있다고 확신합니다.

그렇다면 find명령 결과를 반복하는 가장 좋은 방법은 무엇 입니까?


3
파일 이름을 반복하는 가장 좋은 방법은 실제로 수행하려는 작업에 따라 다르지만 파일 이름에 공백이없는 파일을 보장 할 수 없다면 이것이 최선의 방법은 아닙니다. 그렇다면 파일을 반복하면서 무엇을하고 싶습니까?
Kevin

1
현상금과 관련하여 : 여기에서 주요 아이디어는 가능한 모든 경우 (새로운 줄이있는 파일 이름, 문제가있는 문자 ...)를 포괄하는 정식 답변을 얻는 것입니다. 그런 다음이 파일 이름을 사용하여 몇 가지 작업을 수행하는 것입니다 (다른 명령을 호출하고 이름을 바꾸십시오 ...). 감사!
fedorqui 'SO 중지 피해'

파일 또는 폴더 이름에 ".txt"와 공백 및 다른 문자열 (예 : "something.txt something"또는 "something.txt")이 포함될 수 있음을 잊지 마십시오
Yahya Yahyaoui

var가 아닌 배열을 사용하십시오. x=( $(find . -name "*.txt") ); echo "${x[@]}"그러면 반복 할 수 있습니다for item in "${x[@]}"; { echo "$item"; }
Ivan

답변:


394

TL; DR : 가장 정확한 답변을 위해 여기에 온다면 내 개인적인 취향을 원할 것 find . -name '*.txt' -exec process {} \;입니다 (이 글의 하단 참조). 시간이 있다면 나머지 부분을 읽고 여러 가지 다른 방법과 대부분의 문제를 확인하십시오.


전체 답변 :

가장 좋은 방법은 수행하려는 작업에 따라 다르지만 몇 가지 옵션이 있습니다. 하위 트리의 파일이나 폴더에 이름에 공백이없는 경우 파일을 반복 할 수 있습니다.

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done

조금 더 나은 임시 변수를 잘라내십시오 x.

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done

당신이 할 수있을 때 glob하는 것이 훨씬 좋습니다. 현재 디렉토리의 파일에 대한 공백 안전 :

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

globstar옵션 을 활성화하면 이 디렉토리와 모든 하위 디렉토리에서 일치하는 모든 파일을 가져올 수 있습니다.

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done

예를 들어 파일 이름이 이미 파일에있는 경우 read다음 을 사용해야합니다 .

# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

readfind구분 기호를 적절하게 설정하여 다음 과 같이 안전하게 사용할 수 있습니다 .

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process "$line"
    done

보다 복잡한 검색의 경우 옵션 또는 다음과 find함께을 사용하는 것이 -exec좋습니다 -print0 | xargs -0.

# execute `process` once for each file
find . -name \*.txt -exec process {} \;

# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +

# using xargs*
find . -name \*.txt -print0 | xargs -0 process

# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument

find또한 -execdir대신을 사용하여 명령을 실행하기 전에 각 파일의 디렉토리에 CD를 넣을 -exec수 있으며 -ok대신 -exec(또는 -okdir대신 )을 사용하여 대화식 (각 파일에 대해 명령을 실행하기 전에 프롬프트)으로 만들 수 있습니다 -execdir.

* : 기술적으로 findand xargs(기본적으로)는 모든 파일을 처리하는 데 걸리는 횟수만큼 명령 줄에 입력 할 수있는 인수 수만큼 명령을 실행합니다. 실제로 파일 수가 매우 많지 않은 한 중요하지 않으며 길이를 초과하지만 동일한 명령 줄에 모두 필요한 경우 SOL 은 다른 방법을 찾습니다.


4
의 경우에 것을주의 그것의 가치 done < filename와 표준 입력은 더 이상 사용할 수없는 파이프 (루프 내부에 더 이상 대화 형 물건 →) 다음과 같은 하나,하지만이 필요한 경우에 하나가 사용할 수있는 3<대신 <하고 추가 <&3또는 -u3read부분은 기본적으로 별도의 파일 디스크립터를 사용하여. 또한, 나는 생각 read -d ''과 동일 read -d $'\0'하지만 지금은 그 어떠한 공식 문서를 찾을 수 없습니다.
phk

1
* .txt에서 i의 경우; 일치하는 파일이 없으면 작동하지 않습니다. 하나의 xtra 테스트 (예 : [[-e $ i]])
Michael Brux

2
나는이 부분을 잃어 버렸습니다. -exec process {} \;제 생각에는 그것은 완전히 다른 질문입니다. 그것은 무엇을 의미하며 어떻게 조작합니까? 좋은 Q / A 또는 문서는 어디에 있습니까? 그 위에?
Alex Hall

1
@AlexHall 항상 맨 페이지 ( man find)를 볼 수 있습니다 . 이 경우 다음 명령을 실행하도록 -exec지시 find하고 ;(또는 +)로 종료합니다 . 여기서 {}처리중인 파일의 이름 (또는 +사용 된 경우 해당 조건에 해당하는 모든 파일) 으로 대체됩니다 .
케빈

3
@phk가보다 -d ''낫다 -d $'\0'. 후자는 더 길뿐만 아니라 널 바이트를 포함하는 인수를 전달할 수 있지만 제안 할 수는 없습니다. 첫 번째 널 바이트는 문자열의 끝을 표시합니다. 배쉬 $'a\0bc'과 동일 a$'\0'동일하다 $'\0abc'하거나 빈 문자열 ''. help read" delim의 첫 번째 문자는 입력을 종료하는 데 사용됩니다 "라고 말하므로 ''구분자로 사용하는 것은 약간의 해킹입니다. 빈 문자열의 첫 번째 문자는 널 바이트로, 항상 문자열의 끝을 표시합니다 (명시 적으로 쓰지 않더라도).
Socowi

114

무엇을하든 루프를 사용하지 마십시오for .

# Don't do this
for file in $(find . -name "*.txt")
do
    code using "$file"
done

세 가지 이유 :

  • for 루프가 시작 find되려면 완료까지 실행해야합니다.
  • 파일 이름에 공백 (공백, 탭 또는 줄 바꿈 포함)이 있으면 두 개의 별도 이름으로 처리됩니다.
  • 현재는 아니지만, 명령 행 버퍼를 오버런 할 수 있습니다. 명령 행 버퍼가 32KB를 보유하고 for루프가 40KB의 텍스트를 리턴 한다고 가정하십시오 . 마지막 8KB는 for루프에서 즉시 삭제되며 결코 알 수 없습니다.

항상 while read구문을 사용하십시오 .

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    code using "$file"
done

find명령이 실행 되는 동안 루프 가 실행됩니다. 또한이 명령은 파일 이름에 공백이있는 경우에도 작동합니다. 또한 명령 줄 버퍼가 오버플로되지 않습니다.

-print0파일 분리기 대신 줄 바꿈으로 NULL을 사용하고는 -d $'\0'읽는 동안 분리로 NULL을 사용합니다.


3
파일 이름의 줄 바꿈에는 작동하지 않습니다. -exec대신 find를 사용하십시오 .
사용자 알 수 없음

2
@userunknown-당신이 맞습니다. -exec쉘을 전혀 사용하지 않기 때문에 가장 안전합니다. 그러나 파일 이름의 NL은 매우 드 rare니다. 파일 이름의 공백은 매우 일반적입니다. 요점은 for많은 포스터가 권장 하는 루프 를 사용하지 않는 것 입니다.
David W.

1
@userunknown-여기 이 문제를 해결 했으므로 이제 새 줄, 탭 및 기타 공백이있는 파일을 처리합니다. 게시물의 요점은 for file $(find)관련 문제로 인해 OP를 사용하지 말라고 OP에 알리는 것 입니다.
David W.

4
-exec를 사용할 수 있다면 더 낫지 만 실제로 쉘에 주어진 이름이 필요할 때가 있습니다. 예를 들어 파일 확장자를 제거하려는 경우.
벤 Reser

5
당신은 사용해야 -r하는 옵션 read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
다이라 가산점

102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

참고 : bmargulies로 표시되는 이 방법 (두 번째) 방법은 파일 / 폴더 이름의 공백과 함께 사용하는 것이 안전합니다.

파일 / 폴더 이름에 개행 문자가 포함되도록하기 위해서는 다음 -execfind같은 조건을 사용해야 합니다.

find . -name '*.txt' -exec echo "{}" \;

{}발견 된 항목에 대한 자리 표시 자이며,이 \;종료하는 데 사용되는 -exec술어를.

그리고 완전성을 위해 또 다른 변형을 추가하겠습니다. 다목적 성을 위해 * nix 방법을 좋아해야합니다.

find . -name '*.txt' -print0|xargs -0 -n 1 echo

이것은 인쇄 된 항목을 \0파일 또는 폴더 이름의 파일 시스템에서 허용되지 않는 문자로 분리 하므로 모든베이스를 다루어야합니다. xargs하나씩 하나씩 집어 들고 ...


3
파일 이름에 줄 바꿈이 있으면 실패합니다.
사용자 알 수 없음

2
@user unknown : 당신이 옳습니다, 전혀 고려하지 않은 경우이며, 나는 매우 이국적이라고 생각합니다. 그러나 그에 따라 대답을 조정했습니다.
0xC0000022L

5
아마 가치가 있음을 지적 find -print0하고 xargs -0GNU 확장 및 휴대용하지 (POSIX) 인수 상표입니다. 그래도 시스템이있는 시스템에서 매우 유용합니다!
Toby Speight

1
백 슬래시가 포함 된 파일 이름 ( read -r수정) 또는 공백으로 끝나는 파일 이름 (수정) 도 실패합니다 IFS= read. 따라서 BashFAQ # 1 제안while IFS= read -r filename; do ...
찰스 더피

1
이것의 또 다른 문제 는 루프의 몸체가 동일한 쉘에서 실행되는 것처럼 보이지만 그렇지 않다는 exit것 입니다. 예를 들어 예상대로 작동하지 않고 루프 몸체에 설정된 변수를 루프 후에 사용할 수 없습니다.
EM0

17

파일 이름에는 공백과 제어 문자가 포함될 수 있습니다. bash에서 쉘 확장을위한 공백은 (기본) 구분 기호이며 x=$(find . -name "*.txt")질문 의 결과로 전혀 권장되지 않습니다. find가 공백이있는 파일 이름을 얻는 경우, 예 "the file.txt"를 들어 x루프에서 처리하는 경우 처리를 위해 2 개의 분리 된 문자열을 얻게됩니다 . IFS예를 들어 구분 기호 (bash 변수)를 로 변경하여이를 개선 할 수 \r\n있지만 파일 이름은 제어 문자를 포함 할 수 있으므로 (완전히) 안전한 방법은 아닙니다.

필자의 견해로는 파일 처리에 권장되는 (안전한) 두 가지 패턴이 있습니다.

1. 루프 및 파일 이름 확장에 사용 :

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

2. 읽기-읽기 및 프로세스 대체 사용

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

비고

패턴 1 :

  1. bash는 일치하는 파일이 없으면 검색 패턴 ( "* .txt")을 반환하므로 "파일이 존재하지 않으면 계속합니다"라는 추가 줄이 필요합니다. 참조 배쉬 설명서, 파일 이름 확장을
  2. nullglob이 추가 라인을 피하기 위해 쉘 옵션을 사용할 수 있습니다.
  3. " failglob쉘 옵션이 설정되어 있고 일치하는 것이 없으면 오류 메시지가 인쇄되고 명령이 실행되지 않습니다." (위의 Bash Manual에서)
  4. shell option globstar: "설정하면 파일 이름 확장 컨텍스트에 사용 된 '**'패턴은 모든 파일과 0 개 이상의 디렉토리 및 하위 디렉토리와 일치합니다. 패턴 뒤에 '/'가 있으면 디렉토리와 하위 디렉토리 만 일치합니다." 참조 , 배쉬는 수동 shopt 내부 기본 제공된을
  5. 파일 이름 확장을위한 다른 옵션 : extglob, nocaseglob, dotglob및 쉘 변수GLOBIGNORE

패턴 2 :

  1. 파일명은 공백, 탭, 공간 바꿈을 포함 할 수 있으며, ... 안전한 방법으로 처리 파일명에 find함께 -print0사용된다 : 파일명 모든 제어 문자로 인쇄 및 NUL 종료. 또한 볼 은 GNU findutils의 맨, 안전하지 않은 파일 이름 처리 , 안전한 파일 이름 처리 , 파일 이름에 이상한 문자가 . 이 주제에 대한 자세한 설명은 아래 David A. Wheeler를 참조하십시오.

  2. while 루프에서 찾기 결과를 처리 할 수있는 몇 가지 패턴이 있습니다. 다른 사람들 (kevin, David W.)은 파이프를 사용 하여이 작업을 수행하는 방법을 보여주었습니다.

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
    이 코드를 시도하면 작동하지 않는 것을 알 수 있습니다. files_found항상 "true"이고 코드는 항상 "파일을 찾을 수 없습니다"를 에코합니다. 이유는 다음과 같습니다. 파이프 라인의 각 명령은 별도의 하위 셸에서 실행되므로 루프 내에서 변경된 변수 (별도의 하위 셸)는 기본 셸 스크립트의 변수를 변경하지 않습니다. 그렇기 때문에 프로세스 대체를 "더 나은"보다 유용하고 일반적인 패턴으로 사용하는 것이 좋습니다. 파이프 라인에있는 루프에 변수를 설정하는 방법을
    참조하십시오 . 이 주제에 대한 자세한 논의를 위해 왜 사라지는가? (Greg의 Bash FAQ에서)

추가 참조 및 출처 :


8

(@Socowi의 탁월한 속도 향상을 포함하도록 업데이트)

$SHELL그것을 지원하는 어떤 것으로 (대시 / zsh / bash ...) :

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

끝난.


원래 답변 (더 짧지 만 느림) :

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;

1
당밀 속도는 느리지 만 (각 파일마다 쉘을 실행하기 때문에) 작동합니다. +1
dawg

1
대신 단일 파일에 가능한 많은 파일을 전달 \;하는 +데 사용할 수 있습니다 exec. 그런 다음 "$@"쉘 스크립트 내부를 사용 하여 이러한 모든 매개 변수를 처리하십시오.
Socowi

3
이 코드에는 버그가 있습니다. 루프에 첫 번째 결과가 없습니다. $@일반적으로 스크립트 이름 이므로 생략하기 때문입니다. 우리는 추가 할 필요가 dummy사이에 '하고 {}는 모든 경기가 루프에 의해 처리됩니다 보장, 스크립트 이름의 자리를 차지할 수 있습니다.
BCartolo

새로 만든 셸 외부에서 다른 변수가 필요한 경우 어떻게합니까?
Jodo

OTHERVAR=foo find . -na.....$OTHERVAR새로 만든 셸 내 에서 액세스 할 수 있어야합니다 .
user569825

6
# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one

3
for x in $(find ...)공백이있는 파일 이름은 끊어집니다. 와 같은 find ... | xargs당신이 사용하지 않는 -print0-0
글렌 잭맨

1
find . -name "*.txt -exec process_one {} ";"대신 사용하십시오 . 왜 xargs를 사용하여 결과를 수집해야합니까?
사용자가 알 수 없음

@userunknown 글쎄 모두가 무엇인지에 달려 있습니다 process_one. 실제 명령 의 자리 표시 자 인 경우 오타를 수정하고 뒤에 따옴표를 추가하면 작동합니다 "*.txt. 그러나 process_one사용자 정의 함수 인 경우 코드가 작동하지 않습니다.
toxalot

@toxalot : 그렇습니다. 그러나 호출 할 스크립트에 함수를 작성하는 것은 문제가되지 않습니다.
사용자가 알 수 없음

4

find나중에 출력을 다음과 같이 사용하려면 출력을 배열에 저장할 수 있습니다 .

array=($(find . -name "*.txt"))

이제 각 요소를 줄 바꿈으로 인쇄하려면 for배열의 모든 요소에 대해 루프 반복을 사용하거나 printf 문을 사용할 수 있습니다.

for i in ${array[@]};do echo $i; done

또는

printf '%s\n' "${array[@]}"

다음을 사용할 수도 있습니다.

for file in "`find . -name "*.txt"`"; do echo "$file"; done

이것은 개행으로 각 파일 이름을 인쇄합니다

find출력을 목록 형식으로 만 인쇄하려면 다음 중 하나를 사용할 수 있습니다.

find . -name "*.txt" -print 2>/dev/null

또는

find . -name "*.txt" -print | grep -v 'Permission denied'

그러면 오류 메시지가 제거되고 파일 이름 만 줄 바꿈으로 출력됩니다.

파일 이름으로 무언가를하고 싶다면 배열에 저장하는 것이 좋습니다. 그렇지 않으면 해당 공간을 소비 할 필요가 없으므로의 출력을 직접 인쇄 할 수 있습니다 find.


1
파일 이름에 공백이 있으면 배열을 반복 할 수 없습니다.
EM0

이 답변을 삭제해야합니다. 파일 이름이나 디렉토리 이름에 공백이 있으면 작동하지 않습니다.
jww

4

파일 이름에 줄 바꿈이 포함되어 있지 않다고 가정 find하면 다음 명령을 사용하여 Bash 배열 의 출력을 읽을 수 있습니다 .

readarray -t x < <(find . -name '*.txt')

노트 :

  • -t원인 readarray스트립 줄 바꿈에.
  • readarray파이프에있는 경우 작동하지 않으므로 프로세스 대체입니다.
  • readarray Bash 4부터 사용할 수 있습니다.

Bash 4.4 이상 -d은 구분자를 지정하기위한 매개 변수 도 지원합니다 . 줄 바꿈 대신 널 문자를 사용하여 파일 이름을 구분하면 파일 이름에 줄 바꿈이 포함되는 드문 경우에도 작동합니다.

readarray -d '' x < <(find . -name '*.txt' -print0)

readarraymapfile동일한 옵션 으로 호출 할 수도 있습니다 .

참조 : https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream


이것이 가장 좋은 답변입니다! 작동 : * 파일 이름의 공백 * 일치하는 파일이 없습니다 * exit결과를 반복 할 때
EM0

그러나 가능한 모든 파일 이름으로 작동하지는 않습니다. 즉, 다음을 사용해야합니다.readarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy

3

변수에 처음 할당 된 find를 사용하고 IFS를 다음과 같이 새 줄로 바꿨습니다.

FilesFound=$(find . -name "*.txt")

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

동일한 DATA 세트에서 더 많은 조치를 반복하고 서버에서 찾기가 매우 느린 경우 (I / 0 높은 활용도)


2

다음 find과 같이 반환 된 파일 이름을 배열 에 넣을 수 있습니다 .

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

이제 배열을 반복하여 개별 항목에 액세스하고 원하는 항목을 수행 할 수 있습니다.

참고 : 공백이 안전합니다.


1
bash 4.4 이상에서는 루프 대신 단일 명령을 사용할 수 있습니다 mapfile -t -d '' array < <(find ...). 에 대한 설정 IFS은 필요하지 않습니다 mapfile.
Socowi

1

fd # 3을 사용하여 @phk의 다른 답변과 의견에
따라 (루프 내부에서 stdin을 계속 사용할 수 있음)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")

-1

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

파일이 나열되고 속성에 대한 세부 사항이 제공됩니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.