home search
OS
Linux
[Linux] Bash (3): 변수, 스크립트, 문법
2022. 02. 27

변수 (Variable)

셸 변수

🔥 [set] name=value

변수의 선언(declaration) 방법이다. var1=NewYork 식으로 쓴다. 대부분 앞에 붙는 set을 생략한다. name이라는 이름의 변수를 선언하는데, 앞서 이미 선언된 변수라면 값이 변경된다. 기본으로 값은 문자열 타입으로 저장되며, = 사이에 공백은 허용하지 않음에 주의한다. 또한 변수 이름이나 값에 공백이 들어간다면 따옴표, 쌍따옴표로 묶어줘야 한다. var1="hello world"처럼 말이다.

환경변수의 경우엔 자식 프로세스까지 상속되나, 셸에서 만드는 다른 일반 변수는 기본적으로 전역변수이다. 만약 지역변수를 만들고 싶다면 앞에 local을 붙여 local var_name[=value] 식으로 선언해야 한다. 지역 변수는 재귀함수나 function code block에서 사용하는 경우에만 쓰기 때문에 사용은 거의 안한다.

변수 생성 시 쓸 수 있는 문자를 아래와 같이 정리했다. 따옴표의 경우 둥근 따옴표를 입력하면 오류가 남에 주의한다.

🔥 ${name}

변수 값 사용(expression) 방법으로, echo $var1 처럼 쓰면 var1을 출력한다는 뜻이 된다. 변수 이름 안에 공백이 있는 경우와 같이 변수 이름을 명확히 구별할 수 없는 때를 제외하고는 중괄호를 생략한다.

🔥 unset <name\>

변수 삭제(removal) 방법이다. 그러나 변수는 프로그램이 종료될 때 사라지므로 잘 사용하진 않는다.

환경변수 (Environment Variable)

환경변수란 셸 변수에 상속 속성을 결정하는 것으로, fork하는 경우 자식 프로세스에 상속된다. 새로 시작하는 자식 프로세스에 설정(사용하고 있는 언어, 터미널 종류, 유저 이름 등)을 넘겨주는 용도로 주로 사용한다.

주로 환경변수는 이름으로 대문자를 사용한다.

🔥 export <변수 이름>=<값>

export 명령은 환경변수를 밖으로 ‘내보낸다’는 의미로 쓰인다. 아래 세 가지 선언은 동일한 명령이다. 그 중 2 번째가 가장 많이 쓰이는 형태이다.

$ MY_ENV_VAR="hello"; export MY_ENV_VAR
$ export MY_ENV_VAR="hello"
$ declare -x MY_ENV_VAR="hello"

🔥 export <변수1> <변수2> ...

일반 변수 선언 뒤 환경 변수로 전환할 수도 있다.

$ MY_ENV_VAR="hello"
$ MY_ENV_VAR2="world"
$ export MY_ENV_VAR MY_ENV_VAR2

환경변수도 변수의 하나이므로 사용은 일반변수처럼 $기호 뒤에 변수명을 표기해 사용한다.

빌트인 환경 변수 중 대표적인 것에는 HOME, PWD, PATH, TERM(사용하는 터미널의 종류), SHELL, USER, LANG(로케일 정보(명령어 출력 언어 및 문자세트 지정)), PS1(프롬프트1)가 있다. env(Environment) 명령어로 모든 환경변수를 볼 수 있다. 또는 export, declare -x도 볼 수 있다.

가끔 명령어가 매우 길 때가 있다. 이를 터미널에서 일일히 치면 힘들다. 이때 명령어를 에디터로 열어 작성하고 실행할 수 있는데, 그때 사용할 수 있는 것이 환경변수 EDITOR이다.

$ export EDITOR=vim

이렇게 에디터를 vim으로 바꾼 뒤, ^X ^E를 누르면 입력해두었던 에디터가 실행되며 명령행을 에디터에서 완성할 수 있다. 실제로 ls -al -r -t /usr/share을 타이핑한 뒤 저장하고 나오면 해당 명령이 실행되어 있다.

PATH 환경 변수는 명령어를 탐색하는 기능에 사용된다. 특정 명령을 실행시킬 때는 PATH에 입력되어 있는 경로를 앞에서부터 하나하나 찾아가며 해당 명령어가 있는지 체크한 뒤 있으면 그것을 실행하는 식이다.

