Compound Commands

프로그래밍 언어에서 expression 은 실행됐을 때 value 를 반환합니다. 그래서 변수에 값을 대입할 수 있고 && , || 연산자로 연결해 사용할 수도 있습니다. 그 외에 if else, for, while 같은 문은 따로 값이 반환되는 것이 아니고 코드 실행의 역할을 하기 때문에 statement 라고 하고 expression 처럼 사용될 수 없습니다.

이런 점에서 본다면 shell 의 command line 은 &&, || 로 연결할 수도 있고 $( ) 로 출력을 변수에 대입할 수도 있기 때문에 하나의 expression 으로 볼 수 있습니다. 또한 shell 에서는 if, case, for, while 문도 마지막에 실행된 명령의 종료 상태 값이 반환되기 때문에 하나의 command 처럼 사용될 수 있습니다. 뒤에서 알아보겠지만 compound commands 는 일반 command 와 동일하게 파이프나 redirection 을 붙여 사용할 수도 있습니다.

# if 문을 || 연산자와 연결해 사용할 수 있다.
# if 문에서 마지막으로 실행된 false 명령의 종료 상태 값이 사용된다.
$ if test 1 = 1; then false; fi || echo 123
123

# cd 명령이 성공할 경우 뒤의 while 문이 실행된다.
$ cd /mnt/backup && while true; do echo 123; break; done
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 는 실행되지 않습니다.

# 시스템에 ifconfig 명령이 존재하지 않으면 ip 명령을 사용하게 된다.
{ ifconfig || ip link show up ;} 2> /dev/null | awk ...

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

원래 unix 초기에는 이와 같은 프로그래밍 언어에서나 볼 수 있는 언어 구성이 없었습니다. if 가 명령으로 하나 존재하는 정도였는데요. 참고로 쉘 스크립트를 처음 접하시는 분들은 if ... fi, case ... esac 와 같은 구문이 생소할 수 있는데 이와 같은 구성은 ALGOL 68 언어 에서 찾아볼 수 있습니다. bash ( bourne again shell ) 는 sh ( bourne shell ) 의 업그레이드 버전이라고 할 수 있는데 sh 을 만들었던 steve bourne 이라는 사람이 당시 ALGOL68 언어에 관여했다고 합니다.

열고 닫는 키워드에 redirection, |, & 를 붙여 사용할 수 있다.

열고 닫는 키워드에 redirection, 파이프, & 를 붙이면 안에 있는 전체 명령에 적용됩니다.

command1 | if command2; then        # command1 명령의 출력값이
. . .                               # if 문 안에서 실행되는 명령들의 stdin 으로 연결되고
fi > outfile                        # stdout 출력은 outfile 로 쓰여진다.
------------------------------

command1 | case $var in             # command1 명령의 출력값이
. . .                               # case 문 안에서 실행되는 명령들의 stdin 으로 연결되고
esac > outfile                      # stdout 출력은 outfile 로 쓰여진다.
------------------------------

command1 | while read line; do
. . .
done >&2     # do ~ done 에서 실행되는 명령들의 stdout 출력을 stderr 로 redirect.
--------------------------------------------------------------

if command1; then
    command2 | sed 's/aaa/bbb/g' | sed '/^#define /d' > outfile   # 중복코드
else
    command2 | sed 's/xxx/yyy/g' | sed '/^#define /d' > outfile   # 중복코드
fi

if command1; then
    command2 | sed 's/aaa/bbb/g'   # 파이프 명령 라인에서 사용되는 중복되는 코드를
else
    command2 | sed 's/xxx/yyy/g'
fi |                               
    sed '/^#define /d' > outfile   # fi | 를 이용해 밖으로 뺄 수 있다.

----------------------------------------------------------------------------

$gcc -g $comp -o $tmpfile "$file" "$@" &&           # $gcc 명령이 성공하면 &&
if test -n "$func"; then                            # test 조건에 따라
    $objdump -wCS $tmpfile | sed -n '/....../p'     # (1) 번 명령문 이나
