Functions

함수를 만들어 사용하는 이유는 여러개의 명령들을 하나의 group 으로 묶어서 사용하기 쉬운 함수명을 이용해 같은 context 에서 실행시키는데 있습니다. 명령 group 은 { ;} , ( ) 을 이용해 만들수 있는데 차이점은 { ;} 은 현재 shell 에서 ( ) 는 subshell 에서 실행된다는 점이 다릅니다. 그래서 보통 함수를 정의할때 { ;} 을 사용하지만 필요하다면 ( ) 을 사용할 수도 있습니다.

# echo hello world | read var; 는 파이프로 인해 subshell 에서 실행되어 
# echo "$var" 는 값이 표시되지 않는다.
$ echo hello world | read var; echo "$var"

# { } 을 이용해 명령 group 을 만들면 read, echo 명령이 같은 context 
# 에서 실행되어 정상적으로 값이 표시됩니다.
$ echo hello world | { read var; echo "$var" ;}

# hello 는 터미널에 표시되고 world 만 outfile 에 저장된다.
$ echo hello; echo world > outfile

# 명령 group 을 이용하면 hello world 둘 다 outfile 에 저장된다.
$ { echo hello; echo world ;} > outfile

# 첫번째 명령만 시간 측정이 된다
$ time sleep 2 && sleep 3

# { ;} 안에 있는 명령이 모두 측정된다.
$ time { sleep 2 && sleep 3 ;}


# 명령 group 을 하나의 짧은 이름으로 사용 ( 함수정의 ) 
{ 
    read var1
    read var2
    echo "$var1 $var2"
} < infile

f1() { 
    read var1
    read var2
    echo "$var1 $var2"
}           

$ f1 < infile

함수 이름은 알파벳 (대, 소문자), 숫자, _ 로 만들 수 있으며 이름의 첫 문자로 숫자가 올 수 없습니다. 함수를 사용하는 방법은 일반 명령들과 동일하며 종료 상태 값 지정은 return 명령으로 합니다. 정의한 함수는 subshell 이나 source 한 스크립트 내에서는 별다른 설정 없이 사용할 수 있으나 child shell 에서도 사용하려면 export -f 함수명 해야 합니다.

함수를 export 하는 기능은 bash 만 가능합니다. sh 에서는 함수를 export 할 수 없습니다.

함수를 정의하는 방법

shell 함수는 정의할 때 프로그래밍 언어에서처럼 매개변수를 적지 않습니다.
전달된 인수 값은 함수 내에서 $1 $2 $3 ... 특수 변수에 자동으로 할당됩니다.

function 키워드는 bash 에서만 사용할 수 있고 sh 에서는 사용할 수 없습니다.

# shell 함수는 정의할때 매개변수를 적지 않는다.
X. 함수명 ( p1 p2 p3 ) { ... ;}

1. 함수명 () { ... ;}

2. function 함수명 () { ... ;}      # bash 에서만 가능

정의된 함수 내용 보기

$ declare -f 함수명

현재 shell 에 정의된 모든 함수명 보기

$ compgen -A function 

$ declare -F

정의된 함수 삭제하기

$ unset -f 함수명

함수를 실행할 땐 먼저 정의가 되어 있어야 한다.

foo1            # 여기서는 foo1 함수 정의가 안되어있기 때문에 실행할 수 없다.

foo1() {
    echo "foo1"
    foo2        # foo1 함수를 실행한 곳이 foo2 아래이므로 실행 가능
}

foo2() {
    echo "foo2"
}

foo1            # 여기서는 foo1 함수를 실행할 수 있다.

조건에 따라 다른 함수를 정의할 수 있다.

다음의 경우 $KSH_VERSION 변수가 설정되어 있으면 첫 번째 puts 함수가 정의되고 그렇지 않을 경우 두 번째 puts 함수가 정의됩니다.

if test -n "$KSH_VERSION"; then
    puts() {
        print -r -- "$*"
    }
else
    puts() {
        printf '%s\n' "$*"
    }
fi

변수는 기본적으로 global scope

여기서 global scope 이라는것은 현재 스크립트 파일 입니다. ( source 한 파일도 포함).

#!/bin/bash

BB=200

foo() {
    AA=100
}

foo

echo $AA    # 모두 global scope 가된다
echo $BB

############### output ##############

100
200

