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 *'
. . .