Quotes

Shell 에서 두 번째로 중요한 개념은 quotes 이라고 할 수 있습니다. shell 에서 quotes 은 숫자나 스트링 값을 구분하기 위한 용도로 사용하지 않습니다. 123, "123", '123' 은 모두 같고 abc, "abc", 'abc' 들은 차이가 없으며 모두 다 shell 에서는 스트링입니다. shell 에서 quotes 은 다음과 같은 용도로 사용됩니다.

  • 공백으로 분리되는 여러 개의 스트링을 하나의 인수로 만들 때
    ( sed, awk 스크립트를 quotes 을 이용해 작성하는 이유가 하나의 인수로 만들기 위해서입니다. )

  • 라인 개행이나 둘 이상의 공백을 유지하기 위해

  • 단어분리, globbing 발생을 방지하기 위해

  • shell 키워드, 메타문자, alias 와 같이 shell 에서 특수기능을 하는 문자, 단어를 단순히 명령문의 스트링으로 만들기위해

  • 문자 그대로 스트링을 강조하기 위해

최종적으로 명령이 실행될 때는 사용된 quotes 이 제거된 후에 인수가 전달됩니다.

-------- args.sh --------
#!/bin/bash

echo arg1 : "$1"
echo arg2 : "$2"

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

$ ./args.sh 111 "111"      # quote 을 한것과 하지않은 것은 차이가 없다
arg1 : 111
arg2 : 111

$ ./args.sh 111 222        # 두개의 인수를 나타낸다.
arg1 : 111
arg2 : 222

$ ./args.sh "111 222"      # quote 을 하면 한개의 인수가 됨
arg1 : 111 222
arg2 :

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

$ AA="111 222"

$ ./args.sh $AA            # 두개의 인수가 된다.
arg1 : 111 
arg2 : 222

$ ./args.sh "$AA"          # quote 을 하면 한개의 인수가 됨
arg1 : 111 222
arg2 :

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

# sed 스크립트를 quote 하지 않아 오류 발생
$ sed -n 1p; 2p; 3p file
ERROR

# quote 을 해야 sed 스크립트가 하나의 인수로 전달됩니다.
$ sed -n '1p; 2p; 3p' file
OK

------------------------
#!/bin/bash

ls -al              # 다음 세 명령은 모두 같고 차이가 없습니다.
ls "-al"            # 명령문에서 사용된 quotes 은 shell 에의해 해석된 후에
"ls" '-al'          # 실행될 때는 자동으로 제거됩니다.

특수 기능을 갖는 문자들

다음 세 문자는 shell 메타문자로 명령행 상에서 특수한 기능을 가집니다.