else
    $objdump -wCS $tmpfile                          # (2) 번 명령문을 실행.
fi

if test -n "$llvm"; then                            # test 조건에 따라
    objdump=llvm-objdump
    clang -g -c -o $tmpfile "$file" "$@"            # (1) 번 명령문 이나
else
    $gcc -g $comp -o $tmpfile "$file" "$@"          # (2) 번 명령문을 실행하고
fi &&                                               # 실행이 성공할 경우 fi &&
if test -n "$func"; then                            # test 조건에 따라
    $objdump -wCS $tmpfile | sed -n '/....../p'     # (3) 번 명령문 이나
else
    $objdump -wCS $tmpfile                          # (4) 번 명령문을 실행.
fi

종료 상태 값

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

진입에 성공하였을 경우는 마지막에 실행된 명령의 종료 상태 값이 사용됩니다.

반복문이 break 명령에 의해 종료될 경우는 종료 상태 값은 0 이 됩니다.

$ if test 1 = 1; then false; fi 
$ echo $?
1

# test 명령의 "unary operator expected" 오류의 종료 상태 값은 2 이지만 false 명령의 값이 사용된다.
$ AA=100
$ while test $AA = 100; do AA=; false; done
bash: test: =: unary operator expected
$ echo $?
1            # false 의 종료상태값

$ while test 100 = 100; do false; break; done   # 반복문이 break 에의해 종료될 경우
$ echo $?                                       # 종료 상태 값은 0 이다.
0

$ set -- 100
$ for ARG; do false; done
$ echo $?
1

$ AA=200
$ case $AA in (200) false; esac
$ echo $?
1

예제 )

다음 예제의 경우 num 변수 값이 100 일 경우는 if 블록에 진입하지 못하므로 블록 기본값인 0 이 적용되어 바로 run 함수가 실행됩니다. 그 외의 값에 대해서는 if 블록에 진입이 되어 check 함수가 실행되고 사용자에게 yes, no 를 묻게 됩니다. yes 를 선택했을 경우 run 함수가 실행되고 no 일 경우는 run 함수가 실행되지 않게 됩니다.

run() { ... ;}

check() {
    echo -n "do you want proceed (y/n)? "
    while read ans; do
        echo
        case $ans in
            [Yy]) return 0 ;;
            [Nn]) return 1 ;;
        esac
    done
}

num=$( ... )

if [ "$num" -ne 100 ]; then check; fi && run
--------------------------------------------

# 위 마지막 문장은 다음과 같은것 입니다.
if [ "$num" -ne 100 ]; then
    check && run
else 
    run
fi

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 로 닫습니다.
  • ifelif 다음에 오는 명령은 [ ] , [[ ]] 을 사용하는 것 외에도 어떤 명령이나 group 도 사용할 수 있습니다.
  • ! 키워드를 이용하면 logical NOT 연산을 할 수 있습니다.
if ! mount /mnt/backup &> /dev/null; then
  echo "FATAL: mount backup failed" >&2
  exit 1
fi

-----------------------------------------

if grep -q '^2014-07-20' data.txt; then
    ...
fi

if 문에 else 가 사용되지 않으면 다음 두 명령문은 실질적으로 같게 됩니다.

if test 1 = 1; then                  test 1 = 1 && {
    echo "yes"                           echo "yes"
fi                                   }

-----------------------------------------------------

if ! test 1 = 2; then                ! test 1 = 2 && {
    echo "yes"                           echo "yes"
fi                                   }

아래 두 명령문은 동일한 기능을 하는 것처럼 보이는데요. [ 1 = 1 ] 가 참이면 command1 이 실행되고 거짓이면 command2 가 실행됩니다. 하지만 조금 차이가 있습니다. 두 번째의 경우에는 [ 1 = 1 ] 가 참이어서 command1 이 실행될 경우 만약에 command1 이 오류로 종료하게 되면 command2 도 실행되게 됩니다.

