Bash? 쉘? 스크립트?
쉘은 운영체제에 접근할 수 있으면서 사용자의 명령어를 해석해주는 프로그램이며, Bash 쉘은 쉘의 한 종류이다. 그리고 쉘 스크립트란 쉘 내에서 명령어 반복 처리 등의 목적을 위해 작성할 수 있는 프로그램이다. 간단하게 말하면 (안 간단함) 쉘 스크립트로 C언어처럼 for나 if를 사용해 명령어를 수행할 수 있다.
Bash 쉘의 장점
스크립트 설명 전 잠깐 다른 얘기를 하자면, bash 쉘은 표준 출력이나 산술 연산 등에 유리하다고 알려져있다. 또한 사용자의 명령어 입력을 홈 디렉토리의 .bash_history라는 파일에 보관한다는 장점이 있다.

파일을 직접 확인할 필요 없이, history 명령어를 사용하면 지금까지 사용한 명령어를 확인할 수 있다. 환경변수 HISTSIZE로 보관할 명령어 기록의 수를 정할 수 있으며 기본값은 1000이다. 히스토리 덕분에 기록이 남아있는 명령어를 다시 실행할 수 있다. 재실행 명령어는 다음과 같은 것들이 있다.
>> !! : 방금 전 명령 재실행
>> !n : 히스토리 번호 (history 명령어로 확인 가능)가 n인 명령 재실행
>> !시작문자열 : 시작문자열로 시작하는 마지막 명령 재실행 ex) !gcc
>> !?부분문자열 : 부분문자열을 포함하는 마지막 명령 재실행 ex) !?*.txt → 텍스트파일에 대해 수행한 마지막 명령
이제 더럽게도 깐깐한 쉘 스크립트 문법에 대해 알아보자. 내용은 다른 언어와 크게 다르지 않은데 띄어쓰기 등 자잘한 형식을 잘 지켜야 한다. 틀리기 쉬운 문법은 빨간 색으로 표시했다.
0. 쉘 스크립트 실행
기본적으로 쉘 스크립트는 표준입력으로 실행할 수도 있지만 주로 vi, gedit(메모장 같은거)를 활용해 스크립트 파일을 작성해 실행한다. bash 스크립트 파일은 .bash 확장자를 가지며 첫 줄에 #!/bin/bash 를 적는다. 참고로 쉘 스크립트에서 #는 주석이지만 #!는 주석이 아니고 셔뱅이라고 부르는 문자열이다. 간단히 설명하면 해당 스크립트를 /bin/bash 파일에서 실행하게 해주는 문장으로 웬만하면 빼먹지 말자.
처음 파일을 작성하면 실행 권한이 존재하지 않으므로 파일 실행을 위해선 chmod 명령어로 사용자에게 실행 권한을 부여해야 한다. 관습적으로 chmod 777 파일명 (8진수 모드) 또는 chmod +x 파일명 (기호 모드)를 사용하는 듯.
실행 권한까지 부여했으면 bash 파일명 또는 ./파일명 명령어로 파일을 실행한다. 파일명만 입력해도 실행되는 경우가 있는데 이는 해당 디렉토리가 환경변수 PATH에 등록돼있기 때문일 것이다. 그냥 '현재 디렉토리에 있는 이 파일을 실행한다' 라는 의미의 ./을 쓰자.
1. 변수
생성
쉘 스크립트에서는 변수명=값 형식으로 새 변수를 생성할 수 있다. 띄어쓰기 하지 말자. 띄어쓰기를 기준으로 변수 생성을 인식하기 때문에 다음과 같은 명령도 가능하다.
$ name=xorjs age=25 //name, age 변수 생성
띄어쓰기가 포함된 문자열을 한 변수에 저장하고 싶다면 문자열을 따옴표로 감싸주어야한다.
변수의 자료형을 설정하지는 않는데, 쉘 스크립트에 자료형 구분이 없는 것은 아니고 모든 변수가 기본적으로 문자열 형식 변수이기 때문이다. 원한다면 declare -옵션 변수명 명령어를 이용해 값을 할당하지 않고 선언만 할 수도 있다. -r은 읽기 전용(readonly, 바로 할당해야 함), -i는 정수형(integer), -a는 리스트형 변수(array)이다.
read 명령어를 사용하면 표준입력 내용을 변수 생성에 사용할 수 있다. read 변수1 ... 변수n 명령어는 한 줄을 입력받아 1번 단어를 변수1, 2번 단어를 변수2, ... n번부터 남은 모든 단어를 변수 n에 저장한다. 단어 수가 n보다 적으면 뒤의 변수들은 값을 갖지 않는다.
접근
$변수명으로 변수의 값을 사용할 수 있다. 가능하면 ${변수명}으로 접근하는 편이 안전하다.
프로그램을 짜다보면 다른 언어를 사용하던 습관 때문에 $를 까먹을 때가 많다. 이럴 때 쉘은 그 이름을 변수가 아닌 명령어로 식별해버린다. 참고로 문자열 안에서 변수 값을 사용하고 싶으면 큰따옴표 문자열을 사용해야 한다. 작은따옴표는 문자열을 있는 그대로 저장하는 반면 큰따옴표는 변수를 값으로 치환할 수 있다.

