eval

eval [ arg . . . ]

명령치환 결과로 shell 에서 실행 가능한 명령문이 나온다면 그 명령은 기본적으로 실행이 가능합니다. 하지만 명령문이 조금 복잡해져서 파이프나 redirections, quotes 등이 사용된다면 이제는 그 명령문은 실행할 수 없게 됩니다. 왜냐하면 명령문에서 사용되는 shell 키워드, 메타문자들, quotes 등은 확장과 치환 이전에 해석이 완료되기 때문입니다. 따라서 이와 같은 경우에도 실행이 가능하게 해주는 명령이 eval 명령입니다. eval 은 인수로 주어지는 스트링을 한번 evaluation 한 후에 결과를 다시 명령문으로 실행합니다.

# 명령치환 결과로 ls -l 스트링이 출력되므로 정상적으로 실행된다.
$ $( echo 'ls -l' )  
total 4168
drwxr-xr-x  2 mug896 mug896    4096 May  3 16:53 Desktop
drwxr-xr-x 45 mug896 mug896    4096 Apr 26 20:59 Documents
. . .

# 명령문에 파이프 메타문자가 사용되어 정상적으로 실행되지 않는다.
$ $( echo 'ls -l | wc -l' )
ls: cannot access '|': No such file or directory
ls: cannot access 'wc': No such file or directory

# 이때 eval 를 사용하면 정상적으로 실행할 수 있습니다.
$ eval "$( echo 'ls -l | wc -l' )"
25

몇 가지 예를 들어보면 다음과 같습니다.

# if 는 shell 키워드로 명령치환 이전에 해석이 완료되므로 이후에는 단순 외부명령이 된다.
$ $( echo "if test 1 -eq 1; then echo equal; fi" )
if: command not found

# 이때 eval 을 사용하면 정상적으로 실행됩니다.
$ eval "$( echo "if test 1 -eq 1; then echo equal; fi" )"
equal
........................................................

# 마찬가지로 대입 연산은 명령치환 이전에 해석이 완료되므로 이후에는 단순 외부명령이 된다.
$ $( echo "AA=100" )
AA=100: command not found

$ eval "$( echo "AA=100" )"    # eval 을 사용하여 정상적으로 실행
$ echo $AA
100
.................................................................

# 다음은 명령치환 결과로 export 명령문이 출력되므로 문제없이 실행될것 같지만 명령문에
# " " quotes 이 사용되어 문제가 됩니다. (quotes 해석도 명령치환 이전에 완료됩니다.)
$ echo $( echo 'export LESSCLOSE="/usr/bin/lesspipe %s %s"' )
export LESSCLOSE="/usr/bin/lesspipe %s %s"

$ $( echo 'export LESSCLOSE="/usr/bin/lesspipe %s %s"' )
bash: export: '%s': not a valid identifier
bash: export: '%s"': not a valid identifier

$ eval "$( echo 'export LESSCLOSE="/usr/bin/lesspipe %s %s"' )"

$ echo $LESSCLOSE 
/usr/bin/lesspipe %s %s

eval 명령의 실행단계

eval 명령은 주어진 인수들을 읽어 들인 후 실행합니다. 여기서 주목해야 될 점은 두 단계를 거쳐서 실행이 된다는 점입니다. 첫째. 읽어들이는 단계 , 둘째. 실행하는 단계. 그래서 결과적으로 인수부분에서 확장과 치환이 두번 일어나게 됩니다. 읽어들일 때 한번 실행할 때 한번.

$ AA=100 BB=200

# 인수 부분에서 확장, 치환이 일어나 $BB 가 200 이되고
# 마지막에 값을 표시하는데 불필요한 quotes 을 삭제한다.
$ echo '$AA' $BB
$AA 200

# 1. eval 읽어 들이는 단계에서 위와 같이 확장, 치환이 되고 quotes 이 삭제된다: eval $AA 200
# 2. 실행단계 에서도 확장, 치환이 일어나므로 $AA 는 100 이된다.
$ eval echo '$AA' $BB
100 200

