Shell

Bash 환경에서 작업을 하다보면 연속 된 숫자 값을 이용하는 경우가 종종 있다. 특정 단어에 연속 된 숫자를 붙이거나 연속 된 숫자 값의 개수만큼 Loop를 실행(반복)하거나 하는 작업들이 대표적이다. 오랜 기간 Unix/Linux를 사용해 왔던 분들이라면 보통 아래와 같은 형태로 사용 하는 것을 종종 볼 수 있다.

$ echo `seq 1 10`
1 2 3 4 5 6 7 8 9 10
$ for x in `seq 1 10`
> do
> echo -n "$x "
> done
1 2 3 4 5 6 7 8 9 10$

사실 seq (sequence)는 별도의 실행 파일이기 때문에 bash와 직접적인 관련은 없다. 하지만, 많은 bash 스크립트 예제에서 `seq`는 항상이라고 생각 될 정도로 자주 등장한다. 그러다보니 어느 덧 손에 익은 seq는 나도 모르게 자주 사용되는 명령 중 하나가 되곤 했다.

간단히 seq를 살펴보면 단지 순차적인 숫자를 출력해주는 명령이다. 게다가 증가분까지 지정해서 연속 적으로 숫자를 출력해 준다. (문자 포맷을 비롯한 자세한 옵션은 man 페이지 참조)

$ seq 1 2 10
1
3
5
7
9

seq는 매우 직관적이고 간편한 명령이기 때문에 연속 된 숫자를 생성하는데 있어서 별다른 문제가 되지 않지만 아래와 같은 경우에는 조금 고민이 필요해 지게 된다.

  • webserver1부터 webserver20까지 순차적인 문자+숫자 목록을 만들어내려면 어떻게 해야할까?

문자 뒤에 숫자를 붙이면 되지 않을까?

$ echo "webserver`seq 1 20`"
webserver1
2
3
... 생략 ...

seq는 new line을 구분자로 하여 생성하기 때문에 위와 같이 되어버린다. -s 옵션을 주고 구분자를 공백으로 하더라도 상황은 별 차이가 없다. 앞의 문자열을 반복해주지 않기 때문이다. 그렇기 때문에 결국 아래와 같이 사용하게 된다. (아래 예시에선 개행문자 대신 공백으로 구분해서 출력시켰다)

$ for x in `seq 1 20`
> do
> echo -n "webserver$x "
> done
webserver1 webserver2 webserver3 webserver4 webserver5 webserver6 webserver7 webserver8 webserver9 webserver10 webserver11 webserver12 webserver13 webserver14 webserver15 webserver16 webserver17 webserver18 webserver19 webserver20 $

이러한 문자+숫자 형태의 순차적인 결과 값을 자주 사용하다보면 for 구문을 타이핑하기 귀찮아서 쉘 스크립트로 짜두고 실행하는 경우도 있다. 첫 번째 인자 값으로 공통된 문자를 받고 두 번째 인자로 반복 할 숫자를 받게 했다.

#!/bin/bash
# $1: 문자, $2: 마지막 숫자
for x in `seq 1 $2`
do
	echo -n "${1}${x} "
done

하지만, 사실 이 방법은 썩 좋은 방법이 아니다. 해당 쉘 스크립트가 없는 시스템에서 작업 하게 될 경우도 있기 때문이다. 그래서 printf 유틸리티를 이용해서 아래와 같이 처리 할 수 있다.

$ printf "webserver%d" `seq 1 20`
webserver1 webserver2 webserver3 webserver4 webserver5 webserver6 webserver7 webserver8 webserver9 webserver10 webserver11 webserver12 webserver13 webserver14 webserver15 webserver16 webserver17 webserver18 webserver19 webserver20 $

비록 외부 유틸리티를 이용한 것이지만 (어차피 seq에서 순수 Bash는 잃어버렸다) 간단히 한 줄의 명령을 통해서 원하는 결과를 얻을 수 있다. >_<)b

brace expansion

앞서 살펴본 seq를 이용한 반복 출력을 획기적으로 개선 할 수 있는 괄호 확장(brace expansion)구문이 bash 3.0에서 추가 되었다. 요즘 대부분의 *nix 계열 운영체제는 Bash 3.x 이상 버전을 사용하기 때문에 꽤나 유용하다.

새로 추가된 괄호 확장이라는 구문은 아래와 같이 사용이 가능하다.