문자 기능
$ 매개변수 확장, 산술 확장, 명령 치환에 사용
` 명령 치환에 사용 ( backtick )
! history 확장에 사용 ( 프롬프트상 에서만 )
$ AA=hello

$ echo $AA world `date +%Y`   # $AA 변수가 확장이 되고 date 명령치환이 됩니다.   
hello world 2015

# 특수문자를 escape 할 경우
$ echo \$AA world \`date +%Y\`      
$AA world `date +%Y`

특수기능을 갖는 문자나 단어를 escape 하는 방법

shell 에서는 escape 할때 \ 문자 외에 quotes 을 사용할 수 있습니다. quote 을 하면 특수 기능이 없어지고 단순히 명령문을 위한 스트링이 됩니다.

# shell 에서 사용되는 (  )  ;  메타문자를 quote 하여 기능을 상실. find 명령을 위한 스트링이 됩니다.
# \(  \)  \; 한것과 같습니다.
$  find * -type f '(' -name "*.log"  -or  -name "*.bak" ')' -exec rm -f {} ';'

# background 프로세스를 생성할때 사용하는 & 메타문자를 quote 하여 기능을 상실.
#  \& 한것과 같습니다.
$ echo hello '&'
hello &

# escape 문자인 \ 를 quote 하여 기능을 상실. 결과적으로 \n 가 됩니다.
#  \\n 한것과 같습니다.
$ echo hello world | tr ' ' '\'n
hello
world

# t 문자에 설정돼 있는 alias 가 escape 되어 기능하지 않게됩니다.
# \t 한것과 같습니다.
$ alias t='type -a'
$ 't' time
...

# shell 키워드인 time 이 escape 되어 외부명령인 /usr/bin/time 이 실행됩니다.
# \time 한것과 같습니다.
$ 'time'
Usage: time [-apvV] [-f format] [-o file] [--append] [--verbose]
       [--portability] [--format=format] [--output=file] [--version]
       [--quiet] [--help] command [arg...]

No quotes

No quotes 상태에서는 기본적으로 모든 문자가 escape 됩니다. 따라서 shell 키워드, 메타문자, alias, glob 문자, quotes, whitespace 문자를 escape 하여 해당 기능을 disable 할 수 있습니다.

whitespace 문자는 space, tab, newline 을 말합니다.

$ echo \a\b\c\d\ \!\@\$\%\^\&\*\(\)\{\}\[\]\<\>\/\\ ...     # no quotes
abcd !@$%^&*(){}[]<>/\ ...

$ echo "\a\b\c\d" ...        # double quotes
\a\b\c\d ...

$ echo '\a\b\c\d' ...        # single quotes
\a\b\c\d

명령행 상에서 공백은 인수를 구분하는데 사용됩니다. 둘 이상의 공백은 의미가 없으므로 하나의 공백으로 대체됩니다. no quotes 에서는 공백도 escape 할 수 있습니다. 공백을 escape 하면 두 개의 인수가 하나가 됩니다.

----------- args.sh -----------
#!/bin/bash

echo arg1 : "$1"
echo arg2 : "$2"
-------------------------------

$ ./args.sh hello world           # 인수가 2개
arg1 : hello
arg2 : world

$ ./args.sh hello\ world          # 공백을 escape 하여 인수가 하나가 됩니다.
arg1 : hello world
arg2 : 

$ echo hello             world    # 둘 이상의 whitespace 문자는 single space 로 줄어든다.
hello world                       # tab 문자일 경우 space 로 변경됨

$ echo hello\ \ \ \ \ \ world     # 공백이 유지된다.
hello      world

no quotes 상태에서 escape 문자 사용예.

# 모든 문자가 escape 되므로 t n 이 echo 명령에 전달된다.
$ echo -e foo\tbar\n123  
footbarn123

# 다음과 같이 하면 \t \n 이 echo 명령에 전달되어 escape 문자가 처리된다.
$ echo -e foo\\tbar\\n123
foo    bar
123

! history 확장 escape

$ date
Sat Jul 18 00:06:41 KST 2015

$ echo hello !!     
echo hello date     # 이전 명령 history 확장
hello date

$ echo hello \!!    # escape 하여 history 확장 기능 disable
hello !!

\ 문자 escape

# no quotes 상태에서는 모든문자가 escape 되므로 'tr : n' 와 같아진다.
$ echo 111:222:333 | tr : \n
111n222n333

$ echo 111:222:333 | tr : \\n     # tr : '\n' 와 같은 결과
111
222
333

" , ' quote 문자 escape

$ echo double quotes \" , single quotes \'
double quotes " , single quotes '

행의 마지막에 \ 를 붙이고 개행을 하면 \newline 과 같이 되어 newline 을 escape 한 결과를 같습니다. 이것을 backslash-newline 이라고 하고 \ 뒤에 다른 문자가 오면 안 됩니다.

$ echo "I like \          
> winter and \
> snow"

I like winter and snow         # newline 이 escape 되어 기능을 상실해 한줄이됨.

대입연산 에서도 escape 은 처리됩니다.

$ regex=\w*foo\w*
$ echo "$regex"
w*foow*

$ regex='\w*foo\w*'
$ echo "$regex"
\w*foo\w*

Double quotes ( " " )

Double quotes 안에서는 $ ` ! 특수기능을 하는 문자들이 해석되어 실행되고 공백과 개행이 유지됩니다. 변수 사용 시에도 동일하게 적용되므로 quote 을 하지 않으면 공백과 개행이 유지되지 않습니다.

