카테고리 보관물: Programming

Fortran option parser

gpl에서 사용하기 위해 개발한 포트란 option parser를 소개합니다. Option parser는 command line option들을 분석해서 프로그램에서 사용할 수 있도록 변수로 저장해주는 역할을 합니다. 리눅스나 맥에서 명령을 실행할 때

$ gcc -c -o main.e main.c
$ tar -zxvf file.tgz

와 같은 유닉스 표준 형태의 옵션이 있고,

$ suximage n1=101 d1=0.5 perc=99 < file.su

와 같이 Seismic Un*xMadagascar 등에서 사용하는 형태의 옵션도 있는데, 지금 소개해드리는 option parser는 두 번째 형태의 옵션을 지원합니다. Option parser는 한 번 작성한 프로그램의 재사용을 위해 매우 유용한 기능입니다. 우선 테스트 프로그램을 통해 사용 예를 살펴본 후 자세한 사용법을 보겠습니다.

테스트 프로그램

다음의 테스트 프로그램은 다양한 종류의 변수를 command line option으로부터 읽어서 출력하는 프로그램입니다.
지원하는 자료형은 integer, single precision real, double precision real, logical, character입니다(complex는 아직까지 필요가 없어서 안 넣었습니다). 단일 변수 뿐 아니라 각각의 배열도 지원합니다. 필수 입력 옵션과 기본값을 가진 옵션을 구분하여, 필수 입력 옵션들 중 하나라도 command line option에 없을 경우 도움말을 출력하고 프로그램을 종료합니다.

        program test_optparse
        use gpl_optparse
        implicit none
        integer,parameter:: mxlen=100,mxstr=100
        integer:: i,io,ia(mxlen)
        integer:: j,ni,nf,nb,nd,ns
        real:: f,fo,fa(mxlen)
        logical:: b,bo,ba(mxlen)
        real(kind=8):: d,d_o,da(mxlen)
        character(len=mxstr) :: s,so,sa(mxlen)

        ! required parameters
        call from_par('i',i,'integer number')
        call from_par('f',f,'float: single precision real number')
        call from_par('d',d,'double precision')
        call from_par('b',b,'boolean/logical')
        call from_par('s',s,'string')

        ! optional parameters
        call from_par('io',io,1,'1','optional integer')
        call from_par('fo',fo,1.0,'1.0','optional float')
        call from_par('do',d_o,1.0d0,'1.0d0','optional double')
        call from_par('bo',bo,.true.,'T','optional boolean')
        call from_par('so',so,'','empty','optional string')

        ! array, required parameters
        call from_par('ia',ia,ni,'integer array')
        call from_par('fa',fa,nf,'float array')
        call from_par('da',da,nd,'double array')
        call from_par('ba',ba,nb,'boolean array')
        call from_par('sa',sa,ns,'string array')

        call help_par()
        !call report_par()
        
        print*,'i=',i
        print*,'f=',f
        print*,'d=',d
        print*,'b=',b
        print*,'s=',trim(s)
        print*,''
        print*,'ia=',ia(1:ni)
        print*,'fa=',fa(1:nf)
        print*,'ba=',ba(1:nb)
        print*,'da=',da(1:nd)
        print*,'sa=',(trim(sa(j))//'_',j=1,ns)
        print*,''
        print*,'io=',io
        print*,'fo=',fo
        print*,'do=',d_o
        print*,'bo=',bo
        print*,'so=',so
        end program

실행 결과

위의 프로그램을 컴파일하여 아래와 같이 실행하면 변수들이 제대로 입력된 것을 확인할 수 있습니다. 문자열의 경우 문자열을 둘러싼 따옴표는 제거하고 변수에 저장합니다. 배열은 쉼표를 기준으로 배열의 원소를 나눕니다. 기본값을 가지고 있는 변수의 경우 command line option에 있으면 주어진 값을 저장하고 없으면 기본값을 저장합니다.

$ ./test_optparse s=test.dat i=2 f=2.5 d=3.0 b=T ia=1,2,3,4 fa=1.0,2.0,3.0,4.0 da=1.,2.,3. ba=T,F,T sa=st,'ri',"ng" io=5 so="gpl"
 i=           2
 f=   2.50000000    
 d=   3.0000000000000000     
 b= T
 s=test.dat
 
 ia=           1           2           3           4
 fa=   1.00000000       2.00000000       3.00000000       4.00000000    
 ba= T F T
 da=   1.0000000000000000        2.0000000000000000        3.0000000000000000     
 sa=st_ri_ng_
 
 io=           5
 fo=   1.00000000    
 do=   1.0000000000000000     
 bo= T
 so=gpl

만약 기본값이 없는 필수 옵션 중 하나라도 command line option에 빠져있다면 다음과 같이 도움말을 표시하고 프로그램을 종료합니다.

$ ./test_optparse 
 Required parameters:
     [i] i=             : integer number
     [f] f=             : float: single precision real number
     [d] d=             : double precision
     [b] b=             : boolean/logical
     [s] s=             : string
     [I] ia=            : integer array
     [F] fa=            : float array
     [D] da=            : double array
     [B] ba=            : boolean array
     [S] sa=            : string array
 Optional parameters:
     [i] io=1           : optional integer
     [f] fo=1.0         : optional float
     [d] do=1.0d0       : optional double
     [b] bo=T           : optional boolean
     [s] so=empty       : optional string

위의 도움말에서 i=integer, f=real, d=real(kind=8), b=logical, s=character(len=?)을 의미하고, 각각의 대문자는 배열을 의미합니다.

사용법

그럼 실제 모듈의 사용법을 알아보겠습니다. 모듈은 use gpl_optparse 또는 간편하게 use gpl로 불러올 수 있습니다. 가장 중요한 from_par 서브루틴은 다음과 같이 세 가지 방식으로 사용할 수 있습니다.

call from_par('parname',variable,'help message') !! 필수 옵션
call from_par('parname',variable_arr,len_arr,'help message') !! 필수 배열
call from_par('parname',variable,default,'default message','help message') !! 기본값이 있는 변수

Command line option은 parname=value 형태로 입력 받게 됩니다. 위의 서브루틴들에서 'parname'은 이 때 사용되는 이름이고, valuevariable에 저장됩니다. ’help message’는 도움말 출력시 보여주는 변수 설명입니다.

배열의 경우 parname=value1,value2,value3과 같은 형태로 입력받고, 입력받은 값은 variable_arr에 저장됩니다. 이 때 입력받은 원소는 len_arr 개입니다.

기본값이 있는 변수의 경우 default는 기본값이고, 'default message'는 도움말 출력시 기본값을 보여주기 위한 문자열입니다.

함수 오버로딩을 사용하였기 때문에 정수, 실수 등의 자료형에 상관없이 위의 서브루틴들을 이용할 수 있습니다.

기타 사용할 수 있는 서브루틴들과 함수는 다음과 같습니다.

call help_par()
call help_header('msg before the parameter help msg')
call help_footer('msg after the parameter help msg')
call force_help()

call report_par()

if(given_par('parname')) call do_something()
call from_parfile('parfile.txt')

에서 help_par는 도움말 출력을 위한 서브루틴입니다. 도움말과 관련된 서브루틴들로는 도움말을 보강하기 위한 call help_header('msg'), call help_footer('msg'), 필수 옵션이 주어졌는지와 무관하게 도움말을 출력하기 위한 call force_help()가 있습니다. report_par는 입력받은 변수들을 출력해서 확인하기 위한 서브루틴입니다. 이 외에도 옵션이 주어졌는지 확인하기 위한 logical function given_par('parname') 함수, 옵션들을 저장해놓은 텍스트파일로부터 옵션들을 읽어들이기 위한 call from_parfile('parfile.txt')와 같은 명령이 있습니다.

참고로, command line option에 par=parfile.txt와 같은 옵션이 있으면 parfile.txt에서 먼저 변수를 읽은 후 command line option을 읽습니다. parfile.txt에 주어진 변수가 command line option에 다시 나오면 command line에 주어진 값을 사용합니다.

gpl_optparse 모듈의 소스코드는 gplGitHub에서 받으실 수 있습니다.

Polymorphic Fortran & C

포트란 함수 오버로딩과 중복

앞서 포트란 함수 오버로딩에 관한 글을 올렸습니다. 포트란 모듈과 인터페이스를 이용하면 서로 다른 자료형을 인자로 받는 함수나 서브루틴이라도 같은 이름으로 사용할 수가 있었습니다. 그런데, 동적 자료형을 지원하는 언어와 달리, 포트란에서는 자료형별로 서브루틴들을 따로 만든 후에 같은 이름으로 호출하였습니다. 동적 자료형 언어에서는 함수 자체를 한 번만 작성하면 되지요. 여기에서 포트란 코드 작성에 중복이 발생하게 됩니다. 이러한 중복을 제거하기 위해 파이썬으로 만든 스크립트가 polyfc (Polymorphic Fortran & C)입니다. 예전에 Forpedo에서 아이디어를 얻었는데, Forpedo를 사용하려니 좀 복잡해서 Python 연습도 할 겸 gpl용으로 만들었습니다.

예제

이해하기 쉽게 예제를 살펴보겠습니다. 배열의 내용을 출력하는 서브루틴을 작성하려고 합니다. 배열은 integer, real, real(kind=8), complex, complex(kind=8) 다섯 종류의 자료형을 지원하려고 합니다. 그럼 서브루틴을 자료형에 따라 총 5개를 작성해야 하는데, 선언부만 다르고 나머지는 동일하거나 거의 비슷하게 됩니다. 그래서 일종의 템플릿 서브루틴을 만들고, 필요한 부분만 바꿔가며 서브루틴을 복제하려고 합니다. 그러면 거의 비슷한 코드를 중복해서 작성하는 수고를 덜 수 있겠죠. 아래는 polyfc에 입력으로 들어가는 템플릿 파일입니다.

module polyfc_example

!@interface print_array
contains
!@template print_array ifdcz
    subroutine print_array_<name>(arr)
    <type>,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
!@end
end module

포트란 주석을 이용하여 인터페이스가 들어갈 부분과 템플릿 부분을 표시하고, 자료형에 따라 바뀌어야 하는 부분은 <name>, <type>으로 표시하였습니다. 이 외에 필요할 경우 <kind><esize>도 지원합니다. 바뀌며 들어가는 부분은 다음 표를 보면 알 수 있습니다.

Name Fortran type C type Fotran kind esize
i integer int (kind=4) 4
f real float (kind=4) 4
d real(kind=8) double (kind=8) 8
c complex float complex (kind=4) 8
z complex(kind=8) double complex (kind=8) 16
b logical (not supported) (kind=4) 4
s character(len=*) char (len=*) 1

그래서 polyfc input.f90 > output.f90과 같이 실행했을 때 얻게 되는 파일은 다음과 같습니다.

! This file was generated from pfc.example.f90 by Polyfc at Mon Jul 28 22:12:32 2014.
! Do not edit this file directly.

module polyfc_example

    interface print_array
        module procedure print_array_i
        module procedure print_array_f
        module procedure print_array_d
        module procedure print_array_c
        module procedure print_array_z
    end interface print_array

contains

    subroutine print_array_i(arr)
    integer,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_f(arr)
    real,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_d(arr)
    real(kind=8),intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_c(arr)
    complex,intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
    subroutine print_array_z(arr)
    complex(kind=8),intent(in):: arr(:)
    integer i
    do i=1,size(arr)
        print*, i, arr(i)
    enddo
    end subroutine
 
end module

자동으로 작성하면 손으로 복사했을 경우 생길 수 있는 오류도 피할 수 있겠죠. 단, 코드에 버그가 있을 때에는 output 파일에서 버그가 있는 곳을 찾아 input 파일의 해당 위치를 고쳐줘야 합니다.

참고로, polyfc 이름이 의미하듯, C도 지원합니다. 단, C에서는 <name><type>만 지원합니다. 물론 인터페이스도 지원하지 않습니다. C에서는 아래와 같은 방식으로 사용 가능합니다.

//@template ifdczbs
void abc_<name>(<type> def)
{
    ...
}
//@end

GNU Quick Plot (gnuqp)

Gnuplot은 리눅스에서 텍스트파일에 저장된 값을 빠르게 그림으로 그려주는 프로그램입니다. 다양한 기능을 가지고 있지만, 제 경우에는 주로 수치해석 후 결과 확인용으로 씁니다.
gnuplot으로 그림을 그릴 때에는 command line 상에서 gnuplot이라고 치고 들어가서 gnuplot 명령어들을 이용하여 그림을 그리고 q를 입력하여 빠져나옵니다.
그런데 간단히 결과를 확인해보기 위해서 gnuplot에 들어가서

p 'file1' w l,'file1' u 1:3 w l,'file1' u 1:4 w p

또는

set grid
set xrange[:10]
set log y
p 'file1' w l,'file2' w l,'file3' w l

과 같이 매번 치려니 귀찮다는 생각이 들었습니다. 그래서 gnuqp (GNU Quick Plot)를 만들었습니다. 이 script를 사용하면 command line 상에서 바로 gnuplot 명령어를 사용하여 그림을 그릴 수 있습니다. 사용 방법은 아래와 같습니다.

Usage :
gnuqp [options] filename1 [u 1:2] [w l], filename2 [u 1:2] [w l], filename3 ...

실행파일 이름, 몇 가지 setting 관련 옵션들, 이후에는 gnuplot의 plot 명령어를 입력합니다.

Required parameters :
filename1
Empty filename[2,3,...] will be replaced by the filename1

두 번째 위치부터는 파일명을 생략하면 첫 번째 파일명으로 대체합니다. 하나의 파일에서 여러 column들을 그릴 때 편리합니다.

Optional parameters :
u 1:2   : columns you want to plot
w [lp..]: line style- line, point, dot or impulse ..etc (default: w l)

plot 명령어의 옵션들 중에는 using (columns)과 with (line style)만 지원합니다. 그 외의 명령은 제가 잘 안 써서요^^.
위의 옵션을 주지 않았을 때 기본적으로 with line 옵션으로 그립니다.

-p      : do not run gnuplot. just print the gnuplot command
-c      : no comma seperation - the arguments are filenames seperated with a blank- use with glob pattern
-l       : set logscale y
-g      : set grid
-x[:10] : set xrange [:10]
-y[1:5] : set yrange [1:5]

위의 옵션들은 gnuplot의 setting을 간편하게 하기 위해 만들었습니다.

-p 옵션을 붙이면 gnuplot의 명령어만 출력하고 그림은 안 그립니다.

-c 옵션을 붙이면 파일들을 기본 옵션(with line)으로 그립니다. 이 때 파일명들 사이의 “,”를 생략하고 파일명만 씁니다. command line상에서 glob pattern을 이용하여 여러 그림을 그릴 수 있도록 하기 위한 옵션입니다. 예를 들면, 다음과 같은 경우죠.

./gnuqp.py -p -c file.00*
-> p 'file.0010' w l,'file.0020' w l,'file.0030' w l,'file.0040' w l,'file.0050' w l

나머지 gnuplot setting들은 위의 설명으로 충분할 것이라 생각합니다.
앞에 예를 들었던 명령어들을 gnuqp를 이용하여 실행한다면 다음과 같습니다.

(gnuplot)
p 'file1' w l,'file1' u 1:3 w l,'file1' u 1:4 w p
(q)

gnuqp file1, u 1:3, u 1:4 wp,

(gnuplot)
set grid
set xrange[:10]
set log y
p 'file1' w l,'file2' w l,'file3' w l
(q)

gnuqp file1, file2, file3 -g -l -x[:10]와 같이 실행할 수 있습니다. gnuqp는 gpl에 포함되어 있습니다.

SConstruct 사용법

SConstruct은 Makefile과 비슷한 역할을 하는, Python script입니다. 따라서 Python이라는 언어의 강력한 기능들을 그대로 가져다 쓸 수 있다는 장점이 있습니다. Makefile을 make라는 명령어로 실행하듯이, SConstruct는 scons라는 명령어로 실행합니다. SConstruct file의 작성법은 Makefile이나 Rakefile의 작성법과는 차이가 있습니다. 작성법을 살펴보기 전에 먼저 ‘Environment’와 ‘Builder’라는 개념에 대해 살펴보겠습니다.

Environments

Makefile에서는 기본적으로 Shell의 환경변수들을 가져다가 썼습니다. 물론 PATH 환경변수도 가지고 오기 때문에 compiler의 절대경로를 써주지 않아도 알아서 잘 compile을 했었습니다.
반면에, scons는 기본적으로 Shell의 환경변수들을 가져오지 않습니다. scons를 설치할 때 기본적인 compiler들(gcc, gfortran 등)은 알아서 찾아내기 때문에 보통은 문제가 없지만 특정한 compiler(icc, ifort 등)를 사용하고 싶은 경우 compile 관련 환경변수(construction variables)에 절대경로를 지정해주거나 Shell의 환경변수를 가지고 옵니다. Shell의 환경변수들을 전부 가지고 오고 compiler로 ifort를 사용할 경우 script에 다음과 같이 써줍니다.

import os
DefaultEnvironment(ENV=os.environ, FORTRAN='ifort',
    FORTRANFLAGS='-assume byterecl -O2', LINK='ifort')

scons에는 위에 사용한 Default Environment외에도 사용자가 마음대로 Environment를 만들 수 있습니다. 아래와 같이 쓸 경우 myEnv라는 새로운 Environment를 만들어 사용할 수 있습니다. 이런 식으로 여러 개의 Environment들을 만들어 필요에 따라 같은 프로그램도 옵션을 바꿔가며 compile할 수 있습니다.

import os
DefaultEnvironment(ENV=os.environ, FORTRAN='ifort',
    FORTRANFLAGS='-assume byterecl -O2', LINK='ifort')
myEnv=Environment(ENV=os.environ, CFLAGS='-O3',
    FORTRANFLAGS='-O1')

위의 환경 설정 내용을 이후에 사용하기 위해 myenv.py라는 파일에 저장해두었다고 가정 하겠습니다.

Builders

scons는 기본적으로 많이 사용되는 프로그램들의 compile 방법들을 알고 있습니다. Compile하는 object를 builder라고 하는데, c/c++, fortran, java, TeX, LaTeX, tar, zip 등 다수의 builder들이 존재합니다. 따라서 원하는 builder에 알맞은 target과 source 이름만 넣어주면 scons가 알아서 compile합니다. 필요한 변수(옵션)들은 해당 Environment에서 가지고 옵니다. 기본적인 작성법은 다음과 같습니다.

Program('target1.e', 'source1.f')
myEnv.Program('target2.e', 'source2.c')

첫 번째 줄은 ‘source1.f’라는 파일로부터 ‘target1.e’ 라는 파일을 생성하는 명령입니다. 이 때 필요한 변수들은 Default Environment에서 가지고 옵니다. 두 번째 줄은 ‘source2.c’라는 파일로부터 ‘target2.e’라는 파일을 생성하는 명령이고, 필요한 변수는 myEnv라는 Environment에서 가지고 옵니다. 위에서 Program이라는 명령은 source code에 해당하는 builder를 불러오는 역할을 하죠. 지금까지의 SConstruct script와 실행 결과를 살펴볼까요?

from myenv import *
Program('target1.e','source1.f')
myEnv.Program('target2.e','source2.c')

scons라고 실행하면,

scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
ifort -o source1.o -c -assume byterecl -O2 source1.f
gcc -o source2.o -c -O3 source2.c
ifort -o target1.e source1.o
gcc -o target2.e source2.o
scons: done building targets.

와 같은 화면을 얻게 됩니다. 복잡한 내용은 빼고 실행 결과만 보고 싶을 경우 scons -Q 라고 실행하면 결과를 다음과 같이 보여줍니다.

ifort -o source1.o -c -assume byterecl -O2 source1.f
gcc -o source2.o -c -O3 source2.c
ifort -o target1.e source1.o
gcc -o target2.e source2.o

위의 실행 결과를 보면 source file의 확장자에 따라 필요한 compiler를 사용하고 필요한 환경변수들을 가져다가 사용했음을 알 수 있습니다. 특정 Compiler가 사용하는 환경변수는 scons user manual에서 찾아볼 수 있습니다.

예제

그럼 앞에서 살펴보았던 예제main.f, sub1.f, sub2.f 는 어떻게 compile하는지 살펴보겠습니다.

from myenv import *
Program('main.e',['main.f','sub1.f','sub2.f'])

위와 같이 source file이 여러 개인 경우 source file들을 list로 묶어줍니다. 다른 방법으로 아래와 같이 쓸 수도 있습니다. Split이라는 함수는 문자열을 나눠서 list로 만들어줍니다.

from myenv import *
obj=Split('main.f sub1.f sub2.f')
Program('main.e',obj)

Makefile이나 Rakefile에 비해 상당히 간단하죠? 실행 결과는 다음과 같습니다.

ifort -o main.o -c -assume byterecl -O2 main.f
ifort -o sub1.o -c -assume byterecl -O2 sub1.f
ifort -o sub2.o -c -assume byterecl -O2 sub2.f
ifort -o main.e main.o sub1.o sub2.o

또 앞의 Makefile, Rakefile 예제들과는 달리 clean 이라는 target이 없습니다. scons -c 라고 실행하면 scons는 compile 과정에서 새로 생긴 파일들을 알아서 지워줍니다. 실행 결과는 다음과 같습니다.

scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed main.o
Removed sub1.o
Removed sub2.o
Removed main.e
scons: done cleaning targets.

물론 특정 target만 만들고 싶을 때는 scons main.o와 같이 실행하여 하나의 target만 만들 수도 있습니다. main.o라는 target은 script 내에서 지정해준 적이 없지만 확장자 규칙에 따라 compile 중에 생기는 파일이기 때문에 앞에서와 같이 실행하면 알아서 만들어줍니다. 또한, 많은 경우 dependency도 알아서 check해줍니다.

만약 Program()에서 target을 생략하고 source만 적어주면 list의 첫 번째 source file 이름을 기준으로 target file 이름을 만들어 줍니다.

Program(['main.f','sub.f']) ## -> target='main'

SCons Homepage

예전에 다른 블로그에 올렸던 글인데, 이곳에 복사해둡니다.

Unit number 자동 할당

포트란에서 파일을 열 때 파일에 번호(Logical unit number)를 할당하고 그 번호를 이용하여 파일 내용을 읽거나 파일에 출력을 하게 됩니다. 리눅스에서 다음 세 번호는 기본적으로 할당이 되어(입출력 스트림이 열려) 있습니다.

0: stderr
5: stdin
6: stdout

나머지 4바이트 정수 자료형으로 표현할 수 있는 양의 정수값은 사용자가 파일에 할당하여 사용할 수 있습니다 (컴파일러마다 범위는 차이가 있습니다). 보통은 사용하는데 문제가 없지만 하나의 프로그램에서 여러 개의 파일을 열 경우 앞에서 다른 파일에 할당하여 사용중인 번호를 피해 새로운 번호를 찾아야 합니다. 사용중인 번호를 피하기 위해 앞의 코드를 살펴본다든지, 파일 입출력 서브루틴을 위해 새로운 번호를 전달해줄 경우 귀찮다는 생각이 들게 됩니다. 예를 들면 다음과 같은 경우죠.

subroutine write_a_number(un, filename, n)
integer,intent(in):: un, n
character(len=*),intent(in):: filename
open(un,file=trim(filename))
write(un,*) n
close(un)
end subroutine

이럴 때 inquire함수를 사용하면 특정 번호가 사용중인지 알 수 있고, 따라서 새로운 번호도 자동으로 찾을 수 있습니다. 아래 서브루틴과 함수는 gpl의 포트란 모듈(module_base)에서 복사하였습니다. 파일 번호는 99번부터 10번까지만 사용하게 만들었습니다. 동시에 그 이상의 파일을 열 일은 없다고 생각하고 만들었는데, 많은 파일을 열어야 할 경우 do loop 범위를 조정하면 되겠습니다.

subroutine assign_un(un)
integer, intent(out) :: un
logical :: oflag
integer :: i
do i=99,10,-1
    inquire(unit=i,opened=oflag)
    if(.not.oflag) then
        un=i
        return
    endif
enddo
stop "Error: Logical unit assignment"
end subroutine

logical function un_opened(un) result(val)
integer,intent(in):: un
inquire(unit=un,opened=val)
end function

위의 서브루틴을 사용하면 앞에서 살펴본 write_a_number 서브루틴을 다음과 같이 고칠 수 있습니다. 서브루틴의 인터페이스가 좀 더 간단해졌고, 앞으로는 서브루틴을 사용할 때 파일 번호에 대해 고민할 필요가 없겠죠. 자동화의 간단한 예가 되겠습니다.

subroutine write_a_number_new(filename, n)
integer,intent(in):: n
character(len=*),intent(in):: filename
integer:: un
call assign_un(un)
open(un,file=trim(filename))
write(un,*) n
close(un)
end subroutine

Geophysical Prospecting Library

GPL (Geophysical Prospecting Library)

이 라이브러리는 제가 개인적으로, 대부분 직접 작성하여 사용하는 프로그램들이나 스크립트들을 모아놓은 라이브러리입니다. 연구를 위해 실용적으로 사용하는 잡동사니들의 모음으로, 반복되는 작업들을 자동화하는데 초점을 맞추고 작성한 라이브러리입니다. 내용은 수치해석 프로그래밍을 편리하게 하기 위한 포트란 모듈과 C 함수들, 컴파일을 쉽게 하기 위한 스크립트, 파일 수정 및 부분 추출 등을 위한 프로그램들, 결과 확인 및 논문 그림 그리기를 위한 스크립트들 등이 있습니다. 주로 포트란으로 작성하였고, 그 외에 프로그래밍 연습도 할 겸 C, 파이썬, 루비 등의 언어를 사용하였습니다.

라이브러리 정리 및 간단한 문서화를 위해 블로그를 통해 라이브러리의 기능을 하나 둘씩 공개하려고 합니다. 홈페이지를 방문해주신 누군가에게 도움이 되기를 바랍니다. 코드는 GitHub를 통해 공개하겠습니다.

설치 방법

설치 전에

gpl을 제대로 사용하기 위해서는 Fortran 및 C 컴파일러가 필요하고, Python (2.7)과 Ruby (1.9) 인터프리터가 필요합니다. 모든 gpl 기능을 원활히 사용하기 위해서는 탄성파 자료처리 패키지인 Seismic Un*xMadagascar, Python의 NumpyMatplotlib 라이브러리, SConstruct 그리고 GnuplotImageMagick이 필요합니다.

설치

1. GitHub에서 파일을 받습니다.
2. path_to_gpl/gpl/compiler.py에서 Fortran과 C compiler 관련 설정을 해줍니다.
3. make install을 실행합니다.
4. ~/.bash_profile (Mac에서는 ~/.profile)에 path_to_gpl/etc/env.sh의 내용을 추가해줍니다.

Rakefile 사용법

Rakefile 기본 사용법

Rakefile은 Makefile과 비슷한 역할을 하는, Ruby script입니다. 따라서 Ruby라는 언어의 강력한 기능들을 그대로 가져다 쓸 수 있다는 장점이 있습니다. 단, Ruby를 알아야 제대로 사용할 수 있겠죠. Makefile을 make라는 명령어로 실행하듯이, Rakefile은 rake라는 명령어로 실행합니다. Rakefile 작성법을 Makefile 작성법과 비교하며 살펴보도록 하겠습니다. Makefile의 기본적인 작성법은

Target: Dependency list
[Tab] Command

였죠. Rakefile도 유사합니다. 단, Ruby syntax를 사용하죠. 기본적인 작성법은 다음과 같습니다.

task :name = [:prereq1, :prereq2] do
    Command
end

Makefile에서 Target에 해당하는 것이 Rakefile의 task입니다. 잘 살펴보면 task라는 함수명과 Hash, Block 두 개의 argument로 이루어진 구조라는 것을 알 수 있습니다. Hash의 key는 target이 되고 value는 prerequisites (dependency list)가 됩니다. Block은 실행해야 할 명령들로 이루어집니다. 특별히 compile하는 경우와 같이 파일을 작성하는 task의 경우에는

file "name" = ["prereq1", "prereq2"] do
    Command
end

와 같이 file task를 사용합니다. Command 부분에서 name 또는 dependency list (prereq1, prereq2, … )를 사용하고 싶을 때는

file "name" = ["prereq1", "prereq2"] do |t|
    sh "f77 -o #{t.name} #{t.prerequisites.join(' ')}"
end

과 같이 사용하여 f77 -o name prereq1 prereq2와 같은 결과를 얻을 수도 있습니다.

그럼 앞에서 만들었던 Makefile과 같은 기능을 하는 Rakefile을 만들어 비교해 보겠습니다. 앞에서 만들었던 Makefile은 다음과 같고,

# target: dependency list
# [tab] command
F77=gfortran

all: main

main: main.o sub1.o sub2.o
    $(F77) -O2 -o main main.o sub1.o sub2.o
main.o: main.f
    $(F77) -O2 -c main.f
sub1.o: sub1.f
    $(F77) -O2 -c sub1.f
sub2.o: sub2.f
    $(F77) -O2 -c sub2.f
clean:
    rm main main.o sub1.o sub2.o

이에 해당하는 Rakefile은 다음과 같습니다.

f90='gfortran'
 
task :default => ['main.e']
file 'main.e' => ['main.o','sub1.o','sub2.o'] do |t|
    sh "#{f90} -o #{t.name} main.o sub1.o sub2.o"
end
 
file 'main.o' => ['main.f'] do
    sh "#{f90} -c main.f"
end
file 'sub1.o' => ['sub1.f'] do
    sh "#{f90} -c sub1.f"
end
file 'sub2.o' => ['sub2.f'] do
    sh "#{f90} -c sub2.f"
end
 
require 'rake/clean'
CLEAN.include('*.o')
CLOBBER.include('main.e')

task :default 부분은 Makefile에서 all 이라는 target을 지정해서 사용했던 것과 같은 역할을 합니다. 단, Rakefile에서는 default task가 맨 처음에 나올 필요가 없습니다. 파일 내 아무데나 나와도 잘 인식합니다. 중간 부분은 Makefile과 매우 유사하므로 특별한 설명이 필요 없겠죠? 뒤에 있는 clean task는 rake에 이미 지정되어 있는 task입니다. 사용하기 위해서는 rake/clean을 불러옵니다. rake clean을 실행하면 CLEAN에 포함된 파일들을 지워주고 rake clobber를 실행하면 CLOBBERCLEAN에 지정된 파일들을 모두 지워줍니다. 위에서 볼 수 있는 것처럼, 최종 결과 파일만 CLOBBER에 포함시키고 중간에 생성되는 파일들은 CLEAN에 포함시키면 편리하게 사용할 수 있습니다.

확장자 규칙

Makefile에서는 확장자 규칙을 이용해 편리하게 compile할 수 있었죠? Rakefile에도 같은 기능이 있습니다. 비교해볼까요?

Makefile

# $^ : dependency list
# $@ : target

F77=ifort
FFLAG=-assume byterecl -O2
TARGET=main
OBJECTS=main.o sub1.o sub2.o

all: $(TARGET)

$(TARGET): $(OBJECTS)
    $(F77) -o $@ $^

.SUFFIXES: .o .f
%.o: %.f
    $(F77) ${FFLAG} -c $^

clean:
    rm $(TARGET) $(OBJECTS)

Rakefile

F90='ifort'
FFLAG='-assume byterecl -O2'
TARGET='main.e'
SRC=FileList['*.f']
OBJ=SRC.ext('o')
 
task :default => TARGET
file TARGET => OBJ do
    sh "#{F90} -o #{TARGET} #{OBJ}"
end
rule '.o' => '.f' do |t|
    sh "#{F90} #{FFLAG} -c #{t.source}"
end
 
require 'rake/clean'
CLEAN.include('*.o')
CLOBBER.include('main.e')

Rakefile에서는 rule이라는 함수가 Makefile의 확장자법칙과 같은 역할을 합니다. FileList 명령은 glob pattern (여기서는 ‘*.f’)을 받아들여서 해당하는 파일들의 목록을 만들어주고, FileList 객체의 ext method는 목록에 있는 파일들의 확장자를 원하는 확장자로 바꿔서 새로운 FileList를 만들어줍니다. 앞에서 dependency list를 불러올 때 t.prerequisites.join(' ')이라고 사용했었는데 여기서는 t.source라고 사용했습니다. 앞의 방법은 전체 dependency list를 문자열로 만들어주고(‘ ‘을 이용하여 각각을 합치죠), 뒤의 방법은 dependency list의 첫 번 째 항목만 문자열로 만들어줍니다. 위의 예에서는 dependency list에 ‘.o’에 해당하는 ‘.f’ 파일 하나만 있으니까 t.source라고 사용해도 무관하겠죠?

작업 설명

Makefile에 없고 Rakefile에만 있는 기능 중 하나로, task에 설명을 달 수 있는 기능이 있습니다. task 또는 file task 바로 윗 줄에
desc "description"
이라고 설명을 추가해주면 rake -T라고 실행했을 때 설명과 함께 task 목록을 보여줍니다. Rakefile을 직접 보지 않고도 안에 무슨 task가 있는지 확인할 수 있는 유용한 기능이죠^^

더 자세한 내용은 다음 site를 참고하세요.

예전에 다른 블로그에 올렸던 글인데, 이곳에 복사해둡니다.

포트란과 연산자 오버로딩

함수 오버로딩과 마찬가지로, 연산자 오버로딩도 객체지향 프로그래밍의 다형성과 관련된 개념입니다. 포트란 90에서 연산자 오버로딩을 사용하는 방법을 살펴보겠습니다. 아래 코드는 이차원 좌표 자료형과 두 개의 점을 더하는 함수 예제입니다.

module point2d_op

type point
    real x, y
end type

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3=add(p1,p2)

print*, p3%x,p3%y
end program

새로운 자료형을 정의했으니 새로운 자료형에 대응하는 더하기 연산도 따로 정의할 필요가 있습니다. 그런데 p3=add(p1,p2)와 같이 쓰는 것보다는 p3=p1+p2로 쓰는 것이 더 직관적이고 이해하기 쉽겠죠. 이 때 사용하는 것이 연산자 오버로딩입니다.

연산자 오버로딩

포트란에서는 interface문을 사용하여 이미 존재하는 연산자를 오버로드 하거나 새로운 연산자를 정의할 수 있습니다. 아래 예제는 + 연산자를 오버로드 하는 예제입니다.

module point2d_op

type point
    real x, y
end type

interface operator (+) 
    module procedure:: add 
end interface

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3 = p1 + p2

print*, p3%x,p3%y
end program

수학 연산자를 오버로딩할 때에는 수학적 정의에 합당하도록 또는 이해하기 쉽도록 정의하는 것이 좋습니다. 위의 add 함수를 - 연산자에 오버로딩하는 것도 가능하나 그렇게 되면 프로그래밍할 때 문제가 발생하겠죠.

새로운 연산자 정의

연산자를 오버로드하지 않고 새로 정의할 수도 있습니다. 새로 정의하는 연산자는 .name.과 같이 '.'으로 시작해서 '.'으로 끝나야 합니다. 아래 예제는 add 함수를 .add. 연산자로 정의한 경우입니다. p3=add(p1,p2) 대신 p3=p1.add.p2와 같이 사용한 것을 볼 수 있습니다.

module point2d_op

type point
    real x, y
end type

interface operator (.add.) 
    module procedure:: add 
end interface

contains

    type(point) function add(p1, p2) result(p)
    type(point), intent(in):: p1, p2
    p%x=p1%x+p2%x
    p%y=p1%y+p2%y
    end function

end module

program test_point2d_op
use point2d_op
type(point):: p1,p2,p3

p1%x=1.0 ; p1%y=2.0
p2%x=3.0 ; p2%y=4.0

p3 = p1 .add. p2

print*, p3%x,p3%y
end program

포트란과 함수 오버로딩

포트란과 객체지향 프로그래밍

포트란이라 하면 옛날 언어라고 생각하기 쉬운데, 포트란 언어도 시대의 흐름에 따라 계속 발전해 오고 있습니다. 특히, 2003버전부터는 클래스와 상속 등을 지원하는 객체지향 프로그래밍 언어라 할 수 있습니다. 안타깝게도, 한글로 된 포트란 서적이 별로 없고, 출판된 것도 95버전까지밖에 안 나와 있어서 앞으로 블로그에서 몇 가지 객체지향 프로그래밍과 관련된 기능들을 소개하고자 합니다.

포트란과 함수 오버로딩

함수 오버로딩이란 같은 이름의 서브프로그램(서브루틴, 함수, 메소드 등)이 인자에 따라 다른 기능을 하는 것을 말합니다. 객체지향 프로그래밍의 다형성과 관련된 개념인데, 포트란에서는 90버전에 도입된 interface문을 이용하여 적용 가능합니다.

아래 예제는 2차원 좌표라는 자료형을 정의하고 좌표들 사이의 거리를 구하는 함수를 구현한 예제입니다. 자료형을 정의할 때 single precision 자료형 point_sp와 double precision 자료형 point_dp 두 가지를 정의했고, 따라서 함수도 single precision용 distance_sp와 double precision용 distance_dp 두 가지를 정의했습니다.

module point2d

type point_sp
    real x, y
end type

type point_dp
    real(kind=8):: x,y 
end type

contains

    real function distance_sp(p1, p2) result(dist)
    type(point_sp), intent(in):: p1, p2
    dist=sqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function

    real(kind=8) function distance_dp(p1, p2) result(dist)
    type(point_dp), intent(in):: p1, p2
    dist=dsqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function 

end module

program test_point2d
use point2d
type(point_sp):: s1,s2
type(point_dp):: d1,d2
real:: dist_s
real(kind=8):: dist_d

s1%x=0.0 ; s1%y=0.0
s2%x=3.0 ; s2%y=4.0

d1%x=0.0d0 ; d1%y=0.0d0
d2%x=3.0d0 ; d2%y=4.0d0

dist_s=distance_sp(s1,s2)
dist_d=distance_dp(d1,d2)

print*, dist_s, dist_d
end program

위의 예제에서 두 개의 함수가 하는 일은 같지만, 정적 자료형 언어의 특성상 자료형에 따라 두 개의 함수를 정의해야 했습니다. 이 프로그램은 간단해서 별 문제가 없지만, 프로그램이 복잡해지면 이렇게 같은 기능의 다른 함수들로 인해 인터페이스가 복잡해지게 됩니다. 같은 기능의 함수들을 single/double과 같은 자료형에 상관없이 같은 이름으로 사용할 수 있다면 프로그래밍 인터페이스가 좀 더 단순해지겠죠. 위의 모듈에 아래의 interface 구문을 넣으면 distance_sp 함수와 distance_dp 함수를 모두 distance라는 이름으로 사용할 수 있습니다.

interface distance
    module procedure:: distance_sp
    module procedure:: distance_dp
end interface

위와 같이 선언해 놓으면 두 함수의 인자가 두 개의 point_sp와 두 개의 point_dp로 다르기 때문에 인자의 자료형을 가지고 어떤 함수를 사용해야하는지 판단할 수 있게 됩니다. 만약 두 함수의 인자가 동일하다면 interface문을 사용할 수 없습니다. 아래는 함수 오버로딩을 사용하도록 수정한 예제입니다.

module point2d

type point_sp
    real x, y
end type

type point_dp
    real(kind=8):: x,y 
end type

interface distance
    module procedure:: distance_sp
    module procedure:: distance_dp
end interface

contains

    real function distance_sp(p1, p2) result(dist)
    type(point_sp), intent(in):: p1, p2
    dist=sqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function

    real(kind=8) function distance_dp(p1, p2) result(dist)
    type(point_dp), intent(in):: p1, p2
    dist=dsqrt((p1%x-p2%x)**2+(p1%y-p2%y)**2)
    end function 

end module

program test_point2d
use point2d
type(point_sp):: s1,s2
type(point_dp):: d1,d2
real:: dist_s
real(kind=8):: dist_d

s1%x=0.0 ; s1%y=0.0
s2%x=3.0 ; s2%y=4.0

d1%x=0.0d0 ; d1%y=0.0d0
d2%x=3.0d0 ; d2%y=4.0d0

dist_s=distance(s1,s2)
dist_d=distance(d1,d2)

print*, dist_s, dist_d
end program

자료형에 상관없이 distance라는 함수를 이용하여 거리를 계산하고 있음을 알 수 있습니다. 물론 위에서 distance_spdistance_dp도 사용 가능합니다. 함수 오버로딩을 사용한다고 프로그램의 실행 속도가 빨라지지는 않습니다. 하지만 서브프로그램들의 인터페이스를 단순화하여 프로그래밍을 좀 더 편하게 할 수 있다는 장점이 있습니다.

Makefile 사용법

Makefile의 기초적인 사용법을 알아봅시다.

Makefile이 필요한 경우

먼저, 다음과 같이 3개의 source 파일이 있을 때 컴파일하는 과정을 알아보겠습니다.

첫 번째 파일: main.f

       implicit none
       print *, 'main'
       call sub1()
       call sub2()
       end

두 번째 파일: sub1.f

       subroutine sub1()
       print*, 'sub1'
       end

세 번째 파일: sub2.f

       subroutine sub2()
       print*, 'sub2'
       end

main.f 파일에서 sub1.f와 sub2.f에 있는 subroutine들을 불러오는 매우 간단한 프로그램입니다. 여기서는 하나의 파일 안에 subroutine을 다 넣는 것이 더 편하지만, Makefile 연습을 위해 세 개의 파일로 나누어 놓았습니다.
이렇게 세 개의 파일을 가지고 실행 파일을 만들기 위한 명령은 다음과 같죠.

f77 -c -O2 -o main.o main.f
f77 -c -O2 -o sub1.o sub1.f
f77 -c -O2 -o sub2.o sub2.f
f77 -o main main.o sub1.o sub2.o

위 명령에서 -c 옵션은 source code를 가지고 object file을 생성하라는 의미입니다. 각각의 source file들에 대해 object file을 생성하고 나중에 링크시켜서 실행파일을 만듭니다. -O2는 compile 할 때 optimization level 을 2로 하라는 의미, -o 파일명 은 output file을 -o 다음에 나오는 파일명으로 만들라는 의미입니다. 마지막 줄에서 세 개의 objective file들을 링크시켜서 main 이라는 실행파일을 생성합니다. 실행 파일의 실행 결과는 다음과 같습니다.

$ ./main
main
sub1
sub2

잘 실행됩니다. 그런데 만약 sub1.f 파일을 수정했다면 어떻게 해야할까요? 다시 컴파일하기 위해서는

f77 -c -O2 -o sub1.o sub1.f
f77 -o main main.o sub1.o sub2.o

라고 수정한 파일만 다시 컴파일한 후, 다른 object file들과 링크시켜서 실행파일을 만들어야겠죠. 좀 귀찮습니다. 자동으로 할 수 있으면 좋겠죠. shell script를 하나 만들어서 처음의 컴파일 명령 4줄을 다 써 넣으면 자동으로 실행할 수 있습니다. 하지만, 그렇게 되면 sub1.f 파일을 고쳤을 때 main.f와 sub2.f 파일까지 새로 컴파일하게 됩니다. 프로그램이 간단할 때는 큰 문제가 없지만, 프로그램이 크고 복잡해지면 컴파일 시간이 오래 걸린다는 문제가 생기게 됩니다. Source code 내용에 따라서 특정 code는 다른 code보다 먼저 컴파일해야만 하는 경우도 생길 수 있습니다. 이런 경우에 편리하게 쓸 수 있는 프로그램이 바로 make입니다. make는 실행했을 때 현재 디렉토리에 있는 Makefile 이라는 파일을 찾아 build 작업을 수행합니다.

Makefile 작성법

Makefile의 작성법은

Target: Dependency list
[Tab] Command

와 같습니다. Target은 만들고 싶은 대상, Dependency list는 Target을 만들기 전에 먼저 만들어져야 할 대상들, Command는 Target을 만드는 방법(command line 명령어)입니다. 한 가지 주의할 점은 Command 앞에는 Tab이 들어가야 한다는거죠. 그럼 위의 세 파일을 컴파일하기 위한 기초적인 Makefile을 살펴보겠습니다.

# target: dependency list
# [tab] command
F77=gfortran

all: main

main: main.o sub1.o sub2.o
    $(F77) -O2 -o main main.o sub1.o sub2.o
main.o: main.f
    $(F77) -O2 -c main.f
sub1.o: sub1.f
    $(F77) -O2 -c sub1.f
sub2.o: sub2.f
    $(F77) -O2 -c sub2.f
clean:
    rm main main.o sub1.o sub2.o

F77=gfortran 이라고 먼저 선언을 했습니다. 여기서 F77은 매크로(일종의 변수)입니다. 이런식으로 선언을 해두면 뒤에 $(F77)과 같이 필요할 때 불러서 쓸 수 있습니다. 컴파일러를 바꿀 때 gfortran 대신에 f77 이나 ifort 등으로 바꿔주면 되겠죠.

다음에 나오는게 all 이라는 Target입니다. Dependency list에는 main이 있고 Command는 없네요. Command line에서 make를 실행하면 현재 디렉토리에 있는 Makefile을 찾아 제일 처음에 나오는 Target만 실행합니다. make target1과 같이 실행하면 Makefile내에서 target1 이라는 Target을 찾아 실행합니다. 따라서 맨 처음 Target을 all 이라고 지정해두고 Dependency list에 자신이 만들고 싶은 Target들을 적어두면 make만 쳐서 원하는 Target들을 한 번에 만들 수 있겠죠. 전체적인 compile 과정은 다음과 같습니다.

  1. 제일 처음에 나오는 all 이라는 Target을 만나서 Dependency list를 확인한다. main이라는 Dependency를 찾았다.
  2. Dependency를 만족하기 위해 main이라는 Target을 찾는다. 그리고 main의 Dependency list – main.o, sub1.o, sub2.o 를 찾았다.
  3. main.o라는 Target을 찾아서 Dependency main.f를 찾고 현재 디렉토리에 main.o 파일이 없거나 main.o 파일의 수정 시간이 main.f 파일의 수정시간보다 이전일 때 gfortran -O2 -c main.f 라는 Command를 실행한다. 그렇지 않은 경우에는 아무 것도 실행하지 않는다.
  4. main.o가 잘 만들어졌으면 다시 main 이라는 Target으로 넘어가 sub1.o, sub2.o라는 Dependency를 같은 방법으로 만족하고 돌아온다.
  5. main의 Dependency 세 개가 다 만족되었으면 gfortran -O2 -o main main.o sub1.o sub2.o 라는 Command를 실행하여 main이라는 Target을 만든다.
  6. main이라는 Target이 만들어졌으면 all이라는 Target으로 돌아간다.
  7. all의 Dependency가 다 만족되었지만, Command가 없으므로 make가 끝난다.

clean이라는 Target은 처음에 나오지도 않고 다른 Target의 Dependency 에도 들어가지 않으니 실행이 안 됩니다. 명령줄에서 make clean이라고 실행했을 때만 실행이 되죠. clean은 Dependency list가 비어있으니까 make clean이라고 실행하면 해당하는 Command를 항상 실행하게 됩니다. 보통 make로 생성된 파일들을 지우기 위해 clean이라는 Target을 만듭니다.

여기까지만 배우고 끝내기에는 아쉽습니다. Makefile에는 강력한 기능들이 많기 때문이죠. 몇 개만 더 살펴봅시다.

확장자 규칙

아래의 Makefile은 compiler과 compile option이 약간 바뀐 것 말고는 위의 Makefile과 같은 기능을 합니다.

# $^ : dependency list
# $@ : target

F77=ifort
FFLAG=-assume byterecl -O2
TARGET=main
OBJECTS=main.o sub1.o sub2.o

all: $(TARGET)

$(TARGET): $(OBJECTS)
    $(F77) -o $@ $^

.SUFFIXES: .o .f
%.o: %.f
    $(F77) ${FFLAG} -c $<

clean:
    rm $(TARGET) $(OBJECTS)

Command 위치에서 $^는 Dependency list를 자동으로 입력해줍니다. 또한 $@는 Target 이름을 자동으로 입력해줍니다.

.SUFFIXES: .o .f
%.o: %.f
    $(F77) ${FFLAG} -c $<

는 확장자 규칙으로, .SUFFIXES: .o .f.o 라는 확장자와 .f라는 확장자를 특별히 중요하게 생각하라는 뜻입니다. 그 아래에 나오는 내용은 .o 확장자를 가진 Target에 대해 .f 파일을 이용한 Dependency 와 Command를 자동으로 생성해주는 기능을 합니다. 따라서 처음의 Makefile에 있었던 main.o, sub1.o, sub2.o 라는 Target과 Dependency, Command를 자동으로 만들어줍니다. 한 가지 주의할 점은, 확장자 규칙에서는 앞서 나왔던 $^가 아닌, $<를 사용한다는 점입니다. $<는 확장자 규칙에서만 사용되며, 타겟보다 나중에 변경된 종속 항목들을 의미합니다. Build 과정이 복잡할 때 Makefile은 큰 힘을 발휘합니다.

더 자세한 내용을 알고 싶으신 분들은 임대영님의 GNU Make 강좌를 참고하세요.

예전에 다른 블로그에 올렸던 글인데, 이곳에 복사해둡니다.