$ echo {1,2,3}
1 2 3
$ echo {1..10}
1 2 3 4 5 6 7 8 9 10
$ for x in {1..10}
> do
> echo -n "$x "
> done
1 2 3 4 5 6 7 8 9 10 $

괄호 안에 숫자를 ,로 이어주면 개별적으로 반복하며 ..으로 이어주면 첫 번째 숫자부터 두 번째 숫자까지를 순차적으로 확장해 준다. 괄호 확장이 좋은 점은 해당 구문과 연결 된 문자열도 같이 반복처리를 해준다는 점이다. 앞서 seq를 이용해서 webserver1~20까지를 출력 하는 경우 아래와 같이 한 줄로 처리가 가능하다.

$ echo webserver{1..20}
webserver1 webserver2 webserver3 webserver4 webserver5 webserver6 webserver7 webserver8 webserver9 webserver10 webserver11 webserver12 webserver13 webserver14 webserver15 webserver16 webserver17 webserver18 webserver19 webserver20

이렇게 간편한 괄호 확장은 단순히 숫자처리만 하는 것이 아니라 문자도 확장이 가능하다. 뿐만 아니라 역순으로도 확장이 가능하다.

기본 예시

1. 문자 확장
$ echo /dev/sd{a..z}
/dev/sda /dev/sdb /dev/sdc /dev/sdd /dev/sde /dev/sdf /dev/sdg /dev/sdh /dev/sdi /dev/sdj /dev/sdk /dev/sdl /dev/sdm /dev/sdn /dev/sdo /dev/sdp /dev/sdq /dev/sdr /dev/sds /dev/sdt /dev/sdu /dev/sdv /dev/sdw /dev/sdx /dev/sdy /dev/sdz$ echo 
$ echo /dev/sd{z..r}
/dev/sdz /dev/sdy /dev/sdx /dev/sdw /dev/sdv /dev/sdu /dev/sdt /dev/sds /dev/sdr

2. 디렉토리 생성
$ mkdir -p /mnt/sd{a..c} # /mnt/sda, /mnt/sdb, /mnt/sdc가 생성된다.

