Positional Parameters

  • 스크립트 파일을 실행할 때
  • function 을 호출할 때
  • 스크립트 파일을 source 할 때

인수를 주게되면 해당 스크립트 및 함수 내에서 positional paramters 가 자동으로 설정됩니다. 첫번째 인수는 $1 , 두번째는 $2 ... 식으로 할당되며 scope 은 local 변수와 같습니다. 숫자가 두자리 이상일 경우는 { } 를 사용해야 합니다. sh 과 같이 array 를 사용할 수 없는 환경에서 활용할 수 있습니다.

$ set -- aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn

$ echo $7
ggg

$ echo foo$7bar 123$7456
foogggbar 123ggg456

$ echo $11                       # 두자리 이상 숫자
aaa1                             # $1 가 aaa 로 확장되고 뒤에 1 이 붙는다

$ echo foo$11bar 123$11456 
fooaaa1bar 123aaa1456

$ echo foo${11}bar 123${11}456   # 따라서 두자리 이상 숫자는 { } 를 사용해야 한다.
fookkkbar 123kkk456

positional parameters 에는 직접 값을 대입할 수 없으므로 보통 변수에 먼저 대입한 후 사용합니다.

# positional parameter 는 직접 값을 대입할 수 없다.
$ 2=bbb
2=bbb: command not found

# 따라서 먼저 변수에 대입하여 사용합니다.
login() 
{
    local container=$1
    local user=$2

    if test -z "$container"; then 
        container=ubuntu
    fi

    if test -z "$user"; then
        user=ubuntu
    fi

    lxc exec "$container" -- sudo --login --user "$user"
}

positional parameters 는 local 변수이므로 함수 내에서 set 명령을 이용해 설정한 값은 해당 함수에서만 사용할 수 있습니다.

#!/bin/bash

fun () {
    set -- 11 22 33
    echo $1 $2 $3
}

fun

echo $1 $2 $3

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

./test.sh aa bb cc
11 22 33
aa bb cc             # 함수 밖에서는 'set --' 설정이 적용되지 않는다.

$0

스크립트 파일 이름를 나타냅니다.

$ cat test.sh
#!/bin/sh

echo \$0 : $0
echo \$1 : $1
echo \$2 : $2
.................

$ ./test.sh 11 22
$0 : ./test.sh
$1 : 11
$2 : 22

shell 함수는 $0 값이 /bin/bash 가 됩니다.

$ func() { echo \$0 : $0; echo \$1 : $1; echo \$2 : $2; }

$ func 11 22
$0 : /bin/bash       
$1 : 11
$2 : 22

sh -c 형식으로 실행했을 경우는 첫번째 인수를 가리키게 됩니다.

$ sh -c 'echo $0 $1 $2' 11 22 33
11 22 33

$ sh -c 'echo $0, $1, $2'             # 인수를 주지 않을 경우
sh, ,

$ sh -c 'echo "$@"' 11 22 33          # $@, $* 는 $1 부터 출력하므로
22 33

# 스크립트 파일 실행시 전달한 인수를 그대로 다시 'sh -c' 로 전달하려면
$ cat test.sh
#!/bin/bash

bash -c 'echo "$@"' x "$@"            # $0 값은 "x" 가 된다.

$ ./test.sh 11 22 33
11 22 33

명령을 다른 이름으로 symbolic link 또는 hard link 한 후에 링크 이름으로 실행을 하면 $0 값이 링크 이름 됩니다.

$ cat ccache.sh
#!/bin/sh

echo "Im running '$0'"
---------------------------

$ chmod +x ccache.sh

$ ./ccache.sh
Im running './ccache.sh'

$ ln -s ccache.sh gcc.sh

$ ./gcc.sh
Im running './gcc.sh'

$ ln -s ccache.sh clang.sh

$ ./clang.sh
Im running './clang.sh'

$1, $2, $3 ...

각 인수들을 나타냅니다.

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

echo number of arguments = $#

echo \$0 = "$0"

echo \$1 = "$1"
echo \$2 = "$2"
echo \$3 = "$3"

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

$ ./test.sh 11 22 33
number of arguments = 3
$0 = ./test.sh
$1 = 11
$2 = 22
$3 = 33

$#

$0 을 제외한 전체 인수의 개수를 나타냅니다.

$ cat test.sh
#!/bin/sh