만약 사용자가 PATH를 추가하려 ~/.bashrc 파일에 export PATH=~/development/sdk/bin:$PATH처럼 선언하면 안된다. .bashrc를 여러 번 import(source ~/.bashrc)하면 PATH에 중복 추가가 되는 등 지저분해진다. 실제로 PATH를 추가할 때는 pathmunge 함수를 만들어 사용한다.

변수의 life-cycle

셸 변수의 scope는 프로세스의 life-cycle과 동일하다. 즉 프로세스가 종료되면 셸 변수의 메모리 공간도 해제된다. 만약 계속 유지하려면(permanency) rc(run command, runtime configuration)나 profile에 변수의 값을 저장해야 한다. bash가 실행될 때마다 읽어들이는 파일로 .bashrc, .bash_profile에 넣는 것이다.


scripting

스크립트 파일 만들기

스크립트 파일은 관습적으로 *.sh 파일명을 가진다.

실행 셸의 선언부에 #! 시작하는 쉬뱅(shebang)을 적어준다. (해쉬 + 뱅 -> 쉬뱅) (혹은 env로 실행 가능하다.) 쉬뱅 뒤에 붙는 것은 해당 쉘을 실행시킨 바이너리의 위치이다. 아래는 세 가지 방식인데, 가장 첫 번쨰 방식이 가장 많이 쓰인다.

#! /bin/bash	# 가장 많이 쓰임
#! /usr/bin/env bash	# 바이너리를 탐색해서 쓰라. 딱히 추천하는 방법은 아니라 한다.
#! /usr/bin/env LANG=en_US.UTF-8 bash 	# 특정 환경변수도 설정하며 쓸 때

실행할 스크립트 부분은 선언부 뒤에 위치한다.

#! /bin/bash		# 실행 셸 선언

src_dir=$HOME		# 환경변수 HOME을 src_dir라는 변수에 넣음
echo -n "Your home directory: "
		# -n 옵션은 echo 실행 뒤에 다음 줄로 넘어가지 말란 의미
echo $src_dir

스크립트의 실행 방법은 아래와 같다.

🔥 sh <스크립트 파일>: 본 셸(POSIX 셸) 모드로 작동한다. 이렇게 절대! 쓰지 말아야 한다.

🔥 . <스크립트 파일>: .이나 source를 이용한다. 이들은 실행이 아닌 import용 명령어이다. 자식 프로세스를 만들지 않으므로 에러가 나는 등의 문제가 생기면 셸이 닫힐 수 있다. 따라서 이들은 모듈을 불러오거나 설정파일을 가져올 때만 사용하는 것이 좋다.

🔥 bash <스크립트 파일>: bash가 파일을 읽어서 실행하라는 뜻으로, 실행 권한이 필요 없다.

🔥 ./<스크립트 파일>: 먼저 실행 권한을 부여해주어야 쓸 수 있다.

$ sh myhome.sh
$ . myhome.sh
$ bash myhome.sh
$ ./myhome.sh
-bash: ./myhome.sh: Permission denied
		# x(실행) 권한이 없다는 뜻이므로 부여해줘야 함
$ chmod +x myhome.sh
$ ./myhome.sh

같은 이유로 /bin/bash/bin/sh는 다르다는 것을 알아두어야 한다. 전자와 후자는 같은 기능을 하지 않는다. sh로 실행하면 POSIX shell(본 셸)로 호환 모드로 작동한다. 이는 최소한의 기능만 가지고 있다. 따라서 #!에도 꼭 sh대신 bash를 사용해야 한다

스크립트와 변수의 개념을 이용해 OS 정보를 알아내는 실습을 해보겠다. 먼저, /etc/os-release를 읽어 변수 중 ID, VERSION, 커널 버전을 읽는 스크립트 osinfo.sh를 써본다.

#! /bin/bash

OS_RELEASE=/etc/os-release
OS_REL_ID=`grep '^ID=' $OS_RELEASE | cut -d '=' -f 2`
OS_REL_VER=`grep '^VERSION=' $OS_RELEASE | cut -d '=' -f 2`
OS_KERNEL_VER=`uname -r`
		# `uname -r`로 결과를 읽고 커널 버전을 변수에 저장
echo "$OS_REL_ID $OS_REL_VER | $OS_KERNEL_VER" 

스크립트를 실행해보겠다.

