shell의 기본적인 역할이 무엇인지 앞 글들에서 살펴봤다. linux cli에 익숙해지기 위해서는 기본 shell 자체에 익숙해질 필요가 있다. 커널과 사용자(application)간의 다리역할을 하는 shell의 기본적인 문법을 살펴보자.
At its base, a shell is simply a macro processor that executes commands.
Bash is the shell, or command language interpreter, for the GNU operating system.
유명한 셸의 릴리스 시간 순서로 보자면 아래와 같다.
Bourne shell: /bin/sh - POSIX shell
C shell: /bin/csh - Sun microsystem (BSD)
Korn shell: /bin/ksh
- (아래는 요즘 표준인 셸들) -
bash: /bin/bash
zsh: /bin/zsh
AT&T의 소프트웨어 엔지니어인 Dennis Ritchie 와 Ken Thompson(유닉스의 아버지)이 UNIX™를 디자인한 당시, 그들은 사용자와 새 시스템이 상호 작용할 수 있는 방법을 찾고자 했다. 그 때 당시의 운영 체제는 사용자로부터 명령어를 읽어 들여 해석한 다음 기계가 명령을 실행하도록 하는 명령어 해석기를 갖추고 있었다.
Ritchie 와 Thompson은 이러한 명령어 해석기 보다 나은 것을 만들기 위한 시도가 Steve Bourne에 의해 만들어진 (sh로 알려진) 본셸(Bourne shell)의 개발을 이끌었다. 그 후 BSD종주 역할을 담당했던 썬 마이크로시스템즈의 C셸이 개발되었으나 20여년 가까이 쓰여지다가 지금은 많이 쓰이지 않게 되었다. C셸은 초창기에는 대단했으나 기존 표준셸과 문법도 다르고 버그도 좀 나오고 그래서 꺼려지다가 지금은 쓰이지 않게 되었다.
본셸은 POSIX 표준에 기본 셸로 정해져서 POSIX shell이라고도 부르고, 실행 파일명을 따서 sh - shell이라고도 부른다.
그 다음에 System V를 이은 상용 유닉스의 표준셸은 sh의 특성을 계승하면서 더 많은 기능을 가진 콘셸이 나오게 되었고, 대부분의 유닉스에서 기본 셸의 자리를 잡아갔다. 콘셸의 이름도 만든 사람의 이름인 David Korn에서 따서 만들어졌다.
리눅스가 등장했을 때 콘셸을 사용하지 못한 것은 라이선스 문제(오픈라이선스가 아니었다)가 제일 컸다. 하지만 POSIX shell을 쓰기엔 기능이 너무 제한적인 것이 많았고, 1989년 브라이언 폭스가 GNU프로젝트를 위해 개발한 배시셸은 sh셸을 기반으로 bash라는 셸이 나오게 되었다.
Bash는 "Bourne again shell"라는 뜻이라고 하는데 기본으로 본셸의 기능을 계승하고, Korn Shell(ksh)과 C Shell(csh)의 장점을 흡수하며, 추가적인 기능까지 넣어서 만능 셸처럼 쓰였다. 말장난으로 Born again sh로 부르기도 한다.
현재 bash는 리눅스와 Mac OSX의 기본 셸이며 가장 보편적인 셸이다. 요새는 대부분의 상용 유닉스들도 기본 셸로 bash를 쓰는 경우가 많다.
$
는 일반 유저
, #
는 root 유저
의 프롬프트를 의미한다.echo $SHELL
또는 env | grep SHELL
로 지금 사용중인 shell 종류를 알 수 있다.bash의 특징은 많은 부분의 external command들을 built-in command로 대체했기 때문에 셸 스크립트 작성시 적극적으로 built-in 명령어를 사용하면 성능이 높아지는 장점이 있었다.
하지만 bash에도 플러그인 기능이라든지 현대적인 여러가지 기능을 추가하기는 점점 힘들어졌고, 그래서 좀 더 미려한 디자인과 다양한 플러그인을 만들 수 있도록 디자인된 zsh가 나오게 되었다. 개인 유저들 중에는 zsh를 기본 셸로 쓰는 사람들이 많다. 하지만 회사나 기업은 성능이나 여러가지 안정성, 보안 때문에 bash를 쓰는 경우가 많다.
사용자와 커널 사이에서 명령을 해석해 전달하는 "명령어 해석기" 인 shell 문법에 대해 본격적으로 살펴보자. 참고로 버전별로 지원안하는 문법이 있을 수 있다. 하기 내용은 5.1.8 버전 이상이다.
.sh 파일의 가장 위에 #!/bin/bash
를 명시해 어떤 셸이며, 셸 스크립트의 시작을 알린다. 셸 파일은 기본적으로 확장자를 가리지 않는다. 그래서 이렇게 명시하는 경우가 많다.
echo "Hello Bash!"
로 특정 값을 출력할 수 있다.
변수명=데이터
으로 선언하며, 변수명=데이터 사이에 띄어쓰기는 허용되지 않는다.$변수명
으로 한다. #!/bin/bash
NAME='NUUNG'
AGE=9999 # 숫자는 따옴표 처리 필요 없다.
echo $NAME
${변수명[인덱스번호]}
로 접근하고 사용가능하다.# 기본 문법
변수=(값1 값2 값3 그외 쭉쭉)
# 예시
array=(V1 V2 V3 V4 V5)
# 또는 아래와 같이 가능하다.
array2=()
array2+=(V1)
array2+=(V2)
array2+=(V3)
echo $array2 # 결과 : V1 V2 V3
# 배열의 size는 아래와 같이 가져올 수 있다.
arraySize=${#array[@]}
echo $arraySize # 결과 : 5
echo ${array[1]} # V1
echo ${array[@]} # V1 V2 V3 V4 V5
echo ${array[*]} # V1 V2 V3 V4 V5
echo ${#array[@]} # 5
filelist=( $(ls) )
와 같은 방법으로 ls 결과값을 filelist 변수에 배열형태로 저장할 수 있다. echo ${filelist[*]}
로 출력해보자!$0
: Script를 실행시킬 때 프로그램의 이름이 포함된 첫 번째 문자열을 저장$1, ...$N
: 입력 값들이 순서대로 저장되며, 위치 매개변수(Positional Parameter)라고 표현$*
: 모든 위치 매개변수를 담고 있는 단일 문자열$@
: $*
과 비슷하나 $@
는 "$1", ..."$N"
을 의미$#
: 위치 매개변수의 개수를 저장$?
: 셸에서 최근 실행한 명령어의 종료 상태를 담은 변수, $$
: 실행하는 셸 스크립트의 프로세스 ID를 저장 (pid)$_
: 마지막 인수를 출력하는 변수를 저장# test.sh file
echo $$ $0 $1 $* $# # 결과 : 40386 ./test.sh 0
# $$ 는 pid, $0 은 이름, $1 은 (전달받은) 첫번째 인자
# $* 는 이름을 뺀 나머지 인자 리스트, $# 는 이름을 뺀 나머지 인자 개수
test.sh test 123
와 같이 하면, 40474 ./test.sh test test 123 2
를 출력한다../test.sh test 123 456
-> 40553 ./test.sh test test 123 456 3
./test.sh test 1 2 '34' 5
-> 41018 ./test.sh test test 1 2 34 5 5
./test.sh test 1 2 '34 5' 5
-> 41021 ./test.sh test test 1 2 34 5 5 5
mysql -u root -p ...
또는 java -jar ...
와 같이 말이다. expr
라는 접두사를 활용해 산술 연산을 한다.+
: 두 값을 더한다.-
: 왼쪽값에서 오른쪽 값을 뺀다.*
: 두 값을 곱한다./
: 두 값을 나눈다. %
: 나눈 나머지(모듈러)를 구한다.#!/bin/bash
x=10
y=20
z=`expr $x + $y`
w=$((x+y))
echo "z: $z"
echo "w: $w"
# z는 expr을 사용한 결과이고 w는 변수 처리한 경우
# 결과
# z: 30
# w: 30
num=`expr \( 10 + 20 \) / 8 - 8`
# 결과: -5
숫자인 경우 다음 연산자 사용
-eq
: 두 값이 같으면 true, 다르면 false-ne
: 두 값이 같으면 false, 다르면 true-gt
: 값이 초과하면 true-lt
: 값이 미만이면 true-ge
: 값이 이상이면 true-le
: 값이 이하이면 true==
: 값이 같으면 true문자(열)인 경우 다음 연산자 사용
=
: 같으면 true!=
: 다르면 true-z
: length가 zero이면 true, 즉 null인 경우-n
: lenght가 0이 아니면 true-a
: && 로 둘다 true면 true-o
: || 로 둘중 하나만 true면 true!
: not 연산자 -> true, false의 반대값-e
: file이 존재하면 true -d
: directory면 true-s
: file sizer가 0보다 크면(0이 아니면) true-h
: 파일이 심볼릭 링크 파일이면 true-f
: 파일이 일반파일이면 true-r
: (shell을 실행한 user가) 파일이 읽기 가능하면 true-w
: (shell을 실행한 user가) 파일이 쓰기 가능이면 true-x
: (shell을 실행한 user가) 파일이 실행 가능이면 true-u
: 파일이 set-user-id가 설정되면 trueif [ expression ]
then
statement
elif [ expression ]
then
statement
else
statement
fi
a=10
b=20
if [ $a = $b ]
then
echo "a is equal to b"
else
echo "a is not equal to b"
fi
if [ $1 -eq $2 ] # if [ $1 != $2 ] 를 쓰면 not equal
then
echo "same values"
else
echo "different values"
fi
if [ -e $1 ] # ./file_check.sh 파일명 > 결과 확인
then
echo "file exist"
fi
if [ 1 -eq 1 ]; then echo yes; else echo no; fi
if [ `ls | wc -l` == 1 ]
then
echo "파일이 하나 존재"
else
echo "파일이 여러개 존재"
fi
for
, while
이 있다.# for
for 변수 in 변수값1 변수값2 ....
do
명령문
done
# while
while [ 조건문 ]
do
명령문
done
ls
라는 command를 활용해 셸이 있는, 셸을 실행시키는 디렉토리의 내부 파일들을 모두 출력한다.for files in $(ls)
do
echo $files
done
array=(V1 V2 V3 V4 V5)
for arr in $array
do
echo $arr
done
for files in $(ls); do echo $files; done;
# 1부터 100까지 출력된다. 다른 언어의 for i loop와 같다.
for var in {1..100}
do
echo $var # 1, 2, 3, ... 100
done
# 순차적으로 일정 범위만큼 증가 또는 감소하면서 반복
for var in {1..100..5}
do
echo $var # 1, 6, 11, 16 ... 96
done
for var in {100..1..-5}
do
echo $var # 100, 95, 90 ... 5
done
for ((var=0 ; var < 5 ; var++));
do
echo $var # 0, 1, ... 4
done
# 무한 루프도 가능하다.
for (( ; ; ));
do
echo "Hello World"
done
while
을 활용할 수 있다.lists=$(ls)
num=${#lists[@]}
index=0
while [ $num -ge 0 ]
do
echo ${lists[$index]}
index=`expr $index + 1`
num=`expr $num - 1`
done
(당연하지만) 기본적인 명령어(command)들의 실행하고 그 결과값을 저장하고 활용할 수 있다.
예를들어 cli를 실행할 수 있는 path설정만 된다면 당연하게python
같은 cli도 run할 수 있다. (이 경우 package path에 대한 신경을 따로 써야하긴 한다)
sleep N
#!/bin/bash
if [ -f ./프로젝트이름/requirements.txt ]; then
echo "SUCCESS: requirements.txt exists!"
else
echo "FAILURE: requirements.txt does not exist!"
exit 1
fi
REQUIREMENTS=$(cat ./django_all_about/requirements.txt)
NEW_REQUIREMENTS=$(pip freeze)
if [ "$NEW_REQUIREMENTS" = "$REQUIREMENTS" ]; then
echo "SUCCESS: requirements.txt is up to date!"
exit 0
else
echo "FAILURE: requirements.txt is not up to date!"
pip freeze > ./프로젝트이름/requirements.txt
exit 1
fi%
pre-commit
과정에 포함시킬 수 있다. 커밋하기전에 requirements가 변경되었는지 (추가 package install check) 체크할 수 있다!command_result=($(ls))
grep_result_list=($(grep -oh 'nuri' ./testFile.txt))
#!/bin/bash
ping -c 1 127.0.0.1 1> /dev/null # ip자리에 특수 변수를 활용할 수 도 있다.
if [ $? == 0 ]
then
echo "Gateway ping success!" # 0 일 경우 응답이 온 것이라 성공!
else
echo "Gateway ping failed!" # 응답이 없을 때
fi
# 기본 문법
declare -A 변수
변수[key]=value
# 예시
declare -A map
map[key1]=value1
map[key2]=value2
echo $map
# 출력 결과 : value1 value2
# Map 크기
mapSize=${#map[@]}
# Map 에 특정 key 값이 존재하는지 체크
# 예를 들어, map 에 key3 가 존재하는지 알고 싶다!
# 여기서 사용하는 -z 옵션은 문자열이 null(빈 문자열)이면 true 를 반환하는 아이.
if [ -z ${map[key3]} ]; then
echo "key3 not exist"
else
echo "key3 exist"
fi
# Map과 for
for key in ${!map[@]}; do
value=${key}
echo "key:$key, value:$value"
done
# 출력 결과
# key:key1, value:key1
# key:key2, value:key2
#!/bin/bash
ARG1=""
APP="app*"
PROJ=""
if [ $# -eq 0 ]
then
echo "input command"
exit 0
else
ARG1="$1"
fi
# clean all binary files
if [ "$ARG1" == "clean" ]
then
rm -f $APP
exit 0
# if exist the $1.c file,
elif [ "${ARG1##*.}" == "c" ]
then
PROJ=$ARG1
APP="app_${ARG1:0:-2}"
gcc ${PROJ} -o ${APP} `pkg-config --cflags --libs gstreamer-1.0`
# or, exist the $1.cpp file
elif [ "${ARG1##*.}" == "cpp" ]
then
PROJ=$ARG1
APP="app_${ARG1:0:-4}"
g++ ${PROJ} -o ${APP} `pkg-config --cflags --libs gstreamer-1.0`
else
echo "input compile file name"
echo "${ARG1##*.}"
exit 0
fi
echo "compile ${PROJ}"
echo "create ${APP}"
아주 대표적인 shell 종류 bash의 기본적인 문법과 활용법에 대해 체크했다. 사실 서버에서 작업하고, 실제 open source들의 다양한 command 활용법을 보다 보면 shell을 굉장히 많이 접하게 된다.
특히 java build gradle, maven 등의 빌드 자동화 시스템을 보더라도 확 느껴진다.
특히 awk
로 특정 shell script
를 잘만 짜두어도 정말, 생각보다 많은 도움이 된다. 오히려 실제 개발 할 때에도 shell command
와 혼합해서 사용하는 경우도 더러 있다. 특히 file change를 체크하는 file-watcher system 등과 같은 agent 형태 프로그램에서 많이 볼 수 있다.
awk
, path
, . vs source script
등 아직 다룰 내용이 더 많다. 여기서는 shell script 자체 기본문법에만 집중하고, 언급한 키워드 내용은 다음 글에서 다시 다루려고 한다.