echo $#
.....................

$ ./test.sh 11 22 33
3

인수 값이 null 이면 "$@" 는 인수로 잡히지 않습니다.

$ set --                   # 현재 설정되어 있는 인수들을 모두 삭제

$ ./test.sh "$@"
0
$ ./test.sh "$*"           # "" 이 인수로 잡힌다.
1

$ set 11 22

$ ./test.sh 11 "${@:3}"
1
$ ./test.sh 11 "${*:3}"    # "" 이 인수로 잡힌다.
2

$@ , $*

$@, $* 는 positional parameters 전부를 포함합니다. array 에서 사용되는 @ , * 기호와 의미가 같습니다. 변수를 quote 하지 않으면 단어분리에 의해 두 변수의 차이가 없지만 quote 을 하게 되면 "$@" 의 의미는 "$1" "$2" "$3" ... 와 같게되고 "$*" 의 의미는 "$1x$2x$3 ... " 와 같게 됩니다. ( 여기서 xIFS 변수값의 첫번째 문자 입니다. )

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

echo \$@ : $@
echo \$* : $*

echo '======== "$@" ======='
for v in "$@"; do             # 또는 for v do ...
    echo "$v"
done

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

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

$ ./test.sh 11 22 33
$@ : 11 22 33
$* : 11 22 33
======== "$@" =======
11
22
33
======== "$*" =======
11 22 33

"$*" 는 항상 IFS 변수값의 첫번째 문자를 인수 구분자로 갖습니다.

$ set -- 11 22 33 44 55

$ IFS=XYZ 

$ echo "$*" , $*
11X22X33X44X55 , 11 22 33 44 55

$ echo "$@" , $@             
11 22 33 44 55 , 11 22 33 44 55

$ foo=$*                             # $* 값을 foo 변수에 대입
$ echo "$foo" , $foo
11X22X33X44X55 , 11 22 33 44 55      # "$foo" 변수값도 "$*" 와 같이 출력된다.

$ bar=$@
$ echo "$bar" , $bar
11 22 33 44 55 , 11 22 33 44 55      (bash)
11X22X33X44X55 , 11 22 33 44 55      (sh)

$ IFS=YZ                             # IFS 변수값을 YZ 로 변경

$ echo "$*" , $*
11Y22Y33Y44Y55 , 11 22 33 44 55

$ echo "$foo" , $foo                 # IFS 값이 변경되면 기존의 $foo 변수값은
11X22X33X44X55 , 11X22X33X44X55      # 인수 구분자가 X 로 고정된다.

전달받은 인수들을 그대로 다른 명령의 인수로 다시 전달하려고 할때는 "$@" 를 사용합니다.
외부 명령인 egrep, fgrep 은 shell 스크립트 파일로 활용방법을 볼 수 있습니다.

$ cat /bin/egrep
#!/bin/sh
exec grep -E "$@"      # 전달받은 인수들을 -E 옵션을 추가해서 다시 grep 명령에 전달한다.

$ cat /bin/fgrep
#!/bin/sh
exec grep -F "$@"
---------------------

$ run() { "$@" ;}
$ run date +%D
01/22/16                      # "date" 은 명령 "+%D" 는 인수가 된다.

$ run() { "$*" ;}
$ run date +%D
date +%D: command not found   # "date +%D" 가 명령 이름이 된다.

set

set 명령은 보통 shell 옵션을 설정할 때 사용하지만 positional paramters 를 설정하거나 삭제할 때도 사용됩니다.

$ set 11 22 33
$ set -- 11 22 33   # 설정하려는 인수값에 '-' 문자가 포함될 경우 ( 예: -b -c -d )
$ echo $1 $2 $3
11 22 33

$ set --            # 현재 설정되어 있는 positional parameters 가 모두 삭제됩니다.
$ echo $1 $2 $3
$

사용예 )

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

set -- 11 22 33      # set 명령을 이용하여 script 내에서 positional parameters 를 설정

echo number of arguments = $#

echo \$0 = "$0"

echo \$1 = "$1"
echo \$2 = "$2"
echo \$3 = "$3"

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

$ ./test.sh
number of arguments = 3
$0 = ./test.sh
$1 = 11
$2 = 22
$3 = 33

--------- set 활용 --------

$ date
Sat Dec 31 07:45:56 KST 2016

