Filename Expansion ( Globbing )
디렉토리 에서 파일을 조회할 때 *
문자를 사용해본 적이 있을 겁니다.
프롬프트 에서 ls *.sh
명령을 실행하면 확장자가 .sh
인 파일들을 모두 볼 수 있습니다. 여기서 사용된 *
문자를 glob 문자라 하고 glob 문자에 의해 매칭된 파일들로 치환되는 것을 globbing 이라고 합니다.
glob 문자는 *
외에 ?
[ ]
도 사용할 수 있으며 사실 shell 에서 제공하는 pattern matching 과 동일하게 동작합니다.
globbing 은 꼭 파일이름을 다룰 때만 적용되는 것은 아니며 어떤 스트링이나 변수값에라도 glob 문자가 있으면 발생하므로 주의해야 합니다.
파일이름 앞에 .
이 붙은 경우는 hidden 파일이라고 해서 기본적으로 매칭이 되지 않는데
이때는 직접 .
을 써주거나 아니면 dotglob 옵션 설정을 통하여 매칭할 수 있습니다.
Quote 을 하면 단어분리, globbing 둘 다 일어나지 않습니다.
$ ls
address.c address.h readObject.c readObject.h WriteObject.class
Address.class Address.java ReadObject.class ReadObject.java WriteObject.java
$ ls *.[ch]
address.c address.h readObject.c readObject.h
$ ls "*.[ch]" # quote 을 하면 globbing 이 일어나지 않는다
ls: cannot access *.[ch]: No such file or directory
$ echo *.?
address.c address.h readObject.c readObject.h
$ for file in *.[ch]; do
echo "$file"
done
address.c
address.h
readObject.c
readObject.h
globbing 결과에 대해서는 단어분리, globbing 이 발생하지 않으므로 파일을 select 하는 안전한 방법 입니다. 다음은 bash 전용 옵션인 globstar 를 이용하여 recursive matching 을 하는 예제 입니다.
# globstar 옵션은 기본적으로 off 상태 이므로 on 으로 설정해 줍니다.
$ shopt -s globstar
# 현재 디렉토리 이하 에서 모든 디렉토리, 파일 select
$ for dir_and_file in **; do
echo "$dir_and_file"
done
# 현재 디렉토리 이하 에서 모든 디렉토리 select ( '/' 문자로 끝나면 디렉토리만 선택 )
$ for dir in **/; do
echo "$dir"
done
# $HOME/tmp 디렉토리 이하 에서 디렉토리 이름에 log 가 포함될 경우 select
$ for dir in ~/tmp/**/*log*/; do
echo "$dir"
done
# 현재 디렉토리 이하 에서 확장자가 java 인 파일 select
$ for javafile in **/*.java; do
echo "$javafile"
done
# $HOME/tmp 디렉토리 이하 에서 확장가 java 인 파일 select
$ for javafile in ~/tmp/**/*.java; do
echo "$javafile"
done
# brace 확장과 함께 사용
$ for imgfile in ~/tmp/**/*.{png,jpg,gif}; do
echo "$imgfile"
done
$ awk 'BEGINFILE { print FILENAME; nextfile }' **/*.txt
....
....
파일을 select 할때 globbing 을 이용하는 것만으로는 부족한 경우가 있는데요. 이때는 find 같은 전용 툴을 이용해야 합니다. 이때 다음과 같은 방식으로 사용은 주의할 필요가 있습니다.
for file in $( find * -type f ) ... # 주의!
for file in `find * -type f` ... # 주의!
arr=( $(find * -type f) ) ... # 주의!
.................................................
$ ls
logfile1 logfile2 logfile3
$ touch 'logfile[1-3]' # 임의로 glob 문자를 사용한 파일 생성
$ echo *
logfile1 logfile[1-3] logfile2 logfile3
$ echo logfile[1-3]
logfile1 logfile2 logfile3
# logfile[1-3] 파일명에서 globbing 이 일어나 파일명이 중복 프린트된다.
$ echo $( echo * )
logfile1 logfile1 logfile2 logfile3 logfile2 logfile3
# 따라서 for file in $( find * ) ... 식으로 사용한다면 문제가 되겠죠.
$ echo $( find * )
logfile1 logfile1 logfile2 logfile3 logfile2 logfile3
$ for file in $( find * ); do echo "$file"; done
logfile1
logfile1
logfile2
logfile3
logfile2
logfile3
# globbing 을 회피하기 위해서 다음과 같이 $( find * ) 를 quote 한다면
# 이것은 문제가 없어 보이지만 $( find * ) 출력 전체가 하나의 인수가 되어
# for 문이 한번만 실행되게 됩니다.
$ for file in "$( find * )"; do echo "$file"; done
logfile1
logfile[1-3]
logfile2
logfile3
# for 문이 한번만 실행된다.
$ for file in "$( find * )"; do echo XXX"$file"ZZZ; done
XXXlogfile1 <--- XXX
logfile[1-3]
logfile2
logfile3ZZZ <--- ZZZ
C/C++ 소스 파일같이 파일명에 공백이나 glob 문자가 사용되지 않는다면 위와 같이 해도 됩니다. 하지만 그렇지 않을 경우 단어분리, globbing 에대한 처리를 해주어야 합니다.
set -f; IFS=$'\n'
for file in $(find -type f) ...
set +f; unset -v IFS
# 또는 subshell 을 활용
( set -f; IFS=$'\n'
for file in $(find -type f) ... )
set -f; IFS=$'\n'
arr=( $(find -type f) ) ...
set +f; unset -v IFS
find 명령은 기본적으로 한 줄에 하나씩 파일명을 출력하는데요.
다시 말해 구분자가 newline 이 되므로 만약에 파일명에 newline 이 포함되어 있다면 위 방법은 제대로 동작하지 않습니다.
앞서 리눅스 파일시스템 에서는 파일명으로 NUL
문자와 /
를 제외하고 모두 허용한다고 하였습니다.
그래서 find 명령에서는 파일명을 출력할때 NUL
문자를 구분자로 출력해주는 -print0
옵션을 제공합니다.
# find 명령에서 -print0 을 이용해 출력했으므로 read 명령의 -d 옵션 값을 null 로 설정
find * -name '*.html' -print0 | while read -r -d '' file
do
echo "$file"
done
# 프로세스 치환을 이용
while read -r -d '' file; do
echo "$file"
done < <( find * -name '*.html' -print0 )
# $( ... ) 명령치환은 NUL 문자를 전달하지 못하므로 -print0 옵션과 함께 사용할 수 없습니다.
sh
에서는 globstar 옵션도 사용할 수 없고 read 명령의 -d 옵션도 사용할 수 없습니다.
하지만 다음과 같이 find 명령과 for 문을 이용하면 단어분리, globbing, newline 문제없이 처리할 수 있습니다.
# 다음과 같이 실행할 경우 select 된 파일이 3개라면 각각 3 번의 sh -c 명령이 실행되는 것과 같습니다.
sh$ find * -name '*.gif' -exec sh -c 'mv "$0" "${0%.*}.png"' {} \;
# 1. sh -c '...' "file1"
# 2. sh -c '...' "file2"
# 3. sh -c '...' "file3"
# for 문에서 in words 부분을 생략하면 in "$@" 와 같게 되고 "$1" "$2" "$3" ... 차례로 입력됩니다.
# sh -c 'command ...' 11 22 33 형식으로 명령을 실행하면 command 의 첫번째 인수에 해당하는 11 이
# "$0" 에 할당되어 for 문에서 사용할 수 없게 되므로 임의의 문자 X 를 사용하여 "$1" 자리에 오게 하였습니다.
# 좀 더 효율적으로 명령을 실행하기 위해 find 명령의 마지막에 '{} +' 기호를 사용함으로써
# 한번의 sh -c 명령으로 실행을 완료할 수 있습니다.
sh$ find * -name '*.gif' -exec \
sh -c 'for file; do mv "$file" "${file%.*}.png"; done' X {} +
# 1. sh -c '...' X "file1" "file2" "file3"
다음은 xargs 를 이용하는 방법입니다.
# find 명령에서 -print0 을 이용하여 출력하였으므로 xargs 에서 -0 옵션을 사용하였습니다.
# xargs 명령에서 사용된 {} 와 X 의 의미는 위 예제와 같습니다.
# -i 옵션과 {} 를 이용하는 것은 find 명령에서 -exec ... \; 형식을 사용하는 것과 같습니다.
sh$ find * -name '*.gif' -print0 |
xargs -0 -i sh -c 'mv "$0" "${0%.*}".png' {}
# 1. sh -c '...' "file1"
# 2. sh -c '...' "file2"
# 3. sh -c '...' "file3"
sh$ find * -name '*.gif' -print0 |
xargs -0 sh -c 'for file; do mv "$file" "${file%.*}".png; done' X
# 1. sh -c '...' X "file1" "file2" "file3"
Glob 문자가 들어간 스트링은 주의!
명령문의 인수로 사용되는것은 quote 하지 않는 이상 모두 globbing 대상입니다. 그러므로 스크립트를 실행중인 디렉토리에 매칭 되는 파일이 있을경우 예상치 못한 오류가 발생할 수 있습니다.
다음은 unset 명령의 인수로 사용된 array[12]
이 globbing 에의해 파일 array1
과 매칭이 되어 unset 이 되지 않고 있습니다.
$ array=( [10]=100 [11]=200 [12]=300 )
$ echo ${array[12]}
300
$ touch array1 # 현재 디렉토리에 임의로 array1 파일생성
# unset 을 실행하였으나 globbing 에의해 array[12] 가 array1 파일과 매칭이되어
# 실질적으로 unset array1 명령과 같게되어 unset 이 되지 않고 있습니다.
$ unset -v array[12]
$ echo ${array[12]}
300
$ unset -v 'array[12]' # globbing 을 disable 하기위해 quote.
$ echo ${array[12]} # 이제 정상적으로 unset 이됨
$
-----------------------------------------
# tr 명령을 이용해 non-printable 문자를 삭제하려고 하지만
# [:graph:] 가 파일 a 와 매칭이 되어 실질적으로 'tr -dc a' 와 같은 명령이 되었습니다.
$ touch a
$ head -c100 /dev/urandom | tr -dc [:graph:]
# 다음과 같이 quote 합니다.
$ head -c100 /dev/urandom | tr -dc '[:graph:]'
array 에 입력되는 원소 값에 glob 문자가 포함됨
$ AA="Arch Linux:*:Suse Linux:Fedora Linux"
$ IFS=:
$ ARR=( $AA )
$ IFS=$' \t\n'
$ echo "${ARR[@]}"
Arch Linux 2013-03-19 154412.csv Address.java address.ser
ReadObject.class ReadObject.java 쉘 스크립트 테스팅.txt
WriteObject.class WriteObject.java Suse Linux Fedora Linux
-----------------------------------------------------------
$ set -f; IFS=: # set -f 옵션 설정으로 globbing 을 disable
$ ARR=( $AA )
$ set +f; IFS=$' \t\n'
$ echo "${ARR[@]}"
Arch Linux * Suse Linux Fedora Linux
현재 디렉토리 이하에서 확장자가 .c
인 파일을 모두 찾으려고 하지만
glob 문자에 의해 aaa.c 파일과 매칭이 되어 실질적으로 find -name aaa.c
와 같은 명령이 되었습니다.
$ touch aaa.c
$ find -name *.c
./aaa.c
# '*.c' 스트링이 정상적으로 find 명령에 전달되기 위해서 다음과 같이 quote 합니다.
$ find -name '*.c'
./aaa.c
./fork/fork.c
./write/write.c
./read/read.c
...
위의 예를 통해서 알 수 있듯이 인수로 사용되는 스트링에 glob 문자가 있을경우 항상 quote 해서 사용하거나 escape 해야 하며 필요할 경우 set -o noglob
옵션을 사용해야 합니다.
globbing 을 on, off 하는 방법
globbing 은 파일을 select 하기 위해 사용하는 기능으로 프롬프트 상에서, 스크립트 파일 실행시 모두 default 로 enable 됩니다. 필요에 따라 globbing 을 on, off 하려면 다음과 같은 방법을 사용합니다.
# 1. shebang 라인에서
#!/bin/bash -f
# 2. 옵션 설정을 통해서
set -o noglob # disable
...
...
set +o noglob # enable
set -f # disable
...
...
set +f # enable
Globbing 관련 shell 옵션, 변수
-f | noglob
set -o noglob
은 globbing 기능을 disable 합니다. globbing 을 회피하고자 할 때 사용할 수 있습니다. globbing 과 패턴매칭은 별개의 기능으로 globbing 을 disable 한다고 해서 패턴매칭에 glob 문자를 사용할 수 없는것은 아닙니다.
nullglob
glob 문자를 이용하여 매칭을 시도하였으나 매칭되는 파일이 없을 경우 기본적으로 패턴을 그대로 리턴합니다.
이것은 매칭되는 파일을 처리하는 스크립트 에서 오류의 원인이 될수 있습니다.
이때 shopt -s nullglob
옵션을 설정하면 매칭되는 파일이 없을 경우 패턴을 리턴하지 않습니다.
$ ls
2013-03-19 154412.csv ReadObject.java WriteObject.class
ReadObject.class 쉘 스크립트 테스팅.txt WriteObject.java
$ echo *.sh
*.sh # 매칭되는 파일이 없을경우 패턴을 그대로 리턴
$ shopt -s nullglob
$ echo *.sh
$ # 매칭되는 파일이 없을경우 null 을 리턴
---------------------------------------------------------
# 매칭되는 파일이 없을때 패턴을 그대로 리턴하면 오류의 원인이 될수있다.
$ for f in *.sh; do
cat "$f"
done
cat: *.sh: No such file or directory
# nullglob 옵션을 설정하면 오류를 없앨 수 있다
$ shopt -s nullglob
$ for f in *.sh; do
cat "$f"
done
-----------------------------------------------
# nullglob 을 설정하지 않고 다음과 같이 할수도 있습니다.
for f in *.log; do
[ -e "$f" ] || continue
...
done
하지만 매칭되는 파일이 없을때 패턴을 리턴하지 않고 null 을 리턴하는 것이 다음과 같은 경우에는 문제가 될 수 있습니다. 예를 들어 명령문을 작성할때 다음과 같이 glob 문자가 포함되는 스트링의 경우는 매칭이 발생하는 경우가 없으므로 보통 quote 을 사용하지 않습니다. 이럴 경우 nullglob 옵션이 설정되어 있으면 첫 번째 명령은 두 번째 명령과 같게 됩니다.
$ grep -r foo --include=*.md ---- (1)
$ grep -r foo ---- (2)
----------------------------------------------------------
# 현재 디렉토리에 array[1] 와 매칭되는 파일이 없으므로 null 을 리턴하게 되어 두 번째 명령과 같아진다.
$ array=(11 22 33)
$ unset -v array[1] ---- (1)
$ unset -v ---- (2)
# 결과적으로 unset 이 되지 않고 값이 남아 있다.
$ echo ${array[1]}
22
# globbing 을 disable 하기 위해 quote 을 해야한다.
$ unset -v "array[1]"
failglob
Globbing 매칭 실패시 오류메시지와 함께 $?
값으로 1
을 리턴합니다.
$ ls
2013-03-19 154412.csv ReadObject.java WriteObject.class
ReadObject.class 쉘 스크립트 테스팅.txt WriteObject.java
$ echo *.sh
*.sh
$ echo $?
0
$ shopt -s failglob
$ echo *.sh
bash: no match: *.sh
$ echo $? # failglob 설정후에 매칭실패시 '1' 을 리턴한다.
1
nocaseglob
이 옵션을 설정하면 매칭 시에 대, 소문자 구분을 하지 않습니다.
dotglob
이 옵션을 설정하면 .
( dot ) 으로 시작하는 파일명도 매칭할 수 있습니다.
# 현재 디렉토리에 '.test.sh' 파일이 있다고 하면
# '.' 으로 시작하는 파일명은 기본적으로 매칭이 되지 않는다.
$ ls [.,]test.sh
ls: cannot access '[.,]test.sh': No such file or directory
$ ls ?test.sh
ls: cannot access '?test.sh': No such file or directory
$ ls .test.* # 매칭하려면 직접 '.' 을 써줘야 한다.
.test.sh
# dotglob 옵션을 설정하면 매칭할 수 있다.
$ shopt -s dotglob
$ ls [.,]test.sh
.test.sh
$ ls ?test.sh
.test.sh
globstar
**
문자를 이용하여 하위 디렉토리 까지 recursive 매칭을 할 수 있습니다.
$ echo ** # 모든 파일, 디렉토리
$ echo **/ # 모든 디렉토리
$ echo **/*.sh # 확장자가 .sh 인 모든파일
globasciiranges
C locale 이 아닐 경우 [a-c]
와 같이 -
문자를 이용한 range 매칭시에 기본적으로 대, 소문자 구분을 하지 않습니다. 다시 말해 [aAbBcC]
와 같은 의미가 되는데요. 이 옵션을 설정하면 대, 소문자 구분을 합니다.
GLOBIGNORE
이건 shell 옵션이 아니고 환경변수 입니다.
globbing 에서 사용하는 패턴들을 :
를 구분자로 해서 등록해 놓으면 매칭에서 제외시킵니다.
# json 과 html 파일을 제외하고 모두 출력
$ GLOBIGNORE=*.json:*.html eval 'echo *'
. . .