local 명령을 사용하여 지역변수를 설정할 수 있다.

local 과 declare 은 동일한 기능을 하지만 local 은 global scope 에서 사용할 수 없습니다.
declare 가 함수 내에서 사용되면 local 과 동일한 역할을 합니다.

sh 에서는 local 만 사용할 수 있습니다

#!/bin/bash

foo() {
    local AA=100
    BB=200
}

foo

echo $AA
echo $BB

############### output ##############

200

local 변수를 이용한 recursion

# 자손 프로세스 프린트하기
list_descendants ()
{
  local children=$(ps -o pid= --ppid $1)

  for pid in $children
  do
    list_descendants $pid
  done

  echo $children
}

$ ps jf $(list_descendants $PID)

Shell 은 dynamic scoping 을 사용한다.

Shell 함수에서는 local 로 설정한 변수를 child 함수에서 읽고 쓸수가 있습니다.
그리고 변경한 값도 parent 함수에 적용이 됩니다. ( sh 에서도 동일하게 동작합니다 )

#!/bin/bash 

AA=100

f1() {
    local AA=200
    f2
    echo f1 AA after f2 call : $AA
}

f2() {
    echo f2 AA : $AA     # f1 함수의 local 변수를 읽고 쓸 수 있다.
    AA=300
}

f1

echo global AA : $AA

################ output ################

f2 AA : 200
f1 AA after f2 call : 300
global AA : 100

이와같이 함수 f1 이 실행 중에 있을때 f2 에서 f1 의 local 변수에 접근할 수 있는 것을 dynamic scoping 이라고 합니다. 보통 프로그래밍 언어에서는 lexical scoping (또는 static scoping) 을 사용하기 때문에 프로그램 코드가 분리되어 있는 f2 에서는 f1 함수의 local 변수에 접근할 수가 없죠. dynamic scoping 이 구현하기 쉬워서 sh, bash, powershell, emacs lisp 같은 언어에서 사용되고 있다고 합니다.

dynamic scoping 은 main 함수에서 한번 변수를 local 로 설정해 놓으면 이후에 호출되는 함수에서는 새로운 global 변수를 갖는 것과 같습니다. 그러므로 이전에 존재하던 변수값은 변경되지 않는 장점은 있지만 임의의 child 함수에서 한번 값이 변경되면 이후에 모두에게 적용되므로 주의할 필요가 있습니다.

#!/bin/bash

AA=100

f1() { 
    AA=$(( AA + 100 ))
    echo f1 AA : $AA
}

f2() { echo f2 AA : $AA ;}

main() {
# global 변수 AA 와 동일한 이름의 local 변수를 생성하면
# 이후에 호출되는 함수들은 새로운 global 변수 AA 를 갖는 것과 같다.
    local AA=200
    f1             # 임의의 함수에서 값을 변경하면
    f2             # 이후에 모두에게 적용되므로 주의!
    echo main AA : $AA
}

main

echo global AA : $AA

##### 실행결과 #####

f1 AA : 300
f2 AA : 300
main AA : 300
global AA : 100

f1 함수에서 기존의 AA 값을 연산에 사용하지만 변경한 값이 이후 전체에 적용되지 않게 하려면 다음과 같이 local 로 설정하면 됩니다.

# f1 함수에서 AA 를 local 로 설정
f1() { 
    local AA=$AA
    AA=$(( AA + 100 ))
    echo f1 AA : $AA
}

##### 실행결과 #####

f1 AA : 300
f2 AA : 200
main AA : 200
global AA : 100

함수에서 연산 결과를 리턴하는 방법

shell script 가 프로그래밍 언어와 다른점 중에 하나가 return 명령의 역할입니다. shell 에서는 return 명령이 함수에서 연산한 결과를 반환하는데 사용되는 것이 아니고 exit 명령과 같이 함수가 정상적으로 종료됐는지 아니면 오류가 발생했는지를 나타내는 종료 상태 값을 지정하는 용도로 사용됩니다.

--------- script.sh -------
#!/bin/bash

echo hello
exit 4

---------- func1() --------
func1() {
    echo world
    return 5
}

--------- 실행결과 --------
$ ./script.sh
hello
$ echo $?
4

$ func1
world
$ echo $?
5