$ set -- $(date)

$ echo 오늘은 $2$3$1 요일 $4 시 입니다.
오늘은 Dec 월 31 일 Sat 요일 07:46:29 시 입니다.

shift

shift [n]

shift 명령은 현재 설정되어 있는 positional parameters 를 좌측으로 n 만큼 이동시킵니다.
결과로 n 개의 positional parameters 가 삭제됩니다. 현재 positional parameters 개수 보다 n 값이 크면 삭제가 일어나지 않고 종료 상태 값은 1 이 됩니다. n 값을 주지 않으면 디폴트는 1 입니다.

$ set -- 11 22 33 44 55

$ echo $@
11 22 33 44 55
$ shift 2
$ echo $@
33 44 55
$ shift 2
$ echo $@
55
$ shift 2        # positional parameters 개수가 1 개만 남은 상태에서 shift 2 를 
$ echo $@        # 하였으므로 삭제가 되지 않고 그대로 남아있게 됩니다.
55               
$ shift          # shift 는 shift 1 와 같으므로 마지막 남은 positional parameters
$ echo $@        # 1 개가 삭제되어 $@ 값은 empty 가 됩니다.
$

shift 한 후에 $1 는 다음 값이 됩니다. 모든 인수가 shift 되면 $1 값은 empty 가 됩니다.

$ set -- 11 22 33

$ echo $1
11
$ shift
$ echo $1
22
$ shift
$ echo $1
33
$ shift
$ echo $1
$             <---- 모든 인수가 shift 되면 다음 값은 empty 가 된다.

for 문에서 순회시 사용되는 $@ 값은 shift, set 에의해 변경되는 $@ 값과 별도로 존재하므로 shift 는 for 반복문에 영향을 주지 않습니다.

fun1 () {                                     fun2 () {
    for arg in "$@"; do                           for arg in "$@"; do
        echo forloop : $arg                           echo forloop : $arg
        echo \$1 : $1                                 shift 2 && echo \$1 : $1
        shift 3                                   done
    done                                      }
}
..............................................................................

# 44 에서 shift 성공            # 44 에서 shift 실패
$ fun1 11 22 33 44 55 66      $ fun1 11 22 33 44 55      $ fun2 11 22 33 44 55
forloop : 11                  forloop : 11               forloop : 11
$1 : 11                       $1 : 11                    $1 : 33
forloop : 22                  forloop : 22               forloop : 22
$1 : 44                       $1 : 44                    $1 : 55
forloop : 33                  forloop : 33               forloop : 33
$1 :                          $1 : 44                    forloop : 44
forloop : 44                  forloop : 44               forloop : 55
$1 :                          $1 : 44
forloop : 55                  forloop : 55
$1 :                          $1 : 44
forloop : 66
$1 :

$IFS

$IFS 변수와 set 명령을 이용하여 스트링에서 필드를 분리해낼 수 있습니다.

#!/bin/sh

line="11:22:33:44:55"

set -f; IFS=:        # globbing 을 disable
set -- $line         # IFS 값에 따라 필드를 분리하여 positional parameters 에 할당
set +f; IFS=`echo " \n\t"`

echo number of fields = $#
echo field 1 = "$1"
echo field 2 = "$2"

shift 3
echo \$@ = "$@"

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

number of fields = 5
field 1 = 11
field 2 = 22
$@ = 44 55

이번에는 $* 를 이용한 join

#!/bin/sh

files="aaa.c bbb.c ccc.c"

set -- $files

join2() { local IFS=$1; shift; echo "$*" ;}

join_files=$( join2 ":" "$@" )
echo "$join_files"

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

aaa.c:bbb.c:ccc.c

Substring expansion

Substring expansion 은 bash 에서만 사용할수 있습니다.

$ set -- 11 22 33 44 55

$ echo ${@:3}
33 44 55

$ echo ${@:2:2}
22 33

Quiz

foo/bar/file.txt 파일을 mydir 디렉토리로 복사할 때 결과가 mydir/file.txt 가 아니라 경로를 포함해서 mydir/foo/bar/file.txt 가 될 수 있게 함수를 작성하는 것입니다. 이때 파일 이름에는 glob 문자를 사용할 수 있습니다.