if [ 1 = 1 ]; then                   # [ 1 = 1 ] 가 참이고 command1 이 오류로
    command1 ...                     # 종료하면 command2 도 실행이 된다.
else
    command2 ...                     [ 1 = 1 ] && command1 ... || command2 ...
fi

------------------------------------------------
$ AA=100

$ if [ 1 = 1 ]; then AA=`date -@`; else AA=300; fi
date: invalid option -- '@'
. . .
$ echo $AA
$

$ [ 1 = 1 ] && AA=`date -@` || AA=300
date: invalid option -- '@'
. . .
$ echo $AA
300

case

case word in
      [ [ ( ] pattern [ | pattern ]...) command-list ;; ]...
esac

shell 에서 제일 유용한 기능중 하나가 case 문이 아닐까 생각하는데요. 각 case 를 패턴을 이용하여 매칭할수 있다는 것은 아주 편리한 기능이라고 생각합니다. case 문에서 사용된 pattern 들은 위에서 아래로 쓰여진 순서에 따라 매칭을 하며 처음 매칭된 패턴이 실행이 되고 이후에 중복되는 매칭에 대해서는 실행이 되지 않습니다.

  • case 문은 case, in, esac 키워드와 각 case 를 나타내는 pattern) or (pattern) 을 사용합니다.
  • pattern) 의 종료 문자는 ;; 를 사용하고 패턴에 사용되는 glob 문자를 일반 문자로 사용하려면 escape 하거나 quote 해야합니다.
  • pattern) 에서는 | 을 구분자로 하여 여러개의 값을 사용할 수 있는데 이때 값의 양쪽 끝에 있는 공백은 사용되지 않으므로 확장패턴 에서처럼 서로 붙여쓰지 않아도 됩니다. | 구분자를 포함하는 변수를 pattern 에 직접 사용할수는 없는데 이때는 @(...) 확장패턴을 사용하면 가능합니다.
  • word, pattern 에 공백을 포함하려면 quote 을 하거나 foo\ bar 와같이 escape 하면됩니다.
  • word, pattern 에서 변수나 명령치환을 사용할 경우 [[ ]] 에서 처럼 단어분리나 globbing 이 발생하지 않으므로 quote 하지 않아도 됩니다.
  • word 에 $@, $* 또는 array 가 사용될 경우 전체 원소가 하나의 값으로 사용됩니다.
  • *) 는 모든 매칭을 뜻하므로 default 값으로 사용할 수 있습니다.
  • 대, 소문자 구분없이 매칭하려면 case ${foo,,} in ... 매개변수 확장을 이용하거나 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."
-----------------------------------------------

DIALOG_OK=0
DIALOG_CANCEL=1
DIALOG_HELP=2

return_value=$DIALOG_CANCEL

case $return_value in
  $DIALOG_OK)
    echo "OK pressed.";;
  $DIALOG_CANCEL)
    echo "Cancel pressed.";;
  $DIALOG_HELP)
    echo "Help pressed.";;
esac
--------------------------------------------------------------

# 패턴에 사용되는 glob 문자를 일반 문자로 사용하려면 quote 하거나 escape 합니다.
case $var in                    case $var in
    \?) ...                         '?') ...
    \*) ...                         '*') ...
    \|) ...                         '|') ...
esac                            esac
----------------------------------------------

pat="foo|bar|zoo"
val=bar

case $val in
$pat) echo yes ;;        # '|' 구분자를 포함하는 변수를 패턴에 직접 사용할수는 없습니다.
esac

case $val in
@($pat)) echo yes ;;     # 이때는 @(...) 확장패턴을 사용해야 합니다.
esac                     # shopt -s extglob
yes
------------------------------------

arr=(11 22 33 44 55)

case ${arr[@]} in               # array 는 전체 원소가 하나의 값으로 사용됩니다.
    *"22 33"*) echo yes;;
