bash를 처음 배우는 사람이라면 거듭 듣게 되는 조언이 하나 있어요:
변수를 따옴표로 묶어요!
왜 그럴까요?
$name처럼 변수를 확장할 때는 거의 항상 변수의 _값_을 하나의 문자열(또는 숫자)로 다루고 싶어 해요. 그 값이 예상치 않게 두 개 이상의 별도 문자열로 처리되는 것은 원하지 않아요.
따옴표로 묶기는 어떤 텍스트가 하나의 단어로 처리되도록 보장하는 가장 기본적인 방법이에요. 따옴표에는 크게 두 가지 종류가 있어요.
큰따옴표 안에서는 매개변수 확장, 산술 확장, 명령어 치환이 허용돼요.
echo "This is bash version $BASH_VERSION"
큰따옴표로 묶은 문자열 안에 큰따옴표 자체를 넣고 싶다면 앞에 백슬래시를 붙이면 돼요.
echo "She said \"Let me teach you about bash.\""
작은따옴표는 안에서 어떤 확장도 허용하지 않아요.
echo 'This is bash version $BASH_VERSION'
# ...^..................................^
# prints exactly: This is bash version $BASH_VERSION
작은따옴표로 묶은 문자열 안에 작은따옴표를 넣는 것은 불가능해요.
echo $name은 정확히 무엇이 문제일까요?
셸이 한 줄의 코드를 해석할 때는 여러 종류의 확장을 순서대로 수행해요. 마지막에 수행되는 확장 중 두 가지가 바로 _단어 분할_과 _파일 이름 확장_이에요.
매개변수 확장, 명령어 치환, 산술 확장의 결과 중 _큰따옴표 안에서 일어나지 않은 것_은 모두 "단어 분할"의 대상이 돼요. 셸은 확장된 결과를 가져와서 $IFS 값에 들어 있는 문자 시퀀스를 기준으로 나눠요. 기본값은 공백, 탭, 줄 바꿈이에요.
셸 루프를 사용하면 이 동작을 직접 확인할 수 있어요:
sentence="The quick brown fox jumps."
i=0
for word in "$sentence"; do echo "$((++i)) $word"; done
# ..........^.........^
출력:
1 The quick brown fox jumps.
그런데 따옴표로 묶지 않으면:
i=0
for word in $sentence; do echo "$((++i)) $word"; done
# .........^.........^
출력:
1 The
2 quick
3 brown
4 fox
5 jumps.
$IFS 값을 다르게 하면 어떻게 되는지 볼까요:
csv="first,second,third"
IFS=","
for word in $csv; do echo "$word"; done
출력:
first
second
third
단어 분할을 비활성화하는 한 가지 방법은 $IFS를 빈 문자열로 설정하는 것이에요.
sentence="The quick brown fox jumps."
IFS= # or explicitly, IFS=""
i=0
for word in $sentence; do echo "$((++i)) $word"; done
# .........^.........^
출력:
1 The quick brown fox jumps.
하지만 파일 이름 확장도 함께 비활성화하지 않는 한, 변수를 따옴표로 묶지 않고 그대로 두는 것은 권장하지 않아요. 방법은 아래에서 살펴볼게요.
"Glob" 패턴은 파일 집합을 일치시키는 간결한 패턴을 표현할 때 사용해요. 예를 들어 디렉터리에 있는 모든 Markdown 파일을 *.md로 일치시킬 수 있어요. Glob 패턴은 Unix가 만들어진 이래로 존재해 왔어요.
Glob 와일드카드 문자는 다음과 같아요:
*는 임의의 문자 0개 이상과 일치해요.
?는 임의의 문자 정확히 1개와 일치해요.
[...]는 괄호 안에 나열된 문자 집합에서 정확히 1개의 문자와 일치해요.
[abc]는 a 또는 b 또는 c 중 하나인 문자 정확히 1개와 일치해요.
^ 다음)로 두면 돼요.
[][]는 닫는 대괄호 또는 여는 대괄호와 일치하는 문자 집합이에요.[[\]]
[x-y]는 x부터 y까지의 범위 안에 있는 문자 정확히 1개와 일치해요.
^ 다음)이거나 닫는 대괄호 바로 앞 문자여야 해요.
[-xy]와 [xy-]는 각각 - 또는 x 또는 y 중 하나인 문자 정확히 1개와 일치해요.[x\-y]
[0-9A-Fa-f]는 16진수 숫자와 일치해요.[^abc]는 a 또는 b 또는 c가 아닌 문자 정확히 1개와 일치해요.[^x-y]는 x부터 y까지의 범위에 속하지 않는 문자 정확히 1개와 일치해요.[[:character_class:]]는 "character_class"라는 이름의 문자 클래스에 속하는 문자 1개와 일치해요.
lower -- 소문자upper -- 대문자alpha -- 문자digit -- 숫자alnum -- 문자와 숫자space -- 공백blank -- 가로 공백 (공백과 탭)xdigit -- 16진수 숫자word -- 식별자에 허용되는 문자 (문자, 숫자, 밑줄)punct -- 구두점cntrl -- 제어 문자graph -- "표시되는" 문자print -- 표시되는 문자와 공백[[:alpha:]] 클래스는 [[:lower:][:upper:]]로 이루어져 있어요.[[:alnum:]] 클래스는 [[:alpha:][:digit:]]로 이루어져 있어요.[[:word:]] 클래스는 [[:alnum:]_]로 이루어져 있어요.[[:word:]]이면서 동시에 [[:punct:]]예요.[[:graph:]] 클래스는 [[:alnum:][:punct:]]로 이루어져 있어요.[[:print:]] 클래스는 [[:graph:] ]로 이루어져 있어요. 다른 공백이 아니라 공백(8진수 040)만 포함해요.for문으로 파일 목록을 반복할 수 있어요.
for file in *.csv; do
do_something_with "$file"
done
변수 "$file"이 따옴표로 묶여 있는 것에 주목해요. Glob 패턴은 이름에 공백이 들어 있는 파일을 반환할 수 있어요. do_something_with 명령이 파일 이름이라는 인자 하나만 정확히 받도록 하려면 변수를 따옴표로 묶어야 해요.
Glob 패턴은 정규 표현식이 아니에요.
.
?
.*
*
.*는 "첫 번째 문자가 마침표 자체이고 그 뒤에 임의의 문자가 0개 이상 오는 파일 이름과 일치"를 뜻해요..+
?*
Bash는 정규 표현식에 더 가까운 확장 패턴을 제공해요.
파일 이름 확장을 억제하고 싶을 때가 있어요. 사용자 입력을 처리할 때가 그중 하나예요. 다음 코드를 봐요.
read -p "Enter something: " user_input
echo $user_input
사용자가 별표(*)를 입력하면 무엇이 출력될까요? 현재 디렉터리에 있는 파일 목록이에요. 아마 그건 원하는 동작이 아닐 거예요.
또 다른 예로, 패턴을 기대하는 프로그램에 패턴을 인자로 전달하는 경우가 있어요. tr 프로그램은 텍스트를 조작할 때 자주 사용해요. 변수 값을 소문자로 바꾸려면 이렇게 하면 돼요.
var="My Puppet"
echo "$var" | tr '[:upper:]' '[:lower:]'
출력:
my puppet
흔한 실수는 tr의 인자를 따옴표로 묶는 것을 잊는 거예요: tr [:upper:] [:lower:]
왜 그게 문제일까요?
따옴표로 묶지 않은 glob 패턴은 일치하는 파일 목록으로 확장돼요.
스크립트를 사용하는 사람의 현재 디렉터리에 p, u, w라는 파일이 있다고 해봐요:
touch p u w
var="My Puppet"
echo "$var" | tr [:upper:] [:lower:]
이러면 오류가 발생해요!
따옴표로 묶지 않은 [:upper:] 패턴은 :, u, p, e, r 중 아무 문자와 일치해요.
따옴표로 묶지 않은 [:lower:] 패턴은 :, l, o, w, e, r 중 아무 문자와 일치해요.
Bash는 첫 번째 패턴에 대해 p와 u 파일을, 두 번째 패턴에 대해 w 파일을 찾아내고, tr은 이렇게 호출돼요.
echo "$var" | tr p u w
그런데 이건 tr에 맞지 않는 인자 개수예요.
set 명령으로 파일 이름 확장을 비활성화할 수 있어요.
echo * # prints a list of files
set -f # disable filename expansion
echo * # prints a literal asterisk
set +f # enable filename expansion
내장 shopt 명령에는 파일 이름 확장이 동작하는 방식을 제어하는 여러 설정이 있어요.
그중 흥미로운 몇 가지는 다음과 같아요:
shopt -s nocaseglob -- 대소문자를 구분하지 않고 일치시켜요.shopt -s extglob -- 확장 패턴을 사용할 수 있게 해요.shopt -s nullglob -- 일치하는 파일이 없으면 패턴을 아무것도 아닌 것으로 바꿔요.
기본 동작은 패턴을 리터럴 문자열로 그대로 두는 거예요.