이름에 공백이 있는 파일을 반복하시겠습니까? [복사]

이름에 공백이 있는 파일을 반복하시겠습니까? [복사]

두 디렉터리에 있는 모든 동일한 파일의 출력을 비교하기 위해 다음 스크립트를 작성했습니다.

#!/bin/bash

for file in `find . -name "*.csv"`  
do
     echo "file = $file";
     diff $file /some/other/path/$file;
     read char;
done

나는 이것을 달성하는 다른 방법이 있다는 것을 알고 있습니다. 그런데 이상하게도 파일에 공백이 포함되어 있으면 스크립트가 실패합니다. 이 문제를 어떻게 처리해야 합니까?

find의 출력 예:

./zQuery - abc - Do Not Prompt for Date.csv

답변1

단답형(답변에 가장 가깝지만 공백 처리)

OIFS="$IFS"
IFS=$'\n'
for file in `find . -type f -name "*.csv"`  
do
     echo "file = $file"
     diff "$file" "/some/other/path/$file"
     read line
done
IFS="$OIFS"

더 나은 답변(파일 이름의 와일드카드 및 개행 문자도 처리)

find . -type f -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done

최고의 답변(기준:자일스의 대답)

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' exec-sh {} ';'

sh또는 파일당 하나씩 실행 하지 않는 것이 더 좋습니다 .

find . -type f -name '*.csv' -exec sh -c '
  for file do
    echo "$file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
  done
' exec-sh {} +

긴 대답

세 가지 질문이 있습니다.

  1. 기본적으로 쉘은 명령의 출력을 공백, 탭 및 줄 바꿈으로 나눕니다.
  2. 파일 이름에는 확장되는 와일드카드 문자가 포함될 수 있습니다.
  3. 이름이 로 끝나는 디렉토리가 있다면 어떻게 될까요 *.csv?

1. 개행 문자로만 분할

무엇을 설정할지 알아내기 위해 file쉘은 출력을 가져와 find어떻게든 해석해야 합니다. 그렇지 않으면 file전체 출력이 됩니다 find.

쉘은 IFS기본적으로 설정된 변수를 읽습니다.<space><tab><newline>

그런 다음 출력의 각 문자를 살펴봅니다 find. 에서 문자를 발견하자마자 IFS파일 이름의 끝이 표시되어 있다고 생각하여 file지금까지 본 문자로 설정하고 루프를 실행합니다. 그런 다음 마지막으로 중지된 위치에서 다음 파일 이름을 가져오고 출력 끝에 도달할 때까지 다음 루프를 실행합니다.

따라서 효과적으로 다음을 수행합니다.

for file in "zquery" "-" "abc" ...

줄 바꿈에서만 입력을 분할하도록 지시하려면 다음을 수행해야 합니다.

IFS=$'\n'

당신의 명령 앞에 for ... find.

IFS이는 단일 개행으로 설정되므로 공백과 탭이 아닌 개행으로만 분할됩니다.

, 또는 대신 or sh를 사용하는 경우 다음과 같이 작성 해야 합니다 .dashksh93bashzshIFS=$'\n'

IFS='
'

이것은 스크립트를 작동시키는 데 충분할 수 있지만, 다른 특수한 경우를 적절하게 처리하는 데 관심이 있다면 계속 읽어보세요...

2. $file와일드카드 확장을 사용하지 마세요

루프 내부

diff $file /some/other/path/$file

쉘이 $file(다시!) 확장을 시도합니다.

공백을 포함할 수 있지만 IFS위에서 설정했으므로 여기서는 문제가 없습니다.

그러나 예측할 수 없는 동작을 초래할 수 있는 *또는 같은 와일드카드 문자가 포함될 수도 있습니다 . ?(이 점을 지적해주신 Giles에게 감사드립니다.)

와일드카드를 확장하지 않도록 쉘에 지시하려면 변수를 큰따옴표로 묶으십시오.

diff "$file" "/some/other/path/$file"

같은 문제가 우리를 괴롭힐 수도 있습니다

for file in `find . -name "*.csv"`

예를 들어, 다음 세 개의 파일이 있다면

file1.csv
file2.csv
*.csv

(가능성은 거의 없지만 여전히 가능함)

마치 도망친 것 같은

for file in file1.csv file2.csv *.csv

이는 다음으로 확장됩니다.

for file in file1.csv file2.csv *.csv file1.csv file2.csv

