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 이 삭제된다.
# echo $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
$ 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 "{}") "{}"' \;