리눅스 - shell script (셸 스크립트) 역사, 기본 문법, 특수 변수, 조건문과 반복문

정현우·2023년 1월 23일
3

Linux Basic to Advanced

목록 보기
13/16

리눅스 shell

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.

bash, shell의 역사 흐름

유명한 셸의 릴리스 시간 순서로 보자면 아래와 같다.

Bourne shell: /bin/sh - POSIX shell
C shell: /bin/csh - Sun microsystem (BSD)
Korn shell: /bin/ksh

- (아래는 요즘 표준인 셸들) -
bash: /bin/bash
zsh: /bin/zsh

1. bash 이전의 셸들

  • 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에서 따서 만들어졌다.

2. bash의 등장

  • 리눅스가 등장했을 때 콘셸을 사용하지 못한 것은 라이선스 문제(오픈라이선스가 아니었다)가 제일 컸다. 하지만 POSIX shell을 쓰기엔 기능이 너무 제한적인 것이 많았고, 1989년 브라이언 폭스가 GNU프로젝트를 위해 개발한 배시셸은 sh셸을 기반으로 bash라는 셸이 나오게 되었다.

  • Bash는 "Bourne again shell"라는 뜻이라고 하는데 기본으로 본셸의 기능을 계승하고, Korn Shell(ksh)과 C Shell(csh)의 장점을 흡수하며, 추가적인 기능까지 넣어서 만능 셸처럼 쓰였다. 말장난으로 Born again sh로 부르기도 한다.

  • 현재 bash는 리눅스와 Mac OSX의 기본 셸이며 가장 보편적인 셸이다. 요새는 대부분의 상용 유닉스들도 기본 셸로 bash를 쓰는 경우가 많다.

    • bash shell에서 $일반 유저, #root 유저의 프롬프트를 의미한다.
    • echo $SHELL 또는 env | grep SHELL 로 지금 사용중인 shell 종류를 알 수 있다.
    • "사용자" 마다 셸 종류가 다를 수 있다. 셸 변경은 root 유저만 가능하다!
  • bash의 특징은 많은 부분의 external command들을 built-in command로 대체했기 때문에 셸 스크립트 작성시 적극적으로 built-in 명령어를 사용하면 성능이 높아지는 장점이 있었다.

  • 하지만 bash에도 플러그인 기능이라든지 현대적인 여러가지 기능을 추가하기는 점점 힘들어졌고, 그래서 좀 더 미려한 디자인과 다양한 플러그인을 만들 수 있도록 디자인된 zsh가 나오게 되었다. 개인 유저들 중에는 zsh를 기본 셸로 쓰는 사람들이 많다. 하지만 회사나 기업은 성능이나 여러가지 안정성, 보안 때문에 bash를 쓰는 경우가 많다.

    • zsh은 최신 Mac OSX의 기본셸이 되었다.
    • zsh + oh-my-zsh 조합은 수많은 커스텀이 있고, 생산성이 굉장히 좋다.

shell 기본 문법

사용자와 커널 사이에서 명령을 해석해 전달하는 "명령어 해석기" 인 shell 문법에 대해 본격적으로 살펴보자. 참고로 버전별로 지원안하는 문법이 있을 수 있다. 하기 내용은 5.1.8 버전 이상이다.

1. 변수와 출력

  • .sh 파일의 가장 위에 #!/bin/bash 를 명시해 어떤 셸이며, 셸 스크립트의 시작을 알린다. 셸 파일은 기본적으로 확장자를 가리지 않는다. 그래서 이렇게 명시하는 경우가 많다.

  • echo "Hello Bash!" 로 특정 값을 출력할 수 있다.

변수 선언

  • 변수명=데이터 으로 선언하며, 변수명=데이터 사이에 띄어쓰기는 허용되지 않는다.
  • 숫자는 따옴표 처리 필요 없다.
  • 선언한 변수 접근(참조)은 $변수명 으로 한다.
#!/bin/bash

NAME='NUUNG'
AGE=9999 # 숫자는 따옴표 처리 필요 없다.
echo $NAME

배열(array) 선언

  • ${변수명[인덱스번호]} 로 접근하고 사용가능하다.
  • 그 외 모두 출력, 사이즈(길이) 등에 대한 사용법을 살펴보자.
# 기본 문법
변수=(값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[*]} 로 출력해보자!

