함수 정의와 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처럼 바로 쓸 수 있다.