많은 Bash 스크립트는 엄격한 명령형 스타일로 작성돼요. 명령어를 하나 실행하고, 그다음 명령어를 실행하고, 이런 식이죠. 때로는 개념적으로 하나의 목적을 수행하는 명령어들을 한데 묶어야 할 때가 있어요. 이럴 때 필요한 것이 바로 _함수_예요.
함수는 이렇게 정의해요:
my_function () {
COMMANDS
}
비어 있는 괄호는 그냥 함수를 정의하고 있다는 표시일 뿐이에요. 괄호 안에는 아무것도 들어가지 않아요.
함수는 한 번 정의하고 나면 다른 명령어(내장이든 아니든)와 똑같이 동작해요.
다른 명령어에 그러듯 함수에도 _인자_를 넘길 수 있어요.
함수 안에서는 _위치 매개변수_인 $1, $2 등을 사용해서 인자에 접근해요.
(변수 개념에서 위치 매개변수에 대해 배웠던 걸 떠올려 봐요.)
특수 매개변수 $0은 함수 안에서도 바뀌지 않아요. 여전히 실행 중인 스크립트의 이름이에요.
현재 실행 중인 함수는 $FUNCNAME 변수로 자기 이름에 접근할 수 있어요.
매뉴얼의 3.4.2 특수 매개변수를 참고해요.
함수 안에서 변수를 정의할 수 있어요.
local 명령어로 변수를 선언하면 그 변수의 _스코프_는 현재 함수(그리고 그 함수가 호출하는 함수들)로 제한돼요.
그렇지 않으면 변수는 _전역 스코프_에 놓여요.
지역 변수는 전역 변수와 같은 이름을 가질 수 있어요. 이때 지역 변수는 전역 변수를 "가려요". 예를 들어 함수에서 선언한 지역 변수는 같은 이름의 전역 변수를 숨겨요. 참조와 대입이 모두 지역 변수를 가리키고, 전역 변수는 그대로 남아 있어요. 함수가 반환하면 전역 변수가 다시 보여요.
x=5
myfunc () {
local x=100
echo "in my function, $x == 100"
}
echo "in the global scope, $x == 5"
myfunc
echo "back in the global scope, $x == 5"
출력은 다음과 같아요.
in the global scope, 5 == 5
in my function, 100 == 100
back in the global scope, 5 == 5
함수 안에서는 _호출자_의 스코프에 있는 변수에 접근할 수 있어요. 즉 전역 변수뿐 아니라, 호출자(또는 호출자를 호출한 어떤 함수)에서 선언한 지역 변수도 사용할 수 있어요.
엄밀히 말하면 "전역"이라는 표현이 정확한 건 아니에요. 함수에서 변수를 확장할 때 Bash는 그 변수 이름이 선언된 함수를 찾기 위해, 전역 스코프에 이르기까지 호출 스택을 거슬러 올라가요.
다음 예제는 매뉴얼의 셸 함수 부분을 각색한 거예요:
func1() {
local var='func1 local'
func2
}
func2() {
echo "In func2, var = $var"
}
var=global
func1
func2
출력은 다음과 같아요:
In func2, var = func1 local
In func2, var = global
마찬가지로 변수에 값을 _대입_하면 그 변수가 선언된 스코프에서 대입돼요. 이런 "멀리서 벌어지는 일"은 코드를 따라가기 어렵게 만들 수 있어요. 변수가 어디에서 값을 대입받았는지 항상 분명한 건 아니니까요.
FUNCNAME 배열 변수를 사용하면 호출 스택을 살펴볼 수 있어요.
함수는 다른 명령어와 마찬가지로 _종료 상태_를 가져요. 기본적으로 함수의 상태는 _마지막으로 실행된 명령어_의 종료 상태예요.
return 명령어를 사용하면 특정 종료 상태로 함수에서 반환할 수 있어요.
check_password () {
if [[ $1 == "secret" ]]; then
return 0
else
return 1
fi
}
read -sp "Enter your password: " pass
if check_password "$pass"; then
echo "Correct!"
else
echo "Wrong password."
fi
인자 없이 return을 사용하면 마지막으로 실행된 명령어의 상태를 반환해요.
check_password 함수는 다음과 같이 단순화할 수 있어요:
check_password () { [[ $1 == "secret" ]]; }
[[...]] 조건부 구조는 종료 상태를 가져요: "true"면 0, "false"면 1이에요.{...} 그룹 구조는 닫는 중괄호 앞에 줄바꿈이나 세미콜론이 있어야 해요.함수의 반환 상태는 그냥 숫자예요. 그럼 함수는 어떻게 출력을 만들어 낼까요?
함수는 표준 출력으로 출력할 수 있어요. 익숙한 _명령어 치환_으로 그 출력을 담아 봐요:
d6 () { echo "$(( 1 + RANDOM % 6 ))"; }
die=$( d6 )
echo "You rolled a $die."
출력을 담는 중에도 함수의 종료 상태를 함께 사용할 수 있어요.
roll () {
local n=$1
if (( 4 <= n && n <= 20 )); then
echo "$(( 1 + RANDOM % n ))" # exit status is 0
else
return 1
fi
}
read -p "How many faces does your die have? " faces
if die=$( roll "$faces" ); then
echo "You rolled a $die."
else
echo "I can't roll a die with $faces faces."
fi
함수는 자기 자신을 재귀적으로 호출할 수 있어요. 기본적으로 재귀 깊이에는 제한이 없어요.
예를 들어 볼까요:
fibonacci() {
local n=$1
if (( n <= 1 )); then
echo "1"
else
local a=$(fibonacci "$(( n - 1 ))")
local b=$(fibonacci "$(( n - 2 ))")
echo "$(( a + b ))"
fi
}
for i in {1..10}; do fibonacci "$i"; done
# => 1
# => 2
# => 3
# => 5
# => 8
# => 13
# => 21
# => 34
# => 55
# => 89
FUNCNEST 변수로 재귀 깊이를 제어할 수 있어요.
bash -c '
recur() {
echo $1
recur $(($1 + 1))
}
FUNCNEST=5
recur 1
'
1
2
3
4
5
environment: line 1: recur: maximum function nesting level exceeded (5)