리스트 변수
배열처럼 한 변수에 여러 값을 저장할 수 있다. 리스트 변수는 변수명=(값1 값2 ... 값n) 형식으로 생성하면 된다. 저장할 값을 띄어쓰기로 구분하고 전체를 괄호로 감싼다. 여기서도 등호 좌우는 띄어쓰기 하면 안된다. 그냥 등호는 무조건 붙여쓴다고 생각하자.
리스트 변수에서의 접근은 배열과 비슷하다. 리스트 변수 list의 i번째 값은 ${list[i]}로 참조할 수 있다. 앞에 $를 붙이는 건 잊지 말자. 배열과 마찬가지로 인덱스는 0부터 시작한다. i 자리에 *를 넣으면 모든 원소를 참조한다. 그리고 일반적으로 '개수'를 의미하는 #를 붙인 ${#list[*]}는 원소의 개수, 즉 리스트의 크기를 의미한다.
변수명[인덱스]=값 형식으로 리스트 내부 원소의 값을 바꿀 수 있다. 그런데 이 인덱스가 배열의 크기를 넘어서면 어떻게 될까? 놀랍게도 오류가 나는 게 아니라 해당 인덱스에 값이 추가되어 배열이 확장된다.

cities 리스트에는 0, 1번 인덱스에만 원소가 존재한다. 그런데 2번 인덱스도 쌩까고 3번 인덱스에 값을 추가할 수 있다. 이 경우 정상적으로 추가는 되지만 2번 인덱스는 아예 비어있는 것으로 취급한다.

아직 값이 없는 인덱스에 접근해도 비슷한 결과를 얻는 것을 확인할 수 있다.
빌트인 변수
빌트인 변수는 쉘에서 사전 정의한 변수로, 프로그램 자체 (이름, 명령줄 인수)와 연관이 있다.
>> $0 : 쉘 스크립트 이름 (프로그램명)
>> $1~$9 : 1번째 ~ 9번째 명령줄 인수. 명령줄 인수란 스크립트를 실행하면서 같이 입력한 값들을 의미한다. test.bash 프로그램을 실행할 때 ./test.bash a.txt b.txt라고 입력했다면 $1은 a.txt, $2는 b.txt가 된다. $0가 프로그램명이 되는 것도 같은 맥락이다. 명령줄 인수가 10개가 넘어가면 제대로 작동하지 않는다.
>> $* : 모든 인수의 리스트
>> $@ : 모든 인수 자체
인수로 a, b, c가 들어왔다면 $*는 (a b c)가 되고 $@는 a b c 각각이 된다.
>> $# : 인수의 개수
2. 수식
Bash 쉘 스크립트에서는 비교 연산, 파일 연산, 산술 연산을 사용할 수 있다.
비교 연산
산술 비교는 부등호 등호 대신 약자를 사용한다. 약자를 착취하는 나쁜 놈들이다. '-' 뒤에 2글자 약자가 붙는 형태로 -eq (equal ==), -ne(not equal !=), -gt(greater than >), -ge(greater than or equal >=), -lt(less than <), -le(less than or equal <=) 6가지이다. 정수1 -eq 정수2 꼴로 나타낸다.
이상하게 문자열 비교는 등호를 사용한다.
문자열1 == 문자열2, 문자열1 != 문자열2 로 같고 다름을 확인할 수 있다. 또한 -z 문자열 (zero, 문자열이 null일 경우)와 -n 문자열 (nonzero, null이 아닐 경우)의 단항 연산도 지원한다. 문자열은 변수의 값으로 접근할 때는 따옴표가 필요 없고 상수 문자열일 때는 따옴표가 필요하다. $str1 == "HELLO" 처럼 사용하면 된다.
파일 연산
단항 연산으로, -X 파일명 으로 대충 해당 파일이 X인가?를 확인해 참 거짓을 반환한다. 잘못된 파일 접근으로 낭패볼 상황을 방지할 때 쓰인다. X 자리에 들어갈 연산자는 이런 것들이 있다.
>> e : exist, 파일이 존재하는가
>> r, w, x : read, write, execute, 사용자가 해당 파일에 대한 읽기 / 쓰기 / 실행 권한을 갖는가
>> O : owner, 파일의 소유자인가
>> z : zero, 파일 크기가 0인가
>> f, d : file, directory, 파일인가 / 디렉토리인가
위 두 종류 연산에는 C언어처럼 !, &&, ||의 논리 연산자를 적용할 수 있다. 근데 좀 거지같은데 이유는 밑에 조건식 부분에서 설명한다.
산술 연산
산술 연산은 다행히도 동일한 연산자를 사용한다. 하지만 산술 연산은 특정 명령어 내부에서만 수행될 수 있다. let과 expr 두 가지인데, 둘의 기능이 약간 다르다. 내가 이해한 바로는 let은 연산 후 값을 변수에 저장하고, expr는 연산 후 결과를 출력한다. 또한 문법도 조금 다른데 let은 변수와 수식을 모두 붙여 써야하고 expr는 띄워서 써야한다.
간단한 예시를 보자.