# 1. single quotes 이 escape 됐으므로 read 단계에서 남게된다.
# echo '$AA' 200
# 2. 실행단계 에서 quotes 이 삭제되어 표시된다.
$ eval echo \''$AA'\' $BB
$AA 200

brace 확장에서는 range 값으로 변수를 사용하지 못하는데 ( 변수확장이 뒤에 일어나므로 )
eval 명령을 활용하면 brace 확장에서도 변수를 사용할 수 있습니다.

$ a=1 b=5

# 1. eval 명령 read 단계에서 변수확장이 일어난다.
# echo {1..5}
# 2. 실행단계 에서 brace 확장이 된다.
$ eval echo {$a..$b}
1 2 3 4 5

명령에 선행하는 대입연산을 이용할 때도 eval 명령을 사용할 수 있습니다. 명령 앞에서 대입한 값은 명령이 실행될때 환경변수 형태로 전달되어 사용되므로 명령이 실행이 돼야 사용할 수 있는데 다음의 경우는 echo 명령이 실행되기 전에 $A, $B, $C 변수확장이 일어나므로 기존의 값을 표시하게 됩니다.

$ A=1 B=2 C=3
$ A=4 B=5 C=6 echo $A $B $C
1 2 3

다음과 같이 eval 명령을 사용하면 해결할 수 있습니다.

$ A=1 B=2 C=3

# 1. eval 명령 read 단계에 quotes 이 삭제된다 : echo $A $B $C
# 2. 실행단계 : 이미 eval 명령이 실행중에 있으므로 대입한 값이 사용된다.
$ A=4 B=5 C=6 eval 'echo $A $B $C'
4 5 6
$ echo $A $B $C      # 명령 종료 후에는 원래 값으로 복귀
1 2 3                             

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

$ arr=(foo bar zoo)

# 1. eval 명령 read 단계에 single quotes 이 삭제된다 : echo "${arr[*]}"
# 2. 실행단계 : "${arr[*]}" 가 double quote 되어있으므로 IFS='|' 값을 구분자로 출력된다.
$ IFS='|' eval echo '"${arr[*]}"'
foo|bar|zoo

printf %q 를 이용한 인수 설정

다음과 같이 printf %q 를 이용한 인수 설정시에는 단순히 set 명령만 사용하면 안되고 eval set 을 사용해야 인수가 올바로 설정됩니다. \ 문자를 이용한 escape sequences 처리도 변수확장 전에 완료가 됩니다.

# "foo? bar *zoo[3]" 스트링을 하나의 인수로 설정하기 위해 printf %q 사용.
$ arg=$( printf "%q" "$( echo -e "foo? bar *zoo[3]" )" ) 
.............................................................

# 단순히 set 명령 사용                    # eval set 명령 사용
$ echo $arg                            $ echo set -- $arg
foo\?\ bar\ \*zoo\[3\]                 set -- foo\?\ bar\ \*zoo\[3\]

$ set -- $arg                          $ eval set -- $arg

$ echo $#                              $ echo $#          # 하나의 인수가 된다.
3                                      1

$ echo "$1"                            $ echo "$1"
foo\?\                                 foo? bar *zoo[3]

$ echo "$3"
\*zoo\[3\]

스크립트 실행 중에 명령문을 만들어 실행하기

find . -name "*.sh" 명령문을 다음과 같이 만들어서 실행할 수 있습니다.

$ AA='find . -name'

$ BB='"*.sh"'

$ eval "$AA $BB"
./args.sh
./sub/foo.sh
./tmp/bar.sh
...
...

eval 명령을 사용하다 보면 복잡해 보일 때가 있는데 이때는 읽기 부분은 eval 을 echo 로 바꾸어 실행해 보면 됩니다. echo 명령을 실행해서 나온 결과가 eval 명령이 실행단계에서 실행하게될 명령이 됩니다. 그리고 그 명령이 실행돼서 나온 종료 상태 값이 eval 명령의 종료 상태 값이 됩니다.

$ eval "$(echo hello world | sed 's/hello/wc <<< /')"
1 1 6
$ echo $?
0

# 1. 읽기단계: eval 을 echo 로 변경하여 실행해본다.
$ echo "$(echo hello world | sed 's/hello/wc <<< /')"
wc <<<  world    # 이 명령이 eval 이 실행단계에서 실행하게 될 명령임.

