Compound Commands
프로그래밍 언어에서 expression 은 실행됐을 때 value 를 반환합니다.
그래서 &&
, ||
연산자로 연결해 사용할 수 있고 변수에 값을 대입할 수도 있습니다.
그 외에 if else, for, while 같은 문은 따로 값이 반환되는 것이 아니고
코드 실행의 역할을 하기 때문에 statement 라고 하고 expression 처럼 사용될 수 없습니다.
이런 점에서 본다면 shell 의 command line 은
&&
, ||
로 연결할 수도 있고 $( )
로 출력을 변수에 대입할 수도 있기 때문에
하나의 expression 으로 볼 수 있습니다.
또한 shell 에서는 if, for, while 문도 마지막에 실행된 명령의 종료 상태 값이
반환되기 때문에 하나의 command 처럼 사용될 수 있습니다.
따라서 shell 에서는 이것을 statement 라고 하지 않고 compound command 라고 합니다.
뒤에서 알아보겠지만 compound command 는 일반 command 와 동일하게 redirection 이나 파이프를 붙여 사용할 수도 있습니다. shell 에서는 모두가 종료 상태 값이 반환되는 expression 에 해당합니다.
# if 문을 || 연산자와 연결해 사용할 수 있다.
# if 문에서 마지막으로 실행된 false 명령의 종료 상태 값이 사용된다.
$ if test 1 = 1; then false; fi || echo 123
123
# for 문에서 마지막으로 실행된 명령은 true 명령이므로 echo 123 이 실행된다.
$ for name in foo; do true; done && echo 123
123
# if 문의 출력을 var 변수에 대입할 수 있다.
$ var=$( if test 1 = 1; then echo 123; fi )
$ echo $var
123
shell 에서 명령들을 구성할 수 있는 방법을 분류해 보면 다음과 같습니다
1 . 한줄에 하나씩
command1
command2
command3
...
2 . ;
메타문자를 이용하면 한줄에 여러 명령을 놓을 수 있다
command1; command2; command3 ...
여기서 ;
메타문자는 newline 과 같은 역할을 합니다. 1, 2 번은 각 명령들이 순서대로 실행이 됩니다. command1 이 종료돼야 그다음에 command2 가 실행되고 command2 가 실행을 종료해야 다음에 command3 이 실행됩니다.
3 . 파이프를 이용해 여러명령을 동시에 실행
command1 | command2 | command3
파이프에 연결된 명령들은 동시에 실행 됩니다. command1 의 stdout 출력이 command2 의 stdin 입력으로 들어가고 command2 의 stdout 출력이 command3 의 stdin 입력으로 들어가고 최종적으로 command3 의 stdout 출력이 터미널로 표시됩니다.
4 . &&, || 메타문자를 이용한 간단한 조건부 실행
command1 && command2
&&
메타문자는 command1 의 실행이 정상 종료하면 command2 를 실행하고 command1 이 오류로 종료할 경우는 command2 를 실행하지 않습니다.
command1 || command2
||
메타문자는 command1 이 오류로 종료할 경우 command2 를 실행합니다. command1 이 정상 종료하면 command2 는 실행되지 않습니다.
A && {
B && {
echo "A and B both passed"
} || {
echo "A passed, B failed"
}
} || echo "A failed"
5 . { ... ;} , ( ... ) 을 이용한 명령 grouping
여러 명령들을 하나의 group 으로 만들어 사용할 수 있습니다. 대표적인게 function 인데요. 이렇게 group 을 만들면 안에 있는 명령들이 실행시에 같은 context 를 가집니다.
6 . Shell keyword 를 이용한 복합 명령 구성
위에서 알아본 방법만 가지고는 실용적인 스크립트를 구성할 수 없습니다. 그래서 shell 에서는 프로그래밍 언어에서처럼 조건분기 및 반복기능을 구성할수 있게 keyword 를 제공합니다.
Compound Commands
compgen -k | column
명령으로 볼 수 있는 대부분의 shell 키워드들이 복합 명령 구성을 하는데 사용됩니다. 이 키워드를 이용하면 shell 에서도 if else 문과 for, while 문을 만들 수가 있습니다. 키워드는 명령 이름이 위치하는 곳에서 사용되며 인수 부분에서 사용될 경우 키워드로 기능하지 않습니다. 이 키워드들은 사용방법에 있어 다음과 같은 특징이 있습니다.
키워드로 시작해서 키워드로 끝난다.
테이블을 보면 loop 문은 전부 done 키워드로 끝나는걸 알 수 있습니다.
구분 | 구성 |
---|---|
if 문 | if ... fi |
case 문 | case ... esac |
select 문 | select ... done |
while 문 | while ... done |
until 문 | until ... done |
for 문 | for ... done |
참고로 쉘 스크립트를 처음 접하시는 분들은 if ... fi
, case ... esac
와 같은 구문이 생소할 수 있는데 이와 같은 구성은 ALGOL 68 언어 에서 찾아볼 수 있습니다.
bash
( bourne again shell ) 는 sh
( bourne shell ) 의 업그레이드 버전이라고 할 수 있는데
sh
을 만들었던 steve bourne 이라는 사람이 ALGOL68 언어에 관여했다고 합니다.
열고 닫는 키워드에 redirection, |, & 를 붙여 사용할 수 있다.
열고 닫는 키워드에 redirection, 파이프, & 를 붙이면 안에 있는 전체 명령에 적용됩니다.
command ... | if read line; test "$line" = foo # '|' 파이프
then
echo "$line"
else
...
fi >&3 # >&3
----------------------------------------------------------------
var=1
case $var in
1)
read line
echo "$line"
;;
*)
cat ;;
esac < infile > outfile
---------------------------------
for (( i=10; i<20; i++ )); do
read line
echo "$line"
done < infile > outfile
---------------------------------
while read -r line; do
echo "${line//bananna/banana}"
done < infile > outfile
종료 상태 값
compound command 는 테스트를 통과하여 진입하지 못했을 경우 기본적으로
종료 상태 값이 모두 0
입니다.
그 외는 각 해당 블록에서 실행된 마지막 명령의 종료 상태 값이 사용됩니다.
# 테스트를 통과하여 진입하지 못했을 때는 종료 상태 값이 '0' 이다
$ if test 1 = 0; then false; fi
$ echo $?
0
$ while test 1 = 0; do false; done
$ echo $?
0
$ set --
$ for i; do false; done
$ echo $?
0
$ AA=100
$ case $AA in (200) false; esac
$ echo $?
0
--------------------------------
# 진입에 성공하였을 경우는 마지막에 실행된 명령의 종료 상태 값이 사용된다.
$ if test 1 = 1; then false; fi
$ echo $?
1
$ AA=100
$ while test $AA = 100; do AA=200; false; done
$ echo $?
1
$ set -- 100
$ for AA; do false; done
$ echo $?
1
$ AA=200
$ case $AA in (200) false; esac
$ echo $?
1
Conditional Constructs
if
if test-commands; then
consequent-commands
[ elif more-test-commands; then
more-consequents ]
[ else alternate-consequents ]
fi
- if 문은
if
,elif
,else
,fi
그리고then
키워드를 사용합니다. - 키워드 다음에 명령이 오는 순서입니다. if 다음에 명령; then 다음에 명령; ... 마지막엔 fi 로 닫습니다.
- if 나 elif 다음에 오는 명령은
[ ]
,[[ ]]
을 이용하는 것 외에도 어떤 명령이나 group 도 사용할 수 있습니다. !
키워드를 이용하면 logical NOT 연산을 할 수 있습니다.
if ! mount /mnt/backup &> /dev/null; then
echo "FATAL: backup mount failed" >&2
exit 1
fi
-----------------------------------------
if grep -q '2014-07-20' data.txt; then
...
fi
case
case word in
[ [ ( ] pattern [ | pattern ]...) command-list ;; ]...
esac
shell script 에서 가장 유용한 기능중 하나가 case 문이 아닐까 하는데요. 각 case 를 패턴을 이용하여 매칭할수 있다는 것은 아주 편리한 기능이라고 생각합니다. case 문에서 사용된 pattern 들은 위에서 아래로 쓰여진 순서에 따라 매칭을 하며 처음 매칭된 패턴이 실행이 되고 이후에 중복되는 매칭에 대해서는 실행이 되지 않습니다.
- case 문은
case
,in
,esac
키워드와 각 case 를 나타내는pattern)
을 사용합니다. - pattern) 의 종료 문자는
;;
를 사용합니다. - pattern) 에서는
|
을 구분자로 하여 여러개의 패턴을 사용할 수 있습니다. - word, pattern 에 공백을 포함하려면 quote 을 하거나
foo\ bar
와같이 escape 합니다. - word, pattern 에서 변수를 사용할 경우
[[ ]]
에서 처럼 globbing, 단어분리가 발생하지 않으므로 quote 하지 않아도 됩니다. *)
는 모든 매칭을 뜻하므로 default 값으로 사용할 수 있습니다.shopt -s nocasematch
옵션을 사용하면 대, 소문자 구분없이 매칭할 수 있습니다.
read -p "Enter the name of an animal: " ANIMAL
echo -n "The $ANIMAL has "
case $ANIMAL in
horse | dog | cat)
echo -n 4
;;
man | kangaroo)
echo -n 2
;;
*)
echo -n "an unknown number of"
;;
esac
echo " legs."
-----------------------------------------------
# 패턴 문자를 일반 문자로 사용하려면 다음과 같이 escape 합니다.
case $var in
\?) ...
\*) ...
\|) ...
esac
sh
에서는 변수값을 비교할 때 test 명령의 경우 문자 그대로 스트링 매칭을 하므로
패턴 매칭을 할 수가 없는데요.
case 문을 활용하면 가능합니다.
sh$ AA=y # if then ... 에 해당
sh$ case $AA in ([Yy]) echo YES; esac
YES
sh$ AA=z # if then ... else ... 에 해당
sh$ case $AA in ([Yy]) echo YES;; (*) echo NO; esac
NO
bash 의 경우 pattern) 종료 문자로 ;;
대신에 ;&
( fall-through ) 와 ;;&
( next-matching )
을 사용할 수 있습니다.
;&
는 다음 패턴이 매칭이 되던 상관없이 무조건 실행하고
;;&
는 매칭이 되는 다음 패턴을 찾아 실행합니다.
#!/bin/bash #!/bin/bash
case AAA in case ACD in
AAA) *A*)
echo AAA echo AAA
;& # fall-through ;;& # next-matching
BBB) *X*)
echo BBB echo XXX
;& # fall-through ;;&
CCC) *C*)
echo CCC echo CCC
;; ;;& # next-matching
DDD) *D*)
echo DDD echo DDD
esac esac
---------------- -------------------
# 실행 결과 # 실행 결과
AAA AAA
BBB CCC
CCC DDD
select
select name [ in words ...]
do
commands
done
- select 문은
select
,in
,do
,done
키워드를 사용합니다. - PS3 변수는 프롬프트를 설정하는데 사용됩니다.
- REPLY 변수에는 입력한 값이 저장됩니다.
in words
부분을 생략하면in "$@"
와 같게됩니다.- 값을 입력하지 않고 enter 를 하면 다시 목록을 표시합니다.
- Ctrl-d 는 select loop 를 종료하고 다음 명령으로 진행합니다.
- select 는 반복해서 입력을 받으므로 선택을 완료하고 다음으로 진행하기 위해서는 break 명령으로 종료해야 합니다.
----------------- 공통부분 ------------------
AA=( "horse" "dog" "cat" "man" "kangaroo" )
PS3="Enter number: "
----------------- test1.sh ------------------
select ANIMAL in "${AA[@]}"
do
echo "You picked number $REPLY : $ANIMAL"
done
----------------- test2.sh ------------------
AA+=( exit )
select ANIMAL in "${AA[@]}"
do
case $ANIMAL in
horse | dog | cat)
number_of_legs=4
;;
man | kangaroo)
number_of_legs=2
;;
exit)
exit
;;
*)
echo Not available
break
esac
echo "You picked number $REPLY"
echo "The $ANIMAL has $number_of_legs legs."
done
Looping Constructs
shell 에서도 프로그래밍 언어에서처럼 반복문을 제공합니다. 하지만 차이가 있는데요. shell 은 명령을 다루기 때문에 만약에 반복을 10,000 번을 하게 되면 명령 실행을 위해 OS 가 프로세스를 적어도 10,000 번을 생성해야 됩니다. 따라서 프로그래밍 언어에서 사용하는 loop 문과는 성능 면에서 큰 차이가 있게 됩니다. 다음은 20 개의 random 문자를 갖는 스트링 10,000 개를 /dev/urandom 로 부터 추출하여 파일에 저장하는 작업인데 shell 의 반복문과 awk 반복문의 수행 시간을 비교해 보면 큰 차이가 나는 것을 알 수 있습니다.
# shell 에서의 loop 는 10,000 번 프로세스가 생성돼야 한다.
$ time tr -dc '[:graph:]' < /dev/urandom |
for (( i=0; i<10000; i++)) do head -c 20; echo; done > x1
real 0m29.273s # 약 30 초
user 0m16.639s
sys 0m15.361s
# awk 단일 프로세스 에서의 loop
$ time tr -dc '[:graph:]' < /dev/urandom |
awk -v RS='.{20}' '{ print RT } NR==10000{ exit }' > x2
real 0m0.020s # 1 초도 안 걸린다.
user 0m0.027s
sys 0m0.007s
$ ls -l x1 x2
-rw-rw-r-- 1 mug896 mug896 210000 2018-06-26 17:10 x1
-rw-rw-r-- 1 mug896 mug896 210000 2018-06-26 17:10 x2
while
while test-commands
do
consequent-commands
done
- while 문은
while
,do
,done
키워드를 사용합니다. - 테스트되는 명령이 정상 종료하면 계속해서 반복합니다.
while 과 read 명령을 이용해 파일을 읽어들일때 아래와 같이 파일을 read 명령에 연결하면 안됩니다. 왜냐하면 read -r line < infile
명령이 실행되고 나면 infile 과 연결이 close 되기 때문입니다. 그래서 다음 반복때 실행되면 다시 파일을 open 해서 읽어 들이므로 계속해서 첫라인만 표시되게 됩니다.
while read -r line < infile
do
echo "$line"
done
########## output ##########
Fred:apples:20:June:4
Fred:apples:20:June:4
Fred:apples:20:June:4
...
...
그러므로 파일을 read 명령이 아니라 while 이나 done 키워드에 연결 시켜야 합니다.
done 키워드에 파일을 연결하면 while 문이 종료될때까지 open 되어 사용되며 라인을 읽어들일때 마다 파일 포지션이 다음 라인으로 이동합니다.
while read -r line
do
echo "$line"
done < infile
while 키워드에 |
파이프로 연결한 경우
cat infile | while read -r line
do
echo "$line"
done
until
until test-commands
do
consequent-commands
done
- until 문은
until
,do
,done
키워드를 사용합니다. - 테스트되는 명령이 오류로 종료하면 계속해서 반복합니다.
(until ...
은while ! ...
과 같습니다. )
read -p "Enter Hostname: " hostname
until ping -c 1 "$hostname" > /dev/null
do
sleep 60;
done
curl -O "$hostname/mydata.txt"
for
for name [ in words...]
do
commands
done
- for 문은
for
,in
,do
,done
키워드를 사용합니다. - words 개수만큼 반복하고 매 반복 때마다 name 값이 설정됩니다.
in words
부분을 생략하면in "$@"
와 같게됩니다.
$ set -f; IFS=$'\n'
$ for file in $(find -type f)
do
echo "$file"
done
./WriteObject.java
./WriteObject.class
./ReadObject.java
./2013-03-19 154412.csv
./ReadObject.class
./쉘 스크립트 테스팅.txt
$ set +f; IFS=$' \t\n'
for (( expr1 ; expr2 ; expr3 ))
do
commands
done
(( ))
는 bash 에서 제공하는 산술연산 특수 표현식 입니다.
프로그래밍 언어에서 for 문처럼 사용할 수 있습니다.
$ for (( i = 0; i <= 5; i++ )) {
echo $i;
}
0
1
2
3
4
5
--------------------------------
$ for ((i=-50; i<50; i++)); do
echo "set yrange [-50:50]; plot $i * sin(x) lw 3"
sleep 0.1
done | gnuplot -persist
-------------------------------
$ for ((i=-50; i<50; i++)) do
echo "set isosample 100;
spl [:] [:] [-50:50] $i * sin(x) * cos(y) with pm3d"
done | gnuplot -persist
------------------------------
$ awk 'BEGIN{
for(i=0.0001; i<0.1; i+=0.0005)
printf "set isosample 100; \
spl [:] [:] [-1:1] sin(%g * (x**2 + y**2)) with pm3d\n", i;
}' | gnuplot -persist