2. 특수 변수(사전에 정의된 지역 변수) - $0, $1, $2, $*, $@, $#

  • $0: Script를 실행시킬 때 프로그램의 이름이 포함된 첫 번째 문자열을 저장
  • $1, ...$N: 입력 값들이 순서대로 저장되며, 위치 매개변수(Positional Parameter)라고 표현
  • $*: 모든 위치 매개변수를 담고 있는 단일 문자열
  • $@: $* 과 비슷하나 $@"$1", ..."$N"을 의미
  • $#: 위치 매개변수의 개수를 저장
  • $?: 셸에서 최근 실행한 명령어의 종료 상태를 담은 변수,
    • 0 성공, 1~125 에러, 126 파일이 실행가능하지 않음, 128~255 시그널 발생
  • $$: 실행하는 셸 스크립트의 프로세스 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
    • 변수 내 띄어쓰기가 필요한 경우 single quote(따옴표)를 활용한다!
  • 이런 특수 변수PATH 변수를 활용해 S/W에서 global 하게 사용되는 command를 만들 수 있는 것이다. 예를 들어 mysql -u root -p ... 또는 java -jar ... 와 같이 말이다.

3. 연산자

  • 연산자들은 배열 선언 / 조건문 / 반복문 등 에서 기본이 된다.

산술 연산자(Arithmetic Operators)

  • expr 라는 접두사를 활용해 산술 연산을 한다.
  • 연산자 종류는 아래와 같다.
    • +: 두 값을 더한다.
    • -: 왼쪽값에서 오른쪽 값을 뺀다.
    • *: 두 값을 곱한다.
    • /: 두 값을 나눈다.
    • %: 나눈 나머지(모듈러)를 구한다.

산술 연산 실습

  1. expr 를 사용하는 경우 역 작은 따옴표 (`) 를 사용해야한다.
  2. 연산자 * 와 괄호 ( ) 앞에는 역 슬래시를 같이 사용해야 한다. (붙여 쓴다.)
  3. 연산자와 숫자, 변수, 기호 사이에는 space를 넣어야한다.
#!/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

비교 연산자(Relational Operators)

  • 숫자인 경우 다음 연산자 사용

    • -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

논리 연산자(Boolean Operators)

  • -a: && 로 둘다 true면 true
  • -o: || 로 둘중 하나만 true면 true
  • !: not 연산자 -> true, false의 반대값

파일 연산자(File Operators)

  • 뒤에 기본적으로 (상대 또는 절대경로를 포함한) "파일명" 이 붙는다.
  • -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가 설정되면 true

4. 조건문

  • 조건문의 기본 형태는 아래와 같다.
if [ expression ]
then
    statement
elif [ expression ]
then
    statement
else
    statement
fi

다양한 조건문 실습

  • 기본 equal check
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
  • 세미콜론(;)을 줄바꿈 구분자로 활용해 one-line 작성도 가능하다.
if [ 1 -eq 1 ]; then echo yes; else echo no; fi
  • 조건문 자리 안에서 command를 활용하는 것도 가능하다.
if [ `ls | wc -l` == 1 ]
then
    echo "파일이 하나 존재"
else
    echo "파일이 여러개 존재"
fi

5. 반복문

  • 반복문의 기본 형태는 아래와 같다. 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 
  • 세미콜론(;)을 줄바꿈 구분자로 활용해 one-line 작성도 가능하다.
for files in $(ls); do echo $files; done;
  • 순차 증가, 감소, jump 반복문, for i loop
# 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

6. ETC

(당연하지만) 기본적인 명령어(command)들의 실행하고 그 결과값을 저장하고 활용할 수 있다.
예를들어 cli를 실행할 수 있는 path설정만 된다면 당연하게 python 같은 cli도 run할 수 있다. (이 경우 package path에 대한 신경을 따로 써야하긴 한다)

sleep (잠시 재우기)

sleep N

python requirements file check

#!/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%
  • 조건문과 변수, 기본 command를 활용해 위 shell script를 만들어서 pre-commit 과정에 포함시킬 수 있다. 커밋하기전에 requirements가 변경되었는지 (추가 package install check) 체크할 수 있다!

grep 활용하기

command_result=($(ls))
grep_result_list=($(grep -oh 'nuri' ./testFile.txt))

ping 활용하기

#!/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

다양한 자료구조, Map

# 기본 문법
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

c와 같은 컴파일 언어 빌드 또는 컴파일링 과정 shell script

#!/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 자체 기본문법에만 집중하고, 언급한 키워드 내용은 다음 글에서 다시 다루려고 한다.


출처

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

0개의 댓글