정보
-
업무명 : 쉘 스크립트 (bash) 개발자가 빠지기 쉬운 함정 (1)
-
작성자 : 박진만
-
작성일 : 2020-03-21
-
설 명 :
-
수정이력 :
요약
[특징]
-
쉘 스크립트 개발자가 빠지기 쉬운 함정을 소개.
[활용 자료]
-
없음
[자료 처리 방안 및 활용 분석 기법]
-
없음
[사용 언어]
-
Bash Script
내용
-
이 페이지는 Bash 프로그래머가 흔히 발생하는 오류에 대해 요약하였습니다.
-
아래의 모든 코드의 예시는 어떤 결함을 가지고 있습니다.
-
들어가기에 앞서 기본적으로 따옴표 (“)를 항상 사용하고, 또 절대로 단어 분할을 사용하지 않는다면 대부분의 함정에 빠지지 않을 수 있습니다.
-
따옴표를 사용하지 않았을 때의 단어 분할이 되는 이유는 기본적으로 켜져 있는, Bourne쉘에서 상속받은 레거시 코드의 설계 오류 때문입니다.
-
따라서 하단에서 소개할 결함의 대부분은 이 따옴표와 단어 분할에 관련된 문제가 대부분입니다.
1. for i in $(ls *.mp3)
-
BASH 프로그래머들이 루프를 작성할 때 가장 범한 쉬운 일반적인 실수는 아래와 같은 코드들을 들 수 있습니다.
for i in $(ls *.mp3); do # 잘못된 코드
some command $i # 잘못된 코드
done
for i in $(ls) # 잘못된 코드
for i in `ls` # 잘못된 코드
for i in $(find . -type f) # 잘못된 코드
for i in `find . -type f` # 잘못된 코드
files=($(find . -type f)) # 잘못된 코드
for i in ${files[@]} # 잘못된 코드
-
즉 명령 치환의 경우 어떤 종류의 것도 따옴표없이 사용해서는 안됩니다.
-
여기에는 두 가지 문제가 있습니다.
-
첫 번째는 따옴표 없이 변수의 출력을 인수로 split하여 사용하는 것,
-
그리고 ls 출력을 해석하는 방법입니다.
-
위의 방식대로 ls를 출력하면 결코 출력 결과가 나오지 않을 것입니다.
-
-
왜냐하면 이는 파일 이름에 공백이 포함되어있기 때문에 나타나는 이름의 분리 때문입니다.
-
또 다른 이유로는 $(ls *.mp3)명령 치환의 출력 결과물이 분리되기 때문입니다.
-
가령 “01 - Don't Eat the Yellow Snow.mp3” 라는 파일이 현재 디렉토리에 있다고 가정해봅시다.
-
for 루프는 파일 이름을 구분 한 결과 각 단어를 01, -, Don't, Eat... 와 같이 반복하게 될 것입니다.
-
-
있을 수 있는 더욱 더욱 나쁜 상황은 이전 단어를 split 해 나가고 얻어진 문자열로 인하여 경로명 확장이 발생할 때입니다.
-
예를 들어, ls가 *포함 문자 출력을 발생시키고, 그것을 포함한 캐릭터가 패턴으로 인식되고, 그것과 일치하는 모든 파일 이름이 목록으로 대체되는 경우입니다.
-
또한 큰 따옴표 (")를 대체 하는 것 역시 불가능합니다.
for i in "$(ls *.mp3)"; do # Wrong!
-
위의 코드는 ls전체 출력을 한 단어로 취급하게 되어 버립니다.
-
각 파일의 이름을 반복하는 대신 루프는 모든 파일 이름을 붙인 문자열 i로 단 한 번만 실행될 것입니다.
-
상기한 ls 명령의 사용법이 적절하지 않다는 것을 분명해 보입니다.
-
다시말해, 이 외부 명령 (역자주 : ls명령)의 출력은 사람이 읽을 것을 특히 상정하고 있으며, 특별히 스크립트에서 해석하는 방법을 상정하고 있는 것 같지 않아 보입니다. 그렇다면 올바른 사용법은 무엇일까요?
for i in *.mp3; do # Better! and...
some command "$i" # ...always double-quote expansions!
done
-
Bash 같은 쉘은 기본적으로 glob 기능을 사용할 수 있으며, 즉 쉘 자체에서 파일 이름 매칭 목록에 패턴을 전개하는 것을 가능하게하기 위한 방법이 이미 구현되어 있습니다.
-
즉 외부 명령어의 결과를 해석 할 필요가 없습니다.
-
대신 쉘의 기본 기능인 glob를 이용하여 *.mp3를 직접 적어주는 것만으로 패턴 매치가 제대로 작동되며, 이는 따옴표 없이도 단어 분할의 영향을 받지 않게 됩니다.
-
만약 파일을 재귀적으로 처리 할 필요가 있다면, find 를 사용하는 것도 하나의 방법입니다.
-
여기서 문제가 있습니다.
-
만약 *.mp3-files이 현재 디렉토리에 존재하지 않는다면 어떻게 될까요? 그 경우 i="*.mp3"라는 것이 i에 삽입 된 상태에서 for루프는 한번만 실행되게 됩니다.즉 일치하는 파일이 없다는 빈 공백만이 한번만 출력될 것입니다.
# POSIX
for i in *.mp3; do
[ -e "$i" ] || continue
some command "$i"
done
-
단순히 따옴표를 사용하고, 단어분할을 사용하지 않으면 이러한 일반적인 실수를 방지 할 수 있을 것입니다.
-
즉 오류의 거의 대부분은 따옴표 없이 코드를 작성하는 것과 관련이 있고, 이어서 단어 분할, 그리고 glob 옵션과 관련이 있습니다.
-
위 loop 본문 $i주위의 따옴표에 주목하십시오. 이것은 두 번째 함정과 밀접한 관련이 있습니다.
2. cp $file $target
-
이 위에 표시되는 명령 중 무엇이 잘못일까요?
-
당신은 $file 그리고 $target이 확장 혹은 와일드 카드를 모두 가지고 있지 않다는 사실을 먼저 주목하여야 합니다.
-
그러나 이러한 경우에도 불구하고 파일 이름의 확장 결과는 분할될 것이며 이는 경로명이 확장되는 결과를 낳게 될 것입니다.
-
그러나 따옴표가 처리된 매개 변수는 반드시 파일 전체의 이름을 포함하게 될 것입니다.
cp -- "$file" "$target"
-
만약 큰 따옴표 없이 cp 01 - Don't Eat the Yellow Snow.mp3 /mnt/usb 같은 명령을 수행하게 되는 경우 아래와 같은 에러 메세지가 반환 될 것입니다.
cannot stat `01': No such file or directory
-
$file이 그 안에 와일드 카드 ( *, ?, [) 를 포함하고 있으며, 거기에 그것들과 일치하는 파일이 있었다면 이것들은 확장 될 것입니다.
-
즉 - 부터 시작하는 $file 내용이 있는 경우에는 큰 따옴표가 있어도 당신이 cp 명령 줄 옵션을 주었다고 생각하게 됩니다 (아래의 함정 3을 참고)
-
특히 변수가 여러 파일 이름을 포함하는 경우에는 위와 같은 예시가 매개 변수 확장의 인용에서 평범한 모범 사례라 할 수 있을 것입니다.
-
설령 어떤 일반적이지 않은 상황에서도 변수의 내용이 문제가 없음을 보장 할 수 있습니다.
-
결론적으로 경험을 쌓은 스크립트 작성자는 코드의 문맥 상 분명히 파라미터의 내용이 안전한 것이 보증되어있는 몇 안되는 경우를 제외하고는 항상 따옴표를 사용합니다.
-
상급자의 경우 제목의 cp명령이 잘못되었음을 바로 간파 할 수 있을 것입니다.
3. 대시 (-) 로 시작하는 파일 이름
-
대시 (-)로 시작되는 파일명은 다양한 문제를 일으킵니다.
-
*.mp3 등의 와일드카드로 확장되어 파일 이름이 정렬되는경우, 대시 (-)는 많은 로케일에서 문자 앞 부분에 정렬됩니다.
-
즉 이와 같은 경우 -filename이라는 것을 옵션으로 잘못 해석 하게 될 수 있습니다. 이에 대해서는 두 가지 해결책이 있습니다.
-
첫 번째 해결책은 --명령 ( cp같은)을 인수 사이에 넣는 것입니다. 이는 단순하지만 좋은 방법입니다.
cp -- "$file" "$target"
-
그러나 이 방법은 잠재적인 문제가 있습니다.
-
옵션으로 해석 될 수 있는 상황에서 모든 파라미터의 사용 시작에 --가 삽입 된 것을 매번 확인해야 할 필요가 있고, --를 남용하게 되는 경우 명령 자체가 매우 복잡해지게 될 가능성이 있기 때문입니다.
-
-
대부분의 자주 쓰는 옵션을 파싱하는 라이브러리는 이를 이해하는 동시에 프로그램은 이러한 기능을 제대로 취급 무료로 이 기능을 계승하고 있는 것입니다.
-
그러나 이 옵션의 끝을 인식하는 것은 결국 애플리케이션에 의존하게 됩니다.
-
표준 유틸리티에 대해서도 POSIX에 의해 기술 된 몇 가지 예외가 있을 것입니다. echo가 단적인 하나의 예입니다.
-
다른 방법은 상대경로나 절대경로를 사용하여 파일 이름이 디렉토리로부터 시작되었는지 확인하는 방법입니다.
for i in ./*.mp3; do
cp "$i" /target
...
done
-
이 경우 파일 이름이 -로 시작하더라도 해당 정렬의 경우 변수의 값이 ./-foo.mp3 를 포함하고 있는지 확인하게 됩니다. 이것은 cp명령과 관련된 것에서는 완벽하게 안전하다고 볼 수 있습니다.
-
결국 모든 결과가 같은 접두사를 가지도록 만들게 된다면, loop본문에서 몇 번 변수를 사용해서 간단하게 접두어를 결합 할 수 있을 것입니다.
-
이는 생성 된 각 단어에 대한 몇 가지 여분의 문자를 저장할 때 이론적으로 코드를 절약 할 수 있습니다.
for i in *.mp3; do
cp "./$i" /target
...
done
4. [ $foo = "bar" ]
-
이것은 함정 # 2와 매우 비슷한 경우입니다. 그러나 매우 중요한 것이므로 반복합니다.
-
위의 예는 따옴표가 잘못된 위치에 있습니다.
-
즉 Bash에서는 문자열에 따옴표를 사용할 필요가 없습니다. (메타 문자 혹은 패턴 문자를 포함하지 않는다면). 하지만 확장 혹은 와일드카드를 포함 할 수 있는지 없는지 확실하지 않을 때는 따옴표를 사용해야 할 것입니다.
-
-
그리고 또한 위의 코드는 여러 가지 이유로 잘못될 수 있습니다.
-
만약 [] 속의 변수가 존재하지 않는 경우 또는 공백인 경우, [] 명령은 다음과 같이 될 수 있습니다.
[ = "bar" ] # 잘못된 코드
-
그리고 다음과 같은 오류가 발생할 것입니다.
unary operator expected.
(The = operator is binary, not unary,
so the [ command is rather shocked to see it there.)
-
만약 변수가 내부에 공백을 포함하고 있다면 다음과 같이 [ ]내부의 $foo가 두 단어 이상으로 분할될 것입니다.
[ multiple words here = "bar" ]
-
이것은 언뜻 보았을 때 괜찮은 것처럼 보일지도 모르겠지만 [ 에 관한 한 이것은 구문 오류입니다. 올바른 작성법은 다음과 같습니다.
# POSIX
[ "$foo" = bar ] # Right!
-
POSIX의 []는 전달 된 인수의 수에 따라 그 작용을 결정하기 때문에 $foo이 -로 시작하더라도 POSIX 호환 구현에서 제대로 작동하게 됩니다. 만약 매우 오래된 레거시 코드와 같은 경우 이에 따른 문제가 있을지도 모르지만, 당신이 새로운 코드를 작성하는 경우에는 이에 대해 전혀 걱정할 필요가 없습니다.
-
Bash 나 다른 ksh와 같은 쉘의 경우 [[를 사용하는 새로운 대안이 있습니다.
# Bash / Ksh
[[ $foo == bar ]] # 올바른 방법
-
이 경우 [[ ]]안에있는 =의 왼쪽의 변수에 따옴표를 쓸 필요가 없습니다.
-
또한 공백의 변수도 제대로 처리됩니다.
-
한편, 이 경우에도 따옴표를 사용하는 것 역시 가능합니다.
-
그리고 [[ ]] 를 사용하는 경우 ==를 이용할 수 있습니다. 그러나 [[ 을 사용한 비교는 단순한 문자열 비교가 아닌 오른쪽의 문자열에 대해 패턴 매칭을 실시하게 될 것입니다.
-
오른쪽을 단일 문자열로 하려면 파일 이름에서 패턴 매치의 문맥에서 특별한 의미를 가진 문자열이 사용되는 경우에는 반드시 따옴표를 사용해야 하는 것을 기억하십시오.
# Bash / Ksh
match=b*r
[[ $foo == "$match" ]] # 좋은 코드
-
그리고 당신은 아래와 같은 코드를 본 적이 있을 것입니다.
# POSIX / Bourne
[ x"$foo" = xbar ] # 나쁘지 않지만 불필요한 코드
-
x"$foo" 와 같이 x가 붙은 코드는 매우 원시적인 코드이지만 몇몇 아주 오래된 쉘에서 실행하는 데 필요한 경우가 있습니다.
-
이 방법이 필요한 쉘은 POSIX에 따르지 않는 것으로 기억하시면 됩니다.
-
그러나 Heirloom Bourne Shell 같은 매우 오래된 쉘조차 이 위의 방법이 필요하지 않습니다. 이러한 과도한 명시성은 매우 드문 요구 사항이며 일반적으로 불필요합니다.
-
5. cd $ (dirname "$ f")
-
이것도 또 다른 인용 오류입니다. 변수를 확장하고, 명령 치환의 결과가 분할되어 경로명으로 확장될 것입니다. 따라서 이것에 대해서는 다음과 같이 따옴표를 수행해야합니다.
cd -P -- "$(dirname -- "$f")"
-
여기에서 명확하지 않은 것은 어떻게 따옴표를 중첩하는가에 대한 것입니다.
-
C 프로그래머는 이것을 첫 번째와 두 번째 큰 따옴표를 한 그룹으로 보고 3 번째와 4 번째를 또 다른 그룹으로 보게 될 것입니다.
-
그러나 Bash에서는 그러하지 않습니다. Bash는 큰 따옴표를 하나의 쌍으로 명령 치환에서 취급하게 되며 외부쌍과 내부 쌍으로 나눠지게 됩니다.
-
즉 다른 방법으로 이를 표현하면 parser는 명령 대체를 내부 레벨로 취급하고, 내부의 인용은 외부의 인용과는 별개로 분리되는 것입니다.
6. [ "$foo" = bar && "$bar" = foo ]
-
&&는 [ ]명령에서 사용할 수 없습니다. Bash는 [[ ]]또는 (( )) 외부의 &&에 대해 &&전후에서 명령을 두 개의 명령으로 분할하게 됩니다.
[ bar = "$foo" ] && [ foo = "$bar" ] # Right! (POSIX)
[[ $foo = bar && $bar = foo ]] # Also right! (Bash / Ksh)
-
함정 # 4에서 소개 한 전통적인 이유로 [ ]안의 변수와 상수를 반대하고 있습니다. 또한 [[ ]]의 경우도 마찬가지로 반대하고 있지만, 패턴으로 해석하는 것을 방지하기 위해 전개시 따옴표가 필요합니다.
-
또한 || 역시 마찬가지입니다. 결론적으로 두 경우 모두 대신에, [[ ]] 또는 두 개의 [ ] 명령을 사용하십시오.
-
만약 이를 방지하려면 다음과 같이 할 수도 있습니다.
[ bar = "$foo" -a foo = "$bar" ] # Not portable.
-
-a, -o 등의 바이너리 연산자는 POSIX 표준에 XSI 확장입니다. 이 모든 것은 POSIX-2008 obusolute 로 표시됩니다.
-
이들은 새로운 코드에서 사용하는 것을 권장하지 않습니다.
-
[ A = B -a C = D ]또는 -o를 사용할 때 실질적인 문제 중 하나는 POSIX가 4 개 이상의 인수와 test또는 [ ] 명령의 결과를 명시하지 않을 수 있습니다.
-
-
따라서 이는 아마도 대부분의 쉘에서 작동하겠지만 더이상 권장하지는 않습니다.
-
다시 반복하지만 POSIX 쉘에 대한 코드를 작성해야하는 경우 [[ ]]또는 두 개의 [ ]명령을 사용할 것이 권장됩니다.
7. [[ $foo > 7 ]]
-
여기에서는 여러 문제가 있습니다. 먼저 [[ ]]명령은 산술식을 평가하기 위해서만 사용하여서는 안됩니다.
-
대신 지원되는 테스트 연산자 중 하나 이상을 포함하는 경우에 해당 식을 사용 하여야 합니다.
-
기술적으로 보았을 때 당신이 [[ ]]연산자를 이용하여 계산을 하는 것은 가능하지만, 단지 수치 비교 (또는 다른 쉘의 산술 연산)를 하고 싶은 뿐인 경우에는 (( ))를 사용하는 것이 훨씬 낫습니다.
# Bash / Ksh
((foo > 7)) # Right!
[[ foo -gt 7 ]] # Works, but is pointless. Most will consider it wrong. Use ((...)) or let instead.
-
만약 당신이 [[ ]]내부에서 >연산자를 사용한다면, 그것은 문자열 비교로 처리됩니다. (즉 로케일의 순서로 조합하게 됩니다.)
-
이는 사실상 정수의 비교가 아니며, 실제로는 작동하는 것처럼 보이지만 당신의 기대한 방식으로 작동 하는 것은 아닙니다.
-
또한 >연산자를 [ ]속에서 사용하는 케이스는 더욱 나쁩니다. 즉 출력 자체가 방향 전환이 됩니다. 디렉터리 내부의 7이라는 파일을 수신하게 되고 $foo가 비어 있지 않으면 성공하게 되는 구조로 작동하게 됩니다.
-
만약 엄격한 POSIX 호환이 필요하고, (( )) 를 사용할 수 없는 경우, 기존 방식의 [ ]사용이 올바른 대안입니다.
# POSIX
[ "$foo" -gt 7 ] # Also right!
[ $((foo > 7)) -ne 0 ] # POSIX-compatible equivalent to ((, for more general math operations.
-
만약 산술 식에 입력 ( ((또는 let포함) 또는 수치 비교에 관한 [ ] 테스트 표현을 확인할 수 없다면, 반드시 항상 평가 표현 전에 입력을 검증 해야합니다.
# POSIX
case $foo in
*[^[:digit:]]*)
printf '$foo expanded to a non-digit: %s\n' "$foo" >&2
exit 1
;;
*)
[ $foo -gt 7 ]
esac
8. grep foo bar | while read -r; do ((count++)); done
-
위의 코드는 언뜻 보기에 문제가 없어보입니다.
-
단지 grep -c 등으로 구현할 수도 있지만, 이것은 확장성이 제한되기 때문입니다. 상기 제목과 같은 코드의 경우 다른 SubShell 에서 각각의 명령을 병렬로 실행할되므로 count를 바꾸는 것은 while루프의 외부에 이양되어 있지 않습니다.
-
이 동작은 여러면에서 거의 모든 Bash 초보자를 놀라게합니다.
-
POSIX는 subshell에서 평가 된 병렬의 마지막 요소를 지정하지 않습니다.
-
POSIX는 파이프 라인의 마지막 요소는 서브 쉘로 평가되고 있는지를 지정하지 않습니다.
-
ksh 버전 93 또는 Bash 버전 4.2 이상 shopt -s lastpipe을 실행하는 경우 while루프를 이 예와 같이 원래의 쉘 프로세스에서 실행하고 아무런 부작용 없이 사용할 수 있습니다.
-
따라서 휴대용 스크립트는 하나의 동작에 의존하지 않는 방식으로 설명해야합니다.
9. if [grep foo myfile]
-
많은 초보자들이 하는 실수 중 하나는 [ ]또는 [[ ]] 바로 앞에 if 키워드가 있는 매우 일반적인 패턴을 보고 if문이 잘못 되었다고 판단하는 것입니다.
-
그러나 이것은 실제로 그렇지 않습니다. 위의 코드는 실제로 if명령을 가지고 있습니다. [ ] 명령은 if 명령 구문 마커가 없습니다. 이것은 결과적으로 아래의 명령과 동일합니다.
# POSIX
if [ false ]; then echo "HELP"; fi
if test false; then echo "HELP"; fi
-
위의 두 명령은 동일합니다. 모두 인수 false가 비어 있지 않은지 확인합니다. 두 경우 모두 HELP가 항상 표시됩니다. 쉘 구문을 다른 프로그래밍 언어에서 유추 한 프로그래머는 놀랄 것입니다.
-
if문 구문은 다음과 같습니다.
if COMMANDS
then <COMMANDS>
elif <COMMANDS> # optional
then <COMMANDS>
else <COMMANDS> # optional
fi # required
-
다른 보통의 간단한 명령뿐만 아니라 위의 코드는 인수를 취하고 있습니다. 그리고 if 다른 명령을 포함하여 구성 명령, [ ] 구문 자체가 없습니다!
-
제로 이상의 옵션 elif의 섹션 1 옵션의 else마디가 있을지도 모릅니다.
-
if구성 명령은 두 가지 혹은 그 이상의 명령의 목록을 포함하고 then, elif또는 else에 의해 각각 구분 된 fi키워드에 의해 종료됩니다.
-
첫 번째 섹션의 마지막 명령의 종료 상태와 각각의 다음 elif항목이 각각 어떤 then마디가 평가되는지를 결정합니다.
-
그리고 elif은 then절 중 하나가 실행되기 전에 평가됩니다. 만약 평가되는 then부분이 없다면 else절을 가지고, 또는 else이 주어지지 않으면 if블록은 완료하고 if명령 전체 0 (true)을 반환합니다.
-
만약 grep명령의 출력에 의거 뭔가를 확인하고 싶다면, 괄호와 대괄호, 백틱이나 다른 어떤 구문에서도 쓸 필요가 없습니다! 아래 처럼 그냥 if뒤에 grep명령으로 사용 하면 됩니다.
if grep -q fooregex myfile; then
...
fi
-
만약 grep이 myfile행에 매치한다면, 종료 코드 0 (true)이되고, then마디가 실행됩니다. 그렇지 않은 경우, 즉 일치하는 것이 없는 경우 grep는 0이 아닌 코드를 반환하고 if전체 명령은 0이 됩니다.
10. if [bar="$foo"]; then ...
[bar="$foo"] # 잘못된 코드
[ bar="$foo" ] # 이것 역시 잘못된 코드
-
앞의 예에서 설명했듯이, [ ] 명령입니다, 다른 간단한 명령처럼 Bash는 명령에 공백이 따르기 때문에 첫 번째 인수 이후 다른 확장 등을 기대할 수 있습니다. 즉 공백이 없이 모든 것을 함께 수행 할 수 없습니다! 아래에 올바른 방법이 있습니다.
if [ bar = "$foo" ]; then ...
-
bar, =, "$foo"의 전개 결과 ]는 [ 명령에 있어서 각각 다른 인수입니다. 각각의 인수 쌍 사이에 공백이 없이 할 수 없으며, 이에 따라 Shell은 각각의 인수의 시작과 끝이 어디인지 알 수 있습니다.
참고 문헌
[논문]
- 없음
[보고서]
- 없음
[URL]
- 없음
문의사항
[기상학/프로그래밍 언어]
- sangho.lee.1990@gmail.com
[해양학/천문학/빅데이터]
- saimang0804@gmail.com
본 블로그는 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음
'프로그래밍 언어 > Shell Script' 카테고리의 다른 글
[Shell Script] 쉘 스크립트 (bash) 개발자가 빠지기 쉬운 함정 (3) (0) | 2020.03.23 |
---|---|
[Shell Script] 쉘 스크립트 (bash) 개발자가 빠지기 쉬운 함정 (2) (0) | 2020.03.22 |
[Shell Script] 쉘 스크립트에서 문자열 길이를 계산하는 4가지 방법 (0) | 2020.03.13 |
[Shell Script] 쉘 스크립트에서 수치 계산 소개 (0) | 2020.03.11 |
[ShellScript] 쉘 스크립트 sed 문자열 치환 시 "echo" 대신 "Here String (<<<)" 사용 방법 (2) | 2020.01.19 |
최근댓글