Shell basics

Shell 의 기본적인 역할은 사용자에게 명령을 입력받아 실행하는 것입니다. shell 은 명령문을 작성할 때 사용할 수 있도록 많은 유용한 기능을 제공하므로 실제 명령이 실행되기 전에 shell 에의해 해석 단계를 거치게 됩니다. 명령이 실행되는 과정을 살펴보기 위해 다음과 같이 mycomm 이라는 명령을 만들고 shell 에서 실행해 보도록 하겠습니다.

/* mycomm.c 파일 내용 */
#include <stdio.h>

int main(int argc, char *argv[]) {
    for (int i = 0; i < argc; i++)
        printf("arg%d : %s\n", i, argv[i]);
    return 0;
}
...........................................

$ gcc -o mycomm mycomm.c

mycomm 명령은 단순히 사용자가 입력한 인수들을 화면에 표시합니다.

# 쉘에서 변수 num, str 을 만들고 값을 대입
$ num=3 str=hello

$ ./mycomm {1..3} $(( 1 + $num )) "$(echo $str)"  
arg0 : ./mycomm
arg1 : 1
arg2 : 2
arg3 : 3
arg4 : 4
arg5 : hello

위에 명령 실행 결과를 보면 shell 에서 명령문을 작성할 때는 {1..3} , $(( 1 + $num )), "$(echo $str)" 와 같은 인수들이 사용되었는데 실제 mycomm 명령에 전달되어 프린트 되는 인수들은 다르게 나타나는 것을 볼 수가 있습니다. 이와 같이 되는 이유는 {1..3} 은 쉘에서 제공하는 기능인 brace 확장으로 해석되어 값이 1, 2, 3 으로 변경되고, $(( 1 + $num )) 은 산술연산 표현식이 되어 값이 4 가 됩니다. 그리고 마지막으로 $(echo $str) 은 명령치환으로 해석되어 값이 hello 가 됩니다. 따라서 최종적으로 실제 실행되는 명령문은 다음과 같게 됩니다.

이제부터 명령문을 작성할 때 사용할 수 있는 shell 에서 제공하는 많은 유용한 기능들을 살펴볼 텐데요. 다음에 이어지는 내용들은 명령문을 오류 없이 작성하기 위해 기본적으로 알고 있어야 하는 내용들입니다. 특히 단어분리globbing 은 변수와 명령치환을 quote 하지 않고 사용할때 항상 주의해야 합니다.

파일명

shell 에서 제일 처음이자 중요한 개념 중에 하나가 파일명 입니다. 파일명은 곧 명령을 실행할때 사용되는 이름과 같습니다. 리눅스에서 사용하고 있는 파일시스템에서는 파일 이름으로 NUL , / 두 문자를 제외하고 전부 허용한다고 합니다. 그러므로 다음과 같은 스트링은 모두 명령이름이 될 수 있습니다.

