Functions

{ ;}, ( ) 를 이용해 명령 그룹을 만들게 되면 같은 context 에서 실행이 됩니다. 이것은 명령 그룹 전체가 하나의 명령처럼 실행되는 것과 같은 효과가 있습니다. 따라서 앞에 함수명을 붙여서 함수 정의를 하게 되면 일반 명령과 동일하게 사용될 수 있습니다.

{ ;} 은 현재 shell 에서 ( ) 는 subshell 에서 실행되므로 보통 함수를 정의할때 { ;} 을 사용하지만 필요하다면 ( ) 을 사용할 수도 있습니다.

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

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

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

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

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

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


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

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

$ f1 < infile

함수를 사용하는 방법은 일반 명령들과 동일하며 종료 상태 값 지정은 return 명령으로 합니다. 정의한 함수는 subshell 에서는 별다른 설정 없이 그대로 사용할 수 있으나 스크립트 파일을 실행할 때와같이 새로 생성되는 프로세스에서도 사용하려면 export -f 함수명 해야 합니다. 함수를 export 하게되면 정의된 함수 body 전체가 환경변수 형태로 전달되는데 이것은 bash 에서만 사용이 가능하고 sh 에서는 사용할 수 없습니다.

# export 한 list_descendnats 함수 body 가 환경변수 형태로 전달된다.
$ env | grep -A6 BASH_FUNC_list_descendants
BASH_FUNC_list_descendants%%=() {  local children=$(ps -o pid= --ppid $1);
 for pid in $children;
 do
 list_descendants $pid;
 done;
 echo $children
}

함수를 정의하는 방법

Shell 은 함수를 정의할 때 프로그래밍 언어에서처럼 매개변수를 적지 않습니다. 전달된 인수 값은 함수 내에서 $1 $2 $3 ... 특수 변수에 자동으로 할당됩니다. 함수명에는 shell 에서 사용되는 메타문자나 quotes 등은 사용할 수 없습니다. 또한 외부 명령이나 alias 와 동일한 이름을 사용할 경우 syntax error 가 발생하는데 이때는 앞에 function 키워드를 붙여주면 오류를 방지할 수 있습니다.

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

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

1. foo () { ... ;}
   foo::bar () { ... ;}            # 이런 함수명은 bash 에서만 가능
   ble/array#reverse6/.helper () { ... ;}

2. function grep () { ... ;}       # function 키워드는 bash 에서만 가능

# 다음과 같이 { } 키워드 없이 바로 compound commands 키워드가 와도 됩니다.
$ foo() for arg; do
    echo "$arg"
done

$ foo 111 222 333
111
222
333

# 함수를 정의할 때 redirection 을 함께 정의할 수 있습니다.
$ foo() { date ;} >&3                    # 함수의 stdout 출력이 FD 3 번으로 전달된다.
$ foo() { date ;} > myfile               # 함수의 stdout 출력이 myfile 로 쓰여진다.
$ foo() { date ;} > >(sed 's/^/XXX/')    # 함수의 stdout 출력이 sed 명령에 전달된다.

정의된 함수 내용 보기

$ declare -f 함수명

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

$ declare -F

$ compgen -A function

특정 함수가 정의되어 있는지 체크하기

$ declare -F _dock         # _dock 이라는 함수는 현재 정의되어 있지 않음
$ echo $?               
1

$ declare -F _docker       # _docker 함수는 정의되어 있음
_docker
$ echo $?
0

정의된 함수 삭제하기

$ unset -f 함수명

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

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

foo1() {
    echo "foo1"
    foo2
}
                # 여기서 foo1 함수를 실행할수는 있지만 foo1 함수내에 있는
foo1            # foo2 함수를 실행할수 없으므로 오류가 된다.

foo2() {
    echo "foo2"
}

foo1            # 여기서는 오류없이 foo1, foo2 함수가 모두 실행된다.

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

다음의 경우 $KSH_VERSION 변수가 설정되어 있으면 첫 번째 puts 함수가 정의되고 그렇지 않을 경우 두 번째 puts 함수가 정의됩니다. shell 함수는 필요에 따라 언제든지 재정의해 사용할 수 있습니다.

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 : $AA
echo BB : $BB

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

AA :
BB : 200