$ ./osinfo.sh
ubuntu "18.04.6 LTS (Bionic Beaver)" | 5.4.0-84-generic

따옴표가 들어가 있어 딱 원하는 결과는 아니다. 이를 제거해보자. 스크립트를 아래와 같이 수정한다.

(생략)
OS_REL_ID=`grep '^ID=' $OS_RELEASE | cut -d '=' -f 2 | tr -d '"'`
OS_REL_VER=`grep '^VERSION=' $OS_RELEASE | cut -d '=' -f 2 | tr -d '"'`
		# 특정 문자를 없애는 tr -d 명령
(생략)

다시 실행해보면 바른 결과가 나온다.

$ ./osinfo.sh
ubuntu 18.04.6 LTS (Bionic Beaver) | 5.4.0-84-generic

control statements

제어 구문에는 두 가지가 있다. condition 분기 구문(if, case, select)과 loop/control 구문(while, for, until, break, continue)이다

if 구문

if

🔥 if [ condition ]; then 실행구문 fi: condition(조건절) 앞뒤에 공백이 있어야 하며, condition은 test 실행기로 대체할 수 있다.

if [ $TERM != "xterm-256color" ]; then
    export TERM=xterm-256color
fi

아래처럼 -a, -o로 조건의 결합도 가능하다.

if [ "구문" [-a|-o] "구문" [-a|-o] .... ]; then
    shell_command
fi

if [ "구문" [-a|-o] "구문" [-a|-o] .... ]; then shell_command; fi

test

🔥 test "구문" ...

셸에서 if 구문은 test 구문의 built-in cmd이다. 아래에서 (A)와 (B)의 조건 부분은 같은 일을 한다. test의 결과인 반환값 $?를 검사해 0이면 true, 아니면 false이다.

(A) if [ "구문" [-a|-o] "구문" [-a|-o] .... ]; then shell_command; fi
(B) test "구문" [-a|-o] "구문" [-a|-o] ...

short circuit 방식을 이용해 test문을 대체할 수도 있다.

🔥 "구문1" && "구문2" : 구문1이 true여야 구문2를 수행한다. &&은 AND이다.

예시를 살펴보자.

if [ $USER != "root" ]; then
    echo "!root"		# !는 특수기호라 꼭 따옴표로 묶어야 함
fi

위 코드는 아래 (A), (B)와 같이 대체 가능하다.

(A) test $USER != "root" && echo "!root"
(B) [ $USER != "root" ] && echo "!root"

되도록이면 test 구문보다는 [] 들어간 것으로 쓰는 것이 좋다. 간혹 test를 쓰겠다고 /usr/bin/test $USER != "root" && echo "!root"로 쓰는 사람도 있으나, 정말 쓰면 안된다. full path를 쓰는 것은 외부 명령어를 가져다 쓴다는 것인데, bash의 test 구문은 성능이슈로 인해 자동으로 build-in cmd로 대치된다.

연산자

아래는 test 구문에서 쓸 수 있는 연산자이다.

좌측은 산술 연산, 우측은 문자열 연산이다.

= 양쪽에 공백이 없다면 대입 연산에 해당하여 성공을 반환할 수 있다. 따라서 문자열을 비교할 때는 ` = ` 대신 ==를 쓰는 것도 좋다. = 로 뒤에 공백이 있다면 명령으로 인식할 수도 있다.

아래는 파일 테스트 구문이다.

변수를 사용할 때는 변수에 값이 있는지 없는지를 먼저 검사하는 것이 좋다.

if [ -z $var2 ]; then
    (에러 처리 부분)
fi

위 예에서는 조건절에 non zero 연산자 -z가 있으므로 해당 변수에 값이 있다면 false라 에러 처리 부분은 넘어가게 된다. -z 변수과 같은 기능이나 옛날 방법은 "x${var2}" = "x" 을 조건식 안에 넣는 것이다. 저 변수를 넣어서 아무 것도 없으면 앞뒤가 똑같기 때문이다.

🔥 if [[ condition ]]; then ... fi

조건을 쓰는 방식으로 대괄호 두 개를 쓰는 것이 있다. 콘셸(ksh)에서 온 기능이다. 기존 test 구문에 추가적 기능을 탑재했다. 문자열의 크기 비교(lexicography)가 가능하여 escape가 필요 없다. base(진수)에 따라 숫자 비교(hex, octal도) 가능하며, 파일의 비교 연산의 추가 기능(newer, older 기능) 도 가능하다.