cpd() {
    if test $# -lt 2 -o "$1" == "-h" -o "$1" == "--help"
    then
        echo "Usage : cpd dir1/dir2/file dir3" >&2
        return 1
    fi
    local dir=${1%/}
    if [[ $dir == */* ]]; then
        dir=${@:$#}/${dir%/*}
    else
        dir=${@:$#}                                   # ${@:$#} 는 마지막 인수가 되고
    fi                                                # ${@:1:$#-1} 는 마지막 인수를
    mkdir -p "$dir" && cp -a "${@:1:$#-1}" "$dir"     # 제외한 나머지 인수들이 된다.
}

$ cpd foo/bar/file.txt mydir

$ cpd foo/bar/*.txt mydir 

# 위에서처럼 파일 이름에 glob 문자가 사용되면 명령이 실행될 때는
# cpd foo/bar/a.txt foo/bar/b.txt foo/bar/c.txt mydir 형태가 됩니다.
# 이때 ${@:$#} 값은 마지막 인수인 mydir 가 되고 
# ${@:1:$#-1} 의 값은 mydir 를 제외한 나머지 인수들이 됩니다.

# 마지막 인수값은 ${@:$#} 대신에 indirection 을 이용해 ${!#} 로 나타낼수도 있습니다.
# 현재 전체 인수 개수가 3 개라면 : ${!#} ---> ${$#} ---> ${3}

2 .

uftrace 명령을 이용해 실행파일을 trace 하려면 먼저 소스파일을 gcc -pg -g 옵션으로 다시 컴파일을 해야 하는데요. 직접 소스파일을 trace 해볼 수 있게 스크립트를 작성하는 것입니다. 이때 gcc 에 추가로 옵션을 줄수 있어야 하고 실행파일에도 옵션을 줄수 있어야 합니다.

$ cat uftrace.sh
#!/bin/bash

if (( $# == 0 )); then
    echo "Error: file name required."
    exit 1
fi >&2

file=$1
shift
opts=()
while [[ $1 != "--" && $# != 0 ]]; do
    opts+=( "$1" )
    shift
done
[[ $1 == "--" ]] && shift

gcc -pg -g "$@" "$file" &&
uftrace -a a.out "${opts[@]}"

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

$ cat fibo.c
#include <stdio.h>
#include <stdlib.h>

long fibo(int x) 
{
    if (x == 1) return 1;
    if (x == 2) return 1;
    return fibo(x - 1) + fibo(x - 2);
}

int main(int argc, char *argv[])
{
    printf("%ld\n", fibo(atoi(argv[1])));
}

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

# 1. uftrace.sh 스크립트의 첫번째 인수로 "fibo.c" 소스파일이 오고
# 2. 그다음 실행시 전달할 "10" 옵션 값들이 옵니다.
$ uftrace.sh fibo.c 10 
55
# DURATION     TID     FUNCTION
   1.163 us [140797] | __monstartup();
   0.182 us [140797] | __cxa_atexit();
            [140797] | main(2, 0x7ffc98ea68e8) {
 107.842 us [140797] |   atoi("10") = 10;
            [140797] |   fibo(10) {
            [140797] |     fibo(9) {
            [140797] |       fibo(8) {
. . .
# 3. gcc 에 추가로 옵션을 줄경우 '--' 이후에 적어주면 됩니다.
$ uftrace.sh fibo.c 10 -- -O2
. . .

3 .

함수 A 에 전달된 인수들을 실행 중에 함수 B 를 호출해서 처리하게 하려면 어떻게 할까요?

shell 함수는 호출될 때마다 자동으로 positional parameters 가 설정되므로 함수 A 가 실행 중에 B 를 호출한다고 하더라도 함수 B 에서는 함수 A 의 positional parameters 를 사용할 수가 없습니다. 따라서 이때는 함수 A 에 전달된 인수들을 "$@" 변수를 이용해서 함수 B 를 호출할 때도 전달해야 합니다.

#!/bin/bash

foo () {
    echo "function foo()"
    for (( i = 1; i <= $#; i++ )) do echo "\$$i : ${!i}"; done
    bar      # bar 함수를 호출
}

bar () {
    echo "function bar()"
    for (( i = 1; i <= $#; i++ )) do echo "\$$i : ${!i}"; done
}

foo 11 22 33

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

$ ./test.sh
function foo()
$1 : 11
$2 : 22
$3 : 33
function bar()         # foo 함수 실행중에 bar 함수를 호출했지만 bar 함수에서는
$                      # foo 함수의 positional parameters 를 사용할 수 없다.
------------------------------------------------------------

# foo 함수를 다음과 같이 수정합니다.
foo () {
    echo "function foo()"
    for (( i = 1; i <= $#; i++ )) do echo "\$$i : ${!i}"; done
    bar "$@"           # bar 함수를 호출할 때 positional parameters 를 전달해야 한다.
}

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

$ ./test.sh
function foo()
$1 : 11
$2 : 22
$3 : 33
function bar()
$1 : 11
$2 : 22
$3 : 33

4 .

다음과 같이 test.sh 명령에 인수가 전달되면 스크립트 내에서 $@ 의 값은 Arch Linux, Ubuntu Linux, Suse Linux, Fedora Linux, * 가 되는데요. 이중에서 Suse Linux 원소를 삭제하려면 어떻게 할까요?

$ test.sh "Arch Linux" "Ubuntu Linux" "Suse Linux" "Fedora Linux" "*"

$ cat test.sh
echo 'number of "$@" :' $#
for v; do echo "$v"; done        # 삭제전 $@ 값을 출력

echo "-------------------"

# for 문에서 순회시 사용되는 $@ 값은 shift, set 에의해 변경되는 $@ 값과 별도로 존재합니다.
for arg do shift                             # arg 값이 Arch 일때 shift 를 하면 
    [ "$arg" = "Suse Linux" ] && continue    # $@ 값은 Ubuntu, Suse, Fedora, * 가 된다.
    set -- "$@" "$arg"         # set 이후 $@ 값은 Ubuntu, Suse, Fedora, *, Arch 가 된다.
done

echo 'number of "$@" :' $#
for v; do echo "$v"; done        # 삭제후 $@ 값을 출력

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

$ ./test.sh
number of "$@" : 5
Arch Linux
Ubuntu Linux
Suse Linux
Fedora Linux
*
-------------------
number of "$@" : 4
Arch Linux
Ubuntu Linux         <---- Suse Linux 가 삭제되었다.
Fedora Linux
*

5 .

이번에는 foo 함수에서 생성된 $@ 값을 bar 함수에 전달해서 수정하게 한 후에 결과를 다시 opts 변수를 통해 foo 함수에 전달하는 것입니다. 이때 공백이 포함된 인수가 있을 경우 인수가 분리되지 않도록 해야 합니다.

#!/bin/bash

bar() {
    # foo 함수에서 전달받은 인수들 중에서 "Suse Linux" 원소를 삭제하고
    IFS=$'\n'
    set -- $( for v; do [ "$v" != "Suse Linux" ] && echo "$v"; done )
    unset -v IFS

    # 결과를 foo 함수의 local 변수인 $opts 에 설정
    # 이때 인수들 중에 공백이나 quotes 이 사용될수 있으므로 printf "%q " 를 사용해야 합니다.
    # 그러면 Arch\ Linux Ubuntu\ Linux Fedora\ Linux 와 같이 설정됩니다.
    if [ $# -gt 0 ]; then opts=`printf "%q " "$@"`; fi

    # 또는 다음과 같이 매개변수 transformation 을 이용: ${parameter@Q} 
    # if [ $# -gt 0 ]; then opts=${@@Q}; fi
}

foo() {
    local opts

    echo 'number of "$@" :' $#
    for v; do echo "$v"; done        # 삭제전 "$@" 값을 출력

    echo "--------------------"

    bar "$@"

    # bar 함수가 설정한 $opts 값을 이용해 다시 $@ 값을 설정
    # bar 함수에서 printf "%q " 를 이용해 $opts 값을 설정하였으므로 
    # Arch Linux 는 두 개의 인수가 되지 않고 하나의 인수가 됩니다.
    eval set -- $opts

    echo 'number of "$@" :' $#
    for v; do echo "$v"; done        # 삭제후 "$@" 값을 출력
}

foo "Arch Linux" "Ubuntu Linux" "Suse Linux" "Fedora Linux"

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

$ ./test.sh 
number of "$@" : 4
Arch Linux
Ubuntu Linux
Suse Linux
Fedora Linux
--------------------
number of "$@" : 3
Arch Linux
Ubuntu Linux
Fedora Linux