local 변수를 이용한 recursion

# 자손 프로세스 프린트하기
list_descendants ()
{
    local children=$( cat /proc/$1/task/*/children 2> /dev/null )
    # 또는 local children=$( ps -o pid= --ppid $1 )
    for pid in $children
    do
        list_descendants $pid
    done
    echo $children
}

$ PID=`pidof gdm3`

$ ps f $PID $(list_descendants $PID)
    PID TTY      STAT   TIME COMMAND
   1492 ?        Ssl    0:00 /usr/sbin/gdm3
   1498 ?        Sl     0:00  \_ gdm-session-worker [pam/gdm-autologin]
   1531 tty2     Ssl+   0:00      \_ /usr/libexec/gdm-x-session --register-session ...
   1533 tty2     Sl+   27:25          \_ /usr/lib/xorg/Xorg vt2 -displayfd 3 ...
   1640 tty2     Sl+    0:00          \_ /usr/bin/startplasma-x11
   1745 ?        Ss     0:00              \_ /usr/bin/ssh-agent /usr/bin/

local 변수를 unset 할 경우

local 로 설정한 변수를 unset 하게 되면 그다음부터는 전역변수가 되므로 주의해야 합니다.
local 변수를 유지하면서 기존 변수값을 empty 로 만들려면 num="" 형식을 사용합니다.

num=100                                 num=100

func1 () {                              func1 () {
    local num=0                             local num=0
    func2                                   func2
}                                       }

func2 () {                              func2 () {
    num=$(( num + 1 ))                      unset -v num
    echo $num                               num=$(( num + 1 ))   # num 은 전역변수가 된다
}                                           echo $num
                                        }
func1
func1                                   func1
echo $num                               func1
                                        echo $num

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

1                                       101
1                                       102
100                                     102

Shell 은 dynamic scoping 을 사용합니다.

Shell 함수에서는 local 로 설정한 변수를 child 함수에서 읽고 쓸수가 있습니다.
그리고 변경한 값도 parent 함수에 적용이 됩니다.

이것은 sh 에서도 동일하게 동작하고 perl 언어의 local 키워드를 사용하는 변수와 같습니다.

#!/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 은 f1 함수에서 한번 변수를 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

if, while, for, {...} 에서와 같은 block scope 은 없습니다.

foo () 
{ 
    local bar=111        # local bar 설정
    echo $bar
    if true; then 
        local bar=222    # if 블록에서 local bar 설정
        echo $bar
    fi
    echo $bar            # 하지만 그대로 222 가 출력된다.
}

$ foo
111
222
222

함수를 nesting 해서 작성

함수를 작성할 때 함수 안에 함수를 작성할 수 있습니다. 하지만 shell 에서는 scope 기능은 없고 일단 정의되면 모두 전역 함수가 됩니다. nesting 함수는 외부 함수가 실행되기 전에는 정의된 상태가 아니기 때문에 실행되려면 먼저 외부 함수가 실행돼야 합니다.

foo () {
    bar () {
        echo "this is bar"
    }
    echo "this is foo"
}

$ bar                            # bar 함수는 아직 정의된 상태가 아니다.
Command 'bar' not found, ....

$ foo                            # foo 함수 실행시 bar 함수가 정의된다.
this is foo

$ bar                            # bar 함수는 전역 함수가 된다.
this is bar

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

run_background()
{
    echo -n "starting background process . . . "
    {
        on_exit() {              # on_exit() 함수는 새로 생성된 background process
            do_some_cleaning     # 에서만 정의되어 실행되는 함수가 된다.
        }
        trap on_exit EXIT
        do_some_works
    } &                          # { ... } & 부분이 background process 로 실행된다.
    echo "done"
}

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

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

--------- script.sh -------                   ---------- func1() --------
#!/bin/bash                                   func1() {
                                                  echo world
echo hello                                        return 5
exit 4                                        }

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

$ ./script.sh                                 $ func1
hello                                         world

$ echo $?                                     $ echo $?
4                                             5

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

$ AA=$(expr 1 + 2)       # expr 명령의 stdout 출력이 변수 AA 의 값으로 저장된다.
$ echo $AA
3

$ BB=`date +%Y`          # date 명령의 stdout 출력이 변수 BB 값으로 저장된다.
$ echo $BB
2015

함수에서도 외부 명령과 동일하게 명령치환 을 사용하면 됩니다. 다시 말해서 함수와 외부 명령은 사용방법에 차이가 없습니다.

foo() { expr $1 + $2 ;}
bar() { date "+%Y" ;}
zoo() { echo "hello $1" ;}

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

$ AA=$(foo 1 2)         # foo 함수에서 발생하는 stdout 출력이 변수 AA 값으로 저장된다.
$ echo $AA
3

$ BB=`bar`              # bar 함수에서 발생하는 stdout 출력이 변수 BB 값으로 저장된다.
$ echo $BB
2015

$ CC=$(zoo world)
$ echo $CC
hello world

함수에 인수를 전달하기

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

스크립트 파일 실행시 $0 값은 파일명이 되지만 함수의 경우는 /bin/bash 가 됩니다.

$ 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                   # 함수의 경우 '$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

명령에 선행하는 대입 연산

함수에서도 명령에 선행하는 대입 연산을 사용할 수 있습니다. ( sh 도 동일 )

$ foo() {
    local cmd file=$1
    if test -n "$CC"; then
        cmd="$CC -Wall -O2 $file"
    else
        cmd="gcc -Wall -O2 $file"
    fi
    echo "command : $cmd"
}

$ foo test.c
command : gcc -Wall -O2 test.c

$ CC=clang foo test.c             # 명령에 선행하는 대입연산 사용
command : clang -Wall -O2 test.c

$FUNCNAME 변수

함수 내에서 자신의 이름은 $FUNCNAME 변수를 통해 알 수 있습니다.

$ foobar () { echo "function name is : $FUNCNAME" ;}       # 또는 ${FUNCNAME[0]}

$ foobar
function name is : foobar

예제 )

docker inspect 명령은 로컬에 있는 이미지만 조회가 가능한데요. docker hub 에 있는 이미지를 다운받지 않고 REST API 를 통해 조회하는 예제입니다. main 함수에서 호출하고 있는 get_token, get_digest, get_image_configuration 함수들을 보면 내부적으로 $image, $tag, $token, $digest 변수들을 사용하고 있지만 호출시 따로 인수를 전달하지는 않고 있는데요. 이것은 child 함수에서 parent 함수의 local 변수에 직접 접근이 가능하기 때문입니다.

스크립트 실행을 위해선 JSON 처리기인 jq 명령이 있어야 합니다.

$ cat docker-inspect.sh
#!/bin/bash -e

main() {
    local image tag token digest

    image=$( test "${1%/*}" = "$1" && echo "library/$1" || echo "$1" )
    tag=${2:-latest}
    token=$( get_token )        # get_token "$image"
    digest=$( get_digest )      # get_digest "$image" "$tag" "$token"

    get_image_configuration     # get_image_configuration "$image" "$token" "$digest"
}

get_token() {
    echo -n "Retrieving docker hub token ... " >&2

    curl --silent \
    "https://auth.docker.io/token?scope=repository:$image:pull&service=registry.docker.io" \
    | jq -r '.token'

    echo done. >&2
}

get_digest() {
    echo -n "Retrieving image digest ... " >&2

    curl --silent \
    --header "Accept: application/vnd.docker.distribution.manifest.v2+json" \
    --header "Authorization: Bearer $token" \
    "https://registry-1.docker.io/v2/$image/manifests/$tag" \
    | jq -r '.config.digest'

    echo done. >&2
}

get_image_configuration() {
    echo "Retrieving image configuration ... " >&2

    curl --silent --location \
    --header "Authorization: Bearer $token" \
    "https://registry-1.docker.io/v2/$image/blobs/$digest" \
    |& { 
        result=$(cat)
        if [[ $result =~ ^404 ]]; then
            echo "$result" >&2
            exit 1
        else
            echo "$result" | jq -r '.'
        fi
    }
}

if [ "$#" = 0 -o "$1" = "-h" -o "$1" = "--help" ]; then
cat <<-EOF

Usage: `basename "$0"` <image> <tag>

EOF
    exit 1
fi

main "$@"

########################  실행 방법  ##########################

$ docker-inspect.sh nginx                # library/nginx latest 가 된다.
$ docker-inspect.sh nginx 1.16           # library/nginx 1.16   가 된다.
$ docker-inspect.sh foobar/nginx 1.16

스크립트가 복잡해질 경우는 다음과 같이 직접 인수를 전달해서 사용하는 것이 가독성이나 오류 예방에 좋습니다.

# 함수 호출시
get_token "$image"
get_digest "$image" "$tag" "$token"
get_image_configuration "$image" "$token" "$digest"

# 함수 내에서는 다음과 같이 작성합니다.
get_digest () {
    local image tag token
    image=$1 tag=$2 token=$3
. . .

소스코드 주소: https://ops.tips/blog/inspecting-docker-image-without-pull/

Quiz

함수는 일반 명령과 동일하게 사용되므로 반환값을 변수에 대입하려면 보통 명령치환을 사용하는데요. 하지만 명령치환은 subshell 이 추가로 생성이 되죠. subshell 생성없이 반환값을 구하려면 어떻게 할까요?

add ()
{
    echo $(( $1 + $2 ))
}

res=$( add 100 200 )            # 명령치환을 이용하는 방법 
echo $res                       # (추가로 subshell 이 생성된다)

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

300
--------------------------

add2 () 
{                               # eval 을 이용한 indirection 을 활용
    eval $1=$(( $2 + $3 ))      # ($1=$(( $2 + $3 )) 은 res2=700 이 된다.)

    # array 를 반환할 경우
    # eval $1=\( 11 22 33 44 55 \)
}

add2 res2 300 400
echo $res2

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

700
--------------------------

add3 ()
{
    local -n ret=$1             # named reference 를 이용하는 방법
    shift
    ret=$(( $1 + $2 ))

    # array 를 반환할 경우
    # ret=( 11 22 33 44 55 )
}

add3 res3 500 600
echo $res3

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

1100

2 .

docker hub 에서 특정 이미지의 taglist 를 확인하려면 어떻게 할까요?

docker-taglist ()
{
    local result
    if [ -n "$1" ]; then
        result=$( curl -sS "https://registry.hub.docker.com/v1/repositories/$1/tags" )
        if [[ $result =~ not\ found.$ ]]; then
            echo $result >&2
            return 1
        else
            echo "$result" | grep -Po '(?<="name": ")[^"]+' | sort -V \
            | sed -n '/^latest$/!{p; bX}; h; :X ${g; /^$/!p}'
        fi
    else
        printf '\n%s\n\n' "Usage: ${FUNCNAME} <image>"
        return 1
    fi
}

$ docker-taglist python
. . .

3 .

shell 스크립트로 재귀 함수를 만들어서 factorial 10 을 구하는 것입니다.

$ factorial() { 
    if (( $1 == 1 )); then 
        echo 1
    else 
        echo $(( $1 * $( factorial $(( $1 - 1 )); echo BASHPID: $BASHPID >&2; ) ))
    fi 
}

$ factorial 10
BASHPID: 1172416       # subshell 프로세스가 9 개 생성된다.
BASHPID: 1172415
BASHPID: 1172414
BASHPID: 1172413
BASHPID: 1172412
BASHPID: 1172411
BASHPID: 1172410
BASHPID: 1172409
BASHPID: 1172408
3628800                # 결과: 3628800
-----------------------------------------------

# subshell 프로세스를 생성하지 않고 구하는 방법 
$ factorial() {
    trap 'echo BASHPID: $BASHPID: $num' RETURN
    if (( $1 == 1 )); then 
        (( num = 1 ))
    else 
        factorial $(( $1 - 1 ))
        (( num = num * $1 ))
    fi 
}

$ factorial 10                    # trap 라인을 다음과 같이 변경
BASHPID: 1169898: 1               trap '(( ${#FUNCNAME[@]} == 1 )) && echo $num' RETURN
BASHPID: 1169898: 2
BASHPID: 1169898: 6               $ echo $( factorial 10 )
BASHPID: 1169898: 24              3628800
BASHPID: 1169898: 120
BASHPID: 1169898: 720
BASHPID: 1169898: 5040
BASHPID: 1169898: 40320
BASHPID: 1169898: 362880
BASHPID: 1169898: 3628800