[       {         [10       {echo       if{       {AA=10}       .       :

공백

명령의 기본적인 구조를 살펴보면 다음과 같습니다.

command arg1 arg2 arg3 ...

제일 앞에 명령 이름이 오고 다음에 '공백' 그다음 첫번째 인수 '공백' 두번째 인수 '공백' ... 이렇게 명령문은 기본적으로 공백으로 분리하여 작성합니다. 만약에 공백을 사용하지 않으면 어떻게 될까요? 다음과 같이 되어 명령이 정상적으로 실행되지 않을것입니다.

# 이경우는 'commandarg1' 가 명령이 된다.  
commandarg1 arg2 arg3 ...

# 이경우는 'arg1arg2' 가 첫번째 인수가 된다. 
command arg1arg2 arg3 ...

별로 중요할 것 같지 않은 이 개념이 shell script 를 작성할 때 자주 오류의 원인이 됩니다. if 문에서 주로 사용하는 [ 는 키워드같이 생겼지만 명령으로 test 와 동일한 명령입니다. 다만 차이점은 마지막에 인수로 ] 를 붙인다는 것입니다.

# [ 명령과 첫번째 인수 10 을 붙여 사용하여 오류 발생 ('[10' 가 명령 이름이 됩니다.)
$ [10 -eq 10 ]; echo $?
[10: command not found

# 마지막 인수 ] 를 10 과 붙여사용하여 오류 발생 ('10]' 가 하나의 인수가 됩니다.)
$ [ 10 -eq 10]; echo $?
bash: [: missing ']'

# [ 명령에서 사용되는 연산자들도 모두 인수에 해당합니다.
# 다음의 경우 인수들 사이에 공백을 두지않아 a=b 가 하나의 인수로 인식이 됩니다.
# 그러므로 스트링 "a=b" 과 같은 의미가 돼서 항상 참이 됩니다.
$ [ a=b ]; echo $?
0

# 인수들 사이에 모두 공백을 띄워서 정상적으로 실행되었습니다.
$ [ a = b ]; echo $?
1

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

# '{' 키워드와 echo 명령을 붙여 사용하여 '{echo' 가 명령 이름이 됩니다.
$ {echo 1; echo 2 ;}
bash: syntax error near unexpected token '}'

# 공백을 사용하여 정상적으로 실행되었습니다.
$ { echo 1; echo 2 ;}
1
2

shell 에서는 위에서 살펴본 기본적인 명령 구조를 갖지 않는 문장이 하나 있는데 바로 대입연산 입니다. 대입연산을 하는 문장을 명령을 작성하는 식으로 하면 오류가 발생합니다.

# 변수 AA 가 명령이 되고 = , 10 는 각각 인수로 인식됩니다. 
$ AA = 10
AA: command not found

# 그러므로 shell 에서 대입연산은 반드시 공백 없이 붙여 사용해야 합니다.
$ AA=10
$ echo $AA
10

shell script 를 작성할때 대입연산을 제외하고 모두 공백을 두어 작성하는 것도 오류를 줄일 수 있는 방법이 될 수 있습니다.

참 과 거짓

if 문에서 참, 거짓을 판단할때 프로그래밍 언어에서는 0 이 거짓이고 그외 값은 참이지만 shell 에서는 반대입니다. 0 만 프로그램의 정상종료를 나타내어 참이되고 그외 숫자는 오류를 분류하여 나타내는데 사용되므로 거짓이 됩니다. 그리고 판단에 사용되는 값도 명령의 종료 상태 값 ( $? ) 으로만 합니다.

$? 은 명령의 종료 상태 값을 나타내는 shell 에서 제공하는 특수 변수입니다.

### 종료 상태 값 0 이 참이고 그 외는 거짓이다.

$ date -@      # 인수를 잘못 사용하여 오류발생
date: invalid option -- '@'
Try 'date --help' for more information.

$ echo $?      # 0 이 아닌 종료 상태 값은 if 문에서 모두 거짓에 해당합니다.
1

$ date +%Y
2015

$ echo $?      # 정상종료 됐으므로 0 을 리턴. if 문에서는 참이 됩니다.
0

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

# 전달받은 인수값를 그대로 리턴하는 함수. '$1' 는 첫번째 인수를 나타냄
$ func() { return $1 ;}                            

# 'func 0' 은 종료 상태 값으로 0 을 리턴하므로 참
$ if func 0; then echo true; else echo false; fi 
true                    

# 'func 1' 은 종료 상태 값으로 1 을 리턴하므로 거짓 
$ if func 1; then echo true; else echo false; fi 
false

return

shell 함수에서 사용되는 return 명령은 프로그래밍 언어와 달리 연산 결과를 반환하는데 사용되지 않습니다. shell script 를 종료할때 exit 명령을 이용해 종료 상태 값을 지정하는 것처럼 함수에서는 return 명령을 사용해 종료 상태 값을 지정합니다.

$ func() { expr $1 + $2 ; return 5 ;}

$ func 1 2
3

$ echo $?      # $? 는 명령의 종료 상태 값을 나타내는 특수 변수
5

$ AA=$( func 1 2 )

$ echo $AA     # func 함수의 stdout 출력값이 연산 결과 값이 된다.
3

명령 종료 문자

C/C++ 언어에서는 문장의 종료를 나타내기 위해 마지막에 항상 ; 를 붙여야 하지만 shell script 에서는 반드시 문장 끝에 붙일 필요는 없습니다. 왜냐하면 라인개행을 알아서 인식하기 때문인데요. 하지만 개행을 하지 않고 명령들을 한 줄에 연이어 쓸 경우는 반드시 ; 를 붙여야 합니다. 특히 명령 grouping 을 위해 중괄호 { } 사용 시에는 명령의 인수와 구분을 위해 반드시 마지막에 ; 를 붙여줘야 합니다.

$ for i in {1..3} 
do 
    echo $i 
done
1
2
3

# 명령들을 한줄에 연이어 쓸 경우는 ';' 를 붙여야 한다.
$ for i in {1..3} do echo $i done
> 오류

$ for i in {1..3}; do echo $i; done
1
2
3

# 'echo 1 }' 하면 '}' 까지 프린트된다. 인수와 구분을 위해 ; 를 붙여야 한다
$ { echo 1 } ;}
1 }

$ { echo 1; echo 2 }
> 오류

$ { echo 1; echo 2 ;}
1
2

Shell 메타문자는 예외

Shell 메타문자 는 shell 에서 특별히 취급되는 문자입니다. 따라서 공백 이나 ; 제약 없이 프로그래밍 언어에서처럼 자유롭게 사용될 수 있습니다. 그중에 소괄호 ( ) 는 subshell 을 만들때 사용되는 메타문자 입니다.

# '(' 와 명령 사이에 공백을 두지 않아도 되고 ')' 앞에 ; 를 붙이지 않아도 된다.
$ (echo hello; echo world)
hello
world

Escape

Shell 에서 사용되는 명령문에는 단지 명령문을 위한 스트링만 존재하지 않습니다. script 작성을 위해 shell 에서 제공하는 키워드, 메타문자, glob 문자들이 같이 사용되는 환경이기 때문에 만약에 명령문에서 동일한 문자가 사용된다면 escape 하거나 quote 하여 명령문을 위한 스트링으로 만들어 줘야 오류가 발생하지 않습니다.

# 명령문에 shell 에서 사용하는 glob 문자 '*' 가 포함되어 에러 발생
$ expr 3 * 4       
expr: syntax error

# 다음과 같이 escape 하거나 quote 하여 명령문을 위한 스트링으로 만들어줌
$ expr 3 \* 4       
$ expr 3 '*' 4
12

# '<' , '>' 문자는 shell 에서 사용되는 redirection 메타문자 
# 마찬가지로 escape 하지 않으면 정상적으로 실행되지 않고 오류가 발생합니다
$ [ a \< b ]
$ test a \> b    # 모두 escape 해줘야 한다.
$ expr 3 \> 4    

# '( )' ';' 문자도 shell 에서 사용하는 메타문자
$ find * ( -name "*.log" -or -name "*.bak" ) -exec rm -f {} ;
bash: syntax error near unexpected token '('

# 다음과 같이 모두 escape 해줘야 오류없이 정상적으로 실행이 됩니다.
$ find * \( -name "*.log" -or -name "*.bak" \) -exec rm -f {} \;

단어분리

이것은 shell 이 가지는 고유의 기능 중 하나인데 변수나 명령치환을 quote 하지 않으면 값이 출력될때 IFS ( Internal Field Separator : 기본적으로 공백문자로 구성 ) 에의해 단어가 분리됩니다. 그러므로 뜻하지 않게 인수가 2개 이상으로 늘어난다거나 공백이 포함된 파일이름이 분리가 되는 오류가 발생할 수 있습니다. Expansions and Substitutions -> Word Splitting 에서 좀 더 자세히 다룹니다.

$ dir='스크립트 강좌'

# $dir 변수를 quote 하지 않아 단어분리가 일어나 '스크립트' 가 디렉토리 명이 됩니다.
$ cd $dir
bash: cd: 스크립트: No such file or directory

$ cd "$dir"
OK

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

$ AA="hello world"

$ func() {
    echo arg1 : "$1"
    echo arg2 : "$2"
}

$ func "$AA"
arg1 : hello world
arg2 :

# $AA 변수를 quote 하지 않아 단어분리가 일어나 func 함수에 전달되는 인수가 2 개가 됩니다.
$ func $AA 
arg1 : hello
arg2 : world

Filename Expansion (Globbing)

Shell 에서는 파일을 select 할때 glob 문자를 ( *,?,[ ] ) 이용합니다. 그러므로 변수나 명령치환을 quote 하지 않고 사용할 경우 출력되는 값에 glob 문자가 포함되면 뜻하지 않게 globbing 이 발생해 오류가 발생할 수 있습니다. Expansions and Substitutions -> Filename Expansion 에서 좀 더 자세히 다룹니다

$ AA="User-Agent: *"     # 변수 AA 값으로 glob 문자 '*' 가 사용됨

$ echo "$AA"             # quote 을 하면 globbing 이 발생하지 않음
User-Agent: *

$ echo $AA               # quote 을 하지 않아 globbing 이 발생해 뜻하지 않은 값이 출력됨
User-Agent: 2013-03-19 154412.csv Address.java address.ser 
ReadObject.class ReadObject.java robots.txt 쉘 스크립트 테스팅.txt 
WriteObject.class WriteObject.java

Misc.

명령의 옵션

위에서 명령의 기본 구조를 설명할 때 사실 빠진 부분이 하나 있습니다. 바로 명령 옵션인데요. 옵션은 보통 - 문자로 시작하는데 - 로 시작하고 하나의 문자를 사용하는 short form 과 -- 로 시작하고 여러 개의 문자를 사용하는 long form 두 종류가 사용됩니다.

Short form Long form
-h --help
-o value --option=value
-axjf
합쳐서 쓸수있다
X
사용하기 간편하다 이름을통해 옵션의 의미를 알기쉽다

그런데 이 옵션 때문에 명령을 실행할 때 오류가 발생할 수 있습니다. 다음은 grep 명령을 이용해 현재 디렉토리 이하 파일들로부터 -n 스트링을 찾는 것인데요. 그런데 이때 -n 이 검색할 스트링이 아니라 grep 명령의 옵션으로 인식이 되어 정상적으로 명령이 실행되지 않습니다.

# '-n' 을 grep 명령의 옵션으로 인식해 정상적으로 실행되지 않는다. 
$ grep -r '-n'
Usage: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.

# '--bar' 를 grep 명령의 옵션으로 인식해 정상적으로 실행되지 않는다. 
$ grep -r "--bar"
grep: unrecognized option '--bar'
Usage: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.

이와 같은 경우 -- 를 사용하여 "이 뒤로부터는 옵션이 아니다" 라고 선언할 수 있습니다.

# '--' 는 end of options 을 나타냄

$ grep -r -- "-n"         # '--' 를 이용하여 '-n' 은 옵션이 아님을 선언 
OK

$ grep -r -- "--bar" 
OK

그러므로 script 작성시 명령의 인수로 변수를 사용할 경우 -- 를 이용해 분리해야 오류를 줄일 수 있습니다.

command -o val -- "$arg1" "$arg2"

STDIO 을 나타내는 -

명령들을 사용하다 보면 종종 - 문자가 input 이나 output 에 사용되는 경우가 있습니다. 이때는 입력에 사용되면 stdin 과 같은 역할을 하고 출력에 사용되면 stdout 과 같은 역할을 합니다. 다음 예제를 통해서 의미를 알아볼 수 있습니다.

  • - 가 input 에 사용되어 stdin 을 나타내는 경우
$ cat foo.txt                 $ cat bar.txt
111                           333
222                           444

$ echo hello world | cat foo.txt
111
222

$ echo hello world | cat - foo.txt
hello world
111
222

$ echo hello world | cat foo.txt -
111
222
hello world

$ echo hello world | cat foo.txt - bar.txt
111
222
hello world
333
444
----------------------------------

$ ssh user@remote.com "cat path/to/remote/file" | diff - path/to/local/file
----------------------------------

$ ps jf | vi -
----------------------------------

$ seq 10 | paste - - -
1       2       3
4       5       6
7       8       9
10
----------------------------------

# rsync 나 wc 명령 에서처럼 옵션 값으로 '-' 를 지원하는 경우
# 다음의 경우 find 명령의 출력값이 --files0-from 옵션 값으로 사용됩니다.
$ find /usr/sbin -type f -print0 | wc --files0-from=-
  • - 가 output 에 사용되어 stdout 을 나타내는 경우.
# 컴파일해서 실행파일을 만드는 대신에 어셈블러 코드를 stdout 으로 출력합니다.
$ gcc -S hello.c -o -

# dir1 디렉토리 이하 모든 파일들을 tar 압축하여 stdout 으로 출력하면 파이프로 전달됩니다.
$ tar czv -f - dir1 | nc -N 12.34.56.78 8080

# docker.com 에서 받은 내용을 파일로 저장하지 않고 stdout 으로 출력합니다.
$ wget -q -O - https://get.docker.com
  • 다음은 파이프 왼쪽의 tar 명령에서 사용된 - 는 stdout 을 오른쪽의 split 명령에서 사용된 - 는 stdin 을 나타냅니다.
# dir1 디렉토리 이하 모든 파일들을 tar 압축하여 stdout 으로 출력하면
# split 명령이 stdin 으로부터 받아서 100M 크기 파일로 분리해 저장합니다.
$ tar czv -f - dir1 | split - -d -b 100M tardisk

# cat 명령에 의해 tardisk00, tardisk01 ... 파일들이 모두 합쳐져서 stdout 으로 출력되면
# tar 명령이 stdin 으로부터 받아서 extract 합니다.
$ cat tardisk* | tar xvz -f -     # 여기서 '-' 는 stdin 

# github.com 에서 original.jpg 를 다운받아서 25% 로 resize 한후에 httpbin.org 로 올립니다.
# convert 명령에 두개의 "-" 가 사용되었는데 하나는 stdin 이되고 다른 하나는 stdout 이 됩니다.
$ curl -sL https://octodex.github.com/images/original.jpg | 
  convert - -resize 25% - | http httpbin.org/post

명령문에서 사용되는 --- 도 하나의 명령 인수입니다. 따라서 해당 명령이 - 를 stdin 이나 stdout 으로 처리하지 않고 -- 를 end of options 으로 처리하지 않는다면 해당 기능을 사용할 수 없습니다.

cd 명령은 종료 상태 값을 확인해야 한다.

다음과 같은 경우 만약에 cd 명령이 실패하였다면 현재 디렉토리가 모두 삭제됩니다.

cd ~/tempdir
rm -rf *

그러므로 cd 명령의 성공 여부를 체크해서 사용해야 합니다.

cd ~/tempdir && rm -rf *

# 뒤에 이어지는 명령이 많을 경우
cd ~/tempdir || { echo >&2 "cd ~/tempdir failed"; exit 1 ;}
. . .

Shell 에서 $ 문자를 이용하는 확장

Shell 에서는 $ 문자를 이용하는 확장이 3 가지 종류가 있습니다.

주석

Shell 에서 주석은 # 문자를 사용합니다. 하지만 명령문에서도 # 문자가 사용될 수 있으므로 무조건 # 이후로 주석으로 처리되는 것은 아니고 # 문자 앞에 공백이 있어야 주석으로 처리됩니다. 그러므로 처음 두 라인은 # 이후로 주석으로 처리되지만 마지막 라인은 주석으로 처리되지 않습니다.

주석으로 처리되는 것을 방지하려면 다음과 같이 escape 하거나 quote 하면 됩니다.

$ echo \#1234 | sed s/#/@/
@1234

$ echo "#1234" | sed s/#/@/
@1234

# gcc 에서 사용되는 -### 옵션은 주석으로 처리되지 않고 정상적으로 전달된다.
$ gcc -### ...