그러므로 연산 결과를 반환하는데 return 명령을 사용하면 안됩니다. 그럼 함수에서 연산한 결과를 반환 받으려면 어떻게 해야 하는지가 문제인데요. 앞서 함수는 일반 명령들과 동일하게 사용된다고 했습니다. 다음은 외부 명령을 사용할때 결과값을 받는 방법입니다.

$ AA=$(expr 1 + 2)
$ echo $AA
3

$ AA=`date +%Y`
$ echo $AA
2015

함수에서도 외부 명령과 동일하게 명령치환 을 사용하면 됩니다.

f1() { expr $1 + $2 ;}
f2() { date "+%Y" ;}
f3() { echo "hello $1" ;}

-------------------------

$ AA=$(f1 1 2)
$ echo $AA
3

$ AA=`f2`
$ echo $AA
2015

$ AA=$(f3 world)
$ echo $AA
hello world

함수에 인수를 전달하기

외부 명령을 실행할때 인수를 전달하기 위해 ( ) 를 사용하지 않듯이 함수에서도 ( ) 를 사용하지 않습니다. 전달된 인수는 함수내에서 $1 $2 $3 ... positional parameters 에 자동으로 할당되며 scope 은 local 변수와 같습니다.

$ func1() {
    echo number of arguments: $#
    echo '$0' : "$0"
    echo '$1' : "$1"
    echo '$2' : "$2"
    echo '$3' : "$3"
    echo '$4' : "$4"
    echo '$5' : "$5"
}

$ AA=(22 33 44)

$ func1 11 "${AA[@]}" "55 END"   # 인수를 전달할 때 '( )' 를 사용하지 않는다.

number of arguments: 5   
$0 : /bin/bash
$1 : 11                     
$2 : 22
$3 : 33
$4 : 44
$5 : 55 END

$@, $* 변수는 함수에 전달된 인자들 전부를 포함합니다. 변수를 quote 하지 않으면 단어분리에 의해 두변수의 차이가 없지만 quote 을 하게 되면 "$@" 의 의미는 "$1" "$2" "$3" ... (복수개의 인수) 와 같게되고 "$*" 의 의미는 "$1c$2c$3 ... " (하나의 인수) 와 같게됩니다. 여기서 cIFS 변수값의 첫번째 문자 입니다.

#!/bin/bash

foo() {
    echo \$@ : $@
    echo \$* : $*

    echo '======== "$@" ======='
    for v in "$@"; do
        echo "$v"
    done

    echo '======== "$*" ======='
    for v in "$*"; do
        echo "$v"
    done
}

foo 11 "22     33" 44

############# output #############

$@ : 11 22 33 44          # quote 을 하지 않으면 둘은 차이가 없다.
$* : 11 22 33 44
======== "$@" =======     # 복수개의 인수가 된다.
11
22     33
44
======== "$*" =======     # 함수에 전달한 인수 전체가 하나의 인수가 된다.
11 22     33 44

다음부터 이어지는 내용은 bash 전용 으로 sh 에서는 사용할 수 없습니다.

indirection 을 이용해 array 를 함수에 전달

#!/bin/bash

foo() {
    echo "$1"

    local ARR=( "${!2}" )      # '!2' 부분이 'AA[@]' 로 바뀐다.

    for v in "${ARR[@]}"; do
        echo "$v"
    done

    echo "$3"
}

AA=(22 "33 44" 55)
foo 11 'AA[@]' 66

################ output ###############
11
22
33 44
55
66

Bash version 4.3+ 에서는 named reference 를 사용할 수 있습니다.

declare -n refname
local -n refname

#!/bin/bash

 f2() {
     declare -n RVAR=$1    # declare 는 함수내에서 기본적으로 local 변수를 생성
     RVAR=200
 }

 f1() {
     local VAR=100
     echo f1 : local VAR = $VAR
     f2 VAR
     echo f1 : local VAR after f2 call = $VAR
 }

 f1

 ###################### output #########################

 f1 : local VAR = 100
 f1 : local VAR after f2 call = 200

named reference 를 이용해 array 를 함수에 전달.
indirection 을 이용할 때처럼 새로 array 를 생성하지 않아도 된다.

foo() {
    echo "$1"

    local -n ARR=$2

    for v in "${ARR[@]}"; do
        echo "$v"
    done

    echo "$3"
}

$ AA=(22 "33 44" 55)
$ foo 11 AA 66

################ output ###############
11
22
33 44
55
66