expr는 a와 b의 곱을 계산해 출력할 뿐이다. 역따옴표 ` `를 이용해 출력 내용을 사용할 수는 있지만 어쨌든 그 자체로 값을 할당할 수는 없다. 참고로 * 앞에 백슬래시 \는 띄어쓰기 때문에 쉘이 곱하기 기호 *를 제대로 인식하지 못해 사용하는 것이다. let은 반대의 결과를 보여준다. let 자체로는 계산한 값을 출력하지 않고, 다른 변수에 그 값을 할당하는 것만 가능하다.
산술 연산에는 저런 변수 대신 상수도 사용할 수 있다. 추가로 let은 C에서 사용하는 증감 연산자 ++나 할당 및 대입연산자 += 도 사용할 수 있다.
3. ㅈ건문 (아무튼 오타임)
if, case문을 사용하는 건 동일한데 상당히 지저분하다.
if문
if문의 구조는 다음과 같다. 조건문의 유형에 따라 elif와 else를 생략할 수 있다.
if 조건1
then
명령문
elif 조건2
then
명령문
else
명령문
fi
조건식이 있는 if와 elif 다음에는 then이 들어가야 하며, if, then, else 다음에는 한 칸 띄어 써야 한다. if문의 마지막에는 if문의 종료를 의미하는 fi를 적어줘야 한다. 명령문은 정확히 들여쓰기 할 필요는 없고 then과 다음 조건 사이에만 작성하면 된다.
if에서 사용할 수 있는 조건식은 2가지이다.
1) if [ 조건식 ] : 대괄호를 사용하며, 반드시 대괄호 양 옆에 공백문자가 있어야 한다. 즉 대괄호는 어디에도 붙여쓰면 안 된다. 그러지 않으면 쉘이 제대로 인식을 못한다. 아마 와일드카드로 판단해버리는 듯.
다른 코드를 보면 이중대괄호 [[]]를 사용하는 경우도 있는데, 이중대괄호는 대괄호의 확장 판으로 논리 연산자와 와일드카드를 적용할 수 있다. 이 때는 문자열에 따옴표를 사용하지 않는다.
2) if (( 조건식 )) : 이중 소괄호를 사용하며 공백문자 제한이 따로 있지는 않다. 다만 이 조건식에서는 사용할 수 있는 연산 종류가 다르다. 2번 조건식은 수식, 즉 정수만 계산 가능하며 문자열은 사용할 수 없다(따옴표를 무시해버린다). 따라서 우리에게 익숙한 기호형 연산자를 마음껏 사용할 수 있지만, 문자열 연산이나 파일 연산을 수행할 수 없다.
a=3
b=5
myname=tg
# [] 조건식
if [ $a -gt $b ] //valid
if [ $a > $b ] //valid, 부등호 대신 -gt를 사용하는 게 더 안전
if [ $a > 4 || $b > 4 ] //invalid
if [ $a > 4 -o $b > 4 ] //valid, -o는 or, -a는 and 역할
if [ -n $myname ] //valid
if [ $myname == "tg" ] //valid
if [ -f hello.txt ] //valid
# [[]] 조건식
if [[ $myname == t? ]] //valid, 따옴표가 필요없고 와일드카드 사용 가능
if [[ $a > 4 || $b > 4 ]] //valid
# (()) 조건식
if (($a>$b)) //valid, 띄어쓰기 제약 없음
if (( $a > 4 || $b > 4 )) //valid
if ((-n $myname)) //invalid, 문자형 연산자 일절 사용 불가
if (($myname=="tg")) //invalid, 문자열 사용 불가
case문
case문 역시 C언어의 case문과는 구조가 꽤 다르다.
case $변수 in
패턴1) 명령문;;
패턴2) 명령문;;
...
*) 명령문;;
esac
우선 if와 마찬가지로 case문의 끝은 case를 뒤집은 esac로 명시해줘야 한다. case에서는 문자열 변수를 사용하기 때문에 각 패턴은 "A", "B" 등의 문자열 형태로 사용해야 한다. 문자열에 와일드카드로 패턴을 사용할 수 있다. 패턴 뒤에는 닫는 괄호를 적고 명령문의 끝에는 세미콜론 2개를 작성한다. 쉘의 case문에서는 break가 없고 하나의 case를 실행하면 조건문이 바로 종료된다. C언어의 default는 '나머지 모든 문자열'을 의미하는 와일드카드 *로 대체된다.
다른 패턴에 대해 동일한 명령문을 적용하고 싶으면 패턴1 | 패턴2) 처럼 | 연산자를 이용해 두 case를 하나로 묶을 수 있다.
4. 반복문
반복문은 for와 while 2가지가 있다. 조건문 if와 case에 블록의 끝을 fi, esac같은 뒤집은 단어로 마무리한다는 공통점이 있었다면, 반복문은 반복할 내용을 do와 done으로 감싼다는 공통점이 있다. 다만 쉘에서는 for와 while을 사용하는 상황이 약간 다르다.
for문
for문은 리스트 변수의 각 값에 대해 명령을 수행할 때 쓰인다. 즉 for의 실행 조건은 조건문이 아닌 반복할 리스트이다. for문에도 조건을 사용할 수는 있지만 보통 조건문 기준 반복은 while문에서 쓰인다.
for 변수명 in 리스트
do
명령문
done
과 같은 형태를 가지며, 리스트는 꼭 변수일 필요는 없으며 $*나 * (와일드카드, 모든 파일명 의미) 같은 '여러 값들의 리스트' 형태라면 for문을 사용할 수 있다. 여기서 선언한 변수는 반복문에서 리스트 내의 각 값을 갖는다. 변수이기 때문에 사용할 때 앞에 $를 붙이는 것 잊지 말자.
# for.bash 파일
#!/bin/bash
i=1
for var in $*
do
echo "$i : $var"
let i++
done