두 번 발생 file1.csv하고 file2.csv처리되었습니다.

대신에 우리는 해야 합니다

find . -name "*.csv" -print | while IFS= read -r file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done

read표준 입력에서 줄을 읽고, 줄을 단어로 분할 IFS하고, 지정한 변수 이름에 저장합니다.

여기서는 행을 단어로 분할하지 말고 에 저장하지 말라고 지시합니다 $file.

read line으로 변경되었음을 알려드립니다 read line </dev/tty.

이는 루프 내부에서 표준 입력이 find파이프에서 나오기 때문입니다.

이 작업을 수행하면 read파일 이름의 일부 또는 전체가 소비되고 일부 파일은 건너뛰게 됩니다.

/dev/tty사용자가 스크립트를 실행하는 터미널입니다. cron을 통해 스크립트를 실행하면 오류가 발생하지만 이 경우에는 문제가 되지 않는다고 생각합니다.

그렇다면 파일 이름에 개행 문자가 포함되어 있으면 어떻게 될까요?

파이프라인 끝에서 다음 -print으로 변경 -print0하고 사용하여 이를 처리할 수 있습니다 .read -d ''

find . -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read char </dev/tty
done

이렇게 하면 find각 파일 이름 끝에 널 바이트가 추가됩니다. 널 바이트는 파일 이름에 허용되지 않는 유일한 문자이므로 아무리 이상하더라도 가능한 모든 파일 이름을 처리해야 합니다.

상대방의 파일 이름을 얻으려면 IFS= read -r -d ''.

위에서 사용된 경우 read기본 줄 구분 기호 개행을 사용했지만 이제는 find줄 구분 기호로 null을 사용합니다. 에서는 bash명령(내장 명령이라도)에 대한 인수로 NUL 문자를 전달할 수 없지만 의미로 bash이해할 수 있습니다.-d ''NUL로 구분됨. 따라서 우리는 와 동일한 줄 구분 기호를 사용하여 -d ''make를 사용합니다 . NUL 바이트는 지원되지 않고 빈 문자열로 처리되므로 BTW도 작동합니다 .readfind-d $'\0'bash

-r정확성을 위해 파일 이름의 백슬래시를 특별히 처리하지 않는다는 를 추가했습니다 . 예를 들어 no 는 -r제거 \<newline>되고 \n로 변환됩니다 n.

널 바이트에 대한 위의 모든 규칙을 요구 bash하거나 기억 하지 않는 보다 이식성 있는 작성 방법입니다 (Gilles에게 다시 한 번 감사드립니다).zsh

find . -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read char </dev/tty
' exec-sh {} ';'

*3. 이름이 다음으로 끝나는 디렉터리를 건너뜁니다..csv

find . -name "*.csv"

이름이 지정된 디렉토리도 일치됩니다 something.csv.

이를 방지하려면 명령 -type f에 추가하십시오.find

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' exec-sh {} ';'

~처럼글렌 잭맨두 예제 모두 각 파일에 대해 실행되는 명령은 하위 셸에서 실행되므로 루프 내부의 변수가 변경되면 잊어버리게 된다는 점을 지적하세요.

변수를 설정해야 하고 루프가 끝날 때 계속 설정해야 하는 경우 다음과 같이 프로세스 대체를 사용하도록 변수를 재정의할 수 있습니다.

i=0
while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
    i=$((i+1))
done < <(find . -type f -name '*.csv' -print0)
echo "$i files processed"

이것을 복사하여 명령줄에 붙여넣으려고 하면 read line소비되므로 echo "$i files processed"명령이 실행되지 않습니다.

이를 방지하려면 결과를 삭제 read line </dev/tty하고 결과를 호출기(예: )로 보낼 수 있습니다 less.


노트

;루프 안의 세미콜론( )을 제거했습니다. 원한다면 다시 넣을 수 있지만 필요하지는 않습니다.

오늘날 $(command)에는 `command`. 이는 $(command1 $(command2))주로 `command1 \`command2\``.

read char문자는 실제로 읽혀지지 않습니다. 전체 줄을 읽으므로 로 변경했습니다 read line.

답변2

