1. 상식
bash 스크립트의 기본 규칙:
실행 시 변수를 확장하여 명령과 인자 문자열을 얻고 실행함
일반적인 흐름:
# 1. `.sh` 파일 생성/편집 (도덕적 관례)
vim test.sh
# 2. 실행 권한 추가
chmod +x test.sh
# 3. 실행
./test.sh
간단한 예시:
#!/bin/bash
# 변수 선언
str='hoho'
# 변수 값 출력
echo $str
여기서 첫 번째 줄 #!/bin/bash는 인터프리터의 경로를 나타내며, which bash로 확인할 수 있습니다.
P.S. #!은 shebang(쉬뱅)이라고 부르며, 더 많은 정보는 释伴:Linux 上的 Shebang 符号(#!)을 확인하세요.
2. 변수
1. 환경 변수
HOME # 현재 사용자 디렉토리의 절대 경로
USER # 현재 사용자 이름
PWD # 현재 작업 디렉토리
# ...
# 더 많은 변수는 `env` 명령으로 확인 가능
선언 없이 직접 사용하며, node의 __dirname이나 __filename과 유사합니다.
환경 변수를 생성하는 3가지 방법:
-
bashrc파일(시스템 수준의/etc/bashrc및 사용자 수준의~/.bashrc)에 영구 환경 변수 추가 (새로 생성되는 모든 shell에 적용됨) -
스크립트 실행 시 임시 환경 변수 설정 (스크립트를 실행하는 서브 쉘 내에서만 유효)
-
환경 변수
export(이후에 생성되는 서브 쉘에만 유효)
예:
# 방식 1
# zsh인 경우 해당 파일명은 `~/.zshrc`임
echo _ENV=product >> ~/.bashrc
source ~/.bashrc
echo $_ENV
# 방식 2
_ENV=product ./test.sh
# ./test.sh 내에서 _ENV를 읽을 수 있음
echo $_ENV
# 방식 3
_ENV=product; export _ENV
# 새 shell 열기
bash
echo $_ENV
2. 전역 변수와 지역 변수
#!/bin/bash
# 기본적으로 전역 변수로 선언됨
VAR="global variable"
function fn() {
VAR="updated global variable"
# 함수 내에서만 local 키워드로 지역 변수 선언 가능
local VAR="local variable"
echo $VAR
}
fn
echo $VAR
# 출력
local variable
updated global variable
필요할 때 즉시 선언하며, 기본 형태는 모두 전역 변수입니다. 함수 내부에서만 local 키워드를 통해 지역 변수를 선언할 수 있습니다. 또한 다음 사항에 주의하세요:
-
등호 양옆에 공백이 있어서는 안 됩니다. 각 줄은 '명령 공백 인자'로 처리되기 때문입니다.
-
따옴표는 필수가 아닙니다. CSS와 마찬가지로 내용에 공백이 포함된 경우에만 따옴표가 필요합니다.
-
호이스팅(hosting) 개념이 없습니다. 지역 변수의 스코프는 변수 선언 위치부터 함수 본문 끝까지이며, 전역 변수는 선언 위치부터 파일 끝까지입니다.
3. 변수 값 접근
$변수명으로 변수 값을 가져옵니다. 예: $VAR
변수 보간 규칙: 큰따옴표 내의 변수는 확장(expanded)되지만, 작은따옴표 내의 변수는 확장되지 않습니다. PHP와 동일합니다.
{}를 사용하여 변수명을 격리하고 보호할 수 있습니다.
${VAR}abc # VAR 값 바로 뒤에 문자열 abc가 붙음
배열 요소를 가져올 때는 반드시 이렇게 해야 합니다. 예:
arr=(aa b ccc)
# aa[1] 출력, 예상과 다름
# $arr로 값을 가져오면 첫 번째 요소인 aa를 얻고, 그 뒤에 문자열 [1]이 붙기 때문
echo $arr[1]
# b 출력
echo ${arr[1]}
3. 분기와 반복
1. 조건문
if 조건 # test 명령과 [] 연산자
then
문장...
else # else if는 elif로 씀
문장...
fi
조건 부분은 보통 test 명령이나 [] 연산자를 사용합니다. 예:
if [ $X -lt $Y ]; # X가 Y보다 작음
if [ -n $X ]; # 변수가 비어있지 않음 (문자열 길이가 0이 아님)
if [ -e $path ]; # 파일이 존재함
# 숫자 비교
if test 2 -gt 1; then echo "number 2 > 1"; fi
# 위와 동일
if [ 2 -gt 1 ]; then echo "number 2 > 1"; fi
# 문자열 비교
if test 2 > 11; then echo "string 2 > 11"; fi
# 위와 동일
if [ 2 > 11 ]; then echo "string 2 > 11"; fi
여기에는 피연산자 유형, 세미콜론, 공백이라는 3가지 디테일이 있습니다.
-
-gt는 숫자 크기 비교를,>는 문자열 크기 비교를 의미합니다. 연산 시 자동으로 변환되며 변환할 수 없으면 오류가 발생합니다. -
;은 한 줄 문장에서 블록 구조를 구분하는 데 사용됩니다. 첫 번째 세미콜론은 조건부의 끝을, 두 번째 세미콜론은then부의 끝을 의미하며 둘 다 필수입니다. -
공백은 명령과 인자를 구분하는 데 사용됩니다. (공백 외에도 기본 구분자로 탭과 줄바꿈이 있으며, 아래 IFS 참고)
P.S. 문자열을 숫자로 변환하는 흔한 방식으로는 ((str))와 `expr str`, $(expr str) 등이 있습니다. 전자는 bash 연산자이고 후자 둘은 외부 명령입니다.
공백 예시:
# 공백이 매우 중요함
if [ 1=2 ]; # 1=2 전체를 하나의 피연산자(문자열)로 취급하며 연산자를 인식하지 못함
# [] 내부 양 끝의 공백도 매우 중요함
if [-e $path]; then # [-e 명령을 찾을 수 없다는 오류 발생. '[-e'와 '$path]'로 해석되기 때문
man test 명령을 통해 다른 test 연산자들을 확인할 수 있습니다.
bash도 switch와 유사한 기능을 제공하지만 구문이 매우 특이합니다.
case $variable in
pattern1)
command...
;; # break
pattern2|pattern3)
command...
;;
patternN)
command...
;;
*) # default case
command...
esac
2. 반복문
for, while, until 3가지 반복문이 있습니다. 구문 규칙은 다음과 같습니다.
# for 반복문
for f in $( ls /var/ ); do
echo $f
done
# 또는 한 줄 형식 (세미콜론으로 구조 분리)
for f in $( ls /var/ ); do echo $f; done
# while 반복문
times=6
while [ $times -gt 0 ]; do
echo Value of times is: $times
let times=times-1
done
# 한 줄 형식
times=6; while [ $times -gt 0 ]; do echo Value of times is: $times; let times=times-1; done
# until 반복문
times=0
until [ $times -gt 5 ]; do
echo Value of times is: $times
let times=times+1
done
for...in 외에 C 스타일도 가능합니다.
arr=(1 '2 3' 4)
len=3
for (( i=0; i<$len; i++)); do
echo ${arr[${i}]}
done
반복문의 기본 규칙:
반복문은 IFS(' ', '\t', '\n')로 구분된 항목을 순회함
IFS(Internal Field Separator)는 내부 필드 구분자로, 기본값은 공백, 탭, 줄바꿈입니다. 따라서 다음 상황에 주의하세요.
for f in $( ls -l /var/ ); do echo $f; done
출력 결과가 예상과 다릅니다:
total
0
drwx------
2
root
wheel
68
8
23
2015
agentx
원래는 이래야 합니다:
total 0
drwx------ 2 root wheel 68 8 23 2015 agentx
행 전체를 반복해서 읽으려면 IFS를 수정해야 합니다.
# 구분자를 줄바꿈만 인식하도록 제한
IFS=$'\n'; for f in $( ls -l /var/ ); do echo $f; done
또한 반복문은 보통 와일드카드 *와 함께 사용됩니다. 예:
# 와일드카드
echo * # 현재 디렉토리의 모든 파일/폴더명을 공백으로 구분하여 출력
echo *.html # 현재 디렉토리의 모든 html 형식 파일
# test 디렉토리의 모든 html 파일 찾기
for htmlFile in `echo ~/Documents/projs/test/*.html`; do echo $htmlFile; done
# 하위 디렉토리를 포함하여 test 디렉토리의 모든 html 파일 찾기
for htmlFile in `echo ~/Documents/projs/test/**/*.html`; do echo $htmlFile; done
반복문 + 와일드카드를 이용하면 디렉토리 파일 조작이 매우 편리합니다.
4. 함수
1. 함수 선언
function function_name {
command...
}
# 또는
function_name () {
command...
}
function 키워드를 생략할 경우 함수 이름 뒤에 반드시 ()가 있어야 합니다. 그렇지 않으면 명령으로 간주됩니다. 예:
# 오류 발생, parse error near `}'
fn {echo fn}; fn
function 키워드를 사용하는 경우에는 함수 이름 뒤에 ()가 있어도 되고 없어도 상관없습니다.
함수 선언 순서는 엄격하지 않으나, 먼저 선언한 후 호출해야 합니다. 예:
fn1() {echo fn1:`fn2 $1`}
fn2() {echo fn2:$1}
fn1 hoho
# 출력
# fn1:fn2:hoho
fn2 선언 전에 fn1을 호출하면 fn2를 찾을 수 없다는 오류가 발생합니다.
# 오류 발생, command not found: fn2
fn1() {echo fn1:`fn2 $1`}; fn1 hoho; fn2() {echo fn2:$1}
2. 호출 및 인자 전달
인자는 위치 변수를 통해 전달받으며, 매개변수를 명시적으로 선언하지 않습니다.
# 함수 선언
fn() {echo $0$1}
# 인자 없이 호출
fn
# 문자열 인자 hoho 전달
fn hoho
함수 스코프 내에서는 몇 가지 위치 변수(모두 읽기 전용)가 제공됩니다.
$0 # 함수 이름 (인자 수에 포함되지 않음. $*, $@에 포함되지 않고 $#에도 계상되지 않음)
$n # n번째 인자, 1부터 시작함
$* # 모든 인자를 공백으로 연결한 문자열
$@ # 위와 동일하나, 각 인자가 큰따옴표로 보호됨
$# # 인자의 개수
P.S. $10은 ${10}이 아닙니다. 전자는 $1 뒤에 문자열 0이 붙은 것이고, 후자는 10번째 인자의 값입니다.
또한 이러한 위치 변수들은 커맨드 라인을 통해 스크립트에 인자를 전달할 때도 적용됩니다. 예:
# sub.sh
echo $1-$2=`expr $1 - $2`
# 커맨드 라인 실행
./sum.sh 1 2
# 출력
# 1-2=-1
$*와 $@의 차이는 중요합니다. 간단히 이해하자면 다음과 같습니다.
$*=$1 $2 $3...
$@="$1" "$2" "$3"...
예시:
# 차이가 없음
fn1() {for arg in $*; do echo line:$arg; done}; fn1 "a" "b c" "d"
fn2() {for arg in $@; do echo line:$arg; done}; fn2 "a" "b c" "d"
# 출력
# line:a
# line:b c
# line:d
# 큰따옴표로 감싸면 차이가 확연함
fn1() {for arg in "$*"; do echo line:$arg; done}; fn1 "a" "b c" "d"
# 출력
# line:a
# b c
# d
fn2() {for arg in "$@"; do echo line:$arg; done}; fn2 "a" "b c" "d"
# 출력
# line:a
# line:b c
# line:d
큰따옴표로 감쌌을 때 반복 횟수에 차이가 있습니다. $*는 한 번만 반복하고, $@는 세 번 반복합니다. 따라서 일반적으로 $@를 사용하는 것을 권장합니다.
3. 반환 값
세 가지 방식 모두 사용하기 불편하므로, 특별한 이유가 없다면 반환하지 않는 것이 좋습니다 (직접 외부 변수 수정). 굳이 반환해야 한다면 서브 쉘 실행 후 echo로 전달하는 방식을 권장하지만, 주의 사항(코드 주석 참고) 또한 번거롭습니다. 예시는 다음과 같습니다.
# 1. return
fn() {
return -2
}
fn
# 마지막 명령의 반환 값을 가져옴. 0은 정상, 그 외는 비정상
# 실제 출력은 254임. 범위를 벗어나면 순환됨 (256은 0, -1은 255)
echo $?
# 단점: return은 함수의 실행 상태를 나타내며 [0, 255] 정수만 반환 가능하고 문자열은 반환할 수 없음
# 또한 $?는 반드시 함수 호출 직후에 사용해야 함
# 2. 서브 쉘 실행, echo 반환
fn() {
echo -2
# 오류 메시지가 표준 출력으로 나가는 것을 방지하기 위해 버림
cat xxx 2> /dev/null
}
echo $(fn)
# 단점: 표준 출력 결과가 깨끗하지 않을 수 있음
# (함수 본문에 echo 문이 여러 개 있거나 print, printf 등 표준 출력 문장이 있는 경우)
# 또한 함수 실행 중 오류가 발생하면 오류 메시지도 섞여 들어갈 수 있음 (방지 가능하지만 번거로움)
# 3. 소위 말하는 참조 전달
fn() {
# 첫 번째 인자로 전달된 문자열을 반환용 변수명으로 약속함
local res=$1
# 1+2를 계산한 뒤 반환용 변수명으로 전역 변수를 생성하여 결과를 전달함
eval $res=$(($2 + $3))
}
fn result 1 2
echo $result
# 단점: 사실상 전역 변수를 통한 값 전달이며, 전역 변수명을 동적으로 전달받을 뿐 함수 내에 고정되어 있지 않을 뿐임
5. 배열
1. 선언 및 할당
# 빈 배열
arr=()
# 문자열 배열, 요소를 공백으로 구분하며 쉼표는 쓰지 않음
arr=(1 2 3 'we together')
# 직접 할당, 해당 인덱스가 없으면 새로 추가함
arr[0]=4
arr[6]='sixth'
배열 인덱스는 0부터 시작하며 할당 시 연속성을 보장할 필요는 없습니다. 인덱스가 없으면 새로 추가됩니다.
2. 순회
for 문은 배열의 길이를 알 필요가 없습니다.
arr=(1 2 3 '4 5')
for i in "${arr[@]}"; do echo $i; done
"${arr[@]}"에 특별히 주의하세요. 함수의 위치 변수와 마찬가지로 $*와 $@의 반복 횟수가 다릅니다.
for i in "${arr[@]}"; do echo $i; done
# 출력
# 1
# 2
# 3
# 4 5
for i in "${arr[*]}"; do echo $i; done
# 출력
# 1 2 3 4 5
while과 until은 배열의 길이를 알아야 합니다.
# 배열 길이 구하기
len=${#arr[@]}
i=1
while [ $i -lt $len ]; do echo $arr[$i]; i=$((i+1)); done
# 또는 until 이용
until [ $len -lt 1 ]; do len=$((len-1)); echo ${arr[$len]}; done
주의: ${#str}은 문자열 길이를 구하는 것으로 ${#arr[@]} 배열 길이 구하기와 매우 비슷하여 혼동하기 쉽습니다.
6. 명령 치환
명령 치환이란 bash 스크립트 내에서 shell 명령을 실행하고 그 출력 결과를 얻는 것을 의미합니다.
직접 실행하면 결과가 표준 출력(화면)으로 출력되지만, 그 결과를 변수에 담으려면 명령 치환을 사용해야 합니다.
# 직접 실행 - ls 명령 결과가 화면에 출력됨
ls
# 명령 치환 - 화면에 출력되지 않고 사용자 정의 변수에 저장됨
lsResult=`ls`
# 결과를 버리는 것처럼 보이지만 (출력도 저장도 안 함)
# 실제로는 오류가 발생할 수 있음. `ls` 명령의 결과 문자열이 다시 명령으로 실행되려 하기 때문
`ls`
명령 치환의 흔한 방식은 백틱(`) 확장과 괄호( ) 확장 2가지가 있습니다. 예:
# 백틱 확장, 중첩 불가
files=`ls`
# 괄호 확장, 중첩 가능
files="$(ls)"
# 중첩 예시
# 현재 디렉토리의 파일 및 폴더 수 + 1
$(expr ${#$(ls)[@]} + 1)
흥미로운 점: 두 가지 명령 치환 방식 모두 새로운 shell(즉, 서브 쉘)에서 명령을 실행하므로 현재 shell에는 어떤 영향도 주지 않습니다. 따라서 작업 환경을 격리하기에 매우 편리합니다. 예:
# 새 환경에서 cd 실행
lsParent="$(cd ../; ls)"
# 실행 후 pwd는 변하지 않으므로 다시 cd로 돌아올 필요가 없음
이 정도면 다소 복잡하고 유용한 bash 스크립트를 작성하기에 충분합니다.
아직 댓글이 없습니다