23.1. 복잡 함수와 함수의 복잡성(Complex Functions and Function Complexities)

함수는 인자를 받아 들일 수 있고 다음 작업들을 위해서 종료 상태를 리턴할 수도 있습니다.

function_name $arg1 $arg2

함수는 자신에게 넘어온 인자를 위치 매개변수처럼, 인자의 위치로 참조하는데 예를 들면, 첫번째 인자는 $1, 두번째 인자는 $2 등입니다.

예 23-2. 매개변수를 받는 함수

#!/bin/bash

func2 () {
   if [ -z "$1" ]                    # 첫번째 매개변수 길이가 0 인지 확인.
   then
     echo "-첫번째 매개변수 길이가 0 입니다.-"  # 매개변수가 없을 경우에도.
   else
     echo "-첫번째 매개변수는 \"$1\" 입니다.-"
   fi

   if [ "$2" ]
   then
     echo "-두번째 매개변수는 \"$2\" 입니다.-"
   fi

   return 0
}

echo
   
echo "매개변수 없이 불러보죠."   
func2                          # 매개변수 없이 호출
echo


echo "길이가 0 인 매개변수를 넘김."
func2 ""                       # 길이가 0 인 매개변수로 호출
echo

echo "널 매개변수를 넘김."
func2 "$uninitialized_param"   # 널 매개변수로 호출
echo

echo "매개변수 한 개를 넘김."   
func2 first           # 매개변수 한 개로 호출
echo

echo "매개변수 두 개를 넘김."   
func2 first second    # 매개변수 두 개로 호출
echo

echo "\"\" \"second\" 를 넘김."
func2 "" second       # 첫번째 매개변수 길이는 0
echo                  # 두번째 매개변수는 아스키 문자열

exit 0

참고: 다른 프로그래밍 언어들과는 다르게 쉘 스크립트는 보통, 인자를 값에 의해서 받아 들입니다. [1] 실제로는 포인터인 변수 이름이 함수의 매개변수로 넘어간다면 문자열 그대로 취급될 것이기 때문에 역참조가 불가능해 집니다. 함수는 인자를 문자 그대로 해석합니다.

종료와 리턴

종료 상태

함수는 종료 상태라고 부르는 값을 리턴합니다. 종료 상태는 return 문에 의해서 분명하게 표현되거나 그 함수안에서 마지막으로 실행된 명령어의 종료 상태를 함수 자신의 종료 상태로 가져갑니다(성공이면 0, 실패라면 0이 아닌 에러 코드). $?를 참조해서 종료 상태를 알아낼 수도 있습니다. 이 방법으로써 C 함수와 비슷하게 쉘 스크립트 함수도 효과적으로 "리턴 값"을 가질 수 있습니다.

return

함수를 끝냅니다. return 명령어 [2] 는 임의의 정수 인자를 가질 수 있는데 이는 이 함수를 부른 쪽에게 함수의 "종료 상태"를 알려 주고, 또한 이 종료 상태는 $? 변수에 할당 됩니다.

예 23-3. 두 숫자중 큰 수 찾기

#!/bin/bash
# max.sh: 두 정수중 큰 수 찾기.

E_PARAM_ERR=-198    # 함수로 2개 미만의 매개변수가 넘어 왔을 때.
EQUAL=-199          # 두 매개변수가 같을 경우의 리턴값.

max2 ()             # 두 숫자중 큰 수를 리턴.
{                   # 주의: 비교할 숫자는 257보다 작아야 합니다.
if [ -z "$2" ]
then
  return $E_PARAM_ERR
fi

if [ "$1" -eq "$2" ]
then
  return $EQUAL
else
  if [ "$1" -gt "$2" ]
  then
    return $1
  else
    return $2
  fi
fi
}

max2 33 34
return_val=$?

if [ "$return_val" -eq $E_PARAM_ERR ]
then
  echo "매개변수가 두 개 필요합니다."
elif [ "$return_val" -eq $EQUAL ]
  then
    echo "두 숫자는 같습니다."
else
    echo "두 숫자중 큰 수는 $return_val 입니다."
fi  

  
exit 0

# 독자용 연습문제(초급):
# 대화모드 스크립트로 변경해 보세요.
# 사용자에게 입력(두 숫자)을 물어보게 하면 됩니다.

