정보
-
업무명 : 쉘 스크립트를 사용할 때의 팁
-
작성자 : 박진만
-
작성일 : 2020-07-17
-
설 명 :
-
수정이력 :
내용
[특징]
-
쉘 스크립트를 사용할 때의 유용한 팁 모음
[활용 자료]
-
없음
[자료 처리 방안 및 활용 분석 기법]
-
없음
[사용법]
-
작업 환경 구축
-
소스 코드 작성 및 실행
-
실행 결과 확인
[사용 OS]
-
Linux (CentOS v7.0)
- VMware Workstation Pro v15.5
[사용 언어]
-
Bash Script
소스 코드
[코딩 스타일]
-
여기에 쓰여져있는 내용은 어디 까지나 필자의 취향이며 거의 완전히 주관적임,
-
그러나 경험적으로 최적이라고 판단한 스타일도 있으므로 개인적으로 추천하는 바임,
[스크립트의 헤더를 만들기]
- 최근에는별로 사용되는 곳이 없을지도 모르지만, @(#) 등과 같은 표현과 스크립트의 사용 방법, 요약 파일의 머리 부분에 주석으로 둘 수 있다.
#!/bin/bash
#
# @(#) hoge.sh ver.1.0.0 2008.04.24
#
# Usage:
# hoge.sh param1 param2
# param1 - 매개변수1
# param2 - 매개변수2
#
# Description:
# hoge.shスクリプトです.
#
###########################################################################
# 실제 처리 부분
echo "start..."
[들여 쓰기는 공백 두개가 적절]
-
공백 4 개가 이상적이지만, 쉘 스크립트는 파이프 연산자가 있기 때문에 공백을 2개정도만 사용하기를 권장함.
{
touch hoge
ls hoge
rm hoge
}
[ |과 <> 전후의 공간 ]
-
| 앞뒤로 하나의 공백을 비운다.
hoge | fuga
-
> 앞에 공백을 하나 비운다.
hoge >hoge.txt
-
>> 뿐만 아니라 앞에 공백을 하나 비운다.
hoge >>hoge.txt
-
<, <<도 >마찬가지로 전에 공백을 하나 비운다.
hoge <hoge.txt
hoge <<'__EOT__'
-
덧붙여서, |나 >, >>그리고 <, <<앞뒤에 공백이 전혀 없어도 문법 오류는 아니다.
[if와 then은 동일한 행의 세미콜론 뒤에만 공백을 둔다]
-
then을 if 뒤에 사용하면 if 와 fi이 갖추어지므로 가독성이 좋아진다. ( if과 fi대응하는 단어의 관점).
-
then이 if바로 아래 행에 있는 경우 가독성이 떨어지고 행이 낭비된다.
if [ "$str" = "hoge" ]; then
echo "strはhoge."
fi
[for, while의 do와 done 의 경우]
-
if의 then경우와는 달리, do은 for, while바로 아래 줄에 기술한다. 즉 do와 done이 짝을 맞춰 정렬되기 때문에 가독성이 높아진다. ( do과 done대응하는 단어의 관점)
for i in 1 2 3 4 5
do
total=`expr $total + $i`
done
while [ $total -lt 10 ]
do
total=`expr $total + 1`
done
[함수 정의 function은 생략한다]
-
함수는 function Hoge()로 정의하지만,이 경우 function는 생략 가능하다. function을 생략해도 가독성이 떨어졌다고 느낄 수 없었기 때문에 원칙적으로 생략할 수 있다.
-
※ 쉘 스크립트 초보자 function를 명시 적으로 지정되어있는이 알기 쉬운 것으로, 필자는 초보자를 배려하고 최근에는 function생략하지 않도록 하고 있다.
-
반대로 초보자가 아니라면 원칙적 생략이 좋다고 생각한다. 어쨌든 명시하거나 생략하거나 어느 한쪽으로 통일한다.
Hoge()
{
echo "함수 Hoge() 이 호출됨"
return 0
}
[if문은 생략하지 말자]
-
명령이 성공할 때만 다음 명령을 실행하는 &&를 사용하는 응용 프로그램은 [$ cnt -eq 10] && exit 0과 같은 if 문과 동등한 프로세스를 쉽게 설명 할 수 있다.
-
그러나 해당 경로에 처리를 추가하려는 경우 if 문의 생략은 프로세스 분기점을 이해하기 어렵게 만드는 부정적인 영향을 미치며, 간단한 설명 이외의 장점은 거의 없다.
if [ $cnt -eq 10 ]; then
# 여기서 추가 처리.
exit 0
fi
[비슷한 연속적인 파일로 리디렉션 그룹화하기]
-
연속 된 명령의 실행 결과 출력이 동일한 파일인 경우 {}에 명령 그룹화하고, 해당 그룹의 출력을 파일로 리디렉션 할 수 있다.
-
즉 echo "hoge" >>logfile.log과 같이 각 명령마다 리디렉션하지 않도록 한다.
{
echo "hoge"
echo "fuga"
echo "foo"
echo "bar"
} >>logfile.log
[여러 줄의 일괄 주석]
-
이 Tips은 실제 사용하는 경우 내부적으로는 엉뚱하게 처리되기 때문에 비추천 한다. 그러나 히어 닥의 이해를 위해, 또한 응용력 향상을 위해 읽어 두는 것을 추천한다.
-
쉘 스크립트에서 여러 줄을 주석 처리하는 경우는 대상이되는 각 행의 선두에 #쓸 필요가있다. 즉 C 언어 등의 /* */에 해당하는 여러 행을 한 번에 코멘트하는 기능은 존재하지 않는다.
-
하지만 null 명령과 히어 닥을 응용하여 여러 행 일괄 주석이 가능하게 된다.
-
: (null 명령)은 어떠한 처리를하지 않고 종료하는 명령 에 주석하고 싶은 부분을이 명령에 히어 닥으로하는 것으로, 해당 부분의 처리를 무효화하고 결과적으로 주석 처리 한 경우와 같은 결과를 얻을 수 있다.
: <<'#__COMMENT_OUT__'
echo "여기의 처리는 모두 무효가 된다."
# hoge 파일은 생성되지 않는다.
touch hoge
# # 명령 치환도하지 않는다.
dummy=`touch fuga`
#__COMMENT_OUT__
-
히어 닥은 종료 문자 (※)를 인용 혹은 탈출하여 히어 문서를 완전히 문자열로 취급 할 수 있다 .
-
※ 히어 닥이 종료 부분을 식별하는 캐릭터 라인은 #__COMMENT_OUT__.
-
따라서 변수 확장이나 역 따옴표에 의한 명령 치환도 발생하지 않는다. 즉 어떠한 처리를하지 않는 단순한 문자열로 널 명령의 표준 입력으로 전달된다.
-
널 명령은 아무것도 처리하지 않은 명령이므로, 표준 입력 장치가 있어도이를 무시하고 종료한다. 결과적으로 어떠한 처리를하지 않는 것, 주석 한 경우와 같은 결과가 된다.
-
또한 종료 문자의 시작 부분에 #사용하고 있기 때문에, 주석을 해제하려면, 널 명령 앞에 #를 작성 만 하면 된다 .
-
이를 통해 히어 닥의 시작이 비활성화되면 아래쪽에있는 종료 문자는 미리 선두에 #이 붙어 있기 때문에 자동으로 코멘트가된다 .
[변수 명명 규칙]
-
필자는 다른 언어로도 그렇듯이으로 상수는 대문자 , 상수가 아닌 변수는 소문자 를 사용하도록 하고 있다. 변수 이름이 여러 단어로 이루어진 경우 "_"로 결합한 이른바 뱀 케이스를 사용하는 (eg variable_name).
-
또한 상수는 readonly 선언을 추가하여 읽기 전용으로 할 것을 권장한다 .
# for 루프의 변수는 비 상수이므로 문자가 됨
for i in 1 2 3 4 5
do
echo $i
done
# read 명령 처리를 일시 중지
echo -e "Enter to continue...\c"
read dummy
# 상수는 readonly 선언 된 대문자
readonly MAX_COUNT=10
readonly FILENAME="hoge.txt"
-
일회용이 아닌 상수가 아닌 변수 이름은 a및 버튼 b등의 의미없는 이름 temp등 사용 목적이 명확하지 않은 변수 이름은 사용하지 않는다. temp켜져 사용 뻔 아무런 temp인지 모르기 때문에 가독성이 현저히 저하한다. 원래 변수는 아래에서 모든 temp이다.
[파일 및 디렉토리 이름의 상수화]
- 유지 보수성 향상을 위해 파일 및 디렉토리 이름을 스크립트 상향 변수로 정의하고 그 지정은 변수를 사용하도록하고있는 사람이 대부분이라고 생각하지만, 이 명명 규칙이 개인에 따라 제각각 있기 때문에 (원래 전혀 규칙 성이없는 변수 이름을 사용하는 사람도있다), 가독성의 저하와 그에 따른 버그로 이어지는 경우가 많다.
- 필자는 파일 이름, 디렉터리 이름을 저장하는 변수 이름은 다음 명명 규칙을 사용하고 있다.
[파일 이름]
- 전체 경로 (상대 경로) 스펙 대신 단순히 파일 이름 만 지정하는 경우 는 HOGE_FILENAME(나 HOGE_FILENM)와 같이 FILENAME 변수 이름에 사용한다.
- 반대로 전체 경로 (상대 경로) 지정 파일 이름을 설정하는 경우 에는 NAME이없이 단순하게 HOGE_FILE한다.
- readonly로 정의하고 완전히 상수로 취급 .
[디렉토리 이름]
- 전체 경로 (상대 경로) 스펙 대신 단순히 디렉토리 이름만을 설정하는 경우 는 HOGE_DIRNAME(나 HOGE_DIRNM)와 같이 DIRNAME 을 변수 이름에 사용한다.
- 반대로 전체 경로 (상대 경로) 지정 디렉토리 이름을 설정하는 경우 에는 NAME이없이 단순하게 HOGE_DIR한다.
- 전체 경로 (상대 경로) 지정 디렉터리 이름 또는 단순히 디렉토리 이름만을 설정하는 경우도 끝에 / 부가 하지 않는다.
- readonly로 정의하고 완전히 상수로 취급 .
-
예를 들어 이 명명 규칙을 사용하여 로그 파일과 저장 디렉토리를 변수로 정의하면 다음과 같다.
# 디렉토리 이름
readonly LOG_DIRNAME="hoge"
# 전체 경로 지정 디렉토리 이름
readonly LOG_DIR="/tmp/${LOG_DIRNAME}"
# 파일 이름
readonly LOG_FILENAME="fuga.log"
# 전체 경로 지정 파일 이름
readonly LOG_FILE="${LOG_DIR}/${LOG_FILENAME}"
-
간단히 설명하면,
-
NAME (또는 NM)이 가진 경우 이름 만 (경로 제외)
-
NAME (또는 NM)이 붙지 않는 경우는 전체 (경로 포함 대상 등으로 그대로 사용할 수있다) 가된다.
-
또한 각 변수를 readonly를하고있는 것으로, 처리 도중에 값을 변경하고 예상치 못한 버그가 발생하는 것을 억제 하고있다.
[함수의 동작을 변수로 변경]
-
함수의 동작을 변경하는 방법은 매개 변수가 일반적이지만 함부로 매개 변수화하면 매개 변수 처리가 복잡하고 가독성이 떨어지거나 버그로 이어진다.
-
매개 변수화하는 정도도 섬세한 동작 변경은 변수화 함수 외부에서 정의하면 좋다. 예를 들어 다음과 같은 함수를 만들어 본다.
Message()
{
_SEPARATOR=${_SEPARATOR:-"-----"}
echo "$1"
echo "$_SEPARATOR"
echo "$2"
return 0
}
-
이 함수를 그대로 실행 해 본다.
$ Message hoge fuga
hoge
-----
fuga
-
다음 제 1 파라미터 및 제 2 파라미터의 경계 부분의 문자열을 변경 본다. 변경하려면 함수 호출 전에 변수 _SEPARATOR 변경 후 문자열을 설정한다.
$ _SEPARATOR="*****"
$ Message hoge fuga
hoge
*****
fuga
-
이러한 변수를 사용하는 함수의 동작 변경은 매개 변수 처리를 복잡하지 않고 간단하게 처리를 변경할 수있다.
-
※ 물론 같은 처리를 함수의 출력에 sed 등을 사용 가도 상관 없지만이 방법이 더 간단하고, 변수를 한 번 정의하면 동작을 변경할 수있는 점에서 추천하고 있다.
-
이번에 사용한 ${_SEPARATOR:-"-----"}은 변수 _SEPARATOR가 사용되지 않거나 혹은 설정 값이 하늘 ( "")이면 기본값 ( -----)를 사용하는 의미이다.
-
또한 변수 이름 선행 밑줄은 함수 내에서 사용되는 변수임을 의미한다 (필자의 개인적인 코딩).
-
쉘 스크립트의 변수는 기본적으로 모든 전역 변수가되기 때문에, 함수 내에서 사용되는 변수, 특히 local로 선언되지 않은 변수는 함수 외부 변수와의 충돌을 피하기 위해 변수 이름 앞에 밑줄을 붙이도록하면 좋다.
-
※ local선언 한 경우는 특히 그 필요는 없지만, 함수 내에서 사용되는 변수는 변수 이름을 일률적으로 밑줄로 시작하게하는 것이 좋다 .
-
같은 방법으로 임의의 값을 변경 가능한 값으로 할 수 있지만, 당연히 남용하면 버그로 이어질 때문에 정말 필요한 값만 변경 가능하게한다.
[루프를 쓰지 마라]
-
물론 전혀 사용하지 말라는 의미가 아니라 낭비 루프를 사용하지 마라 는 뜻이다. 예를 들어 다음과 같은 경우를 생각한다.
for str in "hoge" "fuga"
do
echo "$str" >${str}.txt
done
-
가끔 이런 식으로 사용 하고있는 사람을 볼 수 있는데, 문제가되는 것은 hoge과 fuga이 더 이상 증가하는 것은 아닌데, 일부러 for 루프를 사용하고 있다는 점 이다.
-
이런 처리 루프를 사용할 필요가 전혀 없다. 유사한 처리가 100 개, 200 개라면 이야기는 다르지만 2 ~ 5 개 정도면 그대로 적는 것이, 훨씬 가독성이 높다.
echo "hoge" >hoge.txt
echo "fuga" >fuga.txt
-
조금 늘어나도 루프는 사용하지 않고 그대로 기술해야한다.
echo "hoge" >hoge.txt
echo "fuga" >fuga.txt
echo "foo" >foo.txt
echo "bar" >bar.txt
[루프 중첩보다 함수]
-
※ 이 Tips는 비교적 큰 스크립트를 상정하고 있음.
-
예를 들어 다음과 같은 경우를 생각하면 좋겠다.
for i in 1 2 3
do
# 매우 긴 처리 1
・・・
for j in "hoge" "fuga"
do
# 매우 긴 처리 2
・・・
done
done
-
루프 중첩는 유지 보수가 낮은 데다 가독성면에서 불리 하므로 함수로 대체 하는 것을 고려하면 좋다.
-
루프 중첩 함수로 대체하면 다음과 같이된다.
SubNestedLoop()
{
# 매우 긴 처리
・・・
return 0
}
SubNestedLoop 1 "hoge"
SubNestedLoop 2 "hoge"
SubNestedLoop 3 "hoge"
SubNestedLoop 1 "fuga"
SubNestedLoop 2 "fuga"
SubNestedLoop 3 "fuga"
-
이렇게하면 루프 중첩에 비해 상당히 쉽게 읽을 수있을 것이다.
-
포인트는 함수의 정의를 호출 부분의 바로 윗쪽으로 할 것이다. 이렇게함으로써 소스를 위에서 아래로 읽어 나가는 것만으로 처리를 이해하는 것이 가능하게된다 (호출 부분 바로 위가 아니라 스크립트 위쪽으로 정의하면 호출되는 곳에서 위로 스크롤하여 읽지 않으면 안된다).
-
여러 곳에서 호출되는 함수는 스크립트의 위쪽에 정의해야하는데, 특정 장소에서만 호출되지 않은 함수는 바로 위에 정의하면 좋다.
[파이프의 끝은 서브 쉘]
-
bash에서 파이프에 결합 된 그 끝이 서브 쉘에서 실행된다는 사양이 존재하고있다.
-
다음에 예 (pipe_bug.sh)를 보자
#!/bin/bash
str="dummy"
cat <<'__EOT__' >temp.$$
hoge hoge
fuga fuga
foo foo
bar bar
__EOT__
cat temp.$$ | while read line
do
str="$line"
done
rm -f temp.$$
echo $str
-
이 스크립트는 변수 str가 미리 문자열 "dummy"을 설정 한 후, cat 명령의 출력을 파이프로 while 루프에 부어 read 명령으로 흐르는 데이터를 한 줄씩 읽어보자. 또한 가져온 값 (행)에서 변수 str의 값을 무시하고있다.
-
아마 많은 사람들은이 스크립트의 실행 결과는 while 루프에서 변수 str의 마지막에 설정되는 값, 즉 'bar bar "가 출력되는 것을 기대하는 것이다.
-
하지만 실제 실행 결과는 다음과 같이 된다.
$ ./pipe_bug.sh
dummy
-
이것은 전술 한 바와 같이, 파이프로 결합한 앞의 while 루프가 서브 쉘에서 실행되고 있기 때문이다. 서브 쉘에서 실행 됨으로써 변수 str은 while 루프 내부와 외부에서 범위가 다르기 때문이다.
-
따라서 while 루프에서 설정된 값 (루프 변수 str) while 루프 종료와 함께 폐기되고 마지막 echo 명령의 출력 결과는 현재 쉘에서 변수 str 설정 한 값 "dummy"가된다.
-
while 루프를 빠져 나오면 서브 쉘이 종료하므로 다음과 같이 export해도 원래 변수가 소멸되기 때문에 결과는 변하지 않는다.
-
※ pipe_bug.sh의 while 부분 만 다시 작성
cat temp.$$ | while read line
do
str="$line"
export str
done
-
※ export를 추가 한 경우의 실행 결과
$ ./pipe_bug_export.sh
dummy
-
export해서 while 루프의 변경은 반영되지 않을 것을 확인할 수있다.
-
덧붙여서이 while 루프에서 exit도 무효가된다 (정확하게는 exit 명령을 실행하면 서브 쉘을 종료하기 만하면 현재 쉘이 종료한다는 의미).
-
※ pipe_bug.sh의 while 부분 만 다시 작성
cat temp.$$ | while read line
do
str="$line"
exit
done
-
※ exit를 추가 한 경우의 실행 결과
$ ./pipe_bug_exit.sh
dummy
-
exit를 실행하면 스크립트는 종료되지 않고 마지막 echo 명령까지 실행되는 것을 확인할 수 있다.
-
이번 같은 경우 문제를 해결하려면 파이프 대신 리디렉션을 사용하면 좋다 .
-
※ pipe_bug.sh의 while 부분 만 다시 작성
while read line
do
str="$line"
exit
done <temp.$$
-
리디렉션이면 while 루프는 현재 쉘의 상태가되기 때문에, 예상 결과 ( "bar bar"가 출력되는)가된다.
-
실제 실행 결과는 다음과 같다.
$ ./pipe_bug_redirect.sh
bar bar
참고 문헌
[논문]
- 없음
[보고서]
- 없음
[URL]
- 없음
문의사항
[기상학/프로그래밍 언어]
- sangho.lee.1990@gmail.com
[해양학/천문학/빅데이터]
- saimang0804@gmail.com
본 블로그는 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음
'프로그래밍 언어 > Shell Script' 카테고리의 다른 글
[ShellScript] 쉘 스크립트 사용법 (배열을 사용하는 법) (1) | 2020.07.18 |
---|---|
[ShellScript] 쉘 스크립트 사용법 (변수를 사용하는 법) (2) | 2020.07.17 |
[Shell Script] 쉘 스크립트 (bash) 개발자가 빠지기 쉬운 함정 (3) (0) | 2020.03.23 |
[Shell Script] 쉘 스크립트 (bash) 개발자가 빠지기 쉬운 함정 (2) (0) | 2020.03.22 |
[Shell Script] 쉘 스크립트 (bash) 개발자가 빠지기 쉬운 함정 (1) (2) | 2020.03.21 |
최근댓글