Parameter Expansion

프로그래밍 언어에서 함수를 선언할 때 인수를 받는 부분에 사용되는 이름을 매개변수 ( parameter ) 라 하고 함수 내에서 임의로 저장공간을 만들 때 사용되는 이름을 변수 ( variable ) 라고 하듯이 매개변수 와 변수는 사용상에는 차이가 없을지라도 내포하는 의미는 다르다고 할 수 있습니다. 편의상 본문에서 변수확장 이라는 용어를 사용하였지만 정확한 의미로는 매개변수 확장이 맞다고 할 수 있습니다. 아래서 살펴보겠지만 매개변수 확장은 단순히 변수 이름을 값으로 바꾸는 것이 아니라 여러 가지 유용한 연산 작업을 수행합니다. 여러 단계의 명령을 거쳐야 얻을 수 있는 결과를 매개변수 확장을 이용하면 간단히 처리할 수 있으므로 shell script 에서 많이 사용되는 주요 기능이라고 할 수 있습니다.

sh 에서는 사용할 수 없는 기능
substring expansion, search and replace, indirection, case modification

기본 사용법

AA=cde 대입연산은 AA 라는 변수를 만들고 'cde' 라는 값을 저장합니다. 이 값을 사용하기 위해서는 $ 문자를 변수이름 앞에 붙여 구분하는데 이것만으로는 부족할 때가 있습니다. 가령 변수 AA 값을 이용해 'abcdefg' 스트링을 만들고자 할때 echo "ab$AAfg" 와 같이 한다면 변수 'AA' 가 아닌 'AAfg' 값이 적용되어 원하는 결과가 나오지 않게됩니다. 따라서 나머지 스트링과 구분하기 위해 변수이름에 { } 중괄호를 사용할 수 있게 하였는데 이것은 매개변수 확장을 위한 표현식을 작성할 때도 사용됩니다.

$ AA=cde

$ echo "ab$AAfg"       # 현재 변수 $AAfg 값은 null 이므로 'ab' 만 나온다. 
ab

$ echo "ab${AA}fg"     # '{ }' 을 이용하면 나머지 스트링과 구분할 수 있다.
abcdefg

String length

