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

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

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

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

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



답변

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 은 다른 방법을 찾습니다.


답변

무엇을하든 루프를 사용하지 마십시오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을 사용합니다.


답변

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하나씩 하나씩 집어 들고 …


답변

파일 이름에는 공백과 제어 문자가 포함될 수 있습니다. 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에서)

추가 참조 및 출처 :


답변

(@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"
' {} \;


답변

# 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


답변

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.