3. ASCII 순서
$ echo {A..a}
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [  ] ^ _ ` a
$ echo {[..f}
{[..f}

1번 예시에서 처럼 ASCII 알파벳 순서에 의거해서 문자가 확장되는 것을 볼 수 있다. 또한 2번 예시처럼 확장해서 반복처리하는 구문이기 때문에 명령의 인자 값으로 전달도 가능하다. 괄호 확장이 ASCII 값이 기초하기 때문에 3번 예시 같은 형태의 확장도 가능하다. (하지만, 가급적 사용하지 말자. 특히 시작 값을 기호로 주면 확장되지 않는다)

4. 괄호확장 연결
$ echo {a..c}{1..3}
a1 a2 a3 b1 b2 b3 c1 c2 c3

5. 중첩 된 괄호 확장
$ echo {{a..c},{1..3},{A..D}}
a b c 1 2 3 A B C D

6. 기본 괄호 확장
$ mkdir -p /disk/{a,b,z} # /disk/a, /disk/b, /disk/z 디렉토리가 생성된다

4번 예시처럼 두 개의 괄호 확장을 연결하면 두 집합의 곱집합 형태로 결과를 생성해 낸다. 그리고 6번 예시처럼 .. 대신에 ,를 이용해서 사용하는 확장이 일반 적이며 이를 이용해서 5번 예제처럼 중첩 된 괄호 확장을 사용 할 수도 있다. (실제로 이렇게 쓸 일은 별로 없지만 서로 다른 형태의 목록을 한 줄로 표현 할 때 사용 될 수 있다)

변수 예시

이렇게 편리한 괄호 확장을 변수 값을 이용해서 사용하고 싶을 때가 있는데

$ foo=1
$ bar=10
$ echo sample{$foo..$bar}

$foo 변수 값인 1부터 $bar 변수 값인 10까지 확장하고자 의도한 것이지만 실제로 실행해보면 결과는 아래와 같다.

$ echo sample{$foo..$bar}
sample{1..10}

이는 괄호 확장이 변수 값 치환보다 먼저 처리 되기 때문에(괄호 확장으로 인식하지 못하므로) 발생 한다. 이런 경우에는 아래처럼 변수 값을 치환해주는 eval 명령을 통해서 처리가 가능하다.

$ foo=1
$ bar=5
$ eval echo {$foo..$bar}
1 2 3 4 5

0으로 채우기

괄호 확장을 이용하다보면 종종 숫자 값을 0을 채워서 출력하고 싶을 때가 있다. 예를 들어 web-server001 부터 web-server020까지의 호스트명을 출력하고 싶다면 아래와 같이 실행하면 된다.

$ echo web-server{001..020}
web-server001 web-server002 web-server003 web-server004 web-server005 web-server006 web-server007 web-server008 web-server009 web-server010 web-server011 web-server012 web-server013 web-server014 web-server015 web-server016 web-server017 web-server018 web-server019 web-server020

하지만, 대부분 위 예시를 실행해 보면 아래와 같은 결과를 얻게 된다.

$ echo web-server{001..020}
web-server1 web-server2 web-server3 web-server4 web-server5 web-server6 web-server7 web-server8 web-server9 web-server10 web-server11 web-server12 web-server13 web-server14 web-server15 web-server16 web-server17 web-server18 web-server19 web-server20

왜냐하면 0으로 채워주는 기능은 Bash 4.0부터 지원하기 때문이다. 여전히 Bash 3.x 버전이 많이 사용되므로 위와 같은 작성 방법은 하위 호환성에 좋지 않다.

따라서, 편법 적이지만 0으로 값을 채우고자 할 때는 앞서 언급했던 printf를 활용한 아래의 방법으로 사용하면 된다. (쉘 스크립트는 원하는 결과를 얻기 위해 다양한 방법으로 처리가 가능하기 때문에 꼭 이렇게 해야만 하는 것은 아니다)

# 03은 0으로 채워진 3자리 숫자를 의미하며 d는 정수를 의미한다.
$ printf "web-server%03d " {1..20}
web-server001 web-server002 web-server003 web-server004 web-server005 web-server006 web-server007 web-server008 web-server009 web-server010 web-server011 web-server012 web-server013 web-server014 web-server015 web-server016 web-server017 web-server018 web-server019 web-server020 $

그 외 예시

파일을 .bak 확장자를 붙인 이름으로 변경하기

{}안에 ,로 빈 값(null)을 넣게 되면 연결 된 문자열을 단순히 반복하는 점을 이용해서 조금 더 간편하게 파일 명을 변경 할 수 있다. (당연히 cp 같은 명령과 함께해서 복사 처리도 가능하다.)

$ mv /usr/local/chroot/var/log/activity.log{,.bak}

위 명령은 아래와 같은 효과이다. 경로가 길수록 효율이 좋은 습관.

$ mv /usr/local/chroot/var/log/activity.log  /usr/local/chroot/var/log/activity.log.bak

괄호 확장의 증가 값 지정

Bash 버전이 4.0 이상 이라면 seq의 증가분 지정 처럼 아래와 같이 증가 분을 지정해서 출력이 가능하다.

$ echo {1..10..2}
1 3 5 7 9
$ echo {a..f..2}
a c e

옵션 반복

괄호 확장에는 빈 값(null)을 넣을 수 있기 때문에 아래와 같이 사용도 가능하다.

$ command arg{,,,,}

command라는 프로그램을 실행 한다면 아래와 같은 효과를 얻게 된다

$ command arg arg arg arg arg

끝으로

Linux 시스템을 다루다 보면 어떻게 하면 타이핑 양을 줄여볼까 하는 귀차니즘에 의해서 이러한 방법들을 활용하게 되었다. 그리고, 괄호 확장의 경우 bash에 내장된 구문이기 때문에 외부 명령보다 성능적인 효율이 좋을 것으로 예상되지만 실제로 측정해보면 꼭 그렇지 않았다. (물론, 상세한 측정을 위해선 아래와 다른 방법의 측정이 필요하지만 귀찮으므로…)

$ time echo `seq 1 1000000` > /dev/null

real	0m1.432s
user	0m1.418s
sys	0m0.053s

$ time echo {1..1000000} > /dev/null

real	0m1.869s
user	0m1.803s
sys	0m0.061s

이 문서에서 소개한 내용은 Bash의 기초적인 내용이긴 하지만 지금까지 몰랐다면 꼭 알아두면 좋다. 본인의 귀차니즘을 해소해주고 손가락 타이핑을 줄여 줄 것이다.

- END -