스크립트 파일과 shebang
스크립트는 터미널에 치던 명령을 순서대로 적은 텍스트 파일이다. 확장자는 관례상 .sh를 붙이지만 없어도 동작한다.
편집기로 hello.sh를 만들고 아래 네 줄을 넣는다.
#!/bin/bash
# 첫 스크립트
echo "Hello from $(hostname)"
echo "지금 위치: $PWD"
첫 줄 #!/bin/bash를 shebang이라고 부른다. 파일을 직접 실행할 때 어떤 프로그램에 넘길지 커널에 알려 주는 줄이다.
반드시 파일의 맨 첫 줄, 맨 앞 글자부터 있어야 한다. 그 밖에 #로 시작하는 줄은 주석이라 무시된다.
줄 중간의 # 뒤도 주석이 된다.
#!/usr/bin/env bash라고 쓰기도 한다. PATH에서 먼저 찾은 bash를 쓰라는 뜻이라,
bash가 /bin에 없는 시스템이나 conda로 새 bash를 깐 경우에 유리하다. 클러스터에서는 어느 쪽이든 괜찮다.
실행하는 두 가지 방법
첫째는 bash에게 파일을 읽으라고 넘기는 방법이다. 실행 권한이 없어도 된다.
$ bash hello.sh
Hello from kramers
지금 위치: /home/kim/scripts
둘째는 파일 자체를 명령처럼 실행하는 방법이다. 이때는 실행 권한이 필요하다.
권한 없이 실행하면 거부되고, chmod +x로 권한을 준 다음에는 된다.
$ ./hello.sh
bash: ./hello.sh: Permission denied
$ chmod +x hello.sh
$ ./hello.sh
Hello from kramers
지금 위치: /home/kim/scripts
$ hello.sh
bash: hello.sh: command not found
마지막 줄처럼 ./를 빼면 찾지 못한다. 셸은 경로 없이 적힌 명령을 PATH에 등록된 디렉터리에서만 찾고,
현재 디렉터리는 보통 PATH에 없다. ./는 "지금 이 디렉터리에 있는 파일"이라는 경로를 명시한 것이다.
자주 쓰는 스크립트는 ~/bin에 모으고 그 디렉터리를 PATH에 넣으면 어디서든 이름만으로 부를 수 있다.
권한과 PATH는 Linux 스타터팩의 권한,
프로세스와 환경 변수 장에 자세히 있다.
두 방법의 차이가 하나 더 있다. bash hello.sh는 shebang을 무시하고 bash로 돌린다.
sh hello.sh로 실행하면 시스템에 따라 bash가 아닌 dash가 읽어서, bash 전용 문법이 있는 줄에서 오류가 난다.
변수
변수는 이름=값으로 만든다. = 양옆에 공백을 두면 안 된다.
다른 언어 습관대로 띄어 쓰면 bash는 전혀 다른 뜻으로 읽는다.
$ mol = water
bash: mol: command not found
$ mol= water
bash: water: command not found
$ mol=water
$ echo $mol
water
mol = water는 mol이라는 명령에 인자 =와 water를 넘긴 것이 된다.
mol= water는 빈 값의 mol을 잠깐 설정한 채 water라는 명령을 실행하라는 뜻이다.
값을 꺼낼 때는 $mol 또는 ${mol}이라고 쓴다. 변수 이름 바로 뒤에 글자나 밑줄이 이어지면 중괄호가 필요하다.
$ echo "$mol_opt.inp"
.inp
$ echo "${mol}_opt.inp"
water_opt.inp
첫 줄에서 bash는 mol_opt라는 변수를 찾는다. 그런 변수는 없으니 빈 문자열이 되고 .inp만 남는다.
오류도 나지 않는다. 파일 이름을 조립할 때는 습관적으로 중괄호를 쓰는 편이 안전하다.
bash 변수에는 자료형이 없다. 전부 문자열이다. x=5; x+=1의 결과는 6이 아니라 51이다.
숫자 계산은 아래의 $(( ))로 한다.
작은따옴표와 큰따옴표
큰따옴표 안에서는 $변수와 $(명령)이 값으로 바뀐다. 작은따옴표 안의 글자는 하나도 바뀌지 않는다.
$ echo 'single: $mol' "double: $mol"
single: $mol double: water
변수를 쓸 때는 거의 항상 큰따옴표로 감싼다. 따옴표 없는 $f는 공백을 기준으로 여러 단어로 쪼개지고,
* 같은 글자는 파일 이름으로 펼쳐진다. 파일 이름에 공백이 하나만 있어도
cp $f backup/는 엉뚱한 파일 두 개를 찾는다. cp "$f" backup/로 쓰면 이런 일이 없다.
이 문제는 04장에서 다시 다룬다.
명령 치환과 산술
$(명령)은 명령을 실행한 출력으로 바뀐다. 옛 문법인 백틱 `명령`도 같은 일을 하지만 중첩하기 어렵다.
$ n=$(grep -c "^H" water.xyz)
$ echo "H atoms: $n"
H atoms: 2
$ today=$(date +%F)
$ echo "$today"
2026-09-29
정수 계산은 $(( )) 안에서 한다. 괄호 안에서는 변수에 $를 붙이지 않아도 된다.
나눗셈은 몫만 남기고, 소수가 들어가면 오류가 난다.
$ echo $(( 7 / 2 )) $(( 7 % 2 )) $(( 2**10 ))
3 1 1024
$ nprocs=8; echo $(( nprocs * 2 ))
16
$ echo $(( 1.5 * 2 ))
bash: 1.5 * 2 : arithmetic syntax error: invalid arithmetic operator (error token is ".5 * 2 ")
에너지처럼 소수가 필요한 계산은 awk나 bc에 맡긴다. Hartree를 kcal/mol로 바꾸는 예다.
$ awk 'BEGIN { printf "%.2f\n", -76.426369457144 * 627.509 }'
-47958.23
$ echo "-76.426369457144 * 627.509" | bc -l
-47958.234671682974296
입력과 출력: read, echo, printf
read는 키보드에서 한 줄을 받아 변수에 넣는다. -p는 안내 문구, -r은 역슬래시를 그대로 두라는 옵션이다.
-r은 특별한 이유가 없으면 늘 붙인다.
#!/bin/bash
read -r -p "분자 이름: " name
echo "${name}_opt.inp 파일을 만든다"
$ ./ask.sh
분자 이름: benzene
benzene_opt.inp 파일을 만든다
실제로는 입력을 묻는 스크립트보다 인자로 받는 스크립트가 편하다. 작업 스케줄러 안에서는 키보드 입력이 없기 때문이다. 인자는 03장에서 다룬다.
출력은 echo로 충분할 때가 많다. 다만 echo는 -n, -e 같은 옵션 처리가 시스템마다 조금씩 다르다.
자릿수를 맞추거나 탭, 줄바꿈을 정확히 넣으려면 printf를 쓴다. printf는 줄바꿈을 자동으로 붙이지 않으니 \n을 직접 넣는다.
$ printf '%-10s %12.6f\n' water -76.426369457144 ammonia -56.55
water -76.426369
ammonia -56.550000
형식 문자열보다 인자가 많으면 형식을 반복해서 적용한다. 표를 만들 때 편하다.
here-document로 입력 파일 만들기
<<EOF부터 EOF만 있는 줄까지의 텍스트를 명령의 입력으로 넘기는 문법을 here-document라고 한다.
cat > 파일 <<EOF로 쓰면 그 텍스트가 파일이 된다. 안에서 변수와 명령 치환이 펼쳐지니 입력 파일 틀로 쓰기 좋다.
#!/bin/bash
# water.xyz 같은 좌표 파일 하나로 ORCA 입력 파일을 만든다
xyz=water.xyz
mol=${xyz%.xyz}
method="r2SCAN-3c"
nprocs=8
maxcore=3000
charge=0
mult=1
cat > "${mol}_opt.inp" <<EOF
# ${mol}: geometry optimisation + frequencies
! ${method} Opt Freq TightSCF
%pal nprocs ${nprocs} end
%maxcore ${maxcore}
* xyz ${charge} ${mult}
$(tail -n +3 "$xyz")
*
EOF
echo "wrote ${mol}_opt.inp"
tail -n +3은 3번째 줄부터 끝까지 출력한다. xyz 파일의 첫 두 줄(원자 수, 주석)을 버리고 좌표만 넣는 것이다.
$ ./make_inp.sh
wrote water_opt.inp
$ cat water_opt.inp
# water: geometry optimisation + frequencies
! r2SCAN-3c Opt Freq TightSCF
%pal nprocs 8 end
%maxcore 3000
* xyz 0 1
O 0.000000 0.000000 0.119262
H 0.000000 0.763239 -0.477047
H 0.000000 -0.763239 -0.477047
*
끝 표시 EOF는 줄 맨 앞에 있어야 하고 뒤에 공백이 붙으면 안 된다. 이름은 아무 단어나 된다.
구분자를 작은따옴표로 감싸 <<'EOF'라고 쓰면 안의 텍스트가 한 글자도 바뀌지 않는다.
$가 그대로 남아야 하는 내용, 예를 들어 작업 스크립트를 스크립트로 만들 때 쓴다.
변수 일부만 펼치고 일부는 남기려면 따옴표 없는 <<EOF 안에서 남길 쪽에 \$를 쓴다.
$ cat <<'EOF'
! ${method} Opt
cd $PBS_O_WORKDIR
EOF
! ${method} Opt
cd $PBS_O_WORKDIR
$ method=B3LYP
$ cat <<EOF
! ${method} Opt
cd \$PBS_O_WORKDIR
EOF
! B3LYP Opt
cd $PBS_O_WORKDIR
$(tail -n +3 "$xyz")에서 파일 이름이 틀리면 오류 메시지만 화면에 찍히고 입력 파일은 좌표 없이 만들어진다.
ORCA를 돌리기 전에 cat으로 한 번 열어 보는 습관을 들인다. 파일이 있는지 먼저 검사하는 법은 02장에 있다.
파라미터 확장
${...} 안에 기호를 붙이면 변수 값을 잘라 쓸 수 있다. basename, sed를 부르지 않고도
파일 이름에서 확장자나 디렉터리를 떼어 낼 수 있어서 반복문 안에서 자주 쓴다.
| 문법 | 뜻 | f=conformers/ethanol_conf03.xyz일 때 |
|---|---|---|
${f%.xyz} | 끝에서 .xyz를 뗀다 | conformers/ethanol_conf03 |
${f##*/} | 마지막 /까지 앞부분을 뗀다 (파일 이름만) | ethanol_conf03.xyz |
${f%/*} | 마지막 /부터 뒤를 뗀다 (디렉터리만) | conformers |
${f#*_} | 처음 _까지 앞부분을 뗀다 | conf03.xyz |
${f%%_*} | 처음 _부터 뒤를 전부 뗀다 | conformers/ethanol |
${f/.xyz/.inp} | 처음 나오는 .xyz를 .inp로 바꾼다 | conformers/ethanol_conf03.inp |
${n:-4} | n이 비었거나 없으면 4 | (기본값) |
#은 앞에서, %는 뒤에서 자른다. 하나면 가장 짧게, 둘이면 가장 길게 맞는 부분을 자른다.
파일 이름만 남기고 확장자까지 떼려면 두 번에 나눠 쓴다.
$ f=conformers/ethanol_conf03.xyz
$ base=${f##*/}
$ echo "${base%.xyz}"
ethanol_conf03
$ unset nprocs
$ echo "${nprocs:-4}"
4
$ nprocs=16
$ echo "${nprocs:-4}"
16
${nprocs:-4}는 스크립트에 기본값을 주는 흔한 방법이다. 예를 들어 nprocs=${NCPUS:-4}라고 쓰면
PBS 작업 안에서는 요청한 코어 수를, 로그인 노드에서 시험할 때는 4를 쓴다.