셸 스크립트를 짜다보면 eval을 쓰거나 또 다른 스크립트를 만들어내야할 때가 종종 있다. 그냥 장난삼아 짜는 스크립트라면 상관 없을 수도 있지만, 모든 상황에 대비해 돌아갈 수 있게 진지하게 짜는 스크립트라면 값이 뭉개지지 않도록 감싸는 방법(escaping in shell scripts)을 고민해야 한다.
가령, f에 파일 이름이 들어있고 이를 가지고 관련된 파일을 지우는 스크립트를 출력하는 스크립트를 짠다고 생각해보자.
echo rm -f $f.bak >cleanup.sh
f=file.txt라면 cleanup.sh에는 원하는대로 rm -f file.txt.bak가 들어가게 된다.
그런데 f="another file.txt"처럼 공백이 들어가기라도 한다면 문제가 생긴다. cleanup.sh에는 아래처럼 적혀있을 것이고
rm -f another file.txt.bak
another와 file.txt.bak라는 엉뚱한 파일 둘이 지워질테니까! :'( 이 예는 공백이 들어간 경우지만, 사실 \``나',;`와 같이 셸 문법에 해당하는 특수문자가 포함되면 더더욱 심각한 문제들이 생길 수 있다.
자, 그럼 이렇게 f에 들어있는 값이 명령과 뒤섞이는 문제를 어떻게 해결하느냐!?
보통 공백 등의 특수문자가 들어있을 수 있는 경우는 인자가 계속 연결되어 있을 수 있도록 따옴표(")로 감싸준다.
f="another file.txt"
rm -f "$f".bak
위의 f=...에서 따옴표로 감싸지 않았다면 f=another와 file.txt라는 두 명령이 된다. rm ...의 경우도 비슷하게 두 파일을 지우는 의미가 된다. 출력을 다시 스크립트로써 실행해야 하는 경우가 아니라면 따옴표로 충분하다.
만약 다시 실행해도 같은 값이 나오게 하고 싶다면 어떻게 해야할까?
echo rm -f \"$f\".bak >cleanup.sh
위처럼 따옴표까지 출력하면 될까? 그런다 해도 f에 $a처럼 문자열이 들어있기라도 하면, cleanup.sh를 실행할 때 $a의 값을 읽어와 지울 수도 있다. 더 심각한 경우는 f 안에 따옴표 그 자체가 들어있는 것. 그 따옴표가 앞에 있는 것과 짝을 이루면서 $f 다음에 붙여준 따옴표의 쌍이 맞지 않는다는 문법 오류가 튀어나올 것이다. :(
IFS를 가지고 장난치는 방법 등 여러가지 해법이 있을 수 있겠지만, 내가 터득한 가장 확실한 방법을 소개한다. 바로 홑따옴표로 묶는 것.
물론, 이것은 parameter expansion에서 //를 썼기 때문에 bash에서만 동작하며 다른 그냥 POSIX 셸에서는 안된다. 허나 Linux에는 대부분 bash가 있고 Windows에서도 bash를 돌릴 수 있다는 점을 감안하면 그리 제한적인 것만도 아니다. :)
echo rm -f '${f//"'"/"'\\''"}'.bak >cleanup.sh
여기서 '${f//"'"/"'\\\\''"}'를 주목하자.
원래 bash에서 ${x//y/z}는, x의 값에서 나타나는 y들을 모두 z로 바꾼 값을 의미한다. 홑따옴표로 감싼 문자열은 셸이 어떠한 바꿔치기도 하지 않고 그대로 값으로 받아들인다. 따라서 홑따옴표 문자열은 우리의 목적에 매우 적합한 표현 방식이다. 그러나 한 가지 문제는, 홑따옴표 그 자체는 문자열의 끝을 나타내기 때문에 문자열 내에는 넣을 수가 없다는 점이다. 따라서 이를 극복하기 위해, 홑따옴표는 홑따옴표 문자열 밖에 \\'와 같이 적어서 이어붙인다. 요약하면, 홑따옴표(')를 만나면 문자열을 닫고 홑따옴표를 하나 백슬래시(\)를 이용해서 적고 다시 문자열을 시작함으로써 모든 글자들을 원래 그대로 전달할 수 있는 것이다.
사실, 이 방법은 이미 bash가 쓰고 있다. (어쩌면 다른 셸들도 쓰는지 모르겠다.) set -x를 한 다음에 출력되는 내용들을 보면 죄다 홑따옴표로 감싸여있다. 처음엔 이 징그러운 출력의 의미를 몰랐으나, 값이 그대로 전달되게 하기 위해서였음을 깨닫고는 감탄하지 않을 수 없었다. 홑따옴표(') 하나만 가지고 이렇게 완전한 escaping을 할 수도 있구나..라고. XPath에도 비슷한 문제가 있다. 따옴표(")로 감싼 문자열 안에는 홑따옴표(')가 올 수 있고 홑따옴표-문자열 안에는 따옴표가 올 수 있다. 이를 이용해서 보통 글자들은 홑따옴표-문자열 안에 쓰고 홑따옴표만 따옴표-문자열에 써서 이들을 concat() 시키는 방법으로 문자열 값들을 escape을 시킨다.
값을 있는 그대로 다른 세상(? 실행 환경?)으로 전달시키는 일은 메타프로그래밍을 하다보면 매우 중요한 문제가 된다. 예전에 Quine을 만들기 위해 프로그래밍언어에서 필요한 요소가 뭘까 고민하던 적이 있었는데, 그 때 내린 결론도 바로 값을 탈출시킬 수 있느냐였다. 아무튼, 셸 스크립트를 출력하는 셸 스크립트를 짤 상황이 생긴다면 '${f//"'"/"'\\\\''"}'를 떠올리며 요긴하게 써먹기 바란다. :-)