$ echo "I
> like
> winter       and         snow"
I
like
winter       and         snow         # 공백과 개행이 유지 된다.

#####  변수 사용시  #####

$ AA="this          is
two          lines"

$ echo $AA                  # whitespace 문자들이 single space 로 변경되고
this is two lines           # 공백과 개행이 유지되지 않는다.

$ echo "$AA"                # quote 을 하여 공백과 개행이 유지된다.
this          is
two          lines

Double quotes 에서 escape 되는 문자들

double quotes 에서는 위의 문자들이 특수한 기능을 가지고 사용되기 때문에 \ 문자로 escape 할 수가 있습니다. single quotes 과 비교해 보면 다음과 같습니다.

$ echo '\$ \` \" \\'         # single quotes
\$ \` \" \\

$ echo "\$ \` \" \\"         # double quotes
$ ` " \

$ echo "\( \{ \[ \A \@ \'"   # 그외 문자들은 그대로 '\' 가 출력된다.
\( \{ \[ \A \@ \'

# single quotes 은 문자 그대로 출력된다.
$ echo 'quotes\
> test'
quotes\
test

# double quotes 에서는 newline 이 escape 되어 한줄로 나온다. 
$ echo "quotes\
> test"
quotestest

double quotes 을 사용할때 한가지 주의해야될 사항은 ! 문자를 이용한 command history 확장 이 double quotes 에서도 일어난다는 것입니다. 이것은 command history 기능이 사용되는 프롬프트 상에서만 적용되는 것으로 함수나 스크립트 파일 실행시는 해당되지 않습니다. 자세한 내용은 해당 페이지를 참조하세요.

Array 와 관련된 특수기능

double quotes 은 array 와 관련해서 특수한 기능이 있는데 전체 원소를 나타내는 ${arr[@]} 를 quote 하면 그 의미는 "${arr[0]}" "${arr[1]}" "${arr[2]}" ... 와 같게 되고 ${arr[*]} 를 quote 하게 되면 그 의미는 "${arr[0]}X${arr[1]}X${arr[2]}X..." 와 같게 됩니다. 여기서 X$IFS 변수값의 첫 번째 문자를 나타냅니다.

"$@", "$*" positional parameters 에서도 동일하게 적용됩니다.

변수값이 null 일때 quote 한것과 안한것의 차이

$ AA=""

# args.sh 은 명령 라인에서 전달한 인수들을 출력해 주는 스크립트.
$ args.sh xx yy $AA zz
$0 : /home/mug896/bin/args.sh
$1 : xx
$2 : yy      # quote 을 하지 않으면 인수에 포함되지 않는다.
$3 : zz

$ args.sh xx yy "$AA" zz
$0 : /home/mug896/bin/args.sh
$1 : xx
$2 : yy
$3 :         # quote 을 하면 null 값 인수가 생긴다.
$4 : zz

가령 다음과 같은 스크립트에서 $comp 변수값이 -c 일 경우는 tmpfile 이 컴파일된 오브젝트 파일이 되고 $comp 변수값이 null 일 경우는 링크가 완료된 실행파일을 만들고자 한다면 $comp 변수를 double quote 하면 안되겠죠. 왜냐하면 quotes 에의해 null 값이 하나의 인수로 전달되어 두 번째와 같이 오류가 발생하기 때문입니다.

$ gcc $comp -o tmpfile hello.c
........................................

$ gcc "" -o tmpfile hello.c
gcc: error: : No such file or directory

변수를 quote 하는 것은 오류 메시지 출력에도 영향을 줍니다. 변수를 quote 하면 좀 더 명확한 오류메시지가 출력됩니다. 따라서 명령문을 작성할 때 위와 같은 경우가 아니라면 변수를 quote 해서 사용하는 것이 좋습니다.

$ AA=""

$ echo hello 2>& $AA
bash: $AA: ambiguous redirect

# quote 을 하면 좀 더 명확한 오류메시지가 출력된다.
$ echo hello 2>& "$AA"
bash: "$AA": Bad file descriptor
....................................

$ echo hello > $AA
bash: $AA: ambiguous redirect

$ echo hello > "$AA"
bash: : No such file or directory

Single quotes ( ' ' )

별다른 기능 없이 모든 문자를 있는 그대로 표시합니다. escape 도 되지 않습니다. 이 안에서 single quotes 을 사용하려면 뒤에 이어지는 $' ' 를 사용해야 합니다.

$ AA=hello

# 변수값도 확장이 안되고 개행도 있는 그대로 유지된다.
$ echo '$AA world 
> `date` 
> \$AA
> '
$AA world 
`date` 
\$AA

single quotes 사용시 ' 문자를 입력하려면 다음과 같은 방법을 사용할 수 있습니다.

# single quotes 을 분리한후 no quotes 상태에서 ' 를 escape
$ echo 'foo'\''bar'
foo'bar

# 또는 ' 문자를 double quotes 으로 감싸면 됩니다.
$ echo 'foo'"'"'bar'
foo'bar

Single quotes 사용이 필요한 경우

command string 이나 trap handler 를 작성할 때 double quotes 을 사용하면 작성 당시에 변수값이 확장되어 정의가 되므로 실행 시에 원하는 값이 표시되지 않을 수 있습니다.

1. command string 에서
$ AA=100

$ sh -c "AA=200; echo $AA"    # double quotes 사용
100

$ sh -c 'AA=200; echo $AA'    # single quotes 사용
200

다음은 find 명령을 이용해 ~/.cache 내에 있는 각 디렉토리 별로 디스크 사용량을 조회하는 것인데요. -exec 옵션에는 실행할 외부 명령을 작성하고 \; 로 끝을 표시합니다. 이때 명령 스트링에서 사용된 {} 가 매칭 된 디렉토리 명으로 치환되어 실행됩니다.

$ find ~/.cache -maxdepth 1 -type d -exec du -hs {} \; | sort -hr
1.9G    /home/mug896/.cache 
601M    /home/mug896/.cache/google-chrome 
471M    /home/mug896/.cache/mozilla
110M    /home/mug896/.cache/apt-file 
. . . .
. . . .
# 위 명령은 실제 다음과 같이 간단히 할 수 있습니다. 
$ du -h -d1 ~/.cache | sort -hr

위와 동일한 역할을 하는 명령을 single, double quotes 을 비교해보기 위해 sh -c 를 이용해 작성한 것입니다.

명령문에서 sh -c 를 사용하는 것은 command line 개념을 참고하세요.

# double quotes 을 사용할 경우
# {} 가 find 명령에 전달되어 디렉토리 명으로 바뀌기 전에 $( du ... ) 가 실행되므로 오류 발생
$ find ~/.cache -maxdepth 1 -exec \
    sh -c "if test -d \"{}\"; then echo \"$( du -hs \"{}\" )\"; fi" \; | sort -hr
du: cannot access '"{}"': No such file or directory

# 정상적으로 실행되려면 $ 문자를 \$ 로 escape 해서 명령 치환을 방지해야 합니다.
$ find ~/.cache -maxdepth 1 -exec \
    sh -c "if test -d \"{}\"; then echo \"\$( du -hs \"{}\" )\"; fi" \; | sort -hr
OK

# single quotes 을 사용할 경우는 정상적으로 실행됨
$ find ~/.cache -maxdepth 1 -exec \
    sh -c 'if test -d "{}"; then echo "$( du -hs "{}" )"; fi' \; | sort -hr
OK

# 다음은 xargs 명령을 이용한 것인데 -exec 의 경우와 동일하게 적용됩니다.
$ find ~/.cache -maxdepth 1 -print0 |
    xargs -0i sh -c 'if test -d "{}"; then echo "$( du -hs "{}" )"; fi' | sort -hr
OK
2. trap handler 에서

해당 페이지 참조

3. prompt 설정에서

해당 페이지 참조

$' ... '

이것은 ' ' 와 같은데 escape 문자 를 사용할 수 있습니다.
escape 문자가 처리되고 난 후에는 $ 가 제외된 ' ' 상태가 됩니다.

sh 에서는 사용할 수 없습니다.

$ echo -e "aaa\nbbb"       # single, double quotes 에서는 echo -e 옵션을
aaa                        # 사용해 출력할때 '\n' escape 문자가 처리된다.
bbb

$ foo="aaa\nbbb"           # 따라서 대입 연산에서 사용될 경우는
$ echo "$foo"              # '\n' 가 문자 그대로 저장되어 출력된다.
aaa\nbbb

$ foo=$'aaa\nbbb'          # $' ' quotes 을 사용해 대입하면 
$ echo "$foo"              # '\n' escape 문자가 처리되어 저장된다.
aaa
bbb

# echo -e 옵션을 사용하지 않아도 \n, \t, \' escape 문자가 처리되어 출력된다.
$ echo $'I like\n\'winter\'\tand\t\'snow\''
I like
'winter'    and    'snow'

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

$ IFS=$'\n'           # IFS 변수값을 newline 으로 설정.
$ IFS=$' \t\n'        # IFS 변수값을 space, tab, newline 으로 설정.

$" ... "

이것은 double qoutes 과 기능은 동일한데 메시지 localization 을 할때 사용됩니다.

printf %q

printf 명령의 %q 지정자는 명령에 사용되는 인수나 또는 명령문 전체를 다른 명령으로 올바르게 전달할 수 있게 escape 해줍니다.

# 공백문자, glob 문자, quotes 등을 모두 escape 해줍니다.
$ arg=$( printf "%q " "$( echo -e "foo? *bar[3]" )" )
$ echo $arg
foo\?\ \*bar\[3\]

# 입력되는 스트링에 \t, \n 같은 문자가 포함되면 $arg 값은 $' ' 형태가 됩니다.
$ arg=$( printf "%q " "$( echo -e "foo bar\n*zoo[3]" )" )
$ echo $arg
$'foo bar\n*zoo[3]'        # $' ' 형태가 된다.

$ echo echo $arg
echo $'foo bar\n*zoo[3]'

$ eval echo $arg
foo bar
*zoo[3]

Quotes 을 분리해 작성할 경우

Quotes 을 분리해 작성하게 되면 중간의 공백이 하나의 space 로 변경됩니다.

$ echo "ERROR: backup disk drive ( hdd usb )"          "must specified as the first"
argument"ERROR: backup disk drive ( hdd usb ) must specified as the first argument

$ echo "ERROR: backup disk drive ( hdd usb )" \
       "must specified as the first argument"
ERROR: backup disk drive ( hdd usb ) must specified as the first argument

Quotes 을 서로 붙여 사용하기

두개의 quotes 을 공백을 두지 않고 서로 붙이면 하나의 인수가 됩니다. 이 원리는 변수를 포함하는 명령 스트링을 만들거나 명령에 전달할 인수를 하나로 만들때 유용하게 사용할 수 있습니다.

명령 스트링을 만들 때

' ' 을 사용해 명령문을 작성하였는데 그 안에 변수를 사용할 일이 생기면 다음과 같이 ' ' 를 분리한 후 double quote 한 변수를 공백없이 붙여 사용하면 됩니다. 변수를 quote 하지 않으면 만약에 변수값에 공백이 포함될 경우 인수가 두개로 분리될 수 있습니다.

# sed 명령에 's/foo/bar/g' 는 하나의 인수로 전달된다.
sed -E 's/foo/bar/g' 

# single quotes 을 분리한 후 변수를 double quotes 하여 공백 없이 붙인다.
sed -E 's/'"$var1/$var2"'/g'

awk 는 스크립트 내에서 $ 문자를 사용하기 때문에 기본적으로 single quotes 을 이용해 작성하는데요. 다음의 경우를 보면 -exec 옵션에 사용된 sh -c 명령이 single quotes 에의해 작성되고 있는데 그 안에 있는 awk 명령에서도 single quotes 이 사용되고 있습니다. 이와 같은 경우 다음과 같이 일단 quotes 을 분리한후 \'"'" 를 사용해 연결하면 sh -c '...' 내에서 사용되는 awk 명령에서도 single quotes 을 사용할 수 있습니다.

$ 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

명령의 인수를 만들 때

명령에 인수를 만들어 전달할 때도 두 quotes 을 서로 붙여 사용하면 하나의 인수가 됩니다.

$ ./args.sh 11 "hello "$'$world \u2665' 33           # $' ' quotes 을 사용

$0 : ./args.sh
$1 : 11
$2 : hello $world$3 : 33

Perl 언어 에서의 quote 연산자

이와 같이 quotes 을 사용하는 방법이 복잡해 보일 수 있기 때문에 perl 언어에서는 별도로 quote 관련 연산자를 두고 있습니다. ( 그렇다고 이렇게 quotes 을 활용하는 기술이 없어지진 않겠죠. 이것도 하나의 활용 방법이기 때문에 )

$ perl -l - <<\@
$foo = 100;
$bar = qq(it is "worth" $foo);    # "qq" 는 double quote 에 해당
print $bar;                       # $bar = "it is \"worth\" $foo" 와 동일
@
it is "worth" 100

$ perl -l - <<\@
$foo = 100;
$bar = q(it is 'worth' $foo);     # "q" 는 single quote 에 해당
print $bar;                       # $bar = 'it is \'worth\' $foo' 와 동일
@
it is 'worth' $foo

$ perl -l - <<\@
$res = qx(date --date="2 year ago");    # "qx" 는 backtick 에 해당
print $res;                             # res=`date --date="2 year age"` 와 같은 형태
@
Sun Nov 15 14:27:26 KST 2020

Quiz

디렉토리에서 작업을 하다 보니 ? 문자가 포함된 파일이 생성되었습니다. rm 명령을 이용해 삭제하려고 해도 삭제가 되지 않는데요. 어떻게 하면 삭제할 수 있을까요?

ls 명령은 파일명에 nongraphic 문자가 존재할 경우 ? 로 표시하는데요. 다음과 같이 삭제할 수 있습니다.

$ ls
??  bar  foo  zoo

$ rm '??'
rm: cannot remove '??': No such file or directory

$ ls -b                           // 먼저 -b 옵션을 이용해 nongraphic 문자를 8 진수로 출력
\312\004  bar  foo  zoo

$ rm $'\312\004'                  // $' ' quotes 을 이용해 삭제합니다.

$ ls
bar  foo  zoo

2 .

gcc 를 이용해 컴파일을 할때 -D 옵션을 이용하면 명령 라인에서 매크로 값을 설정할 수 있습니다. 매크로 값으로 스트링을 전달하려면 어떻게 할까요?

명령 라인에서 사용된 quotes 은 shell 에의해 해석된 후에 최종적으로 명령이 실행될 때는 제거됩니다. 따라서 다음 gcc 명령의 경우 MESSAGE 값은 "hello" 가 아니라 quotes 이 제거된 hello 가 되므로 오류가 발생하게 됩니다.

$ cat test.c
#include <stdio.h>
int main(void){
    char *str = "DEBUG: *** " MESSAGE " ***";
    puts(str);
}

# char *str = "DEBUG: *** " hello " ***"; 가 되므로 오류가 된다.
$ gcc -D MESSAGE="hello" test.c
...: error: ‘hello’ undeclared (first use in this function); did you mean ‘ftello’?

다음과 같이해야 매크로 값으로 double quotes 을 함께 전달할 수 있습니다.

# char *str = "DEBUG: *** " "hello" " ***"; 가 되므로 OK
$ gcc -D MESSAGE='"hello"' test.c
$ gcc -D MESSAGE=\"hello\" test.c

# char *str = "DEBUG: *** " "hello quotes" " ***"; 가 되므로 OK
$ gcc -D MESSAGE='"hello quotes"' test.c
$ gcc -D MESSAGE="\"hello quotes\"" test.c

$ ./a.out
DEBUG: *** hello quotes ***