esac

실행결과: yes

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 ) 을 사용할 수 있습니다. ;& 는 다음 패턴이 매칭이 되던 상관없이 무조건 실행하고 ;;& 는 매칭이 되는 다음 패턴을 찾아 실행합니다. 따라서 ;;& 를 사용할 땐 디폴트 매칭인 *) 를 사용하기 어렵겠죠 ( 항상 매칭이 되므로 )

;;&( 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
        ;;                                       ;;&
    CCC)                                     *C*)
        echo CCC                                 echo CCC
        ;;                                       ;;&       # next-matching
    *)                                       *D*)
        echo DEFAULT                             echo DDD
esac                                     esac
----------------                         -------------------

# 실행 결과                              # 실행 결과
AAA                                      AAA
BBB                                      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 명령으로 종료해야 합니다.
#!/bin/bash

PS3=$'\n'"[ exit \"x\" ] 번호입력: "

while true; do

clear
echo "번호를 선택해 주세요."
echo

AA=( "horse" "dog" "cat" "man" "kangaroo" )

select ANIMAL in "${AA[@]}"
do
    case $ANIMAL in
        horse | dog | cat) 
            number_of_legs=4 ;;
        man | kangaroo) 
            number_of_legs=2 ;;
        *) 
            [ "$REPLY" = "x" ] && break 2
            echo -e "\nNot available\n"
            read -n1; clear; break
    esac

    echo
    echo "You picked number $REPLY"
    echo "The $ANIMAL has $number_of_legs legs."
    echo

    read -n1; break
done   # select

done   # while

echo
echo "종료되었습니다."

--------------------------------------

번호를 선택해 주세요.

1) horse
2) dog
3) cat
4) man
5) kangaroo

[ exit "x" ] 번호입력: 2

You picked number 2
The dog has 4 legs.

Looping Constructs

shell 에서도 프로그래밍 언어에서처럼 반복문을 제공합니다. 하지만 차이가 있는데요. shell 은 명령을 다루기 때문에 만약에 반복을 10,000 번을 하게 되면 명령 실행을 위해 OS 가 프로세스를 적어도 10,000 번을 생성해야 됩니다. 따라서 프로그래밍 언어에서 사용하는 loop 문과는 속도 면에서 큰 차이가 있게 됩니다. 다음은 20 개의 random 문자를 갖는 스트링 10,000 개를 /dev/urandom 로 부터 추출하여 파일에 저장하는 작업인데 shell 의 반복문과 awk 단일 프로세스 에서 반복문의 실행 시간을 비교해 보면 큰 차이가 나는 것을 볼 수 있습니다.

# shell 에서의 loop 는 10,000 번 head 프로세스가 생성돼야 한다.
$ time tr -dc '[:graph:]' < /dev/urandom | 
    for (( i=0; i<10000; i++)) do head -c 20; echo; done > x1

real    0m19.095s    # 약 20 초
user    0m11.640s
sys     0m8.586s

# awk 단일 프로세스 에서의 loop
$ time tr -dc '[:graph:]' < /dev/urandom | 
    awk -v RS='.{20}' '{ print RT } NR==10000{ exit }' > x2

real    0m0.017s     # 1 초도 안 걸린다.
user    0m0.018s
sys     0m0.012s

$ ls -l x1 x2
-rw-rw-r-- 1 mug896 mug896 210000 2020-04-08 13:17 x1
-rw-rw-r-- 1 mug896 mug896 210000 2020-04-08 13:17 x2

다음은 /usr/bin 디렉토리 에서 각각의 파일 사이즈를 stat 명령으로 구해 총합을 계산하는 예입니다.

# find 명령의 {} \; 는 파일 개수만큼 stat 명령이 실행돼야 한다.
$ time find /usr/bin -type f -exec stat -c %s {} \; | awk '{sum+=$1} END{ print sum}'
1296011570