str1="abc"
str2="aba"
if [[ $str1 > $str2 ]]; then
	echo "$str1 > $str2"
fi

원래는 >는 앞의 출력을 뒤 파일에 저장하는 표준 출력 기능이었다. 그래서 비교에 쓰려면 escape가 필요했는데, [[ ]] 안에 쓰면 필요가 없어진다.

str1=0x30
str2=010
if [[ $str1 -gt $str2 ]]; then
	echo "compare base number: $str1 > $str2 (O)"
else
	echo "compare base number: $str1 > $str2 (X)"
fi

16진수와 8진수를 비교했다. 이렇게 진수에 관련된 기능은 네트워크 관련된 숫자나 표기(MAC 주소, bit mask 계산 등)에 유용히 쓰인다.

순차 다중 분기 if, elif

if [ condition ]; then
    ....
elif [ condition2 ]; then
    ....
else
    ....
fi

여러 개의 조건을 건다. 사용법은 다른 프로그래밍 언어와 같아 직관적이다. 예시 있음

short circuit evaluation

🔥 A && B : A가 진실이어야 B가 가능 🔥 A || B : A가 거짓이면 B가 가능 🔥 A && B || C : A가 진실이며 ㄴB를 아니면 C를 위에서 AND의 경우에는 보았다. 뿐만 아니라 OR 기능도 있고 이를 결합해 쓸 수도 있다. test 구문의 후속 실행을 쉬이 할 수 있다. 그러나 if then fi로 사용하는 것이 더 좋다.

$ num=55
$ [ $num -gt 100 ] && echo "num is greater than 100"
		# 조건에 부합하지 않아 아무것도 출력 안함

$ num=101
$ [ $num -gt 100 ] && echo "num is greater than 100"
num is greater than 100

$ num=33
$ [ $num -gt 100 ] && echo "num > 100" || echo "num <= 100"
num <= 100

case 구문

case $test_var in
value1)
    명령
    ;;
value2)
    명령
    ;;
*)
    default 명령
    ;;
esac

다른 프로그래밍 언어와 유사하다. ;;break의 기능을 한다.

case $answer in
	y*|Y*)
    	commands...
    	;;
	n*|N*)
    	commands...
    	;;
	*)
    	echo "잘못된 명령입니다"
    	;;
esac

위 예의 y*|Y*에서 *은 와일드카드로 작동하여, ‘yes’ 뿐만 아니라 ‘you’도 해당하도록 쓴 것이다.

유명한 함수로 pathmunge 예시도 있다. 환경변수 PATH에 경로를 추가할 때 자주 쓴다.

pathmunge () {
	[ ! -d "$1" ] && return 1
    case ":${PATH}:" in
		*:"$1":*)
        	;;
		*)
			if [ "$2" = "after" ] ; then
				PATH=$PATH:$1
			else
				PATH=$1:$PATH
			fi
	esac
}

해당 함수에 의하면 pathmunge ${HOME}/development/bin after를 하면 기존 PATH의 뒤에 ${HOME}/development/bin 경로가 추가된다.

while 구문

while [ 조건절 ]
do
	명령
done

test 구문이 true일 때 루프를 실행한다. 만약 무한루프를 돌고 싶다면 조건절 괄호 안에 1이라고 하거나, 괄호 없이 : 혹은 true를 쓸 수 있다. 특히 :는 null command라고 하여 0 혹은 true를 의미한다. 무한루프를 멈출 때는 break를 쓰면 된다.

(A) 무한루프 X
idx=10
while [ $idx -eq 0 ] ; do
	echo "idx = $idx"
	idx=`expr $idx - 1`
done

(B) 무한루프 & break
idx=10
while : ; do
	echo "idx = $idx"
    if [ $idx -eq 0 ]; then
		break
	fi
	idx=`expr $idx - 1`
done

사실 위 예시에서 expr $idx - 1 처럼 외부 명령어를 가져다 쓰는 것은 매우 좋지 않으나 일단은 예시로서 보고 있다.

실제 스크립트를 작성하는 예시를 하나 더 보자. 실행시 숫자를 하나 받아서 1~’숫자’의 그 합산을 출력하는데 입력된 숫자가 100이하라면 1~100까지의 합산을, 10000을 넘으면 ‘Argument value too big’을 출력하고 종료한다.

