home search
OS
Linux
[Linux] Bash (4): integer와 array, special
2022. 02. 27

변수 타입 지정

변수는 string(문자열)로 다 저장된다고 했지만, 지정 가능한 타입들도 있다. 하나는 integer(정수형)이고 다른 하나는 array(배열)이다. 배열은 또 다시 숫자 인덱스를 이용하는 indexed array와 associated array로 나뉜다. 여기서 배열은 key=value 쌍으로 움직이는 딕셔너리에 가깝긴 하다. indexed는 키를 숫자로 쓰고 associated는 문자로도 쓸 수 있단 것의 차이다.

integer

🔥 <declare|typeset> -i var_name: integer라는 속성(-i)를 설정한다.

🔥 <declare|typeset> +i var_name: integer라는 속성(-i)를 해제한다. 일반 문자열 변수로 취급된다.

🔥 ((var_name++)) / ((var_name++)) / ((수식)): 괄호 두 개로 감싸면 각각 증가, 감소, 수식의 계산을 할 수 있다. 문자열 변수라 해도 정수가 저장되어 있을 때는 가감 수식은 사용 가능하다.

정수형 변수를 읽는 것은 일반 변수와 같게 $var_name으로 읽는다. 정수형 변수에 정수가 아닌 일반 문자열을 넣으면 0으로 처리됨에 유의한다.

$ num=100
$ ((num+=10))
$ echo $num
110

$ (( numnum = (num*2) ))
$ echo $numnum
10000

$ echo "Value = $(( num + numnum ))"
10110

처음에 numnum이란 변수는 선언되지 않았었는데 (( numnum = (num*2) ))으로 값을 대입함으로써 동시에 선언되었다. 단, 이때 numnum은 일반 변수이다. 또한 연산의 결괏값을 변수로 치환할 떄는 $(( num + numnum ))처럼 앞에 $ 표시만 붙이면 된다. 수식 내에서는 공백은 신경쓰지 않아도 된다.

array

변수의 설정 및 요소값 추가는 아래와 같이 한다.

🔥 <declare|typeset> -a varname[=( "value" [...] )]: indexed 배열을 설정한다. -a 옵션을 쓰며, 선언과 동시에 값을 추가할 수도 있다. value로 여러 개를 입력하면 앞에서부터 순서대로 0, 1, 2… 의 idx가 부여된다. 음수의 index를 사용하면 뒤에서부터 계산한다. 🔥 varname+=( "value" [...] ): indexed 배열에 값을 추가

🔥 <declare|typeset> -A varname[=( [key]="value" [...] )]: associative 배열을 설정한다. -A 옵션을 쓰며, 역시 선언과 동시에 값을 넣을 수도 있다. indexed와는 달리 꼭 key 값과 함께 선언되어야 한다.

🔥 varname+=( [key]="value" [...] ): associative 배열에 값을 추가한다.

배열의 값(value)를 넣을 때는 공백이 없더라도 되도록 따옴표로 감싸는 것이 좋다. 값 선언 및 추가 시 ( ) 괄호 안쪽 공백은 없어도 무관하다. arr+=('var')이나 arr+=( 'var' )이나 같다.

두 배열의 타입 모두 읽는 방법은 아래와 같다.

🔥 ${varname [key]}: 변수의 값(value)를 읽는다. indexed 배열은 key에 숫자를 넣으면 된다.

🔥 ${!varname[@]}: 변수의 key를 읽어 문자열이 나온다.

🔥 \${varname[@]} 또는 "\${varname[@]}" 또는 \${varname[\*]} 또는 "${varname[\*]}": 배열 전체를 읽는다. 따옴표로 묶으면 요소값 중간에 들어간 구분자에 IFS의 영향을 벗어날 수 있다. 따옴표로 묶을 때 @는 요소 하나하나를 개별적인 separated strings로 변환하고, *는 IFS의 첫 문자를 구분자로 하여 single string으로 변환한다.

🔥 ${varname[@]:시작:끝}: 해당 부분의 값을 읽는다. ${varname[@]:2}하면 두 번째부터 끝까지 읽는다.