real    0m5.523s     # 약 5 초
user    0m3.043s
sys     0m2.680s

# find 명령의 {} + 는 stat 명령이 한번만 실행된다.
$ time find /usr/bin -type f -exec stat -c %s {} + | awk '{sum+=$1} END{ print sum}'
1296011570

real    0m0.024s     # 1 초도 안 걸린다.
user    0m0.017s
sys     0m0.015s

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 -q -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 "$@" 와 같게 됩니다.
$ for v in 100 200 300                       $ for v in 1.{0..5} 
do                                           do
    echo $v                                      echo $v
done                                         done

100                                          1.0
200                                          1.1
300                                          1.2
--------------------------                   1.3
                                             1.4
$ set -f; IFS=$'\n'                          1.5

$ 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 (( i = 0; i < 10; i++ )) 를 할 수 있다!

프로그래밍 언어에서 for 문처럼 사용할 수 있습니다.

# 형식 1                                     # 형식 2
for (( i = 0; i < 10; i++ ))                for (( i = 0; i < 10; i++ ))
do                                          {
    echo $i                                     echo $i
done                                        }
---------------------------------------------------------------------------

$ arr=(11 22 33 44 55) i=0
$ for (( j = i + 1; j <= ${#arr[@]} - 2; ++j )) { echo ${arr[j]} ;}
22
33
44
$ for ((i=-50; i<50; i++)); do
    echo "set yrange [-50:50]; plot $i * sin(x) lw 3"
    sleep 0.1
done | gnuplot
-------------------------------

$ { echo "set isosample 100"
for ((i=-50; i<50; i++)); do
    echo "spl [:] [:] [-50:50] $i * sin(x) * cos(y) with pm3d"
done 
echo "pause mouse close"
} | gnuplot
------------------------------

# 소수는 seq 명령을 사용하거나 awk 를 활용
$ { echo "set isosample 100"
for i in `seq 0 0.001 0.1`; do
    echo "spl [:] [:] [-1:1] sin($i * (x**2 + y**2)) with pm3d"
done
echo "pause mouse close"
} | gnuplot

$ awk 'BEGIN{ 
    print "set isosample 100"
    for(i=0; i<0.1; i+=0.001)
        printf "spl [:] [:] [-1:1] sin(%g * (x**2 + y**2)) with pm3d;", i; 
    print "pause mouse close"
}' | gnuplot

break, continue 명령

break [N], continue [N]

while, until, for 반복문에서 사용되는 명령입니다. break 은 현재 실행 중인 반복문이 종료되는 결과를 갖고, continue 는 다음 처리할 원소로 넘어가는 역할을 합니다. 반복문은 중첩될 수가 있는데 이때 명령 인수로 숫자를 추가하면 상위 반복문에 적용시킬 수 있습니다.

반복문이 break 명령에 의해 종료될 경우 종료 상태 값은 0 이 됩니다.

# break                                      # continue
for var1 in 111 222 333                      for var1 in 111 222 333
do                                           do
    for var2 in aaa bbb ccc                      for var2 in aaa bbb ccc
    do                                           do
        [ $var1 = 222 ] && break                     [ $var2 = bbb ] && continue
        echo "$var1, $var2"                          echo "$var1, $var2"
    done                                         done
done                                         done
111, aaa                                     111, aaa
111, bbb                                     111, ccc
111, ccc                                     222, aaa
333, aaa                                     222, ccc
333, bbb                                     333, aaa
333, ccc                                     333, ccc
-------------------------------              --------------------------------
# break 2                                    # continue 2
for var1 in 111 222 333                      for var1 in 111 222 333
do                                           do
    for var2 in aaa bbb ccc                      for var2 in aaa bbb ccc
    do                                           do
        [ $var1 = 222 ] && break 2                   [ $var2 = bbb ] && continue 2
        echo "$var1, $var2"                          echo "$var1, $var2"
    done                                         done
done                                         done
111, aaa                                     111, aaa
111, bbb                                     222, aaa
111, ccc                                     333, aaa

명령에 선행하는 대입 연산은 사용할 수 없다.

compound commands 에서는 명령에 선행하는 대입 연산은 사용할 수 없습니다.

$ str=hello if [ 1 = 1 ]; then echo $str; fi        # if 명령에 선행해서 str 변수를 정의
bash: syntax error near unexpected token 'then'

$ str=hello while true; do echo $str; done
bash: syntax error near unexpected token 'do'

read 명령이 while, until 의 condition 에서 사용될 경우

read 명령이 while, until 문의 condition 에서 사용될 경우 loop 종료 후에는 해당 변수값이 empty 가 됩니다. ( read 에 실패해 종료됐으므로 )

$ while read line; do echo $line; done <<END
111
222
END
111
222

$ echo $line    # while 문 종료 후에는 변수값이 empty 가 된다.
$
----------------------------------------

$ for var in 111 222; do echo $var; done     # for 문의 경우
111
222

$ echo $var
222

Quiz

Go 언어를 설치하면 $GOROOT/src 디렉토리에 소스코드가 함께 설치되는데요. 프로그래밍을 하다 보면 사용 중인 함수가 어떻게 구현되어 있는지 보고 싶을 때가 있습니다. 사용자가 함수명을 인수로 전달하면 소스 디렉토리를 검색해서 함수 내용을 출력해 주는 스크립트를 작성하는 것입니다. 함수명이 여러 파일에 존재할 경우는 select 명령을 이용해서 선택할 수 있어야 합니다.

select 에서 사용되는 case 문을 동적으로 생성하기 위해서는 eval 명령을 사용합니다.

$ cat go-func
#!/bin/bash -e

if [ $# = 0 ]; then
    echo >&2 "Usage: ${0##*/} functionName"
    exit 1
fi

# Go 소스 파일명에는 공백문자나 glob 문자가 사용되지 않으므로 
# 단어 분리나 globbing 방지 처리를 하지 않아도 됩니다.
found=( $(cd "$GOROOT" && find src -type f -name '*.go' \
    -exec awk '/^func[^{]* '"$1"' *[[(]/ { print FILENAME; nextfile }' {} + ) )

if [ ${#found[@]} -gt 1 ]; then 
    PS3=$'\n>>> Select number: '
    select pick in "${found[@]}"
    do
        eval "$(
        echo 'case $pick in'
        for v in "${found[@]}"; do echo "$v ) ;;"; done
        echo 'esac'
        )"
        break 2
    done
else
    pick=${found[0]}
    [ ${#found[@]} = 1 ] && echo "1) $pick"
fi

if [ -n "$pick" ]; then
    sed -En '/^func[^{]* '"$1"'\(/{ p; /\{$/!b; :X n; p; /^}/!{bX}}' "$GOROOT/$pick" |
        vi --not-a-term -R -c 'set ft=go' -
fi

---------------------------------------------------------------------

$ go-func Join
1) src/strings/strings.go          4) src/cmd/go/internal/web/api.go
2) src/bytes/bytes.go              5) src/path/filepath/path.go
3) src/path/path.go

>>> Select number: 1

다음은 type 의 내용을 찾아볼 수 있는 go-type 명령입니다.

$ cat go-type
#!/bin/bash -e

if [ $# = 0 ]; then
    echo >&2 "Usage: ${0##*/} typeName"
    exit 1
fi

found=( $(cd "$GOROOT" && find src -type f -name '*.go' \
    -exec awk '/^type '"$1"'[[ ]/ { print FILENAME; nextfile }' {} + ) )

if [ ${#found[@]} -gt 1 ]; then 
    PS3=$'\n>>> Select number: '
    select pick in "${found[@]}"
    do
        eval "$(
        echo 'case $pick in'
        for v in "${found[@]}"; do echo "$v ) ;;"; done
        echo 'esac'
        )"
        break 2
    done
else
    pick=${found[0]}
    [ ${#found[@]} = 1 ] && echo "1) $pick"
fi

if [ -n "$pick" ]; then
    sed -En '/^type '"$1"'[[ ]/{ p; /\{$/!b; :X n; p; /^}/!{bX}}' "$GOROOT/$pick" |
        vi --not-a-term -R -c 'set ft=go' -
fi

----------------------------------------------------------------

$ go-type Scanner
1) src/database/sql/sql.go      4) src/fmt/scan.go
2) src/text/scanner/scanner.go  5) src/bufio/scan.go
3) src/go/scanner/scanner.go

