shell – expansion

셸 스크립트를 사용하면서, 가장 헷갈리는 부분 중 하나가 문자열 값들이 어떻게 확장되는가 하는 점이다. 여기서는 조금 자세하게 엔터키를 누르는 그 순간에 어떤 ‘마법’이 벌어지는지 보려고한다. 몇 가지 간단한 예제들에서도 이 현상은 우리가 알지 못하는 사이에 쓰고 있는 셈인데, 이를테면 echo 같은 명령에서 이것이 일어난다.

확장 (expansion)

명령을 입력하고 엔터를 누르는 매 순간에 bash는 우리의 명령을 수행하기 이전에 몇 가지 텍스트 관련 처리를 먼저 하게 된다. 이미 echo * 등의 명령에서 어떻게 단순한 글자가 변경되는지를 본 적이 있다. 예를 들어 *는 쉘에서 여러가지 의미를 가지게 된다. 이것이 실제로 벌어지는 것을 우리는 확장(expansion)이라 부른다. 확장을 통해 우리가 무언가를 입력하고, 그것은 다시 다른 무언가로 확장된 후, 실제 명령은 그것을 통해 수행된다. 이를 들여다보기 위해 echo 명령을 좀 더 살펴보자. 이는 아주 단순한 빌트인 명령으로 그저 텍스트를 화면에 출력한다.

$ echo this is a test
this is a test

간단하다. 넘겨지는 모든 인자가 출력된다. 다음은 어떤가?

$ echo *
Desktop Document ls-output.txt Music Pictures Public Templates Videos

무슨 일이 일어났는가? 왜 echo*표를 출력하지 않았나? 이전의 ls 라든지 find 등의 명령에서 *는 파일이름의 일부의 어떤 글자와도 매치되는 와일드 카드로 쓰였다. 하지만 이것이 쉘에서 어떻게 처리되는지는 논의한 바가 없어다. 여기서 * 는 현재 디렉토리 내의 모든 파일이름으로 확장되었고, 그 시점은 echo가 실행되기 직전이다. 엔터키가 눌려지면 bash는 명령줄 내의 모든 확장 가능한 문자를 찾아서 확장한 후 명령을 실행한다. 따라서 echo 명령 자체는 * 문자를 본 적도 없는 셈이다. 암튼 이 원리를 알면 echo 는 실제로는 기대했던 동작을 했음을 알 수 있다.

경로명 확장

와일드 카드가 작동하는 방식을 경로명 확장이라고 한다. 앞서 배웠던 몇 가지 기술들을 이용하면 실제로 그것이 확장되는 것을 볼 수 있다. 홈 디렉토리가 이렇게 생겼다고 가정한다.

$ ls
Desktop  ls-output.txt  Pictures  Templates
Document Music          Public    Videos

우리는 다음의 확장을 사용해 볼 수 있다.

$ echo D*
Desktop  Documents

또,

$ echo *s
Documents  Pictures  Templates  Videos

혹은

$ echo [[:upper:]]*
Desktop  Documents  Music  Pictures  Public  Templates  Videos

