얼마 전 스크립팅에 관한 질문에 대한 답변을 게시한 적이 있습니다. 누군가 다음 명령을 사용하면 안 된다고 지적했습니다.
for x in $(cat file); do something; done
하지만 대신:
while read f; do something; done < file
고양이의 쓸모없는 사용기사에서는 전체 문제를 설명하고 싶었지만 유일한 설명은 다음과 같습니다.
백틱의 결과가 쉘이 허용할 수 있는 명령줄의 길이보다 작거나 같다는 것을 알지 못하는 한 백틱은 매우 위험합니다. (실제로 이것은 커널 제한입니다.limits.h의 상수 ARG_MAX는 시스템이 얼마나 걸릴 수 있는지 알려줍니다. POSIX에서는 ARG_MAX가 최소 4,096바이트가 되도록 요구합니다.
이것을 올바르게 이해했다면 명령에서 매우 큰 파일의 출력을 사용하면 bash(?)가 충돌해야 합니다(limits.h 파일에 정의된 ARG_MAX를 초과해야 함). 그래서 다음 명령으로 ARG_MAX를 확인했습니다.
> grep ARG_MAX /usr/src/kernels/$(uname -r)/include/uapi/linux/limits.h
#define ARG_MAX 131072 /* # bytes of args + environ for exec() */
그런 다음 공백 없이 텍스트가 포함된 파일을 만들었습니다.
> ls -l
-rw-r--r--. 1 root root 100000000 Aug 21 15:37 in_file
그런 다음 다음을 실행합니다.
for i in $(cat in_file); do echo $i; done
아, 끔찍한 일은 없었어요.
그렇다면 "루프가 있는 고양이를 사용하지 마십시오" 전체가 위험한지/어떻게 확인하려면 어떻게 해야 합니까?
답변1
file
포함하려는 항목 에 따라 다릅니다 . 예를 들어, IFS로 구분된 쉘 전역 목록을 포함하려는 경우(기본값은 이라고 가정 $IFS
):
/var/log/*.log /var/adm/*~
/some/dir/*.txt
자, 이것이 for i in $(cat file)
갈 길입니다. 인용 해제가 하는 일은 다음과 같습니다. $(cat file)
후행 개행 문자를 제거한 상태로 출력에 분할+글로브 연산자를 적용합니다. cat file
따라서 이러한 glob의 확장으로 인해 발생하는 각 파일 이름을 반복합니다(glob이 어떤 파일과도 일치하지 않는 경우, 이 경우 glob은 그대로 유지되지만 확장되지는 않습니다).
분리된 각 행을 반복하려면 file
다음과 같이 하세요.
while IFS= read -r line <&3; do
{
something with "$line"
} 3<&-
done 3< file
루프를 사용하면 for
다음을 사용하여 비어 있지 않은 각 줄을 반복할 수 있습니다.
IFS=' ' # 개행 문자로만 분할합니다(실제로 일련의 개행 문자와 # 줄바꿈은 다음과 같기 때문에 선행 및 후행 문자를 무시합니다. #IFS 공백 문자) set -o noglob # 비활성화전반적인 상황분할+글로브 연산자의 일부: $(cat 파일)의 행에 대해 다음을 수행하십시오. "$line"이 있는 것 완벽한
하지만:
while read line; do
something with "$line"
done < file
그것은 말이 되지 않습니다. 그건file
매우 복잡한 방식으로 콘텐츠 읽기여기서 $IFS
및 백슬래시 문자는 특별하게 처리됩니다.
어쨌든 인용한 텍스트가 참조하는 ARG_MAX 제한은 execve()
시스템 호출(인수 및 환경 변수의 누적 크기와 관련)에 있으므로 적용될 수 있는 파일 시스템을 사용하여 명령 실행이 수행되는 경우에만 적용됩니다. 명령 대체 분할+글로브 연산자의 매우 긴 확장입니다(텍스트가 여러 계정에서 오해의 소지가 있고 잘못되었습니다).
예를 들어 다음과 함께 작동합니다.
cat -- $(cat file) # with shell implementations where cat is not builtin
하지만 다음에서는 그렇지 않습니다.
for i in $(cat file)
execve()
시스템 호출은 포함되지 않습니다 .
비교하다:
bash-4.4$ echo '/*/*/*/*' > file
bash-4.4$ true $(cat file)
bash-4.4$ n=0; for f in $(cat file); do ((n++)); done; echo "$n"
523696
bash-4.4$ /bin/true $(cat file)
bash: /bin/true: Argument list too long
bash
사용된 true
내장 명령이나 루프는 작동 for
하지만 실행은 작동하지 않습니다 /bin/true
. 크기는 9바이트에 불과하지만 쉘이 글로브를 확장하므로 몇 메가바이트 file
만큼 확장됩니다 $(cat file)
./*/*/*/*
추가 자료:
답변2
@chepna댓글의 차이점을 설명했습니다.
for i in $(cat in_file)
파일의 행을 반복하지 않고 파일 내용의 토큰화 및 경로 이름 확장으로 인해 발생하는 단어를 반복합니다.
성능 및 리소스 사용량에 대한 영향을 확인하기 위해 1M 행(~19M) 입력을 사용하여 두 경우 모두에 대한 작은 벤치마크를 수행하고 다음을 사용하여 시간과 메모리 사용량을 측정했습니다 /usr/bin/time -v
.
test1.sh:
#!/bin/bash
while read x
do
echo $x > /dev/null
done < input
결과:
Command being timed: "./test1.sh"
User time (seconds): 12.41
System time (seconds): 2.03
Percent of CPU this job got: 110%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:13.07
Maximum resident set size (kbytes): 3088
test2.sh:
#!/bin/bash
for x in $(cat input)
do
echo $x > /dev/null
done
결과:
Command being timed: "./test2.sh"
User time (seconds): 17.19
System time (seconds): 3.13
Percent of CPU this job got: 109%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:18.51
Maximum resident set size (kbytes): 336356
두 테스트의 전체 결과를 다음에 업로드했습니다.페이스트빈. Bash를 사용하면 for i in $(cat ...)
더 많은 메모리를 차지하고 실행 속도가 느려집니다. 그러나 동일한 테스트를 다른 셸에서 실행하는지 여부에 따라 결과가 달라질 수 있습니다.
답변3
while
루프에 문제가 있을 수 있습니다. 가장 분명한 것은 기본적으로 stdin을 사용한다는 것입니다(so ssh -n
). 따라서 다른 것에 stdin이 필요한 경우 while
루프는 실패합니다.
$ find . -name "*.pm" | while read f; do aspell check $f; done
$
아무 것도 하지 않지만 aspell
Perl 모듈 이름 목록이 차지하는 터미널이 필요합니다. 루프가 for
더 적합합니다(파일 이름이 POSIX 토큰화 규칙에 의해 분할되지 않는다고 가정).
$ for f in $(find . -name \*.pm); do aspell check $f; done
...
while
기본적으로 표준 입력을 사용하지 않기 때문입니다.
또한 while
자동 데이터 손실이 발생하기 쉽습니다( for
동일한 입력에 대해 다르게 동작함).
$ echo -n mmm silent data loss | while read line; do echo $line; done
$ for i in $(echo -n mmm silent data loss); do echo $i; done
mmm
silent
data
loss
$
while
따라서 상황에 따라 위험하고 사용해서는 안 되는 주장이 나올 수 있습니다.