...
Shell Script 란
Shell Script는 Shell이나 command line 인터프리터에서 구동되도록 작성된 스크립트다.
윈도우에서는 batch(배치파일, .bat) 을 한번쯤은 본적이 있을 것이다. bat 파일을 실행하면 프로그램이 자동으로 혼자 쭉쭉 실행되는데, 이러한 bat파일의 리눅스 버젼이 바로 shell script라고 봐도 된다. 물론 윈도우와 리눅스의 스크립트는 서로 다른 문법으로 작동된다.
정리를 하자면, 우리가 터미널에서 한땀한땀 입력했던 여러 명령어들을 하나의 스크립트 파일에 나열하여, 스크립트 파일이 자동으로 실행 함으로 효율적이면서 간편하게 작업 처리를 할 수 있는 것이다. 다만 Shell Script는 Interpreter 방식이므로, 한줄 한줄 읽어 실행함으로 다소 속도가 느리다는 단점이 있다.
쉘 스크립트 문법 정리 (Shell Programming)
쉘 스크립트 문법 검사 사이트
쉘 작성 첫번째 행 - Shebang
쉘 스크립트를 작성할 때는 항상 기억해야 할 것이 있다.
바로 첫 번째행이 다음과 같이 작성 해줘야 하는 점이다.
#!/usr/bin/bash
echo $(which bash) # 디렉토리 위치 출력
스크립트 첫 번째 줄에 작성한 내용은 쉘 스크립트가 실행될 때 어떤 쉘로 스크립트를 실행할지 정의하는 곳 이다.
쉘에는 여러 버젼이 있다.
[유닉스 쉘 종류]
- sh : 초기의 유닉스 쉘(Bourne shell), 1977년에 발표
- ksh : 콘 쉘이라고도 불리며 1983년 데이비드 콘이 개발했으며 sh를 확장하여 만듬.
- csh : 1978년 버클리 대학에서 C언어를 기반으로 만든 쉘.
- bash : 1987년 브라이언 폭스에 의해 만들어진 쉘로 sh와 대부분 호환.
이 강의에는 가장 널리쓰이는 bash 쉘을 사용하는 스크립트로 연재할 예정이니, 위와 같이 정의하여 리눅스의 표준 셀인 bash로 실행하겠다는 선언을 하겠다는 말이다.
이러한 쉘 선언문을 Shebang이라고 한다.
vi로 쉘 스크립트 파일을 작성하고, 저장을 해준다.
그리고 실행권한(x)를 주고 쉘을 실행하면, bash의 절대경로가 출력되게 된다.
$ vi script.sh
# ... 작성후 종료
$ chmod +x script.sh or chmod 755 script.sh
$ ./script.sh # 쉘 스크립트 실행
$ vi script.sh
# ... 작성후 종료
$ bash script.sh # bash 로 실행하면 권한을 주지 않고도 실행된다.
쉘 변수 선언
변수의 타입에는 로컬변수와 전역변수, 환경변수, 예약변수, 매개변수 등 다양히이 존재한다.
- 변수는 대, 소문자를 구별한다.
- 변수의 이름은 숫자를 포함할 수 있지만, 숫자로 시작할 수 없다.
- 변수에는 모든 값을 문자열로 저장된다.
- 변수에는 자료형을 기입하지 않는다. (int number, char names[10]), 즉 아무런 값을 다 넣을 수 있다.
- 값을 사용할 때는 변수명 앞에 특수문자 "$"를 사용한다. (Ex. echo ${data})
- 값을 대입(삽입)할 때는 특수문자 "$"를 사용하지 않는다. (Ex. data=mac)
- 변수를 생성할 때는 "=" 대입문자 앞뒤로 공백이 없어야 한다. (Ex. data="abcd")
#!/usr/bin/bash
name="inpa" # 변수 선언 및 대입
pass=123123 # 따옴표로 감싸든 말든 문자열로 저장됨
echo $name # {}가 있으나 없으나 $만으로 변수의 값을 넣어줄 수 있으나, 문자열을 붙여서 쓸려면 ${} 를 사용해야 한다.
echo "my name is mr.${name}"
printf "%s" $pass
echo는 개행을 포함한채 출력한다. C언어의 put으로 생각해도 된다.
printf는 개행을 포함하지 않은채 출력한다. C언어의 printf와 같다
전역 변수 & 지역 변수
쉘에서 선언된 변수는 기본적으로 전역 변수(global variable)다.
단, 함수 안에서만 지역 변수(local variable)를 사용할 수 있는데 사용할려면 변수 명 앞에 local을 붙여주면 된다.
# 기본적으로 전역 변수로 지정
string="hello world"
function string_test() {
# local을 붙여야 지역변수로 인식. 만일 local을 빼면 전역변수 덮어쓰기가 되버림
local string="hello local @@"
echo ${string}
}
# 함수 호출
string_test # > hello local @@
echo ${string} # > hello world
# 변수 초기화
unset string
변수 타입 지정
기본적으로 Bash 변수는 문자열만 저장한다.
다만, 여타 프로그래밍 언어같이 변수 자료형 타입을 미리 지정해주는 문법도 존재한다.
# -r 읽기 전용 타입 (상수 const라고 보면 된다)
declare -r var1
readonly var1
# -i 정수형 타입
declare -i number
number=3
echo "number = $number" # number = 3
# -a 배열 타입
declare -a indices
# -A 연관배열(MAP) 타입
declare -A map
# -f 함수 타입
declare -f
# -x 환경변수(export) 지정
declare -x var3 # 스크립트 외부 환경에서도 이 변수를 쓸 수 있게 해준다.
환경 변수
쉘 스크립트에서 변수 명 앞에 export을 붙여주면 환경 변수(environment variable)로 설정되어 자식 스크립트에서 사용 가능하다.
다만 환경 변수 사용시 시스템에 미리 정의된 예약 변수(reserved variable)와 변수명이 겹치지 않게 주의하자.
# /home/export_test.sh 파일을 만들고 작성
#!/usr/bin/bash
echo ${hello_world}
# 환경 변수 선언
export hello_world="global hello world"
# 자식 스크립트 호출은 스크립트 경로을 쓰면된다.
/home/export_test.sh
# > 자식스크립트의 코드에서 부모스크립트에서 정의한 hello_world변수값이 출력된다.
매개 변수
프로그램에서도 실행할때 인자를 주듯 쉘 스크립트도 역시 그렇게 할 수 있다.
실행한 스크립트 이름은 ${0}, 그 이후는 전달받은 인자 값들이다(${1}, ${2}, ...)
종류 | 설명 |
$0 | 실행된 셸 스크립트명 |
$1 | 스크립트에 넘겨진 첫 번째 아규먼트 |
$2 | 스크립트에 넘겨진 두 번째 아규먼트 |
$3 S4 등등...쭈욱 이후 $숫자 | 그 이후 해당되는 아규먼트 |
$# | 아규먼트 개수 |
$* | 스크립트에 전달된 인자 전체를 하나의 변수에 저장하면 IFS 변수의 첫 번째 문자로 구분 |
$@ | $*와 동일한데 다른 점은 IFS 환경 변수를 사용하지 않는다는 점. |
$! | 실행을 위해 백그라운드로 보내진 마지막 프로그램 프로세스 번호 |
$$ | 셸 스크립트의 PID |
$? | 실행한 뒤의 반환 값 (백그라운드로 실행된 것 제외) |
#!/bin/bash
echo "script name : ${0}"
echo "매개변수 갯수 : ${#}"
echo "전체 매개변수 값 : ${*}"
echo "전체 매개변수 값2 : ${@}"
echo "매개변수 1 : ${1}"
echo "매개변수 2 : ${2}"
$ ./script.sh 가 나 다 라 마 바 사
매개변수 갯수 : 7
전체 매개변수 값 : 가 나 다 라 마 바 사
전체 매개변수 값2 : 가 나 다 라 마 바 사
매개변수 1 : 가
매개변수 2 : 나
예약 변수
쉘 스크립트에서 사용자가 정해서 만들 수 없는 이미 정의된 변수가 존재한다.
이 변수명을 피해서 스크립트를 작성해야 한다.
변수 | 설명 |
HOME | 사용자 홈 디렉토리 |
PATH | 실행 파일의 경로 여러분이 chmod, mkdir 등의 명령어들은 /bin이나 /usr/bin, /sbin에 위치하는데, 이 경로들을 PATH 지정하면 여러분들은 굳이 /bin/chmod를 입력하지 않고, chmod 입력만 해주면 된다. |
LANG | 프로그램 실행 시 지원되는 언어 |
UID | 사용자의 UID |
SHELL | 사용자가 로그인시 실행되는 쉘 |
USER | 사용자의 계정 이름 |
FUNCNAME | 현재 실행되고 있는 함수 이름 |
TERM | 로그인 터미널 |
이외의 변수 명령어
- set : 셸 변수를 출력하는 명령어
- env : 환경 변수를 출력하는 명령어
- export : 특정 변수의 범위를 환경 변수의 데이터 공간으로 전송하여 자식 프로세스에서도 특정 변수를 사용 가능하게 한다. 전역 변수의 개념
- unset : 선언된 변수를 제거한다.
쉘 이스케이프 문자
- \f : 앞 문자열만큼 열을 밀어서 이동
- \n : 새로운 줄로 바꾼다
- \r : 앞 문자열의 앞부분부터 뒷문자열 만큼 대체하고 반환한다.
- \t : 탭 만큼 띄운다.
쉘 산술 연산
쉘 스크립트의 변수 산술연산은 다른 언어의 비해 간단치 않다.
앞서 말했듯이 Bash 변수는 본질적으로 문자열이라 별도의 특수한 문법을 사용해 연산을 해야 한다.
그러면 Bash가 알아서 형변환 하여 정수 연산이나 변수를 비교해 준다.
Bash에서는 계산을 처리할 수 있는 다음 3가지 문법을 제공한다.
- expr
- let
- $(( ))
expr 연산자
- expr는 역따옴표를 반드시 감싸준다. 역따옴표 대신 $(( )) 해줘도 동작은 한다.
- expr을 사용할 때 피연산자와 연산자 사이에 공백이 필요하다.
- 산술 연산할때 우선순위를 지정하기위해 괄호를 사용하려면 \처리를 해줘야 한다.
- 곱셈 문자 *는 \처리를 해주어야 한다.
#!/bin/bash
number1=10
number2=20
plus=`expr $number1 + $number2`
minus=`expr $number1 - $number2`
mul=`expr $number1 \* $number2` # 곱셈에는 \* 를 이용한다.
div=`expr $number1 / $number2`
rem=`expr $number1 % $number2`
echo "plus: ${plus}"
echo "minus: ${minus}"
echo "mul: ${mul}"
echo "div: ${div}"
echo "rem: ${rem}"
$ ./script.sh
plus: 30
minus: -10
mul: 200
div: 0
rem: 10
# 우선순위 산술 연산을 할때는 괄호를 문자 처리해야 한다.
# 연산자 *와 괄호() 앞에는 역슬래시와 같이 사용
num=`expr \( 3 \* 5 \) / 4 + 7`
echo $num
let 연산자
num1=42
num2=9
let re=num1+num2 #Add
echo "add:$re"
let re=num1-num2 #Sub
echo "sub:$re"
let re=num1*num2 #Mul
echo "mul:$re"
let re=num1/num2 #Div
echo "div:$re"
let re=num1%num2 #Mod
echo "mod:$re"
$ ./script.sh
add:51
sub:33
mul:378
div:4
mod:6
$(( )) 연산자
num1=42
num2=9
echo add:$((num1+num2))
echo sub:$((num1-num2))
echo mul:$((num1*num2))
echo div:$((num1/num2))
echo mod:$((num1%num2))
$ ./script.sh
add:51
sub:33
mul:378
div:4
mod:6
쉘 주석
쉘스크립트를 작성할때 한 줄은 쉽게 '#'를 통해 주석이 가능하지만,
블록을 주석하기 위해서는 완전히 다른 문법을 사용해야 한다.
:<<"END" 이 구간부터 주석이 시작되는 블록이고, END 로 주석이 끝나는 블록을 명시해 주면 된다.
echo "여기는 출력된다."
#echo " 여기는 주석처리"
<코드라인>
: << "END" #주석 시작
echo "여기서부터 "
echo "test_01"
echo "test_02"
echo "test_03"
echo "test_04"
echo "test_05"
echo "test_06"
echo "test_07"
echo "여기까지 주석처리 됨"
END #주석 끝
echo "여기는 주석이 안되어 있어 출력 "
vi 에디터로 여러줄 주석 방법
기본 여러줄 주석 문법이 별로라면, vi 단축키를 이용한 주석 처리 방법이 있다.
- vi 편집기를 열고 ctrl + v를 눌러 비주얼 모드로 진입한다. (비주얼 모드는 vim 이 설치 되어 있어야 한다.)
- 주석을 처리하고자 하는 라인까지 방향키 또는 vi방향키를 이용하여 이동한다.
- shift + i 누르고 #을 입력한다.
- esc 키를 여러번 누른다.
- 주석 처리 됨을 확인한다.
쉘 조건문
if 문
배쉬의 if문의 특이한 점은 fi 와 대괄호[ ] 이다.
여타 언어와 달리 중괄호를 안쓰기 떄문에 fi로 if문의 끝을 알려주어야 하며,
주의해야할 점은 if문 뒤에 나오는 대괄호 [ ] 와 조건식 사이에는 반드시 공백이 존재해야 한다.
if [ 값1 조건식 값2 ]
then
수행1
else
수행2
fi
# 가독성 좋기 위해 then을 if [] 와 붙여쓰려면 반드시 세미콜론 ; 을 써야한다.
if [ 값1 조건식 값2 ]; then
수행1
else
수행2
fi
비교 연산
문자1 = 문자2 # 문자1 과 문자2가 일치 (sql같이 = 하나만 써도 일치로 인식)
문자1 == 문자2 # 문자1 과 문자2가 일치
문자1 != 문자2 # 문자1 과 문자2가 일치하지 않음
-z 문자 # 문자가 null 이면 참
-n 문자 # 문자가 null 이 아니면 참
문자 == 패턴 # 문자열이 패턴과 일치
문자 != 패턴 # 문자열이 패턴과 일치하지 않음
값1 -eq 값2 # 값이 같음(equal)
값1 -ne 값2 # 값이 같지 않음(not equal)
값1 -lt 값2 # 값1이 값2보다 작음(less than)
값1 -le 값2 # 값1이 값2보다 작거나 같음(less or equal)
값1 -gt 값2 # 값1이 값2보다 큼(greater than)
값1 -ge 값2 # 값1이 값2보다 크거나 같음(greater or equal)
if [ ${a} -eq ${b} ]; then
echo "a와 b는 같다."
fi
if [ ${a} -ne ${b} ]; then
echo "a와 b는 같지 않다."
fi
if [ ${a} -gt ${b} ]; then
echo "a가 b보다 크다."
fi
if [ ${a} -ge ${b} ]; then
echo "a가 b보다 크거나 같다."
fi
if [ ${a} -lt ${b} ]; then
echo "a가 b보다 작다."
fi
if [ ${a} -le ${b} ]; then
echo "a가 b보다 작거나 같다."
fi
# 한줄로 작성
if [ ${num1} -lt ${num2} ]; then echo "yes"; fi
num1=35
num2=48
# 이중 소괄호를 쓰면 조건문을 문자 대신 기호로 표현 가능하다. 단, 소괄호 안에 따옴표 쓰면 안된다.
if (( ${num1} < ${num2} )); then
echo "yes"
fi
if (( ($num1 * $num2) - $num2 > 200 )); then
echo ">200"
else
echo "<200"
fi
이중 괄호 (( expression ))
expression 에는 수식이나 비교 표현식이 들어갈 수 있다.
! 논리 부정
~ 비트 부정
** 지수화
<< 비트 왼쪽 쉬프트
>> 비트 오른쪽 쉬프트
& 비트 단위AND
| 비트 단위 OR
&& 논리 AND
|| 논리 OR
num++ 후위증가
num-- 후위감소++
num 전위증가
--num 전위감소
파일 검사
if [ -d ${변수} ]; then # ${변수}의 디렉토리가 존재하면 참
if [ ! -d ${변수} ]; then # ${변수}의 디렉토리가 존재하지 않으면 참
if [ -e ${변수} ]; then # ${변수}라는 파일이 존재하면 참
if [ ! -e ${변수} ]; then # ${변수}라는 파일이 존재하지 않으면 참
if [ -r ${변수} ]; then # 파일을 읽을 수 있으면 참
if [ -w ${변수} ]; then # 파일을 쓸 수 있으면 참
if [ -x ${변수} ]; then # 파일을 실행할 수 있으면 참
if [ -s ${변수} ]; then # 파일의 크기가 0보다 크면 참
if [ -L ${변수} ]; then # 파일이 symbolic link이면 참
if [ -S ${변수} ]; then # 파일 타입이 소켓이면 참
if [ -f ${변수} ]; then # 파일이 정규 파일이면 참
if [ -c ${변수} ]; then # 파일이 문자 장치이면 참
if [ ${변수1} -nt ${변수2}]; then # 변수1의 파일이 변수2의 파일보다 최신 파일이면 참
if [ ${변수1} -ot ${변수2}]; then # 변수1의 파일이 변수2의 파일보다 최신이 아니면 참
if [ ${변수1} -ef ${변수2}]; then # 변수1의 파일과 변수2의 파일이 동일하면 참
논리 연산
조건1 -a 조건2 # AND
조건1 -o 조건2 # OR
조건1 && 조건2 # 양쪽 다 성립
조건1 || 조건2 # 한쪽 또는 양쪽다 성립
!조건 # 조건이 성립하지 않음
true # 조건이 언제나 성립
false # 조건이 언제나 성립하지 않음
if [ -f ${file1} -a -f ${file2} ]; then
echo 'file1과 file2는 모두 파일입니다.'
else
echo 'file1과 file2가 모두 파일인 것은 아닙니다.'
fi
if [ -f ${a} -a -d ${b} ]; then
echo "a는 파일이고 b는 디렉토리"
fi
VALUE=10
if [ ${VALUE} -gt 5 -a ${VALUE} -lt 15 ] ; then
echo "VALUE is greater than 5 and less than 15!"
fi
# 둘이 같은 문장이다.
if [ ${VALUE} -gt 5 ] && [ ${VALUE} -lt 15 ] ; then
echo "VALUE is greater than 5 and less than 15!"
fi
# 대괄호 두개를 써서 표현할 수 도 있다.
if [[ ${VALUE} -gt 5 && ${VALUE} -lt 15 ]] ; then
echo "VALUE is greater than 5 and less than 15!"
fi
# AND
if [ ${string1} == ${string2} ] && [ ${string3} == ${string4} ] ;
then
# OR
if [ ${string1} == ${string2} ] || [ ${string3} == ${string4} ];
then
# 다중 조건
if [[ ${string1} == ${string2} || ${string3} == ${string4} ]] && [ ${string5} == ${string6} ];
then
이중 대괄호
[[ "1.8.3" == 1.7.* ]]
&&, ||, = ~ * (정규식 매칭) 과 같은 확장 expression test 기능을 대괄호 내에 사용할 수 있게 한다.
그냥 대괄호의 개선버젼 정도로 생각하면 된다.
if elif else 문
#!/bin/bash
num1="10"
num2="10"
if [ ${num1} -lt ${num2} ]; then # "-lt", A가 B보다 작으면 True
echo "yes"
elif [ ${num1} -eq ${num2} ]; then # "-eq", A와 B가 서로 같으면 True
echo "bbb"
else
echo "no"
fi
# 이중 소괄호를 쓰면 논리연산자 기호 사용 가능
if (( ${num1} < ${num2} )); then
echo "yes"
elif (( ${num1} == ${num2} )); then
echo "bbb"
else
echo "no"
fi
# 한줄 작성
if [ ${num1} -lt ${num2} ]; then echo "yes"; elif [ ${num1} -eq ${num2} ]; then echo "bbb"; else echo "no"; fi
$ ./script.sh
bbb
bbb
bbb
bbb
case 문
C언어의 switch case 문법이라고 보면 된다.
대신, 리눅스 Bash의 switch문 특징이라면 각 case의 끝을 보면 세미콜론 2개로 종료한다.
거기다 case문 만의 강력한 특징이 있는데 바로 패턴을 사용할 수 있다는 점이다.
case 문자열 in
경우1)
명령 명령 명령
;;
경우2)
명령 명령 명령
;;
* )
명령 명령 명령
;;
esac
case ${var} in
"linux") echo "리눅스" ;; # 변수var값이 linux라면 실행
"unix") echo "유닉스" ;;
"windows") echo "윈도우즈" ;;
"MacOS") echo "맥OS" ;;
*) echo "머야" ;; # default 부분
esac
COUNTRY=korea
case $COUNTRY in
"korea"|"japan"|"china") # or 연산도 가능하다
echo "$COUNTRY is Asia"
;;
"USA"|"Canada"|"Mexico")
echo "$COUNTRY is Ameria"
;;
* )
echo "I don't know where is $COUNTRY"
;;
esac
date=210610 # 21년 6월 10일
case $date in # ? 는 임의의 한문자
??03?? | ??04?? | ??05??) echo 봄이구나~! ;; # 3월이거나 4월이거나 5월일 때
??06?? | ??07?? | ??08??) echo 여름이구나~! ;;
??09?? | ??10?? | ??11??) echo 가을이구나~! ;;
??12?? | ??01?? | ??02??) echo 겨울이구나~! ;;
*) echo 아 일하기 싫다 ;;
esac
str="abc"
case $str in
a*) # a이후 임의의 어떠한 문자라도 오면 일치 (0개~여러개)
echo "$str starts with a"
;;
a?) # a이후 반드시 임의의 하나의 문자가 오면 일치
echo "$str starts with a and has two words"
;;
a[bc]) # a이후 b나 c가 오면 일치
echo "$str starts with a and then followed by b or c "
;;
* ) # default
echo "I don't know where is $COUNTRY"
;;
esac
쉘 반복문
for 문
#!/bin/bash
# 초기값; 조건값; 증가값을 사용한 정통적인 for문
for ((i=1; i<=4; i++)); do
echo $i
done
반복문을 빠져 나갈때 : break
현재 반복문이나 조건을 건너 뛸때 : continue
for in 문
#!/bin/bash
# 루프 돌 데이터에 띄어쓰기가 있으면 각각 돌음
for x in 1 2 3 4 5
do
echo "${x}"
done
# 변수를 사용한 반복문
data="1 2 3 4 5"
for x in $data
do
echo ${x}
done
# 배열을 사용한 반복문
arr=(1 2 3 4 5)
for i in "${arr[@]}" # arr[@] : 배열 전체 출력
do
echo "${i}"
done
# sequence를 통한 for문. seq라는 프로세스가 순서대로 숫자를 출력해 주는 역할을 bash에 사용한 것이다.
for num in `seq 1 5`
do
echo $num
done
# range를 사용한 반복문. {..} 중괄호와 점 두개를 쓰면 range처리가 된다.
for x in {1..5}
do
echo ${x}
done
# 파일 리스트 출력
for line in `ls` # 역따옴표 써서 ls를 문자가 아닌 하나의 명령어로 실행
do
echo $line # 해당 위치에 파일이나 디렉토리들이 출력
done
# 한줄 문법 (한줄로 쓰면 터미널에서 직접 스크립트를 실행 할 수 있다)
for line in `ls`; do echo $line; done
세미콜론 ; 은 원래 엔터하고 작성했던걸 한줄로 작성할 수 있게 해주는 것으로 이해해도 된다.
세미콜론은 명령과 명령을 연결해주는 역할을 한다.
for var in $*
do
echo $var
done
--------------------------------------------------
# 쉘 인자를 통한 출력
$ ./test.sh 1 2 3 4 5
1
2
3
4
5
while 문
count=0
while [ ${count} -le 5 ];
do
echo ${count}
count=$(( ${count}+1 ))
done
count=0
while (( ${count} <= 5 )); # 이중괄호 사용하면 논리기호 사용 가능
do
echo ${count}
count=$(( ${count}+1 ))
done
$ ./script.sh
0
1
2
3
4
5
unitil 문
- 수행 조건이 false 일때 실행되는 루프문이다.
- 한다미로 while의 반대버젼이라고 보면 된다. (while은 조건이 true 면 루프)
count2=10
until [ ${count2} -le 5 ]; do
echo ${count2}
count2=$(( ${count2}-1 ))
done
$ ./script.sh
10
9
8
7
6
쉘 배열문
배열 생성 / 추가
#!/bin/bash
# 배열의 크기 지정없이 배열 변수 선언
# 굳이 'declare -a' 명령으로 선언하지 않아도 바로 배열 변수 사용 가능함
declare -a array
arr=("test1" "test2" "test3") # 배열 선언 및 지정
echo ${arr[0]} # test1
# 기존 배열에 1개의 배열 값 추가 3가지 방법
arr[3]="test4"
arr+=("test5")
arr[${#arr[@]}]="test6" # 배열 길이를 인덱스로 사용해 push
echo ${arr[@]} # arr의 모든 데이터 출력
echo ${arr[*]} # arr의 모든 데이터 출력
echo ${#arr[@]} # arr 배열 길이 출력
echo ${arr[@]:2:3} # 2부터 3개의 요소
$ ./script.sh
test1
test1 test2 test3 test4 test5 test6
test1 test2 test3 test4 test5 test6
6
test3 test4 test5
리눅스 쉘은 1차원 배열만 지원한다.
배열 원소 삭제
- /를 사용해 해당 문자열 부분이 있으면 삭제 할 수 있다.
- 다만 unset을 이용해 삭제를 권고하는 편이다.
arr=(1 2 3)
remove_element=(3)
arr=( "${arr[@]/$remove_element}" ) # 배열 1 2 3 에서 / 3을 없앰
echo ${arr[@]} # > 1 2
arr=("abc" "def" "defghi")
unset arr[1] # 배열 특정 인덱스 요소 삭제
echo ${arr[@]} > # abc defghi
unset array # 배열 전체 지우기
연관배열 (MAP)
key와 value 타입으로 저장된 배열을 말한다.
프로그래밍 고급언어에서 자주 등장하는 자료형 타입인데, PHP의 연관배열, 파이썬의 딕셔너리, 자바스크립트의 MAP 자료형 이라고 봐도 된다.
# 연관배열 생성
declare -A map=([hello]='world' [long]='long long long string' [what is it]=123)
declare -p map # 연관배열 정보 출력
# > declare -A map=([long]="long long long string" ["what is it"]="123" [hello]="world" )
echo "map[hello]=${map[hello]}"
# > map[hello]=world
key=hello # 변수를 인덱스로 넣어줘도 된다. (MAP의 특성)
echo "map[key]=${map[${key}]}"
# > map[key]=world
# 연관배열 value 값 모두 출력 (MAP은 순서를 보장하지않는다)
echo "all=${map[@]}" # > long long long string 123 world
# 연관배열 key 인덱스 모두 출력 (MAP은 순서를 보장하지않는다)
echo "keys=${!map[@]}" # > long what is it hello
# 연관배열 길이 출력
echo "length=${#map[@]}" # > length=3
# 원소 추가
map+=([key1]=value)
map+=([key2]=value2 [key3]=value3)
map+=(['long key']='long long long value')
map['like a C++']='value!!!!!'
# 원소 삭제 (키로 삭제)
unset 'map[intput1]'
# 루프1: 순차적으로 접근
for i in "${map[@]}"; do
echo "${i}"
done
# long long long string
# 123
# world
# 루프2: 키로 접근
for key in "${!map[@]}"; do
echo "map[${key}]=${map[${key}]}"
done
# map[long]=long long long string
# map[what is it]=123
# map[hello]=world
쉘 인풋값 입력받기
C언어의 scanf() 같이 터미널에서 사용자로부터 인자를 입력 받아 변수로 사용할 수 있다.
echo -n "String input : "
read [변수명] # <- 터미널에서 입력된 인풋값이 변수에 저장되게 된다.
echo "user input : $변수명 # 입력된 값 출력
쉘 함수(Function)
다른 프로그래밍 언어와 달리 쉘 스크립트에서는 함수명 앞 function은 써주지 않아도 알아서 인식된다. 또한, 함수를 호출할때는 괄호를 써주지 않고 호출해야한다는 점이 다르다. 그리고 함수 호출 코드는 함수 코드보다 반드시 뒤에 있어야 된다. 함수 코드 보다 앞에서 호출 시 오류가 발생하기 때문이다.
#!/bin/bash
func(){
echo "func()"
}
function string_test() {
echo "string test"
echo "인자값: ${@}"
}
#함수 호출
func
# 함수에 인자값 전달하기(공백의로 뛰어서 2개의 인자값을 넘김)
string_test "hello" "world"
$ ./script.sh
func()
string test
인자값: hello world
함수 아규먼트
함수에 인자를 전달하는 방법은 함수를 호출하면서 공백으로 구분하여 이후 인자들을 하나씩 넣어주면 된다.
이렇게 넣어준 인자들은 함수 내부에서 다양한 방식으로 사용할 수 있는데,
대표적으로는 $1, $2처럼 인자가 들어온 순서대로 입력을 받아 사용하게 된다.
function test3()
{
param1=$1
param2=$2
echo $param1 # a
echo $param2 # b
echo $@ # 파라미터 전체 출력
}
test3 "a" "b"
명령어 종료 상태 코드
종료 코드란, exit 명령으로 프로그램을 종료시키면서 사용자에게 프로그램 종료의 이유를 알리기 위하여 반환하는 값이다.
쉘 스크립트 내에서 exit 명령어가 실행되면 스크립트가 종료되며 부모 프로세스에 종료 상태를 전달할 수 있는데 이 값은 프로그램 내에서 임의로 지정할 수도 있다.
#!/bin/bash
exit 16 # 강제 종료
echo "wtf" # 실행안됨
종료 상태는 리턴값(return value, return status)이라고 부르기도 하는데, 이전에 수행했던 명령어나 프로그램이 종료 될 때 넘겨주는 값을 의미하기도 하다.
종료 상태 또는 반환값이 중요한 이유는 쉘 스크립트를 작성하는데 있어서 특정 명령어의 성공 여부에 따라 분기해야 하는 경우 이전 명령어가 정상적으로 수행되었는지 아는 것이 필수적이기 때문이다.
예를 들어 여타 프로그래밍 고급언어에선, If 문에서 참/거짓을 판단할 때 0이 거짓이고 그 외 값은 참으로 인정하는데, 특이하게 Bash shell 에서는 반대로 생각해야 된다.
즉, 0 : 프로그램의 정상종료 , 그 외의 숫자 : 오류 인 셈이다.
이러한 특징을 이용해서, 만일 명령어가 성공할경우 종료 상태 코드를 얻어서 if문에서 -eq 0을 통해 검사해서 분기문을 구축할 수 있게 된다.
종료 상태 값은 위에서 배운 $? 으로 확인할 수 있다.
$ date -@ # 인수를 잘못 사용하여 일부러 명령어 오류발생 시킴 -> 종료 상태 코드 1 반환
date: invalid option -- '@'
Try 'date --help' for more information.
$ echo $? # 0 이 아닌 종료 상태 값은 모두 거짓에 해당
1
$ date +%Y # 정상적인 명령어 실행 -> 종료 상태 코드 0 반환
2015
$ echo $? # 정상종료 됐으므로 0 을 리턴. 참
0
#!/usr/bin/bash
$ date -@ # 명령어 실패 -> $? 에 상태 코드 1이 들어감
# $?(상태 코드)를 조건문에서 검사함. 만일 성공 코드(0) 일경우 실행
if [ $? -eq 0 ]; then
ls -asl # 위의 분기문에 걸러져서 이 명령은 실행이 안됨
fi
shell command 실행
$() : command substitution 이라고 한다. 괄호 안에 쓰여진 문자를 명령어로 인식하여 실행하고 결과값을 반환해준다. 즉 명령어를 수행하고, output을 $() 안에 넣어준다.
그리고 위에서 expert 할때 배웠던 백틱(역따옴표)도, expert뿐만 아니라 명령문을 그대로 써주면 $()와 같이 쉘 명령어로 인식해서 명령어 결과를 반환해 준다.
#!/bin/bash
# 그냥 date 문자열 출력
echo date
# 백틱으로 감싸주면 date 명령어가 실행되게 된다.
echo `date`
# $()도 마찬가지
echo $(date)
# shell execution
echo "I'm in `pwd`"
echo "I'm in $(pwd)"
$ bash test.sh
date
2021년 4월 10일 토요일 04시 10분 28초 KST
2021년 4월 10일 토요일 04시 10분 28초 KST
I'm in /src/shell
I'm in /src/shell
문자열 패턴 비교
패턴 비교는 일종의 문자열을 연산하는 것으로 특정한 pattern을 놓고 변수의 문자열 값이 일부분이라도 이 패턴과 일치하는지 검사할 때 쓰인다.
이 패턴에는 와일드카드 문자 ( *, ?, [ ] 를 포함한 문자 세트)를 포함해도 된다.
${#var} | 변수 var가 가지는 문자열의 길이를 구한다. | |||
${var:-word} | 1. 변수 var의 값이 null이 아니면 : var의 값을 반환, var의 값은 변하지 않는다. 2. 변수 var의 값이 null이면 : word의 값을 반환, var의 값은 변하지 않는다. |
|||
${var:=word} | 1. 변수 var의 값이 null이 아니면 : var의 값을 반환, var의 값은 변하지 않는다. 2. 변수 var의 값이 null이면 : word의 값을 반환, var의 값은 word의 값으로 대체된다. |
|||
${var:+word} | 1. 변수 var의 값이 null이 아니면 : word의 값을 반환, var의 값은 변하지 않는다. 2. 변수 var의 값이 null이면 : null을 반환, var의 값은 변하지 않는다. |
|||
${var:?word} | 1. 변수 var의 값이 null이 아니면 : var의 값을 반환, var의 값은 변하지 않는다. 2. 변수 var의 값이 null이면 : null이면 에러 메시지인 word를 출력하고, 쉘 스크립트를 종료한다. 2번 째 경우이고 word가 생략되었다면 "parameter null or not set" 라는 메시지를 출력하고 쉘 스크립트를 종료한다. |
${var%pattern} | 끝에서 부터 word와 패턴이 일치하는 var의 최소 부분(첫번째 일치)을 제거하고 나머지를 반환한다. | |||
${var%%pattern} | 끝에서 부터 word와 패턴이 일치하는 var의 최대 부분(마지막 일치)을 제거하고 나머지를 반환한다. | |||
${var#pattern} | 처음 부터 word와 패턴이 일치하는 var의 최소 부분(첫 번째 일치)을 제거하고 나머지 부분을 반환한다. | |||
${var##pattern} | 처음 부터 word와 패턴이 일치하는 var의 최대 부분(마지막 일 |
#!/bin/bash
# var not exists
unset var1
echo ${var1:-string2}
# var exists
var1=string1
echo ${var1:-string2}
var1=/var/log/apt
echo ${var1#*/}
echo ${var1##*/}
var2=/var/log/apt/log/ifconfig.cfg
echo ${var2%log*}
echo ${var2%%log*}
$ ./script.sh
string2
string1
var/log/apt
apt
/var/log/apt/
/var/
BASH 쉘 스크립트 연습문제 코드
구구단 출력
#!/usr/bin/bash
for i in {2..9}; do # 2에서 9까지 레인지 리스트를 순회
for j in {1..9}; do # 1에서 9까지 레인지 리스트를 순회
echo "$i * $j = $((i*j))" # $(( )) 는 괄호안의 문자를 산술 처리 해준다.
done
done
현재 디렉토리에 있는 txt파일들 실행권한 다 주기
터미널에서 일일히 해도 되지만, 스크립트로 한방에 해보자.
#!/sur/bin/bash
for file in `ls *.txt` # 역따옴표``로 묶어 ls를 문자가 아닌 쉘명령어로 인식하여, txt파일 목록들을 얻어 순회
do
chmod +x $file # 각 파일마다 실행권한 추가
echo "${file}실행권한 추가 완료"
done
인수로 받은 파일 디렉토리와 파일 이름 출력
터미널에서 인수를 받으면 인수에 맞는 디렉토리와 파일명을 출력
#!/bin/bash
# $()는 백틱과 같이 안의 인수를 명령어 실행 처리
# 터미널에서 받은 인수 $0을 dirname하여 디렉토리명을 얻고 cd 이동하고 pwd로 절대경로값을 변수에 저장
scriptFolder=$(cd $(dirname "$0") && pwd)
# 절대경로값을 인수로 하여 basename 파일명을 변수에 저장
scriptName=$(basename $scriptFolder)
echo "scriptFolder: $scriptFolder"
echo "scriptName: $scriptName"
$ bash test.sh
scriptFolder: /Users/src/shell
scriptName: test.sh
마지막 작업의 종료 상태 확인
rm 명령어의 성공/실패 여부에 따라 결과동작을 종료코드 변수 $? 와 분기문을 통해 구현이 가능하다.
정말 많이 사용하는 패턴이니 꼭 익히자.
위에서 명령어 종료 상태 코드 섹션에서 해당 원리에 대해 설명했다. 모르면 다시 올려보자.
#!/bin/bash
# 파일 경로를 변수에 저장
fileToRemove="/tmp/some-file"
# 파일 삭제 시도. 만일 오류가 나면 오류 재지정으로 보내서 오류 출력되지 않게 처리
rm "$fileToRemove" 2> /dev/null
# 파일 삭제 성공(0) 실패(1) 여부를 따져 분기문 처리
if [ $? -eq 0 ]; then
echo "Success removing file: $fileToRemove"
else
echo "File does not exist: $fileToRemove"
fi
# 만일 /tmp/some-file 이라는 경로 파일이 존재하지 않을 경우
$ bash test.sh
File does not exist: /tmp/some-file
프로그램의 실행시간 측정
아래의 프로그램은 인자로 주어진 명령을 실행하고 걸린시간을 나노초 단위로 측정한다.
#!/bin/bash
# 만일 사용자가 인자를 주지 않았을 경우.. ($#는 인자의 모든 갯수)
if [ $# -eq 0 ]; then
echo "Input the filename, please..."
exit 0 # 스크립트 종료
fit
Command=$1 # 인자를 변수에 저장
echo $Command
time1=`date +%s.%N` # 현재 시간 구함.
$1 # 인자를 명령어로서 실행
time2=`date +%s.%N` # %s : 타임스탬프 구하기. %N : 나노초로 포맷
diff=`echo "$time2 - $time1" | bc` # 실수 연산을 하려면 bc 명령어를 활용한다
echo "======================================="
echo "걸린시간 : $diff"
로그 파일 정리하기
로그 파일 중에 2일 이상 지난 파일들은 압축을 해서 보관해라
압축된 로그 파일 중에 3일 이상 경과한 것들은 삭제해라
#!/bin/bash
LOGDIR=/var/log # 디렉토리 변수 선언
GZIPDAY=1
DELDAY=2
cd $LOGDIR # 로그가 저장되어있는 디렉토리로 이동
echo "cd $LOGDIR"
# 일반파일 타입이고 파일명이 log, log.1 ... 로 끝나고 -mtime +1(수정된지 2일 지난) 파일들 검색하여 gzip 압축한다.
# 2> 단 중간에 오류나도 코드가 죽지않고 오류 리다이렉션을 통해 넘기고 진행 (try.. catch 개념)
sudo find . -type f -name '*log.?' -mtime +$GZIPDAY -exec bash -c "gzip {}" \; 2>
# 일반파일 타입이고 파일확장자가 .gz이고, -mtime +2(수정된지 3일 지난) 파일들 검색하여 삭제한다.
sudo find . -type f -name '*.gz' -mtime +$DELDAY -exec bash -c "rm -f {}" \; 2>
cd 명령은 종료 상태 값을 확인해야 한다
아래와 같이 없는 디렉터리에 접근하려다가 실패할 경우 현재 디렉터리에 있는 모든 파일들을 날려버릴 수 있다.
#!/bin/bash
cd ~/tempdir # 디렉토리 이동
rm -rf *
# 원래대로라면 tempdir에 있는 모든 파일을 지우는 것이겠지만,
# 만일 cd가 실패했을 경우 엉뚱한 디렉토리 파일을 다 날릴 수 있다.
따라서 반드시 cd 명령의 성공 여부를 체크해야 한다.
#!/bin/bash
# and 연산자를 써서 앞의 명령어가 성공해야 뒤의 명령어가 실행되도록
cd ~/tempdir && rm -rf *
# 앞의 명령어가 실패하면, 뒤의 명령어를 실행해서 에러메세지를 출력하고 exit 1 종료한다.
# >&2는 모든 출력을 강제로 쉘 스크립트의 표준 에러로 출력한다.
cd ~/tempdir || { echo >&2 "cd ~/tempdir failed"; exit 1 ;}
자동 백업하기
#!/bin/bash
# -z는 null 검사
if [ -z $1 ] || [ -z $2 ]; then
echo usage : $0 sourcedir targetidir # 만일 터미널에서 인수 2개를 안써줬을 경우 사용 설명서 출력
else
SRCDIR=$1 # 인자를 변수에 넣음
DSTDIR=$2
BACKUPFILE=backup.$(date +%y%m%d%H%M%S).tar.gz # 파일명 형식을 backup.날짜.tar.gz 로 설정
if [ -d $DSTDIR ]; then # 만일 해당 디렉토리가 존재하면
tar -cvzf $DSTDIR/$BACKUPFILE $SRCDIR # 해당 디렉토리에 파일 압축 실행
else # 존재하지 않는다면
mkdir $DSTDIR # 디렉토리 만들고
tar -cvzf $DSTDIR/$BACKUPFILE $SRCDIR # 해당 디렉토리에 파일 압축 실행
fi
fi
< 쉘 스크립트를 실행해서 backupdir/ 디렉토리에 logfile 파일을 아카이빙 압축해서 보관 >
< 파일명 형식은 backup.현재날짜.tar.gz >
$ bash script.sh logfile backupdir
쉘 스크립트 난수 생성 (랜덤 숫자)
#!/bin/bash
# 0부터 9까지의 랜덤 숫자 생성
random_num="$(($RANDOM % 10))"
echo $random_num
# 1부터 10까지의 랜덤 숫자 생성
random_num="$(($RANDOM % 10+1))"
echo $random_num
# 100부터 110까지의 랜덤 숫자 생성
random_num="$(($RANDOM % 11+100))"
echo $random_num
# 200부터 300까지의 랜덤 숫자 생성
random_num="$(($RANDOM % 101+200))"
echo $random_num
데이터를 솎아내서 백업하기
ls -al 의 결과에서 '합계' 와 상대경로 ., .. 을 제외하고 나머지를 파일명과 파일사이즈만 출력하고 그 결과를 파일에 저장
#!/bin/bash
PRE_IFS=$IFS # IFS 백업
IFS="
" # IFS를 개행으로 변환
TOT=0 # 파일용량 합계 변수
cd /home/inpa # 정해진 위치에서
FileName="result.txt"
touch $FileName # 결과를 저장할 빈 파일 생성
echo "---------------------------------------------"
for i in `ls -al /`; do # ls 한 결과 내용를 루프
S=`echo $i | awk '{print $5}'` # 한 행(레코드)의 5번째 필드(파일사이즈) 저장
F=`echo $i | awk '{print $9}'` # 한 행(레코드)의 9번째 필드(파일명) 저장
# 만일 상대경로나 빈칸이면 continue
if [[ ${F} == "." || ${F} == ".." || ${F} == "" ]]; then
continue
fi
#TOT=$(( ${TOT} += ${S} )) 이 수식은 실행되지 않는다. 왜냐하면 awk한 결과는 문자열로 저장되기 떄문에 $(()) 산술연산이 안되기 때문
TOT=`expr $TOT + $S` # expr는 자식프로세스로 터미널에서 실행한 결과이기 때문에 상관없음
echo "$S $F" >> $FileName # 출력 결과를 파일로 리다이렉션 이어쓰기
done
echo $TOT # 파일사이즈 합계 출력
IFS=$PRE_IFS # IFS 롤백
# 참고자료
https://blog.gaerae.com/2015/01/bash-hello-world.html
https://jhnyang.tistory.com/175
https://jhnyang.tistory.com/153
https://se.uzoogom.com/140
https://118k.tistory.com/324
https://probe29.tistory.com/47
https://velog.io/@pingping95/Bash-Shell-Script-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%85%90
https://reakwon.tistory.com/136
https://shlee1990.tistory.com/917
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.