🔥 ${#varname[@]} 또는 ${#varname[\*]}: 특정 개수를 읽는다. 배열 이름 앞에 #이 붙는다.

indexed 배열의 예시다.

$ declare -a arrv=('hello world' 'posix.1' 'SUSv1')
$ echo ${#arrv[@]}
3
$ echo ${arrv[1]}
posix.1
$ echo ${arrv[@]:0:2}
hello world posix.1
$ echo ${arrv[@]:1}
posix.1 SUSv1

$ arrv+=( "SUSv2" )
$ echo ${arrv[@]}
hello world posix.1 SUSv1 SUSv2

associative 배열의 예시이다.

$ declare -A mem spec=([bank]="ddr4" [vendor]="sec" [capacity]="32")

$ echo ${mem_spec[@]} 
ddr4 sec 32 
$ echo ${mem_spec[vendor]} 
sec 
$ echo ${!mem_spec[@]} 
bank vendor capacity

$ mem_spec+=([clock speed]=3200) 		# key 중간 공백은 상관 없음
$ echo ${mem_spec[@]} 
ddr4 sec 3200 32

🔥 unset varname[key]: 해당 요소를 삭제한다.

$ unset arrv[1]
$ echo ${arrv[@]}
hello world SUSv1 SUSv2
$ echo ${#arrv[@]}
3
$ echo ${!arrv[@]}
0 2 3
$ arrv+=( "SUSv3" )
$ echo ${arrv[@]}
hello world SUSv1 SUSv2 SUSv3
$ echo ${!arrv[@]}
0 2 3 4

배열 중간에 있는 요소 삭제 시 indexed 배열이라 해도 index가 앞뒤로 밀리진 않는다. indexed 배열은 key가 정수라는 것에 비유한 점을 떠올리면 쉽게 이해할 수 있다. 중간에 인덱스가 비게 되니 주의하면 된다.

IFS 환경변수

IFS는 Internal Field Separator의 약자로, 데이터를 처리할 때 IFS에 있는 구분자로 값을 필드로 잘라낸다. default 값은 아래와 같이 확인할 수 있다.

$ echo -n "$IFS | hexdump -C"	# -n 옵션으로 줄바꿈 없이 출력. hexdump로 16진수값 출력
00000000	20 09 0a

위에서 출력된 바와 같이 0x20, 0x09, 0x0a는 각각 Space, HT(tab), LF(new line)을 가리키는 아스키 코드이다.

따옴표에 대해 예시를 통해 IFS 사용의 주의점과 배열값 사용의 차이를 알아본다.

### (1) ${varname[@]} 사용
$ for ii in ${arrayv[@]}; do echo "=>| $ii |"; done
=>| hello |
=>| world |
=>| SUSv1 |
=>| SUSv2 |
=>| SUSv3 |

### (2) "${varname[@]}" 사용
$ for ii in "${arrayv[@]}"; do echo "=>| $ii |"; done
=>| hello world |
(이하 생략)

### (3) ${varname[*]} 사용
$ for ii in ${arrayv[*]}; do echo "=>| $ii |"; done
=>| hello |
=>| world |
=>| SUSv1 |
=>| SUSv2 |
=>| SUSv3 |

### (4) "${varname[*]}" 사용
$ for ii in "${arrayv[*]}"; do echo "=>| $ii |"; done
=>| hello world SUSv1 SUSv2 SUSv3 |

(1)을 보면, 분명 ‘hello world’는 하나의 요소로 선언했음에도 foreach 구분에서 따로따로 들어갔다. 해당 요소값의 중간에 IFS의 첫번째 문자 0x20(Space)인 공백이 있기 때문이다. 하지만 (2)에서는 따옴표로 묶어주어 그런 경우를 피했다. (3)은 *를 썼으므로 hello world SUSv1 SUSv2 SUSv3를 하나로 보지만 따옴표로 묶지 않았기 때문에 IFS 구분자인 공백을 기준으로 값이 나눠졌다. 반면 (4)는 따옴표로 묶어주었기에 아예 통째로 하나가 들어간다.

이번엔 IFS를 콤마로 바꿔보자.

$ IFS=','
$ for ii in ${arrayv[*]}; do echo "=>| $ii |"; done
=>| hello world |
(이하 생략)

$ arrv+=( "Debian,RedHat" )
$ for ii in ${arrayv[*]}; do echo "=>| $ii |"; done
=>| hello world|
(중간 생략)
=>| Debian |
=>| RedHat |

$ for ii in "${arrayv[*]}"; do echo "=>| $ii |"; done
=>| hello world,SUSv1,SUSv2,SUSv3,Debian,RedHat |

따옴표로 묶지 않아도 구분자가 공백이 아닌 콤마이므로 hello world가 한 요소로 인식되었다. 하지만 이번엔 콤마가 중간에 들어간 ‘Debian,RedHat’은 두 개의 요소로 분리되었다. 이 역시 따옴표로 묶어주면 되었다. 마지막 출력의 경우, IFS가 콤마이면 "${arrayv[*]}"가 CSV의 형태를 띔을 알 수 있다. 물론 이 경우에는 요소값 안에 콤마가 들어가면 구별이 어렵다.

따라서 IFS의 구애를 받지 않으려면 아예 처음부터 IFS= 혹은 IFS=''으로 null로 변경해주고 마지막에 끝낼 때 IFS=$'\x20'$'\x29'$'\x0a'인 디폴트 값으로 돌려놓는다. 또는 다른 변수에 잠시 IFS 디폴트 값을 저장했다가 되돌릴 수도 있다(OldIFS=IFS; ...; IFS=OldIFS;)


Special Parameters

special parameter 의 종류를 보자.

  • $$: 현재 셸 프로세스의 PID이다. 단, $( )로 묶인 서브셸로 실행된 경우엔 치환이 먼저 이뤄진다. 이렇게 되면 서브셸이 아닌 부모 프로세스의 PID가 나옴에 주의한다.
  • $!: 마지막으로 실행된 백그라운드 프로세스의 PID
  • $?: 최근 foreground 프로세스의 종료값으로, 0=true, non-zero=false
  • $_: 이전 명령어의 마지막 인수로, !$ 와 같다.
  • $-: on이 되어있는 실행 옵션을 보여준다. 예를 들자면 himBH 식이다.
  • $#: 인수의 개수를 반환한다(argc(arguments count)) 단, 0번째 argument는 제외. 이 표현은 함수를 사용할 때 함수의 인수를 지칭하기도 한다.
  • $0, $1, $2, …: 인수 리스트(argv(arguments variables))이다. argv도 array의 일종이다.
  • $*: 명령에 전달된 인수 전체의 목록을 말한다. 따옴표로 감싸면 IFS의 첫번째 문자를 구분자로 하여 1개의 문자열로 만든다.
  • $@: 전달된 인수를 문자열의 배열(array)로 표시한다. 따옴표로 감싼 경우 argv를 입력한 그대로 따옴표로 묶는다. 즉, 각 인수를 따옴표로 감싸서 각각을 배열로 처리한다.

history 기능 쓰기 !:# 식으로 인수의 번호를 직접 지정해 사용한다. 자주 보았던 !$는 숫자 자리에 $(마지막을 의미)를 넣어 마지막 명령어의 인수를 가져온 것이다. !:2는 이전 명령어의 2번째이다. !##:##면 앞의 숫자는 history 번호, 뒤의 숫자는 인수의 번호로 지정해 쓸 수도 있다.

argc와 argv를 더 자세히 예시를 통해 보자. 아래는 각각 스크립트와 해당 스크립트의 실행 내용이다.

#!/bin/bash
echo "argc : $#"
if [ $# -eq 0 ]; then
	echo "Usage: $0 <string> [...]"
    exit 1
fi 
echo "argv[0] : $0" 
echo "argv[1] : $1" 
for i in ${*:2} 
do
	echo "arg : $i" 
done
$ ./argc_argv.sh aaa bbbb cccc
argc : 3 
argv[0] : ./argc_argv.sh 
argv[1] : aaa
arg : bbbb 
arg : cccc

$*$@에 따옴표를 썼을 때의 영향을 살펴보자. 배열의 경우와 매우 유사하다. 특히 argv는 배열의 일종이기 때문이다.

#!/bin/bash 
if [ $# -eq 0 ]; then
	echo "Usage: $0 <quoted string ...>"
	exit 1
fi

for arg_list in $*; do
	echo '$* : param =' $arg_list 
done 

for arg_list in "$*"; do
	echo '"$*" : param =' $arg_list
done

for arg list in $@; do
	echo '$ @ : param =' $arg_list
done 

for arg_list in "$@"; do
	echo '"$@" : param =' $arg list 
done

실행하면 아래와 같은 결과를 얻는다.

$ ./special_quotes.sh aa "b1 b2" cc

$* : param = aa
$* : param = b1 
$* : param = b2 
$* : param = cc 
"$*" : param = aa b1 b2 cc 
$@ : param = aa 
$@ : param = b1 
$@ : param = b2
$@ : param = cc 
"$@" : param = aa 
"$@" : param = b1 b2
"$@" : param = cc

이를 활용해 특정 디렉터리를 현재 날짜가 들어간 파일명으로 백업하는 스크립트를 만들어본다. 첫 번째 스크립트는 이런저런 문제가 있다. 일단 실행해본다.

#!/bin/bash 
if [ $# -eq 0 ]; then
	echo "Usage: $0 <backup dir> [...]" 
    exit 1
fi

destdir=$1
basename=`basename $1`
		# back quotes로 외부 명령어를 불러오느 것은 좋지 않다. 
tarball=${basename}_$(date +%Y%m%d-%H%M%S).tar.gz
		# 역시 명령어를 직접 실행해서 치환하는 것은 좋지 않다. 
        # 해당 부분을 재사용하는 경우에 오류를 일으킬 수 있다.
for tdir in $*;		# 파일명에 공백이 있다면 문제가 된다
do
	if [ ! -d $tdir ]; then
		echo "ENOENT: $tdir"
		exit 1 
	fi 
done
tar cfa $tarball $* 
echo "New tarball: $tarball"

에러 메시지가 뜨며 실패했음을 알린다. 이들은 Permission denied로, 해당 오류는 stderr로 출력된다. 이를 터미널 창에 띄우지 않고 그냥 log 파일에 넘기도록 리다이렉션을 통해 수정해보겠다. 성능 문제 등으로 실제로 이렇게 많이 쓴다.

위 스크립트를 수정해본다.

#!/bin/bash 
if [ $# -eq 0 ]; then
	echo "Usage: $0 <backup dir> [...]" 
    exit 1
fi

destdir=$1
basename=$(basename $1)		# back quotes에서 변경
date_now=$(date +%Y%m%d-%H%M%S)		#변수에 저장하고 쓰는 것이 좋다.
tarball=${basename}_${date_now}.tar.gz		# 변수 불러오기
for tdir in "$@";		# 공백 문제 해결
do
	if [ ! -d $tdir ]; then
		echo "ENOENT: $tdir"
		exit 1 
	fi 
done
tar cfa $tarball $* 2> ${basename}_${date_now}_stderr.log
		# 에러 발생 시 ${basename}_${date_now}_stderr.log 파일을 만들어 에러 내용을 저장한다. 에러 메시지가 화면에 출력되지 않는다.
echo "New tarball: $tarball"

압축은 ${basename}_${date_now}.tar.gz에서 gz 대신 zst를 대신 쓰는 것이 더 좋다. 또한 tar cfa $tarball $* 2> ${basename}_${date_now}_stderr.log2> ${basename}_${date_now}_stderr.log tar c $* | zstd -T2 > $tarball로 더 보기 좋게 만들 수도 있다(-T2로 쓰레드를 2개로 제한했다). 리다이렉션 시 해당 로그를 보지 않고 무시하려면 2>/dev/null로 하면 된다.

arrow_upward arrow_downward
loading