>>> Select number: 5

스크립트에서 사용된 sed 명령의 사용방법은 이곳 을 참고하세요.
스크립트에서 사용된 awk 명령의 사용방법은 이곳 을 참고하세요.

2 .

다음은 select 명령을 활용하여 사지선다형 퀴즈 문제를 작성하는 것입니다. 문제를 quiz.txt 파일에서 array 로 읽어들여서 반복문을 이용해 다음 문제로 넘어갈 수 있게 합니다.

#!/bin/bash -e

i=1
while read -r line; do 
    [[ "$line" =~ ^-+$ ]] && { let ++i; continue ;}
    [ -n "$line" ] && ARR[i]+=$line$'\n'
done < quiz.txt || exit     # quiz.txt 파일에서 문제를 array 로 읽어들입니다.

total_questions=${#ARR[@]} next_question=1 next=true

PS3=$'\n[ exit "x" ] 번호입력: '

while true; do

    $next && eval "${ARR[next_question]}"

    clear
    echo "$Q"          # 문제를 출력
    echo

    AA=( "${A1%?}" "${A2%?}" "${A3%?}" "${A4%?}" )   # 'O','X' 문자는 제외하고 입력

    select pick in "${AA[@]}"
    do
        case $pick in
            ${AA[0]} ) OX=${A1: -1} ;;
            ${AA[1]} ) OX=${A2: -1} ;;
            ${AA[2]} ) OX=${A3: -1} ;;
            ${AA[3]} ) OX=${A4: -1} ;;
            *) 
                [ "$REPLY" = "x" ] && break 2
                echo -e "\n올바른 번호를 입력해 주세요.\n"
                read -n1; next=false; clear; break
        esac

        echo
        echo "$REPLY 번 \"${pick% }\""
        if [ "$OX" = "O" ]; then
            echo "@@@ 정답 @@@ 입니다."
            next=true; let next_question++
            [ "$next_question" -gt "$total_questions" ] && break 2
        else
            echo "틀렸습니다."
            next=false
        fi
        echo

        read -n1; break
    done   # select
done   # while

echo
echo "설문이 종료되었습니다."

quiz.txt 파일 내용. 질문은 Q 변수에, 답은 A1 A2 A3 A4 변수에 설정하고 구분선은 ---- 로 합니다.

$ cat quiz.txt
Q="앞뒤 생각하지 않고 덤빈다는 뜻의 '저돌적'이라는 말은 이 동물이 돌격한다는 뜻인데요.
아이큐가 약 70 정도로 지능이 높은 이 동물은 무엇일까요?"

A1="코뿔소 X"         # 틀린답의 끝에는 'X' 문자를
A2="하마 X"
A3="돼지 O"          # 정답의 끝에는 'O' 문자를 입력
A4="코끼리 X"

---------------------------------------------

Q="흔히 스포츠 경기에서 '간발의 차이'라는 표현을 쓰는데요.
여기서 '간발'은 얼마만큼의 간격을 뜻하는 말일까요?"

A1="발 하나조차 들어갈 수 없는 틈 X"
A2="발톱 하나조차 들어갈 수 없는 틈 X"
A3="손톱 하나조차 들어갈 수 없는 틈 X"
A4="머리카락 하나 조차 들어갈 수 없는 틈 O"