사용하여while 루프POSIX 셸에서 텍스트를 처리하는 것은 일반적으로 나쁜 습관으로 간주됩니까?
~처럼스테판 차젤라스는 다음과 같이 지적합니다., 쉘 루프를 사용하지 않는 몇 가지 이유는 다음과 같습니다.개념적,신뢰할 수 있음,읽기 쉬움,성능그리고안전.
이것답변설명했다신뢰할 수 있음그리고읽기 쉬움측면:
while IFS= read -r line <&3; do
printf '%s\n' "$line"
done 3< "$InputFile"
~을 위한성능, while
루프 및읽다파일이나 파이프에서 읽는 것은 매우 느립니다.쉘 내장 읽기한 번에 한 문자씩 읽습니다.
어때요?개념적그리고안전측면?
답변1
예, 다음과 같은 것을 많이 볼 수 있습니다.
while read line; do
echo $line | cut -c3
done
아니면 더 나쁘다:
for line in $(cat file); do
foo=$(echo $line | awk '{print $2}')
bar=$(echo $line | awk '{print $3}')
doo=$(echo $line | awk '{print $5}')
echo $foo whatever $doo $bar
done
(웃지 마세요. 저는 이런 걸 많이 봤어요.)
일반적으로 쉘 스크립팅 초보자로부터 시작됩니다. 이것은 C나 Python과 같은 명령형 언어에서 수행할 작업을 간단하게 문자 그대로 번역한 것입니다. 하지만 셸에서 작업을 수행하는 방식은 아니며 예제는 매우 비효율적입니다.육각 입력 라인에 대한 하위 프로세스) 완전히 신뢰할 수 없으며(보안 문제를 일으킬 수 있음) 대부분의 버그를 수정하면 코드를 읽을 수 없게 됩니다.
개념적으로
C 또는 대부분의 다른 언어에서 빌딩 블록은 컴퓨터 명령보다 한 수준 위에 있습니다. 프로세서에 수행할 작업과 다음에 수행할 작업을 알려줍니다. 손으로 프로세서를 집어 세세하게 관리합니다. 파일을 열고, 그만큼의 바이트를 읽고, 이런 일을 하고, 저 일을 합니다.
쉘은 고급 언어입니다. 심지어 언어가 아니라고 말할 수도 있습니다. 모든 명령줄 해석기보다 우선합니다. 작업은 사용자가 실행하는 명령에 의해 수행되며 셸은 명령을 조정합니다.
유닉스에서 나온 가장 위대한 것 중 하나는관로기본적으로 모든 명령이 처리하는 기본 stdin/stdout/stderr 스트림입니다.
지난 50년 동안 우리는 이 API보다 명령의 힘을 활용하고 함께 작업하여 작업을 수행할 수 있는 더 나은 방법을 찾지 못했습니다. 이것이 오늘날 사람들이 여전히 조개껍질을 사용하는 주된 이유일 것입니다.
자르기 도구와 음역 도구가 있으면 간단히 다음을 수행할 수 있습니다.
cut -c4-5 < in | tr a b > out
셸은 파이프 작업(파일 열기, 파이프 설정, 명령 호출)만 수행하며 모든 것이 준비되면 셸에서 아무 작업도 수행하지 않고 정상적으로 실행됩니다. 도구는 하나의 도구가 다른 도구를 차단하지 않도록 충분한 버퍼링을 통해 작업을 동시에 효율적으로 자체 속도로 수행합니다. 아름답지만 간단합니다.
그러나 도구를 호출하려면 비용이 발생합니다(성능 측면에서 개발할 예정입니다). 이러한 도구는 C 언어로 작성된 수천 개의 명령일 수 있습니다. 프로세스를 생성하고 도구를 로드하고 초기화한 다음 정리하고 프로세스를 삭제하고 기다려야 합니다.
부르는 cut
것은 부엌 서랍을 열고, 칼을 집어 사용하고, 청소하고, 말리고, 다시 서랍에 넣는 것과 같습니다. 이 작업을 수행할 때:
while read line; do
echo $line | cut -c3
done < file
read
이는 파일의 각 줄에 대해 주방 서랍에서 도구를 가져오는 것과 같습니다 (이는 매우 서투른 접근 방식입니다.이건 이런 용도로 설계되지 않았어), 한 줄을 읽고, 독서 도구를 청소하고 서랍에 다시 넣으십시오. 그런 다음 도구 에 대한 echo
세션을 예약하고 cut
, 서랍에서 꺼내고, 회수하고, 세척하고, 건조하고, 서랍에 다시 넣는 등의 작업을 수행합니다.
이러한 도구( read
및 도구) 중 일부는 대부분의 셸에 내장되어 있지만 여전히 별도의 프로세스에서 실행해야 하므로 echo
여기서는 별 차이가 없습니다 .echo
cut
양파를 다지는 것과 비슷하지만 칼을 씻어 부엌 서랍에 다시 넣어두세요.
여기서 가장 확실한 방법은 cut
서랍에서 도구를 꺼내 양파 전체를 썰어 전체 작업이 끝나면 다시 서랍에 넣는 것입니다.
IOW, 쉘에서는 특히 텍스트를 처리할 때 수천 개의 도구를 순서대로 실행하고 각 도구가 시작되고 실행되고 정리될 때까지 기다리는 대신 가능한 한 적은 수의 유틸리티를 호출하여 작업에 적합하도록 합니다. 다음 도구를 다시 실행하세요.
추가 읽기브루스의 대답은 훌륭합니다.shell의 하위 수준 텍스트 처리 내부 도구는 (아마도 제외 zsh
) 제한적이고 번거로우며 일반적으로 일반 텍스트 처리에 적합하지 않습니다.
성능
앞서 언급했듯이 명령을 실행하는 데는 비용이 듭니다. 명령어가 내장되어 있지 않으면 비용이 엄청나지만, 내장되어 있어도 비용이 엄청납니다.
쉘은 이러한 방식으로 작동하도록 설계되지 않았으며 고성능 프로그래밍 언어라고 주장하지도 않습니다. 그들은 아닙니다. 그들은 단지 명령줄 해석기일 뿐입니다. 따라서 이와 관련하여 최적화가 거의 이루어지지 않았습니다.
또한 셸은 별도의 프로세스에서 명령을 실행합니다. 이러한 빌딩 블록은 공통 메모리나 상태를 공유하지 않습니다. fgets()
or C 에서 하면 fputs()
이것은 stdio의 함수입니다. stdio는 비용이 많이 드는 시스템 호출을 너무 자주 방지하기 위해 모든 stdio 함수의 입력 및 출력을 위한 내부 버퍼를 유지합니다.
해당 내장 쉘 유틸리티( read
, echo
, printf
)조차도 이 작업을 수행할 수 없습니다. read
한 줄을 읽을 수 있도록 설계되었습니다. 개행 문자를 지나서 읽으면 실행하는 다음 명령이 이를 놓친다는 의미입니다. 따라서 read
한 번에 1바이트씩 읽어야 합니다(일부 구현에서는 청크로 읽고 거꾸로 볼 때 입력이 일반 파일인 경우 최적화하지만 이는 일반 파일에만 작동합니다. 예를 들어 bash
128바이트 청크만 읽는 경우, 즉 여전히 훨씬 적습니다. 텍스트 유틸리티보다 일반적임).
출력 측면에서도 마찬가지입니다. echo
출력을 버퍼링할 수는 없으며 실행하는 다음 명령이 해당 버퍼를 공유하지 않기 때문에 즉시 출력해야 합니다.
분명히 명령을 순차적으로 실행한다는 것은 명령을 기다려야 한다는 것을 의미합니다. 이는 셸에서 도구로 제어권을 넘겨주는 작은 스케줄러 댄스입니다. 이는 또한 파이프라인에서 장기 실행 도구 인스턴스를 사용하는 것과 달리 여러 프로세서를 동시에(사용 가능한 경우) 활용할 수 없음을 의미합니다.
빠른 테스트에서 while read
이 루프와 (아마도) 동등한 루프 사이의 CPU 시간 비율은 cut -c3 < file
약 40000(1초 대 반나절)이었습니다. 그러나 쉘 내장 기능만 사용하더라도:
while read line; do
echo ${line:2:1}
done
(여기서 사용됨 bash
)은 여전히 1:600(1초 대 10분) 정도입니다.
신뢰성/가독성
코드를 제대로 맞추는 것이 어렵습니다. 제가 제시한 예는 현장에서 흔히 볼 수 있는 것이지만 버그가 많습니다.
read
다양한 작업을 수행할 수 있는 편리한 도구입니다. 사용자의 입력을 읽고 이를 단어로 분할하여 다른 변수에 저장할 수 있습니다. read line
하다아니요입력 줄을 읽거나 매우 특정한 방식으로 줄을 읽을 수도 있습니다. 실제로 읽는 내용은 다음과 같습니다.성격입력 시 $IFS
구분 기호 또는 줄 바꿈 문자를 이스케이프하기 위해 백슬래시로 구분된 단어를 사용할 수 있습니다.
기본값은 $IFS
다음과 같이 입력합니다.
foo\/bar \
baz
biz
read line
예상한 대로 "foo/bar baz"
저장 되지 $line
않습니다 ." foo\/bar \"
실제로 필요한 줄을 읽으려면 다음이 필요합니다.
IFS= read -r line
이는 매우 직관적이지는 않지만 그대로이며 쉘을 이런 방식으로 사용해서는 안 된다는 점을 기억하십시오.
echo
.extended 시퀀스 와 동일합니다 echo
. 임의의 파일 내용 등 임의의 내용과 함께 사용할 수 없습니다. 여기에 필요합니다 printf
.
물론 대표적인 경우도 있습니다변수를 인용하는 것을 잊었습니다.모두가 그것에 빠진다. 이에 대한 자세한 내용은 다음과 같습니다.
while IFS= read -r line; do
printf '%s\n' "$line" | cut -c3
done < file
이제 몇 가지 참고 사항을 더 살펴보겠습니다.
- 을 제외하고
zsh
입력에 최소한 GNU 텍스트 유틸리티에서는 문제가 발생하지 않는 NUL 문자가 포함되어 있으면 이 방법이 작동하지 않습니다. - 마지막 개행 문자 뒤에 데이터가 있으면 건너뜁니다.
- 루프 내부에서는 stdin이 리디렉션되므로 내부 명령이 stdin에서 읽혀지지 않도록 주의해야 합니다.
- 루프 내부 명령의 경우 성공 여부는 신경 쓰지 않습니다. 일반적으로 오류(디스크 가득 참, 읽기 오류...) 상황은 제대로 처리되지 않으며 일반적으로 다음을 사용하는 것보다 낫습니다.옳은동일한. 많은 명령(여러 구현 포함)
printf
도 종료 상태에서 표준 출력에 쓰기 실패를 반영하지 않습니다.
위의 문제 중 일부를 해결하려면 다음과 같이 됩니다.
while IFS= read -r line <&3; do
{
printf '%s\n' "$line" | cut -c3 || exit
} 3<&-
done 3< file
if [ -n "$line" ]; then
printf '%s' "$line" | cut -c3 || exit
fi
이를 분별하는 것이 점점 더 어려워지고 있습니다.
매개변수를 통해 명령에 데이터를 전달하거나 변수에서 출력을 검색하는 데에는 다른 많은 문제가 있습니다.
- 매개변수 크기에 대한 제한(일부 텍스트 유틸리티 구현에도 제한이 있지만 일반적으로 달성되는 효과는 문제가 덜합니다)
- NUL 문자(텍스트 유틸리티에도 문제가 있음)
-
인수가 (또는 때때로)로 시작될 때+
옵션으로 간주됩니다 .- 이러한 루프 내에서 일반적으로 사용되는 다양한 명령의 다양한 특성, 예 :
expr
....test
- 다양한 셸의 (제한된) 텍스트 연산자는 멀티바이트 문자를 일관되지 않게 처리합니다.
- ...
보안 고려 사항
쉘을 사용하기 시작하면변하기 쉬운그리고명령 매개변수, 당신은 지뢰밭에 들어가고 있습니다.
만약 너라면변수를 인용하는 것을 잊었습니다.,잊다옵션 닫는 태그, 멀티바이트 문자(현재 표준)를 사용하는 로캘에서 작업하면 조만간 취약점이 될 버그가 발생할 수 있습니다.
루프를 사용하고 싶을 때
텍스트를 처리하기 위해 쉘 루프를 사용하는 것은 쉘이 잘하는 일, 즉 외부 프로그램 실행을 수행하는 작업이 포함될 때 의미가 있을 수 있습니다.
예를 들어 다음과 같은 루프가 적합할 수 있습니다.
while IFS= read -r line; do
someprog -f "$line"
done < file-list.txt
위의 간단한 경우(입력이 수정되지 않은 상태로 전달됨) someprog
는 다음을 사용하여 수행할 수도 있습니다 xargs
.
<file-list.txt tr '\n' '\0' | xargs -r0 -n1 someprog -f
또는 GNU를 사용하십시오 xargs
:
xargs -rd '\n' -n1 -a file-list.txt someprog -f
답변2
개념과 가독성 측면에서 쉘은 일반적으로 파일에 관심이 있습니다. "주소 지정 가능 단위"는 파일이고 "주소"는 파일 이름입니다. 셸에는 파일 존재 여부, 파일 유형 및 파일 이름 형식(와일드카드로 시작)을 테스트하는 다양한 방법이 있습니다. 쉘에는 파일 내용을 조작하기 위한 기본 요소가 거의 없습니다. 쉘 프로그래머는 파일 내용을 처리하기 위해 다른 프로그램을 호출해야 합니다.
지적하신 대로 셸에서의 텍스트 조작은 파일 및 파일 이름 방향으로 인해 매우 느리고 불분명하고 왜곡된 프로그래밍 스타일도 필요합니다.
답변3
우리 사이에는 괴짜들을 위한 흥미로운 세부 사항이 많이 포함된 몇 가지 복잡한 답변이 있지만 실제로는 매우 간단합니다. 쉘 루프에서 대용량 파일을 처리하는 것은 너무 느립니다.
질문자는 일반적인 셸 스크립트에 관심이 있는 것 같습니다. 이 스크립트는 명령줄 구문 분석, 환경 설정, 파일 및 디렉터리 확인, 주요 작업을 시작하기 전에 추가 초기화 등으로 시작하여 큰 프로세스를 진행합니다. 줄 지향 텍스트 파일입니다.
첫 번째 부분( initialization
)의 경우 일반적으로 쉘 명령이 느리다는 것은 중요하지 않습니다. 단지 수십 개의 명령과 몇 개의 짧은 루프만 실행합니다. 이 부분을 비효율적으로 작성하더라도 일반적으로 모든 초기화를 완료하는 데 1초도 채 걸리지 않습니다. 이는 좋은 일입니다. 이는 한 번만 발생합니다.
그러나 수천 또는 수백만 줄의 대용량 파일을 다루기 시작하면안좋다셸 스크립트는 한 줄에 몇 분의 1초가 소요됩니다(비록 수십 밀리초에 불과하더라도). 이는 몇 시간까지 걸릴 수 있습니다.
이때 우리는 다른 도구를 사용해야 하며 Unix 쉘 스크립트의 장점은 이를 쉽게 수행할 수 있다는 것입니다.
각 줄을 보기 위해 루프를 사용하는 대신 전체 파일을 전달해야 합니다.명령 파이프라인. 이는 쉘이 명령을 수천 번 또는 수백만 번 호출하지 않고 단 한 번만 호출함을 의미합니다. 사실, 이러한 명령에는 파일을 한 줄씩 처리하기 위한 루프가 있지만 쉘 스크립트는 아니며 빠르고 효율적으로 설계되었습니다.
Unix에는 파이프라인을 구축하는 데 사용할 수 있는 간단한 것부터 복잡한 것까지 훌륭한 내장 도구가 많이 있습니다. 나는 보통 간단한 것부터 시작하고 필요할 때만 더 복잡한 것을 사용합니다.
나는 또한 대부분의 시스템에서 사용할 수 있는 표준 도구를 고수하고 이식성을 유지하려고 노력하지만 이것이 항상 가능한 것은 아닙니다. 좋아하는 언어가 Python이나 Ruby라면 소프트웨어를 실행하는 데 필요한 모든 플랫폼에 해당 언어가 설치되어 있는지 확인하기 위해 추가 노력을 기울이는 것도 괜찮을 것입니다. :-)
간단한 도구에는 head
, tail
, grep
, sort
, cut
, tr
, sed
, join
(2개 파일을 병합할 때) 및 awk
한 줄 명령문이 포함됩니다. 어떤 사람들은 sed
패턴 일치와 명령을 사용하여 놀라운 일을 할 수 있습니다.
이것은 더 복잡해지고 실제로 각 줄에 일부 논리를 적용해야 할 때awk
좋은 옵션입니다. 즉, 한 줄(어떤 사람들은 전체 awk 스크립트를 "한 줄"에 넣습니다. 읽기 쉽지는 않지만) 중 하나입니다. 짧은 외부 스크립트로.
해석된 언어(예: 셸) 로서 awk
라인별 처리를 얼마나 효율적으로 수행할 수 있는지는 놀랍지만 이를 위해 특별히 제작되었으며 정말 빠릅니다.
Perl
텍스트 파일 작업에 매우 능숙하고 유용한 라이브러리가 많이 포함된 다른 스크립팅 언어가 많이 있습니다 .
마지막으로, 필요한 경우 좋은 오래된 C가 있습니다.최대 속도높은 유연성(텍스트 처리가 약간 번거롭기는 하지만) 그러나 직면하는 모든 파일 처리 작업에 대해 새로운 C 프로그램을 작성하는 것은 엄청난 시간 낭비가 될 수 있습니다. 저는 CSV 파일을 많이 사용하기 때문에 다양한 프로젝트에서 재사용할 수 있는 몇 가지 범용 유틸리티를 C로 작성했습니다. 효과적으로 이는 쉘 스크립트에서 호출할 수 있는 "간단하고 빠른 Unix 도구"의 범위를 확장하므로 스크립트만 작성하여 대부분의 프로젝트에서 작업할 수 있습니다. 이는 매번 사용자 정의 C 코드를 작성하고 디버깅하는 것보다 훨씬 더 빠릅니다!
몇 가지 최종 팁:
- 기본 쉘 스크립트를 시작하는 것을 잊지 마십시오
export LANG=C
. 그렇지 않으면 많은 도구가 일반 오래된 ASCII 파일을 유니코드로 처리하여 속도가 훨씬 느려집니다. export LC_ALL=C
sort
환경에 관계없이 일관된 정렬을 생성 하고 싶다면 설정도 고려해 보세요!- 데이터가 필요한 경우
sort
다른 모든 것보다 더 많은 시간(및 리소스: CPU, 메모리, 디스크)이 소요될 수 있으므로 정렬하는 명령 수sort
와 파일 크기를 최소로 유지하십시오. - 가능하면 일반적으로 단일 파이프라인이 가장 효율적입니다. 중간 파일을 사용하여 여러 파이프라인을 순차적으로 실행하면 읽기 쉽고 디버깅이 더 쉬울 수 있지만 프로그램에 소요되는 시간이 늘어납니다.
답변4
수용된 대답은 쉘에서 텍스트 파일을 구문 분석하는 것의 단점을 명확하게 설명하기 때문에 좋습니다. 그러나 사람들은 쉘 루프를 사용하는 모든 것을 비판하기 위해 주요 아이디어(주로 쉘 스크립트가 텍스트 처리 작업을 잘 처리하지 못한다는 것)를 숭배해 왔습니다.
쉘 루프 자체에는 아무런 문제가 없습니다. 쉘 스크립트 내의 루프나 루프 외부의 명령 대체에는 문제가 없다는 의미입니다. 실제로 대부분의 경우 보다 관용적인 구문으로 대체할 수 있습니다. 예를 들어 쓰지 마세요.
for i in $(find . -iname "*.txt"); do
...
done
다음을 작성하세요:
for i in *.txt; do
...
done
awk
다른 시나리오에서는 우수한 텍스트 처리 기능을 갖춘 일반 프로그래밍 언어(예: Perl, Python, Ruby) 또는 특정 파일 유형(XML sed
, HTML, JSON) 과 같은 보다 전문화된 도구에 의존하는 것이 더 좋습니다.cut
join
paste
datamash
miller
그렇긴 하지만, 쉘 루프를 사용하는 것이 올바른 선택입니다.알다:
- 성능이 우선순위가 아닙니다. 스크립트 속도가 중요합니까? 몇 시간마다 크론 작업으로 작업을 실행하고 있습니까? 그렇다면 성능은 문제가 되지 않을 수도 있습니다. 또는 그렇다면 벤치마크를 실행하여 쉘 루프가 병목 현상이 아닌지 확인하십시오. 어떤 도구가 "빠른지" 또는 "느린지"에 대한 직관이나 선입견은 정확한 벤치마크를 대체할 수 없습니다.
- 가독성을 유지하세요. 쉘 루프에 너무 많은 논리를 추가하여 따라가기가 어렵다면 이 접근 방식을 다시 고려해 볼 수 있습니다.
- 복잡성이 크게 증가하지 않습니다.
- 보안이 유지됩니다.
- 테스트 가능성은 문제가 되지 않습니다. 쉘 스크립트를 적절하게 테스트하는 것은 충분히 어렵습니다. 외부 명령을 사용하면 코드에 버그가 있는지 알기가 더 어려워지거나 반환 값에 대해 잘못된 가정 하에 작업하는 경우 문제가 됩니다.
- 쉘 루프는 대체 루프와 동일한 의미를 갖습니다.또는이러한 차이점은 현재 수행 중인 작업에 중요하지 않습니다. 예를 들어
find
위 명령은 하위 디렉터리로 반복되어.
.
이전 진술을 만족시키는 것이 불가능한 작업이 아님을 증명하는 예로서 다음은 잘 알려진 상용 소프트웨어 설치 프로그램에서 사용하는 패턴입니다.
i=1
MD5=... # embedded checksum
for s in $sizes
do
checksum=`echo $VAR | cut -d" " -f $i`
if <checksum condition>; then
md5=`echo $MD5 | cut -d" " -f $i
...
done
매우 드물게 실행되고, 명확한 목적이 있고, 간결하고, 불필요한 복잡성을 추가하지 않으며, 사용자 제어 입력을 사용하지 않으므로 보안은 문제가 되지 않습니다. 루프에서 다른 프로세스를 호출하는 것이 중요합니까? 별말씀을요.