#! /bin/bash
if [ $# -lt 1 ]; then
	echo "Usage: $0 <number>"
    exit 1
fi

sum=0
iter=0
if [ $1 -gt 10000 ] ; then
	echo "[ERROR] Argument value too big == $1"
    exit 2
fi
loops=$1
while [ $iter -le $loops ]; 
do
	sum=`expr $iter + $sum`
    iter=`expr $iter + 1`
done
echo $sum

참고로, 스크립트를 시작할 때 주로 if [ $# -lt 1 ]; then ~~~ 부분을 넣어준다.$#는 인수의 개수를 의미하는데, 스크립트를 실행할 때 파일명.sh 인수1 인수2 ...로 했다면 해당 파일 명이 $0, 그 뒤부터 $1, $2…가 된다. (0은 제외하고 개수를 센다) 따라서 $# -lt 1란 인수의 개수가 1보다 작으면, 즉 ‘사용자가 인수를 입력하지 않았다면’의 뜻이 된다. 보통 이 경우 Usage를 보여주며 ‘이렇게 인수를 써 주세요’하는 메시지를 출력하고 0이 아닌 exit code로 나가도록 스크립트를 작성한다.

foreach 구문

for iter in val1 val2 ...
do
	명령
done

흔히 for문이라고 부르는 구문이다. ‘val1 val2…‘의 문자열 하나하나를 iter에 구분자로 잘라서 투입한다. 편의상 이리 표현하였고, 실제로는 긴 문자열을 IFS의 첫 번째 문자를 구분자로 사용해 토큰화(자르는 것)한다. IFS를 콤마로 변경하면 CSV도 처리할 수 있다고 한다. IFS의 디폴트는 space, HT, LF이다.

for fname in * ; do
	echo "file : $ $fname"
done

*ls *에서도 알 수 있듯이 현재 디렉터리의 모든 파일명을 가져와 리스트로 만든다. 이 요소들을 하나하나 ‘fname’에 꺼내 쓴다.

for names in Steven Sunny Mick ; do
	echo "Name: $names"
done

우리가 흔히 아는 형태로 숫자를 넣을 수도 있다. seq 라는 외부명령을 이용하는 것인데, 해당 명령은 seq 1 10하면 1부터 10까지를 반환한다.

for iter in `seq 시작 [증감] 끝`
do
	command
done

예시로는 아래와 같다.

for i in `seq 1 10`
do
	echo "sequence : $i"		# 1 2 3 ... 10을 차례대로 넣음
done

for i in `seq 1 10`
do
	echo "sequence : $i"		# 1 3 5 7 9를 차례대로 넣음
done

until

until [ 조건절 ]
do
	명령
done

while 구문과 반대된다. test 구문이 false인 경우에 루프를 실행한다.

iter=10
while [ $iter -lt 0 ]
do
	echo $iter
	iter=`expr $iter - 1`
done

select

select menu in [ lists.. ]
do
	title...
done

메뉴를 선택하는 기능으로 사용하는 구문이다. 본래는 ksh(콘셸)에 있던 것으로, 주로 셸 스크립트로 작동할 때 interactive 모드로 작동해야 하는 것들에 쓰인다. 셸 스크립트 자체가 빠르게 실행하기 위함이므로 사용자가에 무언가를 묻고 그를 실행하는 select는 사실 잘 쓰이진 않는다.

PS3="Which color do you like? (choose number) >>"
select userinput in "Blue" "Red" "Yellow" "White" "Exit"
do
	if [ $userinput = "Exit" ]; then
		break
	fi
	echo "Your favorite color : $REPLY) $userinput"
done
echo "+ End of Question"

offset, length

변수의 일부분이나 길이를 알아낼 때 사용한다.

🔥 ${#name}: 변수의 길이

🔥 ${name:offset}: 변수의 offset 위치부터 출력 (숫자는 0부터 시작)

🔥 ${name:offset:len}: 변수의 offset 위치부터 len 만큼 출력. len이 음수일 떈 뒤에서부터 계산

$ name=lumos
$ echo ${#name}
5
$ echo ${name:2}
mos
$ echo ${name:2:2}
mo
$ echo ${name:0:-2}
lum
arrow_upward arrow_downward
loading