${#PARAMETER}

변수이름 앞에 # 문자를 붙이면 변수가 가지는 값의 문자 수를 나타냅니다. array 의 @ , * 경우에는 전체 원소 개수를 나타냅니다.

$ AA="hello world"
$ echo ${#AA}
11

Array 의 경우

$ BB=( Arch Ubuntu Fedora Suse )
$ echo ${#BB[1]}     # [1] 번째 원소 Ubuntu 의 문자 수
6
$ echo ${#BB[@]}     # array BB 의 전체 원소 개수
4

Substring removal

${PARAMETER#PATTERN}
${PARAMETER##PATTERN}
${PARAMETER%PATTERN}
${PARAMETER%%PATTERN}

변수가 가지는 값을 패턴을 이용해 매칭되는 부분을 잘라낼 때 사용합니다. # 기호는 패턴을 앞에서부터 적용한다는 뜻이고 % 기호는 패턴을 뒤에서부터 적용한다는 뜻입니다. 기호가 두개인 ##, %% 는 longest match 를 뜻하고 하나인 #, % 는 shortest match 를 뜻합니다.

PATTERN 에는 *, ?, [ ] glob 문자를 사용할 수 있습니다.

AA="this.is.an.inventory.tar.gz"

$ echo ${AA#*.}               # 앞에서 부터 shortest match
is.an.inventory.tar.gz

$ echo ${AA##*.}              # 앞에서 부터 longest match
gz

$ echo ${AA%.*}               # 뒤에서 부터 shortest match
this.is.an.inventory.tar

$ echo ${AA%%.*}              # 뒤에서 부터 longest match
this

# 디렉토리를 포함한 파일명에서 디렉토리와 파일명을 분리하기
AA="/home/mug896/bash.txt"

$ echo ${AA%/*}               # 디렉토리 부분 구하기
/home/mug896

$ echo ${AA##*/}              # 파일명 부분 구하기
bash.txt

# '~' 문자는 escape 해야한다.
$ AA="~/tmp"
$ echo ${AA#~}
~/tmp
$ echo ${AA#\~}
/tmp
--------------------------------------------------------

$ AA=$( echo -e "title\n111\n222\n333" )
$ echo "$AA"
title
111
222
333

$ echo "${AA%%$'\n'*}"     # 첫번째 라인 구하기  
title
$ echo "${AA#*$'\n'}"      # 나머지 라인 구하기
111
222
333

# sh 에서는 $'\n' 를 사용할 수 없으므로 먼저 newline 변수를 설정해 사용
$ nl='
'
$ echo "${AA%%$nl*}"
title

확장패턴 사용 예제

$ AA="     123 456"               # AA 변수값의 leading space 와 trailing space 를 제거

$ echo "x${AA##+( )}x"            # +( ) 확장패턴 사용
x123 456x                         # +( ) 패턴에 대해서 longest match 가 되므로
                                  # leading space 가 모두 제거된다.
$ AA="123 456     "

$ echo "x${AA%%+([[:blank:]])}x"
x123 456x  

$ echo "x${AA%%+( |$'\t')}x"      # 위와 동일
x123 456x
--------------------------------

$ val=aa-bb_cc.x
$ echo ${val##*@(-|_)}            # @( ) 확장패턴 사용
cc.x

$ val=aa_bb-cc.x
$ echo ${val##*@(-|_)}
cc.x

array[@], array[*], $@, $* 의 경우는 각각의 원소에 대해서 적용됩니다.

$ \ls /usr/bin/bas*
/usr/bin/base32  /usr/bin/base64  /usr/bin/basename  /usr/bin/bashbug

$ AA=( /usr/bin/bas* )

$ declare -p AA
declare -a AA=([0]="/usr/bin/base32" [1]="/usr/bin/base64" [2]="/usr/bin/basename" 
[3]="/usr/bin/bashbug")

$ echo ${AA[@]##*/}              # ${AA[@]}
base32 base64 basename bashbug

$ echo ${AA[1]##*/}              # ${AA[1]}
base64

다음부터 이어지는 기능은 아래와 같이 변수 이름 옆에 공백 없이 연산자를 붙이고 이후에 값이나, 변수, 명령 치환 등을 사용할 수 있는 기능입니다. 연산자를 사용하는 방식에는 두 가지가 있는데 왼쪽과 같이 연산자에 : 를 붙이지 않는 것은 변수의 존재 여부만 체크하고 : 가 붙는 것은 더해서 변수값이 empty 인지도 체크합니다.

참고로 연산자에 : 를 사용하는 것은 나중에 추가된 기능이라고 합니다. 따라서 작성된 스크립트를 보다 보면 :-:+ 를 사용하는 것이 좋을 것 같은 곳에 -+ 를 사용하는 경우를 볼 수가 있는데 이것은 예전 shell 에서 : 를 처리하지 못하기 때문입니다.

Use a default value

${PARAMETER:-WORD}
${PARAMETER-WORD}

${AA:-linux} : AA 변수값을 사용합니다. 그런데 AA 변수가 존재하지 않거나, null 값을 갖는 경우 linux 를 사용합니다. ( 물론 AA 대신에 ${AA[2]:-linux} 와 같이 array 값도 사용할 수 있습니다. )

# BB=${AA:-linux} 는 다음과 같은 것입니다.
if [ -n "$AA" ]; then
    BB=$AA
else
    BB=linux
fi

${AA-linux} : AA 변수값을 사용합니다. 그런데 AA 변수가 존재하지 않을 경우 linux 를 사용합니다. AA 변수가 존재하기만 하면 되므로 이 표현식은 AA 값으로 null 도 허용되는 것입니다.

# BB=${AA-linux} 는 다음과 같은 것입니다.
if [ -v AA ]; then
    BB=$AA
else
    BB=linux
fi

sh 에서는 test 명령에서 변수 존재 여부를 체크하는-v 옵션을 사용할 수 없는데 이 표현식을 활용하면 간접적으로 체크 할 수 있습니다. 예를 들어 BB=${AA-linux} 대입 연산에서 BB 변수가 linux 로 설정됐다면 현재 변수 AA 는 존재하지 않는 상태죠

$ AA=ubuntu

$ echo ${AA:-fedora}        # AA 에 값이 있으면 AA 값을 사용한다
ubuntu
$ echo ${AA-fedora}
ubuntu

$ unset -v AA

$ echo ${AA:-fedora}        # AA 가 unset 되어 존재하지 않는 상태이므로
fedora                      # 값은 fedora 가 된다.
$ echo ${AA-fedora}
fedora

$ AA=""

$ echo ${AA:-fedora}       # ':-' 는 null 은 값이 없는것으로 보고 fedora 를 사용한다.
fedora
$ echo ${AA-fedora}        # '-' 는 변수의 존재 여부만 체크하므로 아무것도 표시되지 않는다.
$


# $TMPDIR 변수값이 없으면 /tmp 디렉토리에서 myfile_* 을 삭제 
$ rm -f ${TMPDIR:-/tmp}/myfile_*

# WORD 부분에도 명령치환을 사용할 수 있다.
$ AA=${AA:-$(date +%Y)}

# FCEDIT 변수값이 없으면 EDITOR 변수값을 사용하고 EDITOR 변수값도 없으면 vi 를 사용
$ AA=${FCEDIT:-${EDITOR:-vi}}

Array 의 경우

$ AA=( 11 22 33 )

$ echo ${AA[@]:-44 55 66}    # AA 에 값이 있으면 AA 값을 사용한다
11 22 33
$ echo ${AA[@]-44 55 66}
11 22 33

$ AA=()                      # 또는 unset AA

$ echo ${AA[@]:-44 55 66}    # AA 가 unset 되어 존재하지 않는 상태이므로
44 55 66                     # 값은 44 55 66 이 된다.
$ echo ${AA[@]-44 55 66}
44 55 66

$ AA=("")

$ echo ${AA[@]:-44 55 66}    # ':-' 는 null 은 값이 없는것으로 보고 44 55 66 을 사용한다
44 55 66
$ echo ${AA[@]-44 55 66}     # '-' 는 변수만 존재하면 되므로 아무것도 표시되지 않는다.
$

연산자 왼쪽에는 공백이 있으면 오류가 되지만 오른쪽의 공백은 그대로 값에 포함됩니다. 이외에는 명령라인 에서와 동일하게 quotes 과 escape 이 처리됩니다. 값에 quotes 을 포함하려면 quotes 을 사용하거나 escape 해주면 됩니다. 그냥 간단히 생각해서 연산자 오른쪽에 있는 값이 문자 그대로( quotes, escape 포함 ) 연산에 사용된다고 보면 됩니다.

# foo=$val 는 $foo 값이 123 이되고 foo=\$val 는 $val, foo='$val' 는 $val 가 된다. 
val=123                                        val=123
foo=${xxx:-     $val    \$val     }            foo=${xxx:-     "$val"    '$val'     }
$ echo yyy"${foo}"zzz                          $ echo yyy"${foo}"zzz
yyy     123    $val     zzz                    yyy     123    $val     zzz

# 대입연산 이므로 변수를 quote 하지않아도 공백과 개행이 유지되고 globbing 이 발생하지 않는다.
val1="aa       bb"                             val2="*"
foo=${xxx:-$val1}                              foo=${xxx:-$val2}
$ echo yyy "${foo}" zzz                        $ echo yyy "${foo}" zzz
yyy aa       bb zzz                            yyy * zzz

# 대입연산이 아닐경우 quote 을 하지않으면 공백이 유지되지 않고 globbing 이 발생한다.
$ echo yyy ${xxx:-$val1} zzz                   $ echo yyy ${xxx:-$val2} zzz
yyy aa bb zzz                                  yyy a.txt b.txt c.txt ...

$ echo yyy ${xxx:-"$val1"} zzz                 $ echo yyy ${xxx:-"$val2"} zzz
yyy aa       bb zzz                            yyy * zzz

$ echo yyy "${xxx:-$val1}" zzz                 $ echo yyy "${xxx:-$val2}" zzz
yyy aa       bb zzz                            yyy * zzz

arr=(11 "22 22" 33)                            # ${arr[@]} 를 double quote
$ args.sh ${xxx-${arr[@]}}                     $ args.sh ${xxx-"${arr[@]}"}
$1 : 11                                        $1 : 11
$2 : 22                                        $2 : 22 22
$3 : 22                                        $3 : 33
$4 : 33

# 명령치환 에서처럼 ${ ... } 안에서 quotes 이 중첩되어도 됩니다.
$ echo "${X-"${Y-"${Z-"foo    bar"}"}"}"
foo    bar

다음과 같이 quotes 을 이용하면 multi-line 으로 작성할 수도 있습니다.

$ script=${NOT_EXIST-'
arr = []
while 1:
    try:
        line = input()
        arr.append(int(line))
    except EOFError:
        break
print(f"max : {max(arr)}, min : {min(arr)}, sum : {sum(arr)}")
'}

$ shuf -i 1-10 | python3 -c "$script"
max : 10, min : 1, sum : 55

Assign a default value

${PARAMETER:=WORD}
${PARAMETER=WORD}

이것은 위의 Use a default value 와 동일하게 동작합니다. 하지만 차이점이 하나 있는데요. 바로 대체값을 사용하게 될때 그값을 AA 변수에도 대입한다는 것입니다.

AA=""

$ echo ${AA:-linux}
linux
$ echo $AA             # ':-' 는 AA 변수에 값을 대입하지 않는다.
$

$ echo ${AA:=linux}
linux
$ echo $AA             # ':=' 는 AA 변수에 값을 대입한다.
linux


# $TMPDIR 값이 없으면 /tmp 를 사용하고 TMPDIR 변수에 대입
$ cp myfile ${TMPDIR:=/tmp}
# $TMPDIR 변수 설정이 보장되므로 cd 할 수 있다.
$ cd $TMPDIR

# 변수값이 설정되어 있을 경우 기존 값을 사용하지만 그렇지 않을 경우 값을 설정
: ${BIN_PATH:=/usr/local/bin}
: ${CONFIG_PATH:=/usr/local/www}
: ${LOGFILE:=/var/log/abc.log}

# 다음은 변수가 존재하지 않을 경우 설정
: ${CP="cp -f"}
: ${ECHO="printf %s\n"}
: ${MV="mv -f"}
: ${RM="rm -f"}

# 실행 결과로 AA 값이 not_set 으로 설정됐다면 변수 AA 는 존재하지 않는 상태였다는 것을 알수있다.
: ${AA=not_set}

# 현재 FOO 변수가 존재하지 않을 경우 존재하는 상태로 된다.
: ${FOO=}

Array 값은 대입되지 않습니다.

$ AA=()

$ : ${AA:=(11 22 33)}

$ echo ${AA[1]}

$ echo $AA         # (11 22 33) 이 AA 변수값이 된다.
(11 22 33)

Use an alternate value

${PARAMETER:+WORD}
${PARAMETER+WORD}

${AA:+linux} : AA 변수가 값을 가지고 있으면 linux 를 사용합니다. 변수가 존재하지 않거나 값이 null 이면 null 을 리턴합니다.

# BB=${AA:+linux} 는 다음과 같은 것입니다.
if [ -n "$AA" ]; then
    BB=linux
else
    BB=""
fi

${AA+linux} : AA 변수가 존재하기만 하면 linux 를 사용합니다. 변수가 존재하지 않으면 null 을 리턴합니다.

# BB=${AA+linux} 는 다음과 같은 것입니다.
if [ -v AA ]; then
    BB=linux
else
    BB=""
fi
$ AA=hello

$ echo ${AA:+linux}       # AA 에 값이 있으므로 대체값 linux 를 사용한다.
linux
$ echo ${AA+linux}
linux

$ AA=""

$ echo ${AA:+linux}       # ':+' 는 null 은 값으로 취급하지 않기 때문에 null 을 리턴한다.

$ echo ${AA+linux}        # '+' 는 변수가 존재하기만 하면 되므로 대체값 linux 가 사용된다.
linux

$ unset -v AA

$ echo ${AA:+linux}       # 변수가 존재하지 않으므로 null 을 리턴한다.

$ echo ${AA+linux}
-----------------------

# 함수이름을 갖는 FUNCNAME 변수가 있을 경우 뒤에 '()' 를 붙여서 프린트 하고 싶다면
echo ${FUNCNAME:+$FUNCNAME()} 

# $debian_chroot 변수에 값이 존재하면 '( )' 가 붙어서 출력되므로 다음과 같게 되고
# PS1='($debian_chroot)\u@\h:\w\$ '
# $debian_chroot 변수에 값이 존재하지 않으면 다음과 같게 된다.
# PS1='\u@\h:\w\$ '
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '

Display error if null or unset

${PARAMETER:?[error message]}
${PARAMETER?[error message]}

${AA:?error message} : AA 변수값을 사용합니다. 그런데 AA 변수가 존재하지 않거나 null 값이면 error message 를 출력하고 스크립트 실행이 종료됩니다. 이때 종료 상태 값으로 1 (bash) or 2 (sh) 가 설정됩니다.

${AA?error message} : AA 변수값을 사용합니다. 그런데 AA 변수가 존재하지 않으면 error message 를 출력하고 스크립트 실행이 종료됩니다. 이때 종료 상태 값으로 1 (bash) or 2 (sh) 가 설정됩니다.

error message 를 생략하면 parameter null or not set 메시지가 사용됩니다.

$ AA=hello

$ echo ${AA:?null or not set}      # AA 에 값이 있으므로 AA 값을 사용한다
hello
$ echo ${AA?not set}
hello

$ AA=""

$ echo ${AA:?null or not set}       # ':?' 는 null 은 값으로 취급하지 않기 때문에
bash: AA: null or not set           # error message 를 출력하고
$ echo $?                           # 종료 상태 값으로 1 을 리턴한다.
1
$ echo ${AA?not set}                # '?' 는 변수가 존재하기만 하면 되므로
                                    # 아무것도 표시되지 않는다.

$ unset -v AA

$ echo ${AA:?null or not set}       # 변수가 존재하지 않는 상태이므로 
bash: AA: null or not set           # 모두 error message 출력 후 종료한다.
$ echo $?
1
$ echo ${AA?not set}
bash: AA: not set
$ echo $?
1
$ echo ${AA:?}                       # error message 는 생략할 수 있다.
bash: AA: parameter null or not set
$ echo ${AA?}
bash: AA: parameter not set


# 예제
case ${AA:?"missing pattern; try '$0 --help' for help"} in
    (abc) ... ;;
    (*) ... ;;
esac

이후부터는 bash 전용

Substring expansion

${PARAMETER:OFFSET}
${PARAMETER:OFFSET:LENGTH}

변수가 가지는 값에서 특정부분을 추출할때 사용합니다. offset 위치에서부터 length 길이 만큼을 추출합니다. offset 은 0 부터 시작하고 length 값을 주지 않으면 끝까지 해당됩니다.

offset 과 length 를 음수로 줄수 있는데 이경우 카운트를 왼쪽에서부터 하지 않고 오른쪽에서부터 좌측 방향으로 하며 length 만큼 제외 하게 됩니다. 이때 사용되는 음수의 - 기호는 매개변수 확장에서 사용되는 :- 기호와 겹치므로 공백을 두거나 ( ) 를 사용해야 합니다.

offset 이나 length 값을 변수로 사용할 때는 $ 문자는 붙이지 않아도 됩니다.

AA="Ubuntu Linux"

$ echo "${AA:1}"
buntu Linux
$ echo "${AA:2:4}"
untu
$ echo "${AA: -5}"     또는  "${AA:(-5)}"
Linux
$ echo "${AA: -5:2}"   또는  "${AA:(-5):2}"
Li
$ echo "${AA: -5:-1}"  또는  "${AA:(-5):-1}"
Linu

# 변수 AA 값이 %foobar@ 일경우
$ echo "${AA: -1}"    # 마지막 문자 추출
@
$ echo "${AA:0:1}"    # 첫번째 문자 추출
%
$ echo "${AA:1:-1}"   # 양 끝문자 제거
foobar

# 다음과 같은경우 오류가 발생할 수 있습니다.
$ AA=a
$ echo ${AA:1:-1}
bash: -1: substring expression < 0

Array 의 경우 ( length 에 음수 사용불가 )

$ ARR=(11 22 33 44 55)

$ echo ${ARR[@]:1:2}
22 33

$ echo ${ARR[@]:2}
33 44 55

Positional parameters 의 경우는 index 가 1 부터 시작합니다. ( length 에 음수 사용불가 )

$ set -- 11 22 33 44 55

$ echo ${@:3}
33 44 55

$ echo ${@:2:2}
22 33

Search and replace

${PARAMETER/PATTERN/STRING}
${PARAMETER//PATTERN/STRING}
${PARAMETER/PATTERN}
${PARAMETER//PATTERN}

변수가 가지는 값을 패턴을 이용해 매칭되는 부분을 다른 스트링으로 바꾸거나 삭제할때 사용합니다. 매칭되는 부분이 여러군대 나타날수 있는데 // 기호는 모두를 나타내고 /첫번째 하나만 나타냅니다. 바꾸는 스트링 값을 주지 않으면 매칭되는 부분이 삭제됩니다. array[@], array[*], $@, $* 의 경우는 각각의 원소에 대해서 적용됩니다.

regex 에서처럼 & 문자는 패턴에 의해 매칭된 값을 나타냅니다.

$ AA="Arch Linux Ubuntu Linux Fedora Linux"

$ echo ${AA/Linux/XXX}                # Arch Linux 만 바뀌였다.
Arch XXX Ubuntu Linux Fedora Linux

$ echo ${AA//Linux/XXX}               # Linux 가 모두 XXX 로 바뀌였다
Arch XXX Ubuntu XXX Fedora XXX

$ echo ${AA/Linux}                    # 바꾸는 스트링을 주지 않으면 매칭되는 부분이 삭제된다.
Arch Ubuntu Linux Fedora Linux

$ echo ${AA//Linux}
Arch Ubuntu Fedora

$ AA=foo456
$ echo ${AA//[^0-9]}         # 숫자가 아닌 문자를 모두 삭제
456
----------------------------------------------------

$ AA="Linux Ubuntu Linux Fedora Linux"

$ echo ${AA/#Linux/XXX}               # '#Linux' 는 맨 앞 단어를 의미
XXX Ubuntu Linux Fedora Linux

$ echo ${AA/%Linux/XXX}               # '%Linux' 는 맨 뒤 단어를 의미
Linux Ubuntu Linux Fedora XXX

$ AA=12345

$ echo ${AA/#/X}
X12345

$ echo ${AA/%/X}
12345X

$ AA=X12345X

$ echo ${AA/#?/}     # `?` 패턴에 의해 첫번째 문자가 삭제된다.
12345X

$ echo ${AA/%?/}
X12345

$ BB=AB12345CD

$ echo ${BB/#+([[:alpha:]])/}     # +( ) 확장패턴을 이용해 [:alpha:] 문자들을 삭제
12345CD

$ echo ${BB/%+([[:alpha:]])/}
AB12345
-------------------------------------

$ CC=foobar

$ echo ${CC/f??/&-}     # "&" 문자는 f?? 패턴의 매칭값인 foo 가 된다.
foo-bar

$ echo ${CC/f??/\&-}
&-bar

Array 는 각각의 원소별로 적용됩니다.

$ AA="aaa.c bbb.c ccc.c"        # AA 가 array 가 아닐경우.

$ echo ${AA/#/common/}
common/aaa.c bbb.c ccc.c

$ AA=( aaa.c bbb.c ccc.c )      # AA 가 array 일 경우 각각의 원소에 적용된다.

$ echo ${AA[@]/#/common/}
common/aaa.c common/bbb.c common/ccc.c
---------------------------------------------------

$ CC=( "Arch Linux" "Ubuntu Linux" "Fedora Linux" )

$ echo ${CC[@]/u/X}                   # Ubuntu Linux 는 첫번째 'u' 만 바뀌었다
Arch LinXx UbXntu Linux Fedora LinXx

$ echo ${CC[@]//u/X}                  # // 를 사용해 모두 바뀌였다
Arch LinXx UbXntX LinXx Fedora LinXx
---------------------------------------------------

$ OPT=( -a --bbb -bc --dd-dd )

$ echo ${OPT[@]/#--*/}
-a -bc

$ echo ${OPT[@]/#-[^-]*/}
--bbb --dd-dd

Array 처리결과를 array 변수에 대입하려면 항상 =( ) 를 사용해야 합니다.

$ AA=( aaa.c bbb.c ccc.c )

$ BB=${AA[@]/#/common/}               # BB 는 array 가 되지 않는다.

$ echo ${#BB[@]} : ${BB[1]} : $BB
1 : : common/aaa.c common/bbb.c common/ccc.c

$ BB=( "${AA[@]/#/common/}" )         # 대입시 `=( )` 를 사용해야 array 가 된다.

$ echo ${#BB[@]} : ${BB[1]} : $BB
3 : common/bbb.c : common/aaa.c

확장패턴 을 활용하면 regex 에서처럼 가변 길이의 문자도 매칭이 가능합니다.

$ AA="   #   include    <stdio.h>"
$ echo x${AA/#*( )#*( )include*( )/}x              # *(  ) 확장 패턴 사용
x<stdio.h>x

$ AA="   #include    <stdio.h>"
$ echo x${AA/#*( )#*( )include*( )/}x
x<stdio.h>x

$ AA="   #include    #  include   <stdio.h>"
$ echo x${AA/#+(*( )#*( )include*( ))/}x           # +(  ) 를 추가해 복수개 매칭
x<stdio.h>x

$ AA="   #include    #  include   <stdio.h>    #  include"
$ echo x${AA//*( )#*( )include*( )/}x
x<stdio.h>x

Case modification

${PARAMETER^} : 첫 문자를 대문자로 변경합니다.
${PARAMETER^^} : 모든 문자를 대문자로 변경합니다.
${PARAMETER,} : 첫 문자를 소문자로 변경합니다.
${PARAMETER,,} : 모든 문자를 소문자로 변경합니다.
${PARAMETER~} : 첫 문자를 반전합니다.
${PARAMETER~~} : 모든 문자를 반전합니다.

변수값을 ,, 을 이용해 모두 소문자로 바꿔주면 대,소문자 구분없이 매칭을 할 수 있습니다.

$ AA=( "ubuntu" "fedora" "suse" )           $ AA=( "UBUNTU" "FEDORA" "SUSE" )

$ echo ${AA[@]^}                            $ echo ${AA[@],}
Ubuntu Fedora Suse                          uBUNTU fEDORA sUSE

$ echo ${AA[@]^^}                           $ echo ${AA[@],,}
UBUNTU FEDORA SUSE                          ubuntu fedora suse

Indirection

${!PARAMETER}

스크립트 실행 중에 스트링으로 변수 이름을 만들어서 사용할 수 있습니다.

$ hello=123

$ linux=hello

$ echo ${linux}
hello

$ echo ${!linux}    # '!linux' 부분이 'hello' 로 바뀐다고 생각하면 됩니다. 
123                 # !linux -> $linux -> hello 

$ echo ${!linux/2/X}
1X3

sh 에서는 다음과 같이 eval 명령을 이용해서 indirection 을 사용할 수 있습니다.

$ hello=123

$ linux=hello

$ eval echo '$'$linux
123
----------------------

if test "$hello" = "$(eval echo '$'$linux)"; then
...
fi

함수에 전달된 인수를 표시할 때

---------- args.sh ------------
#!/bin/bash

for (( i = 0; i <= $#; i++ )) 
do
    echo \$$i : ${!i}              # !i -> $i -> 0,1,2,3
done

-------------------------------
$ args.sh 11 22 33
$0 : ./args.sh
$1 : 11                    
$2 : 22
$3 : 33

함수에 array 인자를 전달할 때도 사용할 수 있다.

#!/bin/bash

foo() {
    echo "$1"
    local ARR=( "${!2}" )      # '!2' 부분이 'AA[@]' 로 바뀐다.
                               # !2 -> $2 -> AA[@]
    for v in "${ARR[@]}"; do
        echo "$v"
    done
    echo "$3"
}

AA=(22 33 44)
foo 11 'AA[@]' 55

################ output ###############
11
22
33
44
55

Parameter transformation

${parameter@operator} : operator 에는 a, P, Q, E, A 가 있습니다.

1 . ${varname@a} : 변수 속성을 반환합니다.

$ AA=(1 2 3)
$ echo ${AA@a}          # indexed array
a

$ declare -A BB=( [ab]=100 [cd]=200 )    # associative array
$ echo ${BB@a}
A

$ declare -i CC=100     # integer 속성
$ echo ${CC@a}
i

$ declare -r CC         # readonly 속성 추가
$ echo ${CC@a}
ir

$ DD=100
$ echo ${DD@a}          # 일반 변수는 값이 없다
$

2 . ${varname@P}

PS1, PS2 ... 프롬프트 변수값으로 사용할 경우 결과가 어떻게 출력되는지 알아볼 수 있습니다.

$ myprompt='\u@\h \@ \w \\$'

$ echo ${myprompt@P}
mug896@EliteBook 07:57 PM /usr/local $

3 . ${varname@Q} : 변수값을 $' ' 형태로 출력해 줍니다.

$ cat tmp.txt                       $ set -- "foo bar" "bar zoo"
'hello'
"world"                             $ echo "$@"
                                    foo bar bar zoo
$ AA=`< tmp.txt`
                                    $ echo "${@@Q}"
$ echo "$AA"                        'foo bar' 'bar zoo'
'hello'
"world"

$ echo "${AA@Q}"
$'\'hello\'\n"world"'

4 . ${varname@E} : 변수값에 있는 모든 escape 문자를 확장해서 출력합니다.

$ foo='one\ntwo\n\tlast'

$ echo "${foo}"
one\ntwo\n\tlast

$ echo "${foo@E}"
one
two
        last

5 . ${varname@A} : 대입연산 형태로 출력해 줍니다.

$ foo=100
$ echo ${foo@A}
foo='100'

$ declare -i bar=100
$ echo ${bar@A}     
declare -i bar='100'

$ zoo=( 11 22 33 )
$ echo ${zoo[@]@A}
declare -a zoo=([0]="11" [1]="22" [2]="33")

$ declare -A goo=( [ab]=100 [cd]=200 )
$ echo ${goo[@]@A}
declare -A goo=([ab]="100" [cd]="200" )

Quiz

Linux 와 Unix 운영체제 에서는 경로를 구분하는 문자로 / 를 사용하는데요. Windows 운영체제 에서는 반대로 \ 문자를 사용합니다. 다음과 같은 Windows 경로가 주어졌을때 Unix 경로로 변경하려면 어떻게 할까요?

$ project_dir="\Documents\Projects\Newsletters\Summer"

# '\' 문자는 escape 할때 사용되므로 '\\' 와 같이 escape 해야 한다.
$ echo "${project_dir//\\//}"
/Documents/Projects/Newsletters/Summer

2 .

16 진수에서 Most Significant Byte ( 제일 왼쪽 문자 ) 가 8 이상이면 sign bit 이 1 이 되기 때문에 음수가 되는데요. 16 진수 값을 부호 있는 정수값으로 표시하려면 어떻게 할까요?

# MSB 값이 8 미만이면 바로 hex 값을 정수로 출력하고
$ echo $(( 0x7f ))
127
$ echo $(( 0x7fff ))
32767

# MSB 값이 8 이상이면 음수가 되므로 입력된 수의 자리수에 맞추어 빼기를 해준다.
$ echo $(( 0x8f - 0xff - 1 ))         # 2 자리
-113
$ echo $(( 0x8fff - 0xffff - 1 ))     # 4 자리
-28673

hex2int () {
    local hex msb
    hex=${1#0[xX]}     # 입력값이 0x, 0X 로 시작할 경우 제거
    msb=${hex:0:1}     # MSB 값 추출

    if test $(( 0x$msb )) -ge 8; then
        echo $(( 0x$hex - 0x${hex//?/f} - 1 ))   # '?' 는 glob 문자
    else
        echo $(( 0x$hex ))
    fi
}

##################### 실행 결과 ###################

$ hex2int 0xff
-1
$ hex2int 0xfe
-2
$ hex2int 0x79
121
$ hex2int 0x78
120

3 .

명령의 인수로 파일이 주어지면 파일에서 라인을 읽어들여 처리하고, 그렇지 않으면 stdin ( 터미널, 파이프, redirection ) 에서 라인을 읽어서 처리하는 명령을 만드는 것입니다.

$ func1 () {                                      $ cat tmp.txt
    local total_lines                             111
    while IFS= read -r line                       222
    do                                            333
        total_lines+=$line$'\n'
    done < "${1:-/dev/stdin}" || return

    echo ----------------------
    echo -n "$total_lines"
    echo ----------------------
}

##################################################################

$ func1 tmp.txt    # 인수로 파일이 주어지면 파일에서 라인을 읽어 들임
----------------------
111
222
333
----------------------

$ func1      # enter 후 라인 입력
111
222
333
             # ctrl-d 로 입력 종료
----------------------
111
222
333
----------------------

$ func1 <<\@                              $ echo -e "foo\nbar" | func1
> 111                                     ----------------------
> 222                                     foo
> 333                                     bar
> @                                       ----------------------
----------------------                    
111                                       $ func1 <<< hello
222                                       ----------------------
333                                       hello
----------------------                    ----------------------