[Linux] 쉘(shell) 스크립트 기초
[Linux] 쉘(shell) 스크립트 기초
l Linux
쉘 스크립트(shell script)는 셸이나 명령줄 인터프리터(Interpreter)에서 실행되도록 작성되었거나 한 운영 체제를 위해 쓰인 스크립트이다. 쉘 스크립트가 수행하는 일반 기능으로는 파일 이용, 프로그램 실행, 문자열 출력 등이 있다. 쉘 스크립트에서는 .sh라는 파일 확장자를 가진 파일이 특정 종류의 쉘 스크립트를 가리키는 것이 보통이지만, 대부분의 셸 스크립트는 파일 확장자를 지니지 않는다. 쉘 스크립트는 인터프리터 방식이므로 라인별로 읽어 실행함으로 속도가 다소 느리다는 단점이 있다.
쉘 스크립트라는 말은 유닉스 쉘을 위해 쓰인 스크립트를 말하는 반면, command.com(도스)과 cmd.exe (윈도우) 명령 줄 스크립트는 보통 배치 파일이라고 불린다. 최근엔 마이크로소프트가 개발한 CLI쉘인 파워쉘이 있다.
이번 포스트에서는 가장 널리 쓰이는 bash 쉘을 사용하는 스크립트를 설명한다.
첫 문장의 시작 (#!/bin/bash)
쉘 스크립트의 첫 번째줄에는 항상 #!/bin/bash가 기입되어 있어야 한다. 이 내용은 쉘이 실행될 때 어떤 쉘로 스크립트를 실행할지 정의하는 곳이다. 쉘에는 다양한 버전이 있다. 아래 예시는 hello.sh 라는 쉘 스크립트를 생성하고 실행하는 방법을 설명한다. 아래 스크립트를 hello.sh라는 파일을 생성하고 입력한다.
#!/bin/bash echo "Hello, Shell Script" |
아래 명령을 사용하여 스크립트를 실행하면 Hello, Shell Script를 출력한다. 스크립트 명령어에 대한 부분은 아래에서 자세히 설명하도록 한다. 만약 실행이 되지 않는다면 실행 권한이 없어 발생한것으로 chmod 777 hello.sh를 실행하여 권한 변경 후 실행한다.
$sh hello.sh |
주석 처리 (#)
쉘 스크립트에서 단일 행 주석은 샵(#)을 사용한다.
#!/bin/bash #여기는 주석입니다. #주석은 실행되지 않습니다. echo "Hello, Shell Script" |
블록 주석은 : <<”END” ~ ~ END 를 사용한다.
#!/bin/bash : << “END” #주석 시작 여기는 주석입니다. 주석은 실행되지 않습니다. END #주석 끝 echo "Hello, Shell Script" |
입력(read), 출력(echo)
echo는 출력, read는 입력을 할 수 있다. 위에서 이미 echo는 사용하였으므로 read를 사용하여 입력한 값을 출력하도록 스크립트를 작성해 본다. 입력 받은 값을 변수로 사용하려면 $기호를 사용한다.
#!/bin/bash read input_value echo "Hello, $input_value!" |
스크립트를 실행하면 입력값이 발생할 때까지 실행되지 않고 대기한다. hahaha라고 입력하면 입력 받은 값이 변수로 사용되어 결과가 출력된다.
$sh hello.sh hahaha |
쉘 변수 특징
l 변수의 타입에는 로컬변수와 전역변수, 환경변수, 예약변수, 매개변수 등 다양하다.
l 변수는 대, 소문자를 구별한다.
l 변수명에는 영문자, 숫자, 언더바( _ )가 사용된다.
l 변수의 이름은 숫자를 포함할 수 있지만, 숫자로 시작할 수 없다.
l 변수에는 모든 값을 문자열로 저장된다.
l 변수에는 자료형을 기입하지 않기 때문에 어떠한 값도 입력할 수 있다. (변수에 타입을 지정할 수도 있다.)
l 값을 사용할 때는 변수명 앞에 특수문자 "$"를 사용한다. (Ex. echo ${data})
l 값을 대입(삽입)할 때는 특수문자 "$"를 사용하지 않는다. (Ex. data=mac)
l 변수를 생성할 때는 "=" 대입문자 앞뒤로 공백이 없어야 한다. (Ex. data="abcd") 문자열인 경우 “ “로 감싼다.
l 하나의 변수에 한 개의 값만 보존된다.
l 변수 값이 덮어 쓰기되는 것을 방지하기 위해서는 readonly를 사용한다.
l 변수를 unset으로 삭제할 수 있다. (readonly로 선언된 것은 삭제가 불가능하다.)
쉘 스크립트에서 시스템이 사용하는 예약어가 있다. 몇 가지 대표적인 예약어이다.
변수 | 설명 |
HOME | 사용자 홈 디렉토리 의미 |
PATH | 실행 파일의 경로 |
LANG | 프로그램 실행 시 지원되는 언어 |
UID | 사용자의 UID |
SHELL | 사용자가 로그인시 실행되는 쉘 |
USER | 사용자 계정 이름 |
FUNCNAME | 현재 실행되고 있는 함수 이름 |
TERM | 로그인 터미널 |
Set | 셸 변수를 출력하는 명령어 |
Env | 환경 변수를 출력하는 명령어 |
Export | 특정 변수의 범위를 환경 변수의 데이터 공간으로 전송하여 자식 프로세스에서도 특정 변수를 사용 가능하게 한다. |
unset | 선언된 변수를 제거 |
전역변수, 지역변수
쉘에서 선언된 변수는 기본적으로 전역변수(global variable)이며, 함수안에서 지역변수(local variable)로 사용할 수 있는데, 이때에는 local을 붙여서 사용한다. 함수안에서 local을 사용하지 않으면 전역 변수의 값을 덮어쓰게 된다.
#!/bin/bash # 전역 변수 지정 string="hello shell, global variable" function string_test() { # local을 붙여야 지역변수로 인식. 만약 local을 빼면 전역변수에 덮어쓰기로 적용 local string="hello shell, local variable" echo ${string} } # 함수 호출 string_test # > hello shell, local variable echo ${string} # > hello shell, global variabl # 변수 초기화 unset string |
변수 타입 지정
기본적으로 Bash는 문자열만 저장하지만 데이터 타입을 지정할 수도 있다.
변수타입 | 설명 |
declare -r | 읽기 전용 타입 |
declare -i | 정수형 타입 |
declare -a | 배열 타입 |
declare -A | 연관배열(MAP) 타입 |
declare -f | 함수 타입 |
declare -x | 환경변수(export) 지정으로 외부 환경에서도 이 변수를 사용할 수 있다. |
읽기전용 변수 (readonly)
변수에 값을 덮어 쓸수 있지만 readonly로 지정한 경우 변수를 덮어쓸수 없다. 아래 예시는 readonly에서 변수 덮어쓰기를 시도할 때 발생한 오류를 확인할 수 있다.
#!/bin/bash var="변수1" VaR_2="변수2" echo "Var_2=$VaR_2" VaR_2="VaR_2를 변경합니다." echo ${VaR_2} readonly var var="readonly var 변수를 변경해 봅니다." |
변수 값의 치환
쉘 변수의 값을 치환할 때 사용하는 문법을 표로 정리하였다.
문법 | 설명 |
${var} | 변수 값을 바꿔 넣는다. |
${var:-word} | 변수가 아직 세팅되지 않거나 공백 문자열의 경우 word를 반환한다. var에는 저장되지 않는다. |
${var:=word} | 변수가 아직 세팅되지 않거나 공백 문자열의 word를 반환한다. var에 저장된다. |
${var:?word} | 변수가 아직 세팅되지 않거나 공백 문자열의 경우 치환에 실패하고, 스탠다드 에러에 에러가 표시된다. |
${var:+word} | 변수가 세팅되지 않은 경우 word가 반환된다. var에는 저장되지 않는다. |
#!/bin/sh echo "1 - ${var:-wordSetInEcho1}" echo "2 - var = ${var}" echo "3 - ${var:=wordSetInEcho3}" echo "4 - var = ${var}" unset var echo "5 - ${var:+wordSetInEcho5}" echo "6 - var = $var" var="newVarValue" echo "7 - ${var:+wordSetInEcho7}" echo "8 - var = $var" echo "9 - ${var:?StandardErrorMessage}" echo "10 - var = ${var}" |
문자열 패턴 비교는 아래 조건을 사용할 수 있다.
문법 | 설명 |
${var%pattern} | 끝에서 부터 word와 패턴이 일치하는 var의 최소 부분(첫번째 일치)을 제거하고 나머지를 반환 |
${var%%pattern} | 끝에서 부터 word와 패턴이 일치하는 var의 최대 부분(마지막 일치)을 제거하고 나머지를 반환 |
${var#pattern} | 처음 부터 word와 패턴이 일치하는 var의 최소 부분(첫 번째 일치)을 제거하고 나머지 부분을 반환 |
${var##pattern} | 처음 부터 word와 패턴이 일치하는 var의 최대 부분(마지막 일치)을 제거하고 나머지 부분을 반환 |
환경 변수 (export)
쉘 스크립트에서 변수명 앞에 export를 붙여 환경변수(environment variable)로 사용할 수 있다. 다만 환경변수 사용시 시스템에 미리 정의된 예약변수와 겹치지 않게 주의한다. 실습을 위해 두 개의 파일을 준비한다.
shell1.sh |
#!/bin/bash echo ${export_variable} |
shell2.sh |
#!/bin/bash #환경변수 선언 export export_variable="export test, hello shell" #변수값을 받아서 사용할 스크립트 호출 /파일 경로/shell1.sh |
shell2.sh를 실행한다. Export 변수의 값은 shell1.sh를 호출할 때 사용된다.
매개변수
프로그램에서 실행할 때 인자를 사용하듯 쉘 스크립트에서도 매개변수를 사용할 수 있다. 이 때 규칙이 있는데, 실행한 스크립트 이름은 ${0}, 이후 인자 값들은 ${1}. ${2}…으로 사용한다.
변수 | 기능 |
$0 | 스크립트명 |
$1 ~ $9 | 인수 1 번째부터 인수 9번째까지 사용 |
$# | 스크립트에 전달될 인수의 수 |
$* | 모든 인수를 모아 하나로 처리 |
$@ | 모든 인수를 각각 처리 |
$? | 직전에 실행한 커멘드의 종료 값(0 성공, 1 실패) |
$$ | 쉘 스크립트의 프로세스ID |
$! | 마지막으로 실행한 백그라운드 프로세스 ID |
아래 스크립트는 매개변수를 입력 받아 어떻게 사용되는지 알아보는 예시이다.
#!/bin/sh echo "\$0(script name): $0" echo "\$1(1st param): $1" echo "\$2(2nd param): $2" echo "\$#(number of param): $#" echo "\"\$*\": \"$*\"" echo "\"\$@\": \"$@\"" VAR="exit code is 0 " echo $? |
실행 결과는 아래와 같이 나타난다.
배열 (Array)
쉘 스크립트에서 배열은 1차원 배열만 지원하며 중괄호를 사용해야 한다. 배열 원소는 소괄호안에 공백으로 구분하며, 배열 안 원소는 문자열이든 숫자형이던 상관없이 쓸 수 있다.
#!/bin/bash arr=("hello" "shell" 1 2 3 4 5) echo "배열 전체 : ${arr[@]}" echo "배열 원소의 갯수 : ${#arr[@]}" echo "배열 첫번째 : ${arr}, 혹은 ${arr[0]}" echo "4번 index를 갖는 배열의 원소 : ${arr[4]}" arr[5]="four" echo "4번째 index를 갖는 배열의 원소 : ${arr[4]}" # 4번째 값 해제 unset arr[4] echo "4번 배열 값 삭제 완료" echo "4번 index를 갖는 배열의 원소 : ${arr[4]}" echo "5번 index를 갖는 배열의 원소 : ${arr[5]}" |
함수(function)
쉘 스크립트에서도 함수를 사용할 수 있다. 함수의 정의 형식은 아래와 같다. 주의할 점은 함수가 호출되기 전에 함수가 정의되어 있어야 하며 호출할 때는 괄호를 사용하지 않고 호출한다.
#!/bin/bash function func(){ echo "func()" } #함수 호출 func |
비교문(if..fi)
쉘에서는 비교문이 약간 특이하다. bash의 if문의 특이한 점은 fi 와 대괄호([ ]) 이다. 다른 언어와 달리 중괄호를 사용하지 않기 때문에 fi로 if문의 끝을 알려주어야 한다. 주의해야 할 점은 if문 뒤에 나오는 대괄호 [ ] 와 조건식 사이에는 반드시 공백이 존재해야 한다.
비교문에는 다양한 연산자를 사용할 수 있다.
연산자 | 설명 |
값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) |
문자열 비교
연산자 | 설명 |
문자1 = 문자2 | # 문자1 과 문자2가 일치 (sql같이 = 하나만 써도 일치로 인식) |
문자1 == 문자2 | # 문자1 과 문자2가 일치 |
문자1 != 문자2 | # 문자1 과 문자2가 일치하지 않음 |
-z 문자 | # 문자가 null 이면 참 |
-n 문자 | # 문자가 null 이 아니면 참 |
문자 == 패턴 | # 문자열이 패턴과 일치 |
문자 != 패턴 | # 문자열이 패턴과 일치하지 않음 |
논리 연산자
연산자 | 설명 |
조건1 -a 조건2 | # AND |
조건1 -o 조건2 | # OR |
조건1 && 조건2 | # 양쪽 다 성립 |
조건1 || 조건2 | # 한쪽 또는 양쪽다 성립 |
!조건 | # 조건이 성립하지 않음 |
true | # 조건이 언제나 성립 |
false | # 조건이 언제나 성립하지 않음 |
산술연산자 (쉘에서는 expr 숫자 연산자 숫자’ 형식으로 사용한다.)
연산자 | 의미 | 예시 |
+ | 덧셈 | echo `expr 10 + 20` => 30 |
- | 뺄셈 | echo `expr 20 - 10` => 10 |
\* | 제곱 | echo `expr 11 \* 11` => 121 |
/ | 나눗셈 | echo `expr 10 / 2` => 5 |
% | 나머지 | echo `expr 10 % 4` => 2 |
= | 저장 | a=$b b의 값은 a에 저장 |
== | 동일 | [ "$a" == "$b" ] $a과 $b가 동일하는 경우 TRUE가 반환 |
!= | 다름 | [ "$a" != "$b" ] $a과 $b가 동일하지 않는 경우 TRUE가 반환 |
예제로 살펴본다.
#!/bin/bash function func(){ a=10 b=5 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 } #함수 호출 func |
여러 조건을 사용할때, 다른 언어에서는 else if역할을 하는 것을 쉘 스크립트에서는 elif라는 것을 사용하여 else if와 동일한 역할을 할 수 있다. elif[ 조건 ]; then 형식으로 사용한다. 만약 반복문에서 사용하여 조건이 참일때 반복문을 멈추고 싶을때 break라는 키워드를 사용하여 반복문을 멈출 수 있다.
#!/bin/bash case ${1} in "linux") echo "리눅스" ;; "unix") echo "유닉스" ;; "windows") echo "윈도우즈" ;; "MacOS") echo "맥OS" ;; *) echo "해당없음" ;; esac |
파일 관련 조건문
쉘 스크립트는 명령어를 다루는 스크립트이기 때문에 파일이 존재하는지, 디렉토리가 존재하는지 등 파일과 관련된 조건 여부도 조건문을 통해 알 수 있다. 아래 표는 각 조건을 정리한 것이다. 참, 거짓을 판단할 때, 느낌표(!)를 사용하면 반대로 결과를 출력한다.
조건 | 설명 |
if [ -d ${변수} ]; then | ${변수}의 디렉토리가 존재하면 true. |
if [ ! -d ${변수} ]; then | ${변수}의 디렉토리가 존재하지 않으면 true. |
if [ -e ${변수} ]; then | ${변수}라는 파일이 존재하면 true. |
if [ ! -e ${변수} ]; then | ${변수}라는 파일이 존재하지 않으면 true. |
if [ -L ${변수} ]; then | 파일이 symbolic link이면 true. |
if [ -s ${변수} ]; then | 파일의 크기가 0보다 크면 true. |
if [ -S ${변수} ]; then | 파일 타입이 소켓이면 true. |
if [ -r ${변수} ]; then | 파일을 읽을 수 있으면 true. |
if [ -w ${변수} ]; then | 파일을 쓸 수 있으면 true. |
if [ -x ${변수} ]; then | 파일을 실행할 수 있으면 true. |
if [ -f ${변수} ]; then | 파일이 정규 파일이면 true. |
if [ -c ${변수} ]; then | 파일이 문자 장치이면 true. |
if [ ${변수1} -nt ${변수2}]; then | 변수1의 파일이 변수2의 파일보다 최신 파일이면 true. |
if [ ${변수1} -ot ${변수2}]; then | 변수1의 파일이 변수2의 파일보다 최신이 아니면 true. |
if [ ${변수1} -ef ${변수2}]; then | 변수1의 파일과 변수2의 파일이 동일하면 true. |
CASE문
일반적인 CASE 사용법과 비슷하지만 각 CASE 끝에 세미콜론을 2개씩 사용하는 특징이 있다. 그리고 CASE 끝은 ESAC로 닫아준다.
#!/bin/bash DRINK="coffee" case "$DRINK" in "beer") echo "맥주입니다" ;; "juice") echo "주스입니다" ;; "coffee") echo "개발자에겐 카페인 필수!" ;; esac |
CASE에서 OR 연산도 가능하다.
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 |
반복문(for)
반복문을 빠져나갈때에는 break를 사용하고, 현재 반복분이나 조건을 건너뛸때에는 continue를 사용한다.
#!/bin/bash # 초기값; 조건값; 증가값을 사용한 정통적인 for문 for ((i=1; i<=4; i++)); do echo $i done |
반복문(for in)
#!/bin/bash function func(){ echo "사용예1" for i in 1 2 3 4 5 do echo "${i}" done echo "사용예2" list="1 2 3 4 5" for i in ${list} do echo "${i}" done echo "사용예3" for i in {1..5} do echo "${i}" done echo "사용예4: 크기를 2만큼 증가시키면서 출력" for i in {1..5..2} do echo "${i}" done echo "사용예5: 배열을 이용" arr=(1 2 3 4 5) for i in "${arr[@]}" do echo "${i}" done echo "사용예6: C와 유사한 형식의 for문" for ((i=0; i<5; i++)); do echo "${i}" done } #함수 호출 func |
반복문 (while)
수행 조건이 true일때 실행되는 반복문이다.
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 |
반복문(until)
수행 조건이 false 일때 실행되는 반복문이다. 조건이 While의 반대라고 생각하면 된다.
count2=10 until [ ${count2} -le 5 ]; do echo ${count2} count2=$(( ${count2}-1 )) done |
종료 상태 코드
명령어가 실패할 경우 리턴되는 결과에 따라 다른 동작을 하려면 명령어의 종료 코드를 반환 받아 사용하면 된다. 명령어의 종료 코드는 $?를 사용하여 가장 최근 실행한 명령어의 종료 코드를 받을 수 있다. 정상적인 종료는 0이 반환되며 0이 아닌 값은 오류라고 판단할 수 있다. 아래 표는 종료 코드에 따른 설명을 정리한 것이다.
종료 코드 | 설명 |
0 | 성공 (Success) |
1 | 일반적 오류 (Catchall for general errors) |
2 | 쉘 내장 명령의 틀린 사용 (Misuse of shell builtins) |
126 | 파일이 실행 가능하지 않음 (Command invoked cannot execute) |
127 | 명령어를 찾을 수 없음 (Command not found) |
128 | 종료할 때 잘못된 인수 적용 (Invalid argument to exit) |
128+n | 치명적인 시그널 n 에러 (Fatal error signal “n”) |
130 | [Ctrl + C]키 조합에 의한 종료 (Script terminated by Control-C) |
쉘 이스케이프 문자
명령어 | 설명 |
\f | 앞 문자열만큼 열을 밀어서 이동한다. |
\n | 새로운 줄로 바꾼다. |
\r | 앞 문자열의 앞부분 부터 뒷문자열 만큼 대체하고 반환한다. |
\t | 탭 만큼 띄운다. |
이 외에도 다양한 쉘 명령이 있다. 쉘 명령을 잘 활용하면 리눅스 환경에서 시스템 관련 다양한 정보들을 스크립트로 얻을 수 있다.
[참고자료]
l https://ko.wikipedia.org/wiki/%EC%85%B8_%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8
l https://reakwon.tistory.com/136
l https://engineer-mole.tistory.com/200
2023-09-05 / Sungwook Kang / https://sungwookkang.com
리눅스, Linux, 쉘스크립트, 쉘사용법, 쉘명령어, shell command, shell script