리스트 $*는 (a b c d e)가 되고, 변수 var는 매 반복문마다 a, b, c, d, e로 변한다.
while문
while은 조건식을 사용한다.
while 조건식
do
명령문
done
여기서 사용하는 조건식은 if의 조건식과 동일하다.
5. 함수와 디버깅
쉘에서도 함수를 정의할 수 있다. 다행히도 함수 정의문은 복잡하지 않다.
함수 정의
함수명 () {
함수 내용
}
함수 사용
함수명 매개변수1, 매개변수2, ... 매개변수n
함수의 매개변수는 프로그램 인수처럼 $1, $2 ... $n으로 접근할 수 있다.
#!/bin/bash
func() {
echo "This is line $2 in file $1"
awk -v num=$2 'NR == num {print $0}' $1
}
if [ $# -eq 2 ] && [ -f $1 ] && [ $2 -gt 0 ]
then
func $1 $2
fi
앞에서 배운 awk 유틸리티를 사용하는 함수 func을 작성했다. 함수의 내용보다는 프로그램 구조를 보자. 프로그램에 들어온 인수가 조건을 만족하면 func $1 $2 명령을 통해 앞에 선언한 함수 func를 호출하고 인자를 전달한다. 호출 시 괄호를 사용하지 않으며, 인자가 여러 개일 경우 띄어쓰기만 사용해 전달한다는 점이 C언어와 다른 점이다.
디버깅
bash 명령어의 옵션 -v와 -x를 사용해 쉘 스크립트에 대해 간단한 디버깅을 수행할 수 있다. -v 옵션은 쉘이 각 줄을 읽을 때마다 내용을 그대로 보여준다. -x 옵션은 실제 실행되는 내용을 보여준다. 즉 변수나 명령어, 대표문자 대치에 실제 값을 대입한 내용을 보여준다. 예를 들어 num=3일 때 bash -x 명령어로 if [ $num -lt 4 ]라는 줄을 검사하면 num 자리에 실제 값 3을 대입하여 검사 결과로 if [ 3 -lt 4 ]가 출력될 것이다.
6. 예제
추후 추가 예정
'프로그래밍 > 리눅스' 카테고리의 다른 글
리눅스 - 리눅스 개발 환경 (gcc, make, gdb, vim) (0) | 2023.09.23 |
---|---|
리눅스 - 디스크 정보, TAR, GZIP, AWK (0) | 2023.09.20 |
리눅스 - 파일 조작 명령어(find, grep ...) (0) | 2023.09.20 |
리눅스 - 프로세스 명령어와 시그널 (0) | 2023.09.18 |
리눅스 - 쉘 기본 사용법 (입출력 재지정, 대치 등) (0) | 2023.09.15 |