[[를 써야 하지?

이렇게도

$ echo /usr/*/share
/usr/kerberos/share  /usr/local/share

틸드 확장

쉘에서 ~는 사용자의 홈 디렉토리를 가리킨다고 했다. 이것도 실제로는 쉘이 ~ 문자를 해당 경로로 확장하기 때문에 그렇게 취급되는 것이다.

$ echo ~
/home/me

$ echo ~foo
/home/foo

수식 확장

수식 계산 확장의 폼은 $(( EXPRESSSION )) 으로 이루어진다. EXPRESSION은 값과 연산자들로 이루어진 수식이다. 이 확장은 모든 값이 정수인 숫자일 때만 일어날 수 있으며 몇 가지 정해진 연산 종류만을 지원한다. (더하기, 빼기, 곱하기, 나누기, 나머지, 지수) 이 때 나누기의 결과는 항상 정수임을 명심하자.

수식 확장에서 연산자와 피연산자간을 공백으로 구분하는 것은 별로 의미 없다.

$ echo $(($((5**2)) * 3))
75

속에 위치한 $(( )) 는 안써도 상관없으며 순서가 중요한 경우 그냥 1개 짜리 괄호를 써도 된다.

Brace 확장

중괄호를 이용한 확장은 꽤나 이상해보이는데, 이는 단순 치환이 아니라 패턴을 이용해서 복수개의 문자열을 생성할 수 있다. 일단 가장 간단한 예를 보자.

$ echo Front-{A,B,C}-Back
Front-A-Back  Front-B-Back  Front-C-Back

중괄호 확장은 치환자로 기능하는 것이 아니라 그 내부의 리스트로부터 전체 문자열이 맵핑되는 효과를 갖는다. 리스트는 콤마로 이어지는 연속열일 수도 있지만, 숫자나 연속된 알파벳 같은 경우에는 ..으로 범위를 줄 수 있다.

$ echo Number_{1..5}
Number_1  Number_2  Number_3  Number_4  Number_5  

숫자값은 0으로 패딩을 넣을 수 있고, 알파벳도 이런식으로 붙인다. 역순 또한 가능하고, 네스팅도 된다.

만약 2007년부터 2015년 사이의 01~12월에 해당하는 모든 폴더를 만들려고 한다면, for를 쓸 것도 없이 한줄이면 완성이다.

$ mkdir Photos
$ cd Photos
$ mkdir {2007..2015}-{01..12}
$ ls
...

파라미터 확장

파라미터 확장은 일종의 특수 변수임만 알면된다. bash 쉘이 실행될 때의 인자값이나 환경변수들은 일반 변수처럼 $ 사인을 앞에 붙여쓰면 그 값을 확장된다.

환경 변수들을 알아보고 싶으면,

$ printenv | less

해보자.

한가지 특이한 점은, 확장을 위한 패턴을 쓰다 오타가나면 쉘은 해당 패턴을 인지하지 못해서 잘못 쓰인 패턴이 그대로 확장되지 않지만, 파라미터 확장에서는 없는 파라미터를 확장하면 그냥 빈 문자열이 돼버린다.

명령 치환

확장에 앞서 특정한 명령을 실행한 그 결과로 패턴을 치환하는 것이다.

$ echo $(ls)
Desktop Documents ls-output.txt Music Pictures Public Templates Videos

이거 좋더라.

$ ls -l $(which cp)
-rwxr-xr-x 1 root root 71516 2007-12-05 08:58 /bin/cp

which cp 는 명령줄에서 cp를 실행하면 어디에 있는 파일이 실행되는지를 찾는 것인데, 그 결과로 해당 파일의 특성들을 뽑아보는 명령이다.

따옴표에 넣기

지금까지 쉘이 수행하는 확장에 어떤 것이 있는지 살펴보았다. 이제는 이 확장을 어떻게 컨트롤 할 수 있는지를 배울 차례이다.

$ echo this is a       test
this is a test

$ echo the total is $100.00
The total is 00.00

첫번째 예제에서 쉘에 의한 단어 자르기 (토큰 구분)에서 연속되는 공백들은 모두 무시되어 echo 는 하나의 공백만을 출력한다. (즉 각단어는 echo의 서로 다른 인자이며, echo는 없는 인자는 무시해버린다. ) 두 번째 예제에서는 $1이 잘못된 파라미터로 해석되면서 사라진다. 따라서 백딸라를 쓸 수 없다. 이런 문제들은 따옴표에 넣기를 통해서 원치 않는 확장을 제거할 수 있다.

이중 따옴표

첫번재 따옴표 종류는 큰따옴표이다. 큰 따옴표 내에 문자열을 넣으면, 모든 특별한 문자들은 그 의미를 잃고 단순한 글자로 취급된다. 이 때에도 예외는 있는데, $, \, 그리고 백팃 (esc 아래 있는)이다. 이 말은 즉, 큰 따옴표 내에서는 단어 자르기, 경로확장, 틸드확장, 괄호확장은 무시되지만 파라미터확장, 수식확장, 명령확장은 여전히 가능하다는 이야기다. 이중 따옴표를 이용하여 우리는 파일 이름 내에 공백이 있는 파일을 다룰 수 있게 되었다!

$ ls -l two words.txt
ls : cannot access two: No such file or directory
ls : cannot access words.txt: No such file or directory

$ ls -l "two words.txt"
-rw-rw-r-- 1 me me 18 2016-02-20 13:03 two words.txt

하지만 여전히 명령확장, 수식확장, 파라미터 확장은 일어난다는 점을 잊지말자.

여기서 명령확장은 본 명령을 수행하기 전에 서브쉘을 구동하여 해당 패턴의 명령을 수행하고, 서브쉘의 표준 출력이 패턴을 대체하게 된다. 따라서 따옴표 내의 명령확장과 그냥 명령확장은 다르다. 따옴표 내의 명령확장은 명령의 결과가 단어 나누기로부터 보호된다.

$ echo $(cal)
February 2016 Su Mo Tu We Th Fr Sa 1 2 3 4 5 6 7 8 9 10 11 12 13 14
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

/// 따옴표 내에서 실행확장이 이루어지면 단어쪼개기로부터 보호된다.
$ echo "$(cal)"
February 2016
Su Mo Tu We Th Fr Sa
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29

작은 따옴표

만약 모든 확장을 무시할 생각이면, 작은 따옴표를 쓴다. 작은 따옴표를 쓰면 모든 확장이 무시된다.

PS1="\`if [ \$? = 0 ]; then echo ..... @\[\e[0m\]; fi \`[\u@\w]\\$ "
#    ^
#     \- 여기서는 큰 따옴표 내에서 백팃을 이스케이프해서 썼다. 
#        명령확장은 큰 따옴표 내에서도 동작하는데 왜 백팃을 이스케이핑했을까?

이스케이핑

간혹 한 글자만 따옴표로 감싸고 싶을 때가 있다. 이를 위해서는 글자 앞에 백슬래쉬를 붙여서 해당 문자를 이스케이핑할 수 있다. 만약 큰 따옴표 안에서 글자를 이스케이핑하면 선택적으로 확장을 방지하게 된다.

$ eho "The balane for user $USER is: \$5.60"
The balance for user me is $5.60

이는 특히 파일명 내의 특수문자의 쉘용 의미를 없애는데 널리 사용된다. 예를 들어 $, &, ! 과 같은 글자가 파일 이름에 사용될 때 적용할 수 있다.

$ mv bad\&filename good_filename

백슬래시 자체가 이스케이핑의 용도로 사용되므로 이를 온전히 표시하기 위해서는 백슬래시를 두 번써서 (//) 사용하면 된다. 이 역시 원래의 의미(다음 글자를 이스케이핑)를 없애는 용도로 사용됐다. 반대로 백슬래시가 특정 알파벳에 붙어서 특별한 의미를 가지기도 한다. 개행문자는 키보드로 입력할 수 없고 \n으로 입력한다. 그외에 출력이 불가능한 특수문자들도 있는데 예를 들어 \a는 컴퓨터의 비프 음을 내는 문자이다. 이는 echo 명령으로 출력되지 않는데, echo -e 옵션을 주어 소리를 실제로 낼 수 있다.