작은 정보: 함수가 문자열이나 배열을 리턴하기 위해서는 전용 변수를 쓰면 됩니다.
count_lines_in_etc_passwd()
{
  [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
  # /etc/passwd 를 읽을 수 있으면 REPLY 에 줄 수를 세팅.
  # 매개변수 값과 상태 정보를 같이 리턴.
}

if count_lines_in_etc_passwd
then
  echo "/etc/passwd 에는 $REPLY 개의 줄이 있습니다."
else
  echo "/etc/passwd 의 줄 수를 셀 수가 없습니다."
fi

# Thanks, S.C.

예 23-4. 숫자를 로마 숫자로 바꾸기

#!/bin/bash

# 아라비아 숫자를 로마 숫자로 바꾸기
# 범위: 0 - 200
# 조잡하지만 동작은 합니다.

# 처리 가능한 숫자의 범위를 늘리거나 스크립트의 기능을 향상시키는 것은
# 독자 여러분을 위해 연습문제로 남겨 놓습니다.

# 사용법: roman number-to-convert

LIMIT=200
E_ARG_ERR=65
E_OUT_OF_RANGE=66

if [ -z "$1" ]
then
  echo "사용법: `basename $0` number-to-convert"
  exit $E_ARG_ERR
fi  

num=$1
if [ "$num" -gt $LIMIT ]
then
  echo "처리 가능 범위 초과!"
  exit $E_OUT_OF_RANGE
fi  

to_roman ()   # 함수를 부르기 전에 미리 선언해 줘야 됩니다.
{
number=$1
factor=$2
rchar=$3
let "remainder = number - factor"
while [ "$remainder" -ge 0 ]
do
  echo -n $rchar
  let "number -= factor"
  let "remainder = number - factor"
done  

return $number
       # 독자용 연습문제:
       # 이 함수가 어떻게 동작하는지 설명해 보세요.
       # 힌트: 연속적 빼기에 의한 나누기.
}
   

to_roman $num 100 C
num=$?
to_roman $num 90 LXXXX
num=$?
to_roman $num 50 L
num=$?
to_roman $num 40 XL
num=$?
to_roman $num 10 X
num=$?
to_roman $num 9 IX
num=$?
to_roman $num 5 V
num=$?
to_roman $num 4 IV
num=$?
to_roman $num 1 I

echo

exit 0

예 10-26 도 참고.

중요: 함수가 리턴할 수 있는 가장 큰 양수는 256입니다. return 명령어는 이 제한 사항의 원인이 되는 종료 상태의 개념에 아주 밀접하게 연결되어 있습니다. 다행히도, 함수에서 아주 큰 정수를 리턴해야 하는 경우가 생기면 이런 제한 사항을 해결할 방법이 있습니다.

예 23-5. 함수에서 큰 값을 리턴하는지 테스트하기

#!/bin/bash
# return-test.sh

# 함수에서 리턴할 수 있는 가장 큰 양수는 256 입니다.

return_test ()         # 무조건 넘어온 것을 리턴.
{
  return $1
}

return_test 27         # o.k.
echo $?                # 27 리턴.
  
return_test 256        # 역시 o.k.
echo $?                # 256 리턴.

return_test 257        # 에러!
echo $?                # 1 리턴(자질구레한 에러용 코드 리턴).

return_test -151896    # 하지만, 큰 음수는 됩니다.
echo $?                # -151896 리턴.

exit 0

살펴본 것처럼, 함수는 큰 음수값을 리턴할 수 있습니다. 이를 이용하면 약간의 꽁수를 써서 큰 양수를 리턴할 수도 있습니다.

다른 방법으로는 "리턴 값"을 그냥 전역 변수에 할당하면 됩니다.
Return_Val=   # 초과되는 함수 리턴값을 담을 전역 변수.

alt_return_test ()
{
  fvar=$1
  Return_Val=$fvar
  return   # 0 리턴(성공).
}

alt_return_test 1
echo $?                              # 0
echo "return value = $Return_Val"    # 1

alt_return_test 256
echo "return value = $Return_Val"    # 256

alt_return_test 257
echo "return value = $Return_Val"    # 257

alt_return_test 25701
echo "return value = $Return_Val"    #25701

예 23-6. 큰 두 정수 비교하기

#!/bin/bash
# max2.sh: 아주 큰 두 정수중 큰 수 구하기.

# 이 스크립트는 앞에서 소개했던 "max.sh" 예제를
# 큰 정수에 대해서 동작하도록 수정한 것입니다.

EQUAL=0             # 두 매개변수가 같을 경우의 리턴 값.
MAXRETVAL=256       # 함수가 리턴할 수 있는 최대 양수.
E_PARAM_ERR=-99999  # 매개변수 에러.
E_NPARAM_ERR=99999  # "일반화된"(Normalized) 매개변수 에러.

max2 ()             # 두 숫자중 큰 수를 리턴.
{
if [ -z "$2" ]
then
  return $E_PARAM_ERR
fi

if [ "$1" -eq "$2" ]
then
  return $EQUAL
else
  if [ "$1" -gt "$2" ]
  then
    retval=$1
  else
    retval=$2
  fi
fi

# -------------------------------------------------------------- #
# 여기가 큰 정수를 리턴할 수 있게 해 주는 부분입니다.
if [ "$retval" -gt "$MAXRETVAL" ]    # 범위를 넘는 수라면,
then                                 
  let "retval = (( 0 - $retval ))"   # 음수로 조절해 줍니다.
  # (( 0 - $VALUE )) 가 VALUE 의 부호를 바꿔줍니다.
fi
# 다행스럽게도, 큰 *음수* 리턴은 가능합니다.
# -------------------------------------------------------------- #

return $retval
}

max2 33001 33997
return_val=$?

# -------------------------------------------------------------------------- #
if [ "$return_val" -lt 0 ]                  # "조절된" 음수라면,
then                                       
  let "return_val = (( 0 - $return_val ))"  # 양수로 변환.
fi                                          # $return_val의 "절대값".
# -------------------------------------------------------------------------- #


if [ "$return_val" -eq "$E_NPARAM_ERR" ]
then                   # 매개변수 에러 "플래그"도 부호가 바뀝니다.
  echo "에러: 매개변수가 모자랍니다."
elif [ "$return_val" -eq "$EQUAL" ]
  then
    echo "두 숫자가 같습니다."
else
    echo "두 숫자중 큰 수는 $return_val 입니다."
fi  
  
exit 0

예 A-6 참고.

독자용 연습문제: 여기서 배운 것을 가지고 앞에서 살펴봤던 로마 숫자 예제가 임의의 큰 입력값을 처리하도록 고쳐 보세요.

재지향

함수의 표준입력을 재지향

함수는 본질적으로 코드 블럭인데 이것은 함수의 표준입력이 재지향 될 수 있다는 뜻입니다(예 4-1에 있는 것처럼).

예 23-7. 사용자 계정 이름에서 실제 이름을 알아내기

#!/bin/bash

# /etc/passwd 를 보고 사용자 계정이름에서 "실제 이름"을 알아냄.

ARGCOUNT=1  # 한 개 인자만 필요.
E_WRONGARGS=65

file=/etc/passwd
pattern=$1

if [ $# -ne "$ARGCOUNT" ]
then
  echo "사용법: `basename $0` USERNAME"
  exit $E_WRONGARGS
fi  

file_excerpt ()  # 파일에서 패턴을 검색해서 해당 부분을 출력.
{
while read line  # while 에서 꼭 "[ condition ]" 을 안 써도 됩니다.
do
  echo "$line" | grep $1 | awk -F":" '{ print $5 }'  # awk 가 ":" 구분자를 쓰도록.
done
} <$file  # 함수 표준입력으로 재지향.

file_excerpt $pattern

# 이 전체 스크립트는 다음처럼 해도 됩니다.
#       grep PATTERN /etc/passwd | awk -F":" '{ print $5 }'
# 아니면
#       awk -F: '/PATTERN/ {print $5}'
# 혹은
#       awk -F: '($1 == "username") { print $5 }' # 사용자 이름을 보고 실제이름을 찾아냄
# 하지만, 이런 방법은 교육적이지는 않은 것 같네요.

exit 0

함수의 표준입력을 재지향 해주는 덜 헷갈린 다른 방법이 있는데, 함수 안에 들어 있는 중괄호 코드 블럭으로 표준입력을 재지향 시키는 것입니다.
# 이렇게 하지 말고,
Function ()
{
 ...
 } < file

# 이렇게 하세요.
Function ()
{
  {
    ...
   } < file
}

# 비슷하게,

Function ()  # 이건 되지만,
{
  {
   echo $*
  } | tr a b
}

Function ()  # 이건 안 됩니다.
{
  echo $*
} | tr a b   # 여기서는 중첩된 코드 블럭이 꼭 있어야 됩니다.


# Thanks, S.C.

주석

[1]

간접 변수 참조(예 35-2 참조)는 함수에 변수 포인터를 넘겨주는 서투른 방법을 제공해 줍니다.
#!/bin/bash

ITERATIONS=3  # 입력을 몇 번이나 받을 것인지.
icount=1

my_read () {
  # my_read varname 이라고 부르세요.
  # 이 함수는 기본값으로 처리될 이전 입력값을 대괄호로 묶어서 보여주고,
  # 새 값을 물어봅니다.

  local local_var

  echo -n "값을 넣으세요"
  eval 'echo -n "[$'$1'] "'  # 이전 값.
  read local_var
  [ -n "$local_var" ] && eval $1=\$local_var

  # "And-list": "local_var" 가 참이라면 "$1"을 "loval_var"로 세팅.
}

echo

while [ "$icount" -le "$ITERATIONS" ]
do
  my_read var
  echo "Entry #$icount = $var"
  let "icount += 1"
  echo
done  


# 이 교육적인 예제는 Stephane Chazelas 가 제공해 주었습니다.

exit 0

[2]

return 명령어는 Bash의 내장 명령입니다.