$ wc <<<  world  # 2. 실행단계
1 1 6            # 이 값이 eval 명령의 stdout 값이 되고
$ echo $?
0                # 이 값이 eval 명령의 종료 상태 값이 된다.

다음은 select 명령문에서 사용되는 case 문을 eval 을 사용해서 동적으로 생성합니다.
eval 을 echo 로 변경해서 실행해보면 완성된 case 문을 볼 수 있습니다.

#!/bin/bash

PS3=$'\n>>> Select number: '

files=( `find /usr/bin -type f \( -name 'c??' -o -name 'd??' -o -name 'e??' \)` )

select pick in "${files[@]}"
do
    eval "$(
    echo 'case $pick in'
    str=""; for v in "${files[@]}"; do [[ ${v##*/} =~ ^c ]] && str+="$v|"; done
    echo "${str%|}) char=c ;;"
    str=""; for v in "${files[@]}"; do [[ ${v##*/} =~ ^d ]] && str+="$v|"; done
    echo "${str%|}) char=d ;;"
    str=""; for v in "${files[@]}"; do [[ ${v##*/} =~ ^e ]] && str+="$v|"; done
    echo "${str%|}) char=e ;;"
    echo 'esac'
    )"
    echo
    echo "\"$pick\" command starts with \"$char\"."
    echo
    break
done

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

$ ./test.sh
1) /usr/bin/eog    4) /usr/bin/cmp   7) /usr/bin/col  10) /usr/bin/cvt
2) /usr/bin/dir    5) /usr/bin/cut   8) /usr/bin/env  11) /usr/bin/ctr
3) /usr/bin/cat    6) /usr/bin/dig   9) /usr/bin/eqn

>>> Select number: 3

"/usr/bin/cat" command starts with "c".

다음과 같이 함수를 동적으로 만들어 실행하는 것도 가능합니다.

$ main() {
>     local fbody='() { echo "function name is : $FUNCNAME"; }'
>     local fname
>     for fname in foo{1..10}; do
>         eval "${fname}${fbody}"   # 이 부분에서 함수 정의가 됨
>         $fname                    # 함수명으로 실행
>     done
> }

