함수 정의와 local
함수는 이름() { 명령; }으로 정의하고, 명령처럼 이름만 써서 부른다. 괄호 안에는 아무것도 적지 않는다.
인자는 부를 때 공백으로 이어 붙인다. 함수는 부르기 전에 정의되어 있어야 하므로 스크립트 위쪽에 둔다.
greet() {
echo "hello, $1"
}
greet water # hello, water
function greet { ... }라는 형식도 있지만 bash 전용이다. 위의 형식이 더 흔하다.
함수 안에서 만든 변수는 기본적으로 스크립트 전체에서 보인다. 같은 이름의 바깥 변수를 덮어쓰는 일이 생기니,
함수 안에서만 쓸 변수는 local로 선언한다.
$ mol=benzene
$ set_mol() { mol=$1; }
$ set_mol toluene; echo "$mol"
toluene
$ mol=benzene
$ set_mol() { local mol=$1; echo "inside: $mol"; }
$ set_mol toluene; echo "outside: $mol"
inside: toluene
outside: benzene
인자: $1, $#, "$@"
함수와 스크립트는 인자를 같은 방식으로 받는다. 스크립트에서는 명령줄에 적은 인자가, 함수에서는 부를 때 넘긴 인자가 들어온다.
| 변수 | 내용 |
|---|---|
$0 | 스크립트 이름 (함수 안에서도 스크립트 이름) |
$1, $2, ... ${10} | 첫째, 둘째 인자. 10번째부터는 중괄호가 필요하다 |
$# | 인자 개수 |
"$@" | 모든 인자를 각각 따로 따옴표로 감싼 것처럼 넘긴다 |
"$*" | 모든 인자를 공백으로 이어 붙인 문자열 하나 |
인자를 다른 명령에 그대로 넘길 때 차이가 드러난다. 인자 개수만 세는 함수에 공백이 들어간 파일 이름을 넘겨 보자.
$ count_args() { echo "$# args"; }
$ wrap_at() { count_args "$@"; }
$ wrap_star() { count_args $*; }
$ wrap_qstar() { count_args "$*"; }
$ wrap_at water.xyz "ethanol conf1.xyz"
2 args
$ wrap_star water.xyz "ethanol conf1.xyz"
3 args
$ wrap_qstar water.xyz "ethanol conf1.xyz"
1 args
파일 두 개를 제대로 넘기는 것은 "$@"뿐이다. 인자 목록을 반복할 때도 for f in "$@"로 쓴다.
"$*"는 로그 메시지처럼 인자를 한 줄로 찍을 때만 쓴다.
shift
shift는 $1을 버리고 나머지를 한 칸씩 당긴다. 앞의 몇 개는 설정값이고 나머지는 파일 목록인 경우에 쓴다.
$ f() { echo "cores: $1"; shift; echo "files: $*"; }
$ f 8 a.xyz b.xyz
cores: 8
files: a.xyz b.xyz
반환 코드와 출력
bash 함수는 값을 돌려주는 방법이 두 가지다. 하나는 return으로 종료 코드를 남기는 것이다.
0에서 255 사이 정수만 되고, 성공과 실패를 알리는 데 쓴다. return이 없으면 마지막 명령의 종료 코드가 함수의 종료 코드가 된다.
orca_done() {
grep -q "ORCA TERMINATED NORMALLY" "$1"
}
if orca_done water/water.out; then
echo "water finished"
fi
다른 하나는 결과를 출력하고, 부르는 쪽에서 $( )로 받는 것이다. 에너지 같은 문자열이나 소수는 이렇게 돌려준다.
$ orca_energy() {
> grep "FINAL SINGLE POINT ENERGY" "$1" | tail -n 1 | awk '{print $5}'
> }
$ e=$(orca_energy water_opt/water_opt.out)
$ echo "E = $e"
E = -76.426369457144
이 방식을 쓰는 함수 안에서는 결과 말고 다른 것을 표준 출력으로 찍으면 안 된다. 그것까지 변수에 섞여 들어간다.
진행 메시지는 echo "..." >&2로 표준 오류에 보낸다.
getopts로 옵션 받기
스크립트가 -m r2SCAN-3c -n 8 같은 옵션을 받게 하려면 getopts를 쓴다.
아래 make_inputs.sh는 xyz 파일마다 디렉터리를 만들고 ORCA 입력을 쓴다. 방법과 코어 수, 전하는 옵션으로 바꿀 수 있다.
#!/bin/bash
# xyz 파일마다 디렉터리를 만들고 ORCA 입력 파일을 쓴다
usage() {
cat <<EOF
Usage: ${0##*/} [-m method] [-n nprocs] [-c charge] file.xyz ...
-m method line (default: r2SCAN-3c)
-n number of cores (default: 4)
-c total charge (default: 0)
-h show this help
EOF
}
method="r2SCAN-3c"
nprocs=4
charge=0
while getopts "m:n:c:h" opt; do
case $opt in
m) method=$OPTARG ;;
n) nprocs=$OPTARG ;;
c) charge=$OPTARG ;;
h) usage; exit 0 ;;
*) usage >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1))
if [[ $# -eq 0 ]]; then
echo "error: no xyz files given" >&2
usage >&2
exit 1
fi
write_input() {
local xyz=$1
local mol=${xyz##*/}
mol=${mol%.xyz}
mkdir -p "$mol"
cat > "$mol/$mol.inp" <<EOF
! ${method} Opt Freq
%pal nprocs ${nprocs} end
%maxcore 3000
* xyz ${charge} 1
$(tail -n +3 "$xyz")
*
EOF
echo "$mol/$mol.inp"
}
for xyz in "$@"; do
write_input "$xyz"
done
getopts의 첫 인자 "m:n:c:h"는 받을 옵션 목록이다. 글자 뒤에 :가 붙은 옵션은 값을 받고,
그 값은 $OPTARG에 들어온다. 옵션을 다 읽은 뒤 shift $((OPTIND - 1))로 옵션 부분을 버리면 "$@"에 파일 목록만 남는다.
$ ./make_inputs.sh -m r2SCAN-3c -n 8 *.xyz
ethanol conf1/ethanol conf1.inp
methane/methane.inp
water/water.inp
$ ./make_inputs.sh -m "B3LYP D3BJ def2-SVP" -n 8 water.xyz
water/water.inp
$ head -n 2 water/water.inp
! B3LYP D3BJ def2-SVP Opt Freq
%pal nprocs 8 end
값에 공백이 있으면 따옴표로 감싸서 넘긴다. 이름에 공백이 있는 ethanol conf1.xyz도 문제없이 처리된 것은
스크립트 안에서 변수를 전부 따옴표로 감쌌기 때문이다. 잘못 부르면 사용법을 보여 주고 1로 끝난다.
$ ./make_inputs.sh -n
./make_inputs.sh: option requires an argument -- n
Usage: make_inputs.sh [-m method] [-n nprocs] [-c charge] file.xyz ...
-m method line (default: r2SCAN-3c)
-n number of cores (default: 4)
-c total charge (default: 0)
-h show this help
-h로 요청한 도움말은 표준 출력에 찍고 0으로 끝낸다. 잘못된 사용 때문에 나오는 사용법은 >&2로 표준 오류에 보내고 0이 아닌 값으로 끝낸다.
그래야 다른 스크립트가 이 스크립트를 부를 때 실패를 알아챈다.
getopts는 -n 8 같은 한 글자 옵션만 다룬다. --nprocs 8 같은 긴 옵션이 필요하면
while과 case, shift로 직접 파싱해야 하는데, 연구용 스크립트에서는 한 글자 옵션이면 충분하다.
함수 모음을 source로 불러오기
여러 스크립트에서 같은 함수를 쓴다면 함수만 모은 파일을 따로 두고 source로 불러온다.
source 파일은 그 파일을 현재 셸에서 한 줄씩 실행한다. . 파일로 줄여 쓰기도 한다.
#!/bin/bash
# orca_utils.sh: 다른 스크립트에서 source로 불러 쓰는 함수 모음
orca_done() {
grep -q "ORCA TERMINATED NORMALLY" "$1"
}
orca_energy() {
grep "FINAL SINGLE POINT ENERGY" "$1" | tail -n 1 | awk '{print $5}'
}
#!/bin/bash
# summary.sh: 출력 파일마다 마지막 에너지 또는 상태를 찍는다
source "$(dirname "$0")/orca_utils.sh"
for out in "$@"; do
if orca_done "$out"; then
printf '%-40s %s\n' "$out" "$(orca_energy "$out")"
else
printf '%-40s %s\n' "$out" "not finished"
fi
done
$ ~/bin/summary.sh jobs/*/*.out
jobs/methane_opt/methane_opt.out not finished
jobs/water_opt/water_opt.out -76.426369457144
jobs/water_sp/water_sp.out -76.321274411145
source orca_utils.sh라고만 쓰면 스크립트를 실행한 위치에서 파일을 찾는다. 다른 디렉터리에서 부르면 못 찾으니
$(dirname "$0")로 스크립트가 있는 디렉터리를 기준으로 삼는다. ~/.bashrc에 source로 넣어 두면
터미널에서도 orca_energy water.out처럼 바로 쓸 수 있다.