[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) 방법이다. 그러나 변수는 프로그램이 종료될 때 사라지므로 잘 사용하진 않는다.
환경변수란 셸 변수에 상속 속성을 결정하는 것으로, 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 함수를 만들어 사용한다.
셸 변수의 scope는 프로세스의 life-cycle과 동일하다. 즉 프로세스가 종료되면 셸 변수의 메모리 공간도 해제된다. 만약 계속 유지하려면(permanency) rc
(run command, runtime configuration)나 profile에 변수의 값을 저장해야 한다. bash가 실행될 때마다 읽어들이는 파일로 .bashrc
, .bash_profile
에 넣는 것이다.
스크립트 파일은 관습적으로 *.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
제어 구문에는 두 가지가 있다. condition 분기 구문(if, case, select)과 loop/control 구문(while, for, until, break, continue)이다
🔥 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 "구문" ...
셸에서 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 [ condition ]; then
....
elif [ condition2 ]; then
....
else
....
fi
여러 개의 조건을 건다. 사용법은 다른 프로그래밍 언어와 같아 직관적이다. 예시 있음
🔥 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 $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 [ 조건절 ]
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로 나가도록 스크립트를 작성한다.
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 [ 조건절 ]
do
명령
done
while 구문과 반대된다. test 구문이 false인 경우에 루프를 실행한다.
iter=10
while [ $iter -lt 0 ]
do
echo $iter
iter=`expr $iter - 1`
done
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"
변수의 일부분이나 길이를 알아낼 때 사용한다.
🔥 ${#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