본 문서는 Bash 쉘 스크립트에 대한 FAQ 내용을 정리한 문서입니다. 따라서, 기본적인 Bash 쉘 스크립트의 문법과 사용 방법에 대해서는 알고 있다는 가정하에 작성되었습니다. Part 1이라고 제목을 붙인 이유는 자주묻는 질문이 추가로 생기거나 생각나는대로 FAQ를 정리해서 올리려고 합니다. 어찌보면 위키로 정리하는게 적합한 문서이기도 합니다.

[[ ]] vs [ ]

if 구문과 단짝처럼 사용되는 []에 대해서 [[]]과 []의 차이점을 묻는 경우가 많다. 먼저, 각각의 의미를 살펴보도록 하자.

if/then 구문의 경우 해당 구문의 결과(exit 값)에 대한 조건을 판단하고 구문의 내용을 실행하도록 되어있다. 그렇기에 원래 if/then 구문은 아래와 같이 사용되었다.

if test 3 \> 2; then
	echo "3이 2보다 큽니다"
fi

test 명령은 주어진 내용에 대한 참, 거짓을 판별하여 리턴 값을 결정하는 명령이며 보통 /bin/test에 위치해 있다. 그리고, 이 test 명령을 좀 더 보기 편하게 쓰기 위해서 존재하는 명령이 있는데 그게 [ 이다. 흔히들 [는 Bash의 문법으로 알고 있지만 실제로 존재하는 실행 파일이며 보통 /bin/[에 위치해 있다. (어떤 배포판의 경우에는 test에 대한 심볼릭 링크로 존재하기도 한다)

$ ls -l /bin/[
-rwxr-xr-x. 1 root root 41448 Jun 10  2014 /bin/[

그래서 우리가 알고 있는 if 구문에서 -f와 같은 연산자가 실질적으로 test 명령의 옵션이다. 옵션처럼(-로 시작하는)생긴 테스트 연산자를 사용하는 이유가 [는 test는 실질적으로 같은형태의 명령이다. 그리고, 이 둘은 Bash Builtin(내장형 명령)으로도 구현이 되어있다. 성능상 이유로 Bash 내장형 명령으로 구현이 되어있으며 단순히 명령 형태로 실행을 하면 /bin아래의 실행 파일로 처리를 하고 Bash 인터프리터로 처리하면 내장형 명령이 동일한 기능을 수행하여 처리한다.

$ test 3 \> 2
$ [ 3\> 2 ]

위와 같이 사용하면 /bin 아래에 있는 명령을 실행하게 된다

그리고, Bash 2.02 버전에서는 확장된 테스트 명령으로 [[라는 키워드를 선보였다. (즉, [처럼 외부 프로그램으로 존재하지는 않는다) 이 확장된 테스트 명령은 기존 [과 달리 일반적인 프로그래밍 언어에서 사용되는 비교 명령처럼 동작을 하는 차이점을 갖는다. (ksh으로 부터 차용된 기능이다)

f="/bin/unknown"

if [[ -e $f ]]; then
  echo "unknown exists"
fi

if [ -e $f ]; then
  echo "unknown exists"
fi

위 두가지 형태의 사용은 동일한 결과를 갖는다. 일반적인 파일에 대한 테스트에서는 별다른 차이점을 보기 힘들지만 아래와 같은 수식 연산에 대한 부분에서는 차이점을 느낄 수 있다.

x=10
if [[ $x -gt $z ]]; then
  echo "X가 Z보다 크다"
fi
if [ $x -gt $z ]; then
  echo "X가 Z보다 크다"
fi

위에서 [으로만 테스트한 구문은 -bash: [: 10: unary operator expected 같은 오류 메시지를 발생시킨다. 왜냐하면 $z는 존재하지 않는 변수이기 때문에 null로 대체 되고 [ $x > ]는 미완성 구문이 되어서 오류가 일어나는 것이다. 하지만, [[ 의 경우는 1차적으로 내부 구문에 대해서 번역을 시도하고 존재하지 않는 변수 $z를 0으로 처리한다. 그래서 0보다 큰 10에 대한 숫자 비교 처리가 가능하다. 또한, [[ 키워드는 아래와 같은 차이점을 보인다.

위 표를 살펴보면 AND, OR를 나타내는 -a, -o 대신에 보다 프로그래밍 언어같은 &&, ||를 제공하며 정규표현식 매칭을 제공한 다는 것을 확인 할 수 있다. 즉, [[ 구문이 [보다 유용하고 장점이 많아 보인다.

[[[보다 안전한 테스트 구문을 만들 수 있다는 점이 좋지만 POSIX 표준은 아니기 때문에 범용성 면에서는 떨어진다. 다만, Bash 2.02 버전 이상을 사용한다는 전제 조건만 있다면 [[ 구문이 더 많은 장점을 갖게 된다. 그리고 [을 사용할 경우에는 아래와 같이 변수를 ""로 감싸는 습관을 갖는 것이 좋다. 위에서 오류가 발생한 구문을 ""로 감싸서 실행해 보면

x=10
if [ "$x" -gt "$z" ]; then
  echo "X가 Z보다 크다"
fi

-bash: [: : integer expression expected

좀 더 명확한 오류를 찾을 수 있는 메시지(숫자연산에서 문자열이 사용되었음)가 나타나게 된다. 그리고 문자열의 경우에 있어서도 ""로 감싸게 되면 null 문자열로 비교 테스트가 가능하다.

#!/bin/bash
# 매개변수를 주지않으면 아래 테스트 구문은 연산 오류를 일으킨다
if [ $1 = "hi" ]; then
    echo "there"
fi

# 아래와 같이 ""로 감싸게 되면 null 문자열("")로 처리되어 오류가 발생하지는 않는다
if [ "$1" = "hi" ]; then
    echo "there"
fi

# 종종 아래와 같이 양쪽 변수에 공통의 문자를 붙여서 매개변수가 누락되어도 오류가 발생하지 않는 트릭을 사용하기도 한다
if [ x$1 = xhi ]; then
    echo "there"
fi

시그널 처리

쉘 스크립트도 시그널에 대한 처리를 수행 할 수 있다. trap이란 커맨드를 통해서 사용 가능하며 사용방법은 아래와 같다.

trap 처리명령 시그널

처리명령은 Bash 함수를 지정해도 되기 때문에 아래와 같은 형태로 구현이 가능하다.

sig_int() {
  echo "Interrupted"
  exit 1
}
sig_exit() {
  echo "Exit"
  exit 0
}

trap sig_int TERM
trap sig_int INT
trap sig_exit EXIT

위 예시의 경우 Interrupt(INT), Terminate(TERM), Exit(EXIT) 시그널을 해당 스크립트가 받게 되면 각각 지정된 함수가 실행되고 종료값을 달리하여 끝마칠 수가 있다. 만약 처리명령에 ''을 지정하게 되면 해당 시그널은 무시하게 되고 -를 지정하면 Bash 기본 시그널처리로 동작하게 된다.

버전 비교

Bash 스크립트를 작성해서 사용하다보면 버전 비교가 필요한 경우가 있다. 버전 번호를 확인하고 최신버전이면 패키지를 업데이트 한다거나 특정 버전을 충족 하는지 비교해보고자 할 때 등이 그러한 경우인데 버전 번호를 비교하는 함수를 만들어두고 사용하면 좋다. 버전 번호를 비교하는 함수에 대한 구현은 다양한 방법이 있지만 그 중에서 한가지 방법을 소개한다.

version_comp() {
    if [[ $1 == $2 ]]; then # string match
        return 0
    fi
    local i
    ver1=(${1//./ })
    ver2=(${2//./ })
    for ((i=${#ver1[@]}; i<${#ver2[@]}; i++))
    do
        ver1[i]=0
    done
    for ((i=0; i<${#ver1[@]}; i++))
    do
        if [[ -z ${ver2[i]} ]]; then
            ver2[i]=0
        fi
        if [[ ${ver1[i]} -gt ${ver2[i]} ]]; then
            return 1
        fi
        if [[ ${ver1[i]} -lt ${ver2[i]} ]]; then
            return 2
        fi
    done
    return 0
}

위 함수는 2개의 인자 값을 받고 그 값을 비교하는 함수이다. 첫 번재 인자 값이 현재 버전 두 번째 인자 값이 확인하려는 버전 값이라고 가정을 하여 동작하도록 되어있는데 순서대로 살펴보도록하자.

    if [[ $1 == $2 ]]; then # string match
        return 0
    fi

먼저, 2개의 문자열이 동일한지 확인한다. 완전히 일치하는 2개의 값이라면 굳이 비교 연산을 따로 할 필요가 없기 때문에 바로 0을 리턴하도록 한다.

    ver1=(${1//./ })
    ver2=(${2//./ })

그리고, 2개의 변수를 .을 기준으로 분리한다. Bash에서 제공하는 문자열 처리 기능을 이용한 것인데 이를 잠깐 살펴보면 아래와 같다.

${#string} - 문자열 길이를 리턴한다
${string:position} - 지정 된 위치(position)로부터 문자를 추출한다
${string:position:length} - 지정 된 위치(position)로 부터 지정 된 길이(length)만큼 문자를 추출한다
${string#substring} - string에서 substring과 앞에서부터 가장 짧게 매칭되는 부분을 삭제
${string##substring} - string에서 substring과 앞에서부터 가장 길게 매칭되는 부분을 삭제
${string%substring} - 위에서 사용된 #의 반대 형태로 뒤에서부터 가장 짧게 매칭되는 부분을 삭제
${string%%substring} - 위에서 사용된 ##의 반대 형태로 뒤에서부터 가장 짧게 매칭되는 부분을 삭제
${string/substring/replacement}	- 첫 번째로 매칭되는 substring을 replacement로 대체
${string//substring/replacement} - 모든 substring을 replacement로 대체
${string/#substring/replacement} - 앞에서부터 가장 먼저 매칭되는 substring을 replacement로 대체
${string/%substring/replacement} - 뒤에서부터 가장 먼저 매칭되는 substring을 replacement로 대체

이러한 문자열 처리에 대한 예시는 이 문서를 참고하도록 하자.

    for ((i=${#ver1[@]}; i<${#ver2[@]}; i++))
    do
        ver1[i]=0
    done

앞서 2개의 변수를 . 기준으로 분리하여 배열로 저장을 했는데 이 변수는 다시 첫 번째 변수의 배열 길이를 기준으로 두 번째 변수의 배열 길이까지 루프를 돌게 된다. 이러한 부분이 추가된 이유는 현재 버전 값이 3.4인데 확인하려는 버전 값이 3.4.1로 버전 번호 구분 자릿 수가 다른 경우에 대해서 0으로 채워서 비교가 가능하도록 한다.

    for ((i=0; i<${#ver1[@]}; i++))
    do
        if [[ -z ${ver2[i]} ]]; then
            ver2[i]=0
        fi
        if [[ ${ver1[i]} -gt ${ver2[i]} ]]; then
            return 1
        fi
        if [[ ${ver1[i]} -lt ${ver2[i]} ]]; then
            return 2
        fi
    done

이제 현재 버전 값($ver1)을 기준으로 루프를 돌면서 각 자릿수를 비교하는데 만약 확인하려는 버전 값($ver2)이 자릿수가 다를경우 0으로 채워주고 비교를 한다. 비교 결과 현재 버전이 더 높으면 1을 더 낮으면 2를 리턴하도록 하여 이 함수를 사용하여 리턴 값을 보고 버전 판단을 하면 된다.

if vs case

비교 구문에 있어서 가장 일반적인 것은 if문이지만 case가 좀 더 유용한 경우가 있다. 특정 값에 대한 판별을 할 때 한가지 조건이 아니라 여러 값인 경우가 대표적인데 예를 들어 특정 스크립트에 옵션으로 주어진 값에 대한 처리를 할 때 유용하다.

예를 들면, 첫 번째 인자 값이 -d, –delete, -r, –remove 인 경우에 대해서 동일 한 액션을 취한다고 했을 때 if 문을 이용하면 각 경우에 대해서 모두 테스트 처리를 해야하지만 case를 이용하면 아래와 같이 사용이 가능하다

case $1 in
    -r|--remove|-d|--delete) echo "Delete option";;
esac

따라서, 단일 값에 대한 여러 문자 판단 및 간단한 패턴 판탄의 경우에는 csae를 사용하는 것이 가독성도 좋고 사용하기도 편리하다.

sed -i 옵션의 위험성

Bash 쉘 자체에 대한 이야기는 아니지만 쉘 스크립트를 작성 할 때 자주 보이는 실수이기 때문에 sed -i옵션을 사용하는 경우에 대해서 간단히 소개하고자 한다. sed -i의 경우 sed로 수정 한 내용을 바로 파일에 적용하는 옵션인데 주의 할 사항은 sed가 처리한 문자열에 대해서 파일에 적용 할 때 오픈 된 파일에 대해서 내용을 수정해서 저장하는 것이 아니라 임시파일을 생성해서 덮어씌우는 형태로 동작한다는 것이다. sed를 -i 옵션을 주고 실행할 때 동작을 strace로 살펴보면 아래와 같다.

open("./sed1nw6ql", O_RDWR|O_CREAT|O_EXCL, 0600) = 4
umask(02)                               = 0700
fcntl(4, F_GETFL)                       = 0x8002 (flags O_RDWR|O_LARGEFILE)
fstat(3, {st_mode=S_IFREG|0775, st_size=18, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f64fc144000
read(3, "this is test text\n", 4096)    = 18
fstat(4, {st_mode=S_IFREG, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f64fc143000
write(4, "this is test file\n", 18)     = 18
... 생략 ...
rename("./sed1nw6ql", "string.txt")     = 0
close(1)                                = 0
close(2)                                = 0

위 내용을 보면 임시파일을 생성해서 처리한 문자열을 저장하고 임시파일을 rename해서 원본 파일을 생성하는 것을 볼 수 있다. 일반적인 경우에는 별 문제가 되지 않지만 대상 파일이 심볼릭 링크의 경우에는 이야기가 달라진다. 보통 RHEL/CentOS 계열 배포판의 경우 rc.local 파일이 /etc/rc.d/rc.local에 존재하고 /etc/rc.local에 심볼릭 링크를 걸어서 사용한다. 그런데 sed -i의 대상을 /etc/rc.local 심볼릭 파일로 지정하게 되면 처리한 내용을 임시파일에 저장하고 이를 /etc/rc.local로 rename해서 생성해버리기 때문에 /etc/rc.local은 일반 파일로 변해버리고 실제 시스템이 사용하는 /etc/rc.d/rc.local과 별개의 파일로 다뤄지게 된다. 아래에 예시를 보자.

$ ls -l /etc/rc.local
lrwxrwxrwx. 1 root root 13 Oct 20 16:28 /etc/rc.local -> rc.d/rc.local
$ sed -i "s/performance/powersave/" /etc/rc.local
$ ls -l /etc/rc.local
-rwxr-xr-x 1 root root 1361 Mar 18 14:33 /etc/rc.local

위와 같이 기존 심볼릭링크 파일은 일반 파일로 변해버리기 때문에 의도한대로 시스템에 반영이 되지 않는 경우를 겪을 수 있다. rc.local과 유사하게 /etc/grub.conf (RHEL/CentOS 6까지)의 경우도 /boot/grub 아래에 있는 원본파일을 수정하려는 목적과는 달리 별개의 파일이 /etc/grub.conf로 생성되는 일을 겪을 수 있다.

따라서, 수정하는 대상 파일을 즉시 반영하는 -i 옵션에 대해서는 심볼릭 링크인지 여부를 판단하고 심볼릭 링크이면 대상 파일을 직접 수정하는 형태로 예외처리를 해주는 것이 좋다. 아래는 $_target 변수에 대해서 대상 파일이 심볼릭 링크이면 링크의 대상으로 바꿔주는 처리의 예시이다.

_target=""
if [ -L "$_target" ]; then
  _target=$( readlink $_target )
fi

Bash 변수 값 반올림

가끔 수치 데이터에 대해서 스크립트 처리를 할 때 반올림이 필요한 경우가 있다. 안타깝게도 Bash에는 반올림을 직접 처리해주는 builtin 명령은 없다. 그래서 awk,python,perl과 같은 별도 스크립트 언어를 이용해서 처리하기도 하는데 가장 Bash 스크립트 고유의 형태에 가깝게 반올림 처리하는 방법에 대해서 알아보자. (사실 이것도 bc라는 외부 프로그램을 사용한다)

roundup() {
    echo $( printf %.0f $( echo "scale=1;((10*$1)+0.5)/10" | bc ))
}

$ roundup 1.1
1
$ roundup 1.4
1
$ roundup 1.5
2

위 명령은 소수값으로 전달 된 인자에 대해서 반올림을 하여 정수로 만들어준다. 내부 내용을 살펴보면 주어진 값을 10으로 곱하고 0.5를 더해서 다시 10으로 나눠서 값을 정리하는데 scale 옵션을 1로 주어 소수 첫 째자리까지 만을 bc에서 다루도록 한다. 이렇게하면 쉽게 반올림 기능을 구현할 수 있다. 하지만, 사실 저 방법은 소수 첫째 자리기준으로만 정확하게 동작하기 때문에 완벽한 방법은 아니다. 예를 들어 1.48을 매개변수로 주게되면 (14.8+0.5)/10이 되므로 1.53으로 여겨져서 결과 값이 2가 되어버린다. 따라서, 자리수에 상관없이 소수점 첫째 자리에서 반올림 하도록 정밀도를 높이기 위해서는 아래와 같이 하면 된다.

roundup2() {
    minor=${1#*.} # 소수자리만 남기고 지운다
    prc=$(( 10 ** ${#minor})) # 자리수 개수만큼 10의 제곱으로 구한다
    echo $( printf %.0f $( echo "scale=${#minor};(($prc*$1)+0.5)/$prc" | bc ))
}

$ roundup2 1.4999999
1

또는 직접 두 번째 인자 값을 받아서 반올림 할 위치를 명시적으로 두고 계산하는 방법도 있다. 위에서 언급한 방법은 수 많은 방법 중의 일부분일 뿐이다.

의존성이 있는 커널 모듈 한 번에 제거하기 (재귀함수)

특정 커널 모듈을 제거하는데 있어서 해당 모듈을 사용 중에 있는 다른 모듈이 존재 할 경우에는 제거가 되지 않는데 이를 자동으로 따라가며 삭제하는 방법을 생각해 보자. 보통 lsmod의 실행 결과는 아래와 같다.

$ lsmod
Module                  Size  Used by
drm                   311588  4 ttm,drm_kms_helper,vmwgfx
scsi_transport_sas     41034  1 mptsas
mptscsih               40150  1 mptsas
i2c_core               40325  3 drm,i2c_piix4,drm_kms_helper
libata                218854  3 pata_acpi,ata_generic,ata_piix
mptbase               105960  2 mptsas,mptscsih
... 생략 ...

각 모듈의 이름과 이 모듈을 사용중인 다른 모듈에 대한 목록을 얻을 수 있는데 이것을 이용해서 의존성에 따른 모듈을 먼저 찾아내서 제거하고 마지막으로 원하는 모듈을 삭제하는 방법을 들 수 있다.

r_rmmod() {
    _list=$( lsmod | grep "^$1 " | awk '{print $4}' | sed "s/,/ /g" )
    for x in $_list
    do
        r_rmmod $x
    done
    rmmod $1
}

위의 방법은 재귀함수를 이용한 방법이다. 간단히 제거하고자 하는 모듈을 인자 값으로 받고 lsmod에서 보여주는 의존성이 있는 모듈의 목록을 얻은 후 이 모듈들에 대해서 다시 의존성 모듈 목록을 얻고 차례대로 rmmod로 제거하는 형태이다. 재귀를 이용하기 때문에 스크립트는 그리 길지 않다. 다만, 실제 사용중인 (마운트 등) 모듈의 경우에는 위와 같은 방법으로도 제거되지 않을 수 있으며 모듈 제거는 위험한작업 중 하나이기 때문에 주의해야 한다. 여기에서는 재귀함수 사용의 예시로 소개했다.

여러대의 원격서버에 명령 수행

요즘에는 ansible을 비롯한 좋은 배포 툴들이 있기 때문에 그 활용도가 많이 줄어들었지만 간단히 여러대의 서버에 원하는 명령을 실행하고 싶을 때에는 xargs를 이용하면 편리하다. 먼저 접속하여 실행 할 대상 서버목록을 target.txt로 저장해두고 아래와 같이 실행하면 된다.

$ cat target.txt | xargs -ILUNA ssh -l username LUNA 'swapon -a'

위 명령은 target.txt 파일에 저장된 대상 서버 호스트명(주소)을 xargs에 넘겨서 각각 서버에 username으로 접속하여 스왑을 활성화 하는 명령을 수행한다. -I 옵션은 대치 문자열을 지정하는 것이다. 일반적으로 xargs는 표준입력으로 넘겨 받은 값에 대해서 맨 마지막에 인자로 더해서 실행을 하지만 -I옵션으로 대치 문자열을 지정하면 원하는 위치에 표준입력으로 넘겨 받은 값을 지정 할 수가 있다. 따라서, 위의 예시에서는 ssh로 접속 할 서버의 호스트명(주소) 위치에 LUNA라는 대치문자열로 지정하여 사용하는 것이다.

$ cat target.txt | xargs -P10 -ILUNA ssh -l username LUNA 'swapon -a'

위의 예시는 -P 옵션이 추가되었는데 이는 동시에 실행 할 최대 프로세스의 개수를 지정하는 것인데 -P10의 경우 넘겨받은 대상 서버를 10대씩 접속하여 처리 할 수가 있다. 많은 수의 서버에 대해서 수행 할 때에는 -P옵션을 주면 유용하다.

xargs와 유사한 툴로는 GNU Parallel이 있다. 이 툴은 병렬처리에 최적화 된 툴로 xargs도 병렬 처리를 제공하지만 GNU Parallel의 경우에는 CPU Core 당 작업 개수 지정 및 출력의 그룹화와 같은 병렬처리에 최적화 된 기능을 많이 제공하고 있다. (상세한 차이점은 공식문서 참조) 아무래도 프로그램의 태생의 차이가 다르기 때문에 병렬처리에 있어서는 GNU Parallel이 xargs과 같은 툴보다는 우세하다. 다만, 간단한 작업을 돌리기에는 xargs로도 충분하고 간편하다.

실행 결과에 타임스탬프 남기기

보통 아래와 같이 함수를 생성해 두고 특정 작업에 대한 출력 결과를 파이프로 전달해서 실행 된 시간마다 타임스탬프를 기록하며 사용한다.

ts_gen() {
    while IFS= read -r line; do
        echo "$(date +%Y%m%d-%H:%M:%S) $line"
    done
}

$ find / -name "*.so" | ts_gen
20160318-15:47:48 /opt/pypy/lib/libtk.so
20160318-15:47:48 /opt/pypy/lib/libssl.so
20160318-15:47:48 /opt/pypy/lib/libcrypto.so
20160318-15:47:48 /opt/pypy/lib/libgdbm.so
20160318-15:47:48 /opt/pypy/lib/libexpat.so
20160318-15:47:48 /opt/pypy/lib/libsqlite3.so
20160318-15:47:48 /opt/pypy/lib/libffi.so
20160318-15:47:48 /opt/pypy/lib/libtcl.so
... 생략 ...

예시를 든 find 명령의 경우는 별 의미는 없는 예시이지만 위 실행 결과가 어떤식으로 보여지는지를 나타내려고 넣었다. 특정 어플리케이션을 --debug 모드로 실행 할 때 끊임 없이 화면을 쳐다보기 보다는 이렇게 타임스탬프를 찍게해서 확인할 때 편리하다. 다만, 중간에 buffer가 끼어들게 되면 정확도는 조금 달라질 수 있지만. 간단히 체크하기에는 무난하다.

$( … ) vs `…`

아마 쉘 스크립트 관련 된 문서를 보면 명령치환의 방법으로 `…`을 사용하는 경우를 많이 봤을 것이다. 그리고, 요즘에는 $(…) 형태의 명령치환도 자주 보인다. 어떤 것이 더 좋은 방법이냐는 질문을 종종 받았는데 결론부터 이야기하면 $(…)을 추천한다. (그러면서도 습관적으로 `…`를 쓰는 내 자신을 보기도 한다)

$(…)과 `…`은 일반적으로는 동일한 명령치환으로 알려져 있지만 사용을 해보면 차이점을 느낄 수 있다.

먼저, `…`의 경우에는 중첩 된 명령치환을 하는데 번거롭고 가독성이 떨어진다.

_cnt=$( grep -ic "$(basename "$1")" /tmp/tfile)
_cnt=`grep -ic "\`basename \"$1\"\`" /tmp/tfile`

위 명령은 특정 파일에서 주어진 경로 값의 파일 명을 포함하고 있는 라인 개수를 세는 동작이다. 중첩해서 사용하는데 있어서 \가 많이 사용되고 그로인해 가독성도 떨어진다. \에 의해서 가독성만 떨어지는 것이 아니라 \ 문자 자체를 사용하고자 할 때도 복잡하다. `…`구문 안에서 \문자를 출력한다고 가정해보자.

$ echo "`echo "this is back-slash: \"`"
this is back-slash:
$ echo "`echo "this is back-slash: \\"`"
echo "`echo "this is back-slash: \\"`"
-bash: command substitution: line 1: unexpected EOF while looking for matching `"'
-bash: command substitution: line 2: syntax error: unexpected end of file
$ echo "`echo "this is back-slash: \\\\"`"
this is back-slash: \

$ echo "$(echo "this is back-slash: \\")"
this is back-slash: \

위의 예시처럼 \를 출력하기 위해서 \를 4번이나 사용해야 한다. 흔히 \를 앞에 사용해서 이스케이프 시켰다고 생각해서 \\를 사용하지만 결과는 \\를 이스케이프 처리를 위한 \처럼 다루어서 오류를 일으킨다. 그래서 보통 변수가 많지 않은 경우에는 "(쌍따옴표) 대신에 ' (홑따옴표)를 이용해서 사용하는데 여기에서도 `…`은 아래와 같은 모습을 보인다.

$ echo "`echo 'this is back-slash: \'`"
this is back-slash: \
$ echo "`echo 'this is back-slash: \\'`"
this is back-slash: \

$ echo "$(echo 'this is back-slash: \')"
this is back-slash: \
$ echo "$(echo 'this is back-slash: \\')"
this is back-slash: \\

\를 1개를 사용한 결과와 2개를 사용한 결과가 같다. $(…)이 보여주는 '안에서의 일관된 결과와는 사못 다르다.

따라서, POSIX에 맞지도 않고 오래된 방식인 `…`보다는 $(…) 사용을 개인적으로 권장한다.