파일 이름에 공백이나 쉘 와일드카드가 포함되어 있으면 이 스크립트는 실패합니다 \[?*. 이 find명령은 한 줄에 하나의 파일 이름을 출력합니다. 그러면 쉘은 `find …`다음과 같이 명령 대체를 평가합니다.

  1. find명령을 실행 하고 출력을 얻습니다.
  2. 출력을 find별도의 단어로 분할합니다. 공백 문자는 단어 구분 기호입니다.
  3. 각 단어에 대해 와일드카드 패턴인 경우 일치하는 파일 목록으로 확장합니다.

예를 들어, 현재 디렉토리에 `foo* bar.csv, foo 1.txt및 이라는 세 개의 파일이 있다고 가정합니다 foo 2.txt.

  1. 명령 find은 를 반환합니다 ./foo* bar.csv.
  2. 쉘은 문자열을 공백으로 분할하여 두 단어인 ./foo*및 를 생성합니다 bar.csv.
  3. ./foo*와일드카드 메타 문자( ./foo 1.txt및 )가 포함되어 있으므로 일치하는 파일 목록으로 확장됩니다 ./foo 2.txt.
  4. 따라서 for루프는 ./foo 1.txt, ./foo 2.txt및 을 실행합니다 bar.csv.

단어 분리를 줄이고 와일드카드를 끄면 이 단계에서 대부분의 문제를 피할 수 있습니다. 단어 분리 효과를 약화시키려면 IFS변수를 단일 줄 바꿈으로 설정하십시오. 이렇게 하면 출력이 find줄 바꿈에서만 분할되고 공백이 유지됩니다. 와일드카드를 끄려면 다음을 실행하십시오 set -f. 코드의 이 부분은 파일 이름에 개행 문자가 포함되어 있지 않은 한 작동합니다.

IFS='
'
set -f
for file in $(find . -name "*.csv"); do …

(이것은 귀하의 질문의 일부가 아니지만 $(…)over 를 사용하는 것이 좋습니다 `…`. 의미는 같지만 백틱 버전에는 이상한 인용 규칙이 있습니다.)

diff $file /some/other/path/$file아래에 또 다른 질문 이 있습니다.

diff "$file" "/some/other/path/$file"

그렇지 않으면 값이 $file단어로 분할되고 해당 단어는 위의 명령 대체와 마찬가지로 전역 패턴으로 처리됩니다. 쉘 프로그래밍에 관해 한 가지 기억해야 할 것이 있다면 다음을 기억하십시오:$foo변수 확장( ) 및 명령 대체( ) $(bar)주위에는 항상 큰따옴표를 사용하십시오., 당신이 분할하고 싶다는 것을 알지 않는 한. (위에서 우리는 find출력을 여러 줄로 나누고 싶다는 것을 알고 있었습니다 .)

이를 호출하는 안정적인 방법 find은 발견된 각 파일에 대해 명령을 실행하도록 지시하는 것입니다.

find . -name '*.csv' -exec sh -c '
  echo "$0"
  diff "$0" "/some/other/path/$0"
' {} ';'

이 경우 또 다른 접근 방식은 두 디렉터리를 비교하는 것입니다. 단, 모든 "지루한" 파일을 명시적으로 제외해야 합니다.

diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path

답변3

나는 그 언급을 보지 못했다는 것에 놀랐습니다 readarray. 연산자와 결합하면 매우 쉬워집니다 <<<.

$ touch oneword "two words"

$ readarray -t files <<<"$(ls)"

$ for file in "${files[@]}"; do echo "|$file|"; done
|oneword|
|two words|

<<<"$expansion"구성을 사용하면 개행 문자가 포함된 변수를 배열로 분할할 수도 있습니다. 예를 들면 다음과 같습니다.

$ string=$(dmesg)
$ readarray -t lines <<<"$string"
$ echo "${lines[0]}"
[    0.000000] Initializing cgroup subsys cpuset

readarray이는 Bash에서 수년 동안 사용되었으므로 아마도 이것이 Bash에서 이를 수행하는 표준적인 방법일 것입니다.

답변4

Afaik find에는 필요한 모든 것이 있습니다.

find . -okdir diff {} /some/other/path/{} ";"

find는 호출 프로그램을 저장하는 역할을 담당합니다. -okdir은 diff하기 전에 메시지를 표시합니다(예/아니오로 확신합니다).

쉘이 포함되지 않으며 와일드카드, 광대, 파이, 파, 포가 없습니다.

참고로 find를 for/while/do/xargs와 결합하면 대부분의 경우 잘못 수행하는 것입니다. :)

관련 정보