$ main
function name is : foo1
function name is : foo2
function name is : foo3
...
...
if test yes = "$_G_HAVE_XSI_OPS"; then
  _d='case $1 in
        */*) func_dirname_result=${1%/*}$2 ;;
        *  ) func_dirname_result=$3        ;;
      esac'
else
  _d='func_dirname_result=`$ECHO "$1" | $SED "$sed_dirname"`
      if test "X$func_dirname_result" = "X$1"; then
        func_dirname_result=$3
      else
        func_append func_dirname_result "$2"
      fi'
fi

eval 'func_dirname ()
{
    $debug_cmd

    '"$_d"'
}'

eval 명령을 이용한 indirection

다음과 같이 eval 명령을 indirection 에 활용할 수 있습니다.

$ AA=BB                        $ hello=123
$ echo $AA                     $ linux=hello
BB                             $ eval echo \$$linux
$ eval $AA=100                 123
$ echo $BB
100                            if test $hello -eq $(eval echo \$$linux); then
                                   . . .
                               fi

대입연산은 shell keyword 와 같이 변수확장 전에 처리되므로 단순히 $AA=100 로 할 수 없습니다.

$ AA=BB

$ $AA=100
BB=100: command not found

다음은 indirection 을 활용하여 마지막 인수 값을 file 변수에 대입합니다.

$ cat test.sh                                 foo () {
#!/bin/sh                                         eval $1='$(echo $'$2')'
                                                  # 또는 eval $1='${!2}'
eval file='$'$#                               }

echo file name : "$file"                      $ aaa=100 bbb=200
--------------------------                    
                                              $ foo aaa bbb
$ ./test.sh -a -b -c data.txt                    
file name : data.txt                          $ echo $aaa
                                              200

파이프 와 redirection

eval 은 명령이기 때문에 파이프와 같이 사용될 경우 한쪽에만 적용되는 것입니다.
그러므로 다음과 같은 경우 eval 은 파이프 왼쪽에 위치한 명령에만 적용됩니다.

# command1 에만 eval 이 적용
$ eval command1 ... | command2 ...

전체 명령을 eval 하려면 다음과 같이합니다.

# command2 에도 eval 이 적용되려면
$ eval command1 ... | eval command2 ...

# 전체 명령을 quote 하면 하나의 eval 만 사용할 수 있습니다.
$ eval "command1 ... | command2 ..."

# 다음과 같이 파이프를 escape 할 수도 있습니다.
$ eval command1 ... \| command2 ...

# 파이프를 escape 할 경우 redirection 이 사용되면 같이 escape 해줘야 합니다.
$ eval command1  ... '2>&1 |' command2 ...

$IFS 와 eval

$ cmd='echo hello eval'

$ $cmd              # 단어분리에 의해 echo 는 명령 hello eval 은 인수가 된다.
hello eval

$ IFS=$'\n'         # IFS 값을 newline 으로 변경하면

$ $cmd              # 단어분리가 일어나지 않아 'echo hello eval' 전체가 하나의 명령이 된다.
echo hello eval: command not found

$ eval $cmd         # 이때 eval 을 사용하면 실행이 된다.
hello eval

quotes 과 eval

$ foo='"hello         eval"'     # foo 변수값에 공백과 quotes 이 포함됨

$ echo $foo
"hello eval"

$ echo "$foo"
"hello         eval"

$ eval echo '$foo'         # single quotes 은 변수값에서 quotes 을 유지하는 역할을 한다.
"hello eval"

$ eval echo "$foo"         # 변수값에서 double quotes 이 제거됨.
hello         eval

$ eval echo '"$foo"'       # double quotes 은 변수값에서 공백을 유지하는 역할을 한다.
"hello         eval"

Quiz

single quotes, double quotes 이 모두 포함된 명령문을 변수에 대입해서 eval 을 이용해 실행하려고 합니다. 이때는 변수에 대입할 때 quotes 을 escape 하는 게 복잡해질 수 있는데요. quotes 을 따로 escape 할 필요 없이 그대로 변수에 대입하려면 어떻게 할까요?

다음과 같이 here document 를 활용하면 프롬프트에서 실행한 명령문 그대로 변수에 대입할 수 있습니다.

$ find * -name 'Packages*' -type f -exec \
    sh -c 'echo $(md5sum "{}" | awk '\''{print $1}'\'') $(stat -c %s "{}") "{}"' \;

b49dd0f63bca9b3a139c5af3dd94c816 380 Packages
e805c26ff46c6e138e3cd198cff281ea 301 Packages.bz2
997a7252f202566a1e5fdc5b50c2ffdf 283 Packages.gz

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

$ read -rd '' CMD1 <<\EOF || :
find * -name 'Packages*' -type f -exec \
EOF

$ read -rd '' CMD2 <<\EOF || :
sh -c 'echo $(md5sum "{}" | awk '\''{print $1}'\'') $(stat -c %s "{}") "{}"' \;
EOF

$ echo "$CMD1$CMD2"
find * -name 'Packages*' -type f -exec \
sh -c 'echo $(md5sum "{}" | awk '\''{print $1}'\'') $(stat -c %s "{}") "{}"' \;

$ eval "$CMD1$CMD2"
b49dd0f63bca9b3a139c5af3dd94c816 380 Packages
e805c26ff46c6e138e3cd198cff281ea 301 Packages.bz2
997a7252f202566a1e5fdc5b50c2ffdf 283 Packages.gz

---------------------------------------------------  또는

$ CMD=$( cat <<\EOF
find * -name 'Packages*' -type f -exec \
EOF
)$'\n'

$ CMD+=$( cat <<\EOF
sh -c 'echo $(md5sum "{}" | awk '\''{print $1}'\'') $(stat -c %s "{}") "{}"' \;
EOF
)

$ echo "$CMD"
find * -name 'Packages*' -type f -exec \
sh -c 'echo $(md5sum "{}" | awk '\''{print $1}'\'') $(stat -c %s "{}") "{}"' \;