종료 코드와 $?
모든 명령은 끝날 때 0에서 255 사이의 정수를 남긴다. 이것을 종료 코드(exit status)라고 한다.
0은 성공, 나머지는 실패다. 셸의 조건문은 전부 이 숫자를 보고 움직인다.
바로 앞 명령의 종료 코드는 $?에 들어 있다.
$ grep -q "ORCA TERMINATED NORMALLY" water_sp.out
$ echo $?
0
$ grep -q "ORCA TERMINATED NORMALLY" methane_opt.out
$ echo $?
1
$ grep -q "ORCA TERMINATED NORMALLY" nofile.out
grep: nofile.out: No such file or directory
$ echo $?
2
grep은 찾으면 0, 못 찾으면 1, 파일이 없는 등 오류가 나면 2를 돌려준다. -q는 아무것도 출력하지 말라는 옵션이다.
$?는 명령을 하나 실행할 때마다 새로 덮어써진다. echo도 명령이다.
나중에 쓸 값이라면 바로 변수에 옮겨 둔다.
$ ls nofile
ls: cannot access 'nofile': No such file or directory
$ echo "exit status: $?"
exit status: 2
$ echo "now: $?"
now: 0
if, elif, else
if 뒤에는 명령이 온다. 그 명령의 종료 코드가 0이면 then 블록을 실행한다.
괄호 안에 조건식을 쓰는 다른 언어와 달리, 아무 명령이나 조건이 될 수 있다.
if grep -q "ORCA TERMINATED NORMALLY" water.out; then
echo "finished"
elif grep -q "ERROR" water.out; then
echo "error"
else
echo "still running or killed"
fi
then을 같은 줄에 쓰려면 앞에 ;가 필요하다. 블록은 fi로 닫는다.
들여쓰기는 문법상 의미가 없지만 읽기 편하도록 넣는다.
[[ ]]로 검사하기
파일이 있는지, 문자열이 비었는지, 숫자가 큰지 같은 조건은 [[ ]]로 검사한다. [[ 뒤와 ]] 앞에는 공백이 꼭 있어야 한다.
| 검사 | 참인 경우 |
|---|---|
[[ -f $f ]] | 일반 파일이 있다 |
[[ -d $d ]] | 디렉터리가 있다 |
[[ -e $p ]] | 종류와 상관없이 있다 |
[[ -s $f ]] | 파일이 있고 비어 있지 않다 |
[[ -z $s ]] / [[ -n $s ]] | 문자열이 비었다 / 비어 있지 않다 |
[[ $a == $b ]], != | 문자열이 같다, 다르다. 오른쪽에 *.xyz 같은 패턴을 쓸 수 있다 |
[[ $n -lt 8 ]] | 정수 비교. -eq -ne -lt -le -gt -ge |
[[ ! -f $f ]] | 부정 |
[[ -f $a && -f $b ]] | 둘 다 참. ||는 하나라도 참 |
숫자를 비교할 때 <, >를 쓰면 안 된다. [[ ]] 안의 >는 사전 순 문자열 비교라서
10이 9보다 작다고 나온다. 정수는 -gt를 쓰거나 산술용 (( ))를 쓴다.
$ [[ 10 > 9 ]] && echo yes || echo no
no
$ [[ 10 -gt 9 ]] && echo yes || echo no
yes
$ n=12; (( n > 8 )) && echo big
big
$ f=conf03.xyz; [[ $f == *.xyz ]] && echo "xyz file"
xyz file
오래된 스크립트에서 보이는 [ ]는 test 명령의 다른 이름이다. 변수를 따옴표로 감싸지 않으면 빈 값일 때 깨진다.
[[ ]]는 bash 문법이라 안에서 단어 분리가 일어나지 않고 패턴 비교도 된다. bash 스크립트에서는 [[ ]]를 쓴다.
$ x=
$ [ $x = "" ] && echo empty
bash: [: =: unary operator expected
$ [[ $x == "" ]] && echo empty
empty
&&와 ||
A && B는 A가 성공했을 때만 B를 실행한다. A || B는 A가 실패했을 때만 B를 실행한다.
짧은 조건은 if 대신 이렇게 쓴다.
mkdir -p opt && cp water.xyz opt/
[[ -f water.xyz ]] || echo "water.xyz is missing"
cd "$workdir" || exit 1
A && B || C는 if-else처럼 보이지만 같지 않다. B가 실패해도 C가 실행된다. B가 echo처럼 실패할 일이 없는 명령일 때만 쓴다.
for 반복문
for 변수 in 목록은 목록의 각 항목을 차례로 변수에 넣고 블록을 실행한다. 파일을 다룰 때는 목록 자리에 와일드카드를 쓴다.
for f in *.xyz; do
mol=${f%.xyz}
mkdir -p "$mol"
cp "$f" "$mol/"
done
for f in $(ls *.xyz)라고 쓰는 경우가 많은데 틀린 방법이다. ls의 출력이 공백에서 쪼개져서
이름에 공백이 있는 파일은 두 조각이 된다. 와일드카드는 쪼개지지 않는다.
$ for f in $(ls *.xyz); do echo "[$f]"; done
[ethanol]
[conf1.xyz]
[methane.xyz]
[water.xyz]
$ for f in *.xyz; do echo "[$f]"; done
[ethanol conf1.xyz]
[methane.xyz]
[water.xyz]
맞는 파일이 하나도 없으면 bash는 패턴을 글자 그대로 남긴다. 그래서 *.gjf라는 이름으로 반복문이 한 번 돈다.
shopt -s nullglob을 켜 두면 빈 목록이 되어 반복문이 아예 돌지 않는다.
$ for f in *.gjf; do echo "[$f]"; done
[*.gjf]
$ shopt -s nullglob
$ for f in *.gjf; do echo "[$f]"; done
$ shopt -u nullglob
숫자 목록은 중괄호 범위로 만든다. {01..10}처럼 앞에 0을 붙이면 자릿수가 맞춰진다.
다만 중괄호 안에는 변수를 쓸 수 없다. {1..$n}은 펼쳐지지 않으니 seq나 C 형식 for를 쓴다.
$ echo {1..5} {01..10..3} conf{a,b}
1 2 3 4 5 01 04 07 10 confa confb
$ n=3; echo {1..$n}
{1..$n}
$ for (( i=0; i<5; i++ )); do
> r=$(awk -v i="$i" 'BEGIN { printf "%.2f", 0.90 + 0.05*i }')
> echo "scan_$i: R = $r"
> done
scan_0: R = 0.90
scan_1: R = 0.95
scan_2: R = 1.00
scan_3: R = 1.05
scan_4: R = 1.10
마지막 예는 결합 길이 스캔 값을 만든다. 반복 횟수는 정수로 세고, 소수 값은 awk가 계산한다.
>는 명령이 이어진다는 표시로 셸이 띄우는 프롬프트다.
while read로 파일 한 줄씩 읽기
분자 이름, 전하, 스핀 다중도를 적은 목록 파일 mols.txt가 있다고 하자.
water 0 1
methane 0 1
nitrate -1 1
while read -r에 파일을 넘기면 한 줄씩 읽는다. 변수를 여러 개 주면 공백 기준으로 나눠 담고, 남는 부분은 마지막 변수에 들어간다.
$ while read -r name charge mult; do
> echo "$name: charge $charge, mult $mult"
> done < mols.txt
water: charge 0, mult 1
methane: charge 0, mult 1
nitrate: charge -1, mult 1
같은 일을 cat mols.txt | while read ...로 쓰면 반복문 안에서 바꾼 변수가 밖으로 나오지 않는다.
파이프의 각 부분이 별도의 하위 셸에서 돌기 때문이다.
$ count=0
$ cat mols.txt | while read -r line; do count=$((count + 1)); done
$ echo "count = $count"
count = 0
$ count=0
$ while read -r line; do count=$((count + 1)); done < mols.txt
$ echo "count = $count"
count = 3
$ count=0
$ while read -r f; do count=$((count + 1)); done < <(find jobs -name '*.out')
$ echo "count = $count"
count = 3
파일은 < 파일로, 다른 명령의 출력은 프로세스 치환 < <(명령)으로 넘기면 반복문이 현재 셸에서 돈다.
두 < 사이의 공백을 빼먹지 않는다.
Windows 편집기로 만든 목록 파일은 마지막 줄 끝에 줄바꿈이 없는 경우가 있다. read는 줄바꿈을 못 만나면 실패를 돌려주고,
마지막 줄은 반복문에서 빠진다. while read -r line || [[ -n $line ]]로 쓰면 그 줄도 처리한다.
case
값 하나를 여러 패턴과 비교할 때는 if를 늘어놓는 것보다 case가 읽기 쉽다. 확장자별로 처리를 나누는 예다.
for f in water.xyz run.inp job.out notes.txt geom.gjf; do
case "$f" in
*.xyz) echo "$f: coordinates" ;;
*.inp|*.gjf) echo "$f: input" ;;
*.out|*.log) echo "$f: output" ;;
*) echo "$f: skipped" ;;
esac
done
water.xyz: coordinates
run.inp: input
job.out: output
notes.txt: skipped
geom.gjf: input
각 갈래는 ;;로 끝나고, 위에서부터 처음 맞는 하나만 실행된다. *)는 나머지 전부를 받는다.
반복문 안에서 continue는 이번 차례를 건너뛰고, break는 반복문을 끝낸다.
예제: 계산이 정상 종료했는지 확인하기
jobs/ 아래에 계산마다 디렉터리가 있고, 디렉터리 이름과 같은 이름의 .out 파일이 있다고 하자.
ORCA는 정상적으로 끝나면 출력 끝에 ****ORCA TERMINATED NORMALLY****를 쓴다. 이 줄이 있는지로 상태를 나눈다.
#!/bin/bash
# jobs/ 아래 계산 디렉터리마다 ORCA가 정상 종료했는지 확인한다
ok=0
bad=0
for d in jobs/*/; do
d=${d%/}
name=${d##*/}
out="$d/$name.out"
if [[ ! -f $out ]]; then
echo "NOOUT $name"
bad=$((bad + 1))
elif grep -q "ORCA TERMINATED NORMALLY" "$out"; then
echo "OK $name"
ok=$((ok + 1))
else
echo "FAIL $name"
bad=$((bad + 1))
fi
done
echo "normal: $ok, not normal: $bad"
$ ./check_jobs.sh
NOOUT benzene_opt
FAIL methane_opt
OK water_opt
OK water_sp
normal: 2, not normal: 2
jobs/*/처럼 끝에 /를 붙이면 디렉터리만 고른다. 그 /는 ${d%/}로 떼어 낸다.
FAIL은 아직 돌고 있는 계산일 수도 있다. 큐에 남은 작업인지는 qstat이나 squeue로 따로 확인한다.
출력에서 에너지까지 뽑아 표로 만드는 법은 06장에서 다룬다.