Iterative execution script

리눅스에서 동일한 작업을 이름 끝에 숫자가 붙은 여러 파일에 대해 반복적으로 하고 싶을 때 사용할 수 있는 shell script입니다.

01: #!/bin/bash
02: # Iterative execution script for numbered files
03: # http://plusha.com, 24 Mar 2007
04: #
05: # Procedure:
06: #   1. Modify variables - EXE, OPT, IN, OUT, NMIN, NMAX
07: #   2. Modify NUM format
08: #   3. Modify Input/Output type
09: #   4. Run the script
10: ### put no space after '='
11:
12: EXE=transp      # Execution file
13: OPT='n1=381'    # Options
14:
15: IN=seismogram.          # basename of input files
16: OUT=trseismogram.       # basename of output files
17:
18: NMIN=11         # Minimum number
19: NMAX=391        # Maximum number
20: INC=10          # Number increment
21:
22: for (( i=$NMIN ; i<= $NMAX ; i=i+$INC ))
23: do
24:   ### NUM format
25:   #  NUM=$( printf "%02d\n" $i )  # 01,10,...,99
26:     NUM=$( printf "%03d\n" $i )  # 001,010,...,999
27:   #  NUM=$( printf "%04d\n" $i )  # 0001,0010,...,9999
28:   echo $NUM
29:
30:   ### Input/Output type
31:   #  $EXE $OPT   $IN$NUM   $OUT$NUM
32:     $EXE $OPT < $IN$NUM > $OUT$NUM
33:   #  $EXE $OPT < $IN$NUM
34: done

위의 내용을 그대로 실행하면 아래의 내용을 실행한 것과 같습니다.

transp n1=381 trseismogram.011
transp n1=381 trseismogram.021

transp n1=381 trseismogram.391

변수들을 바꿔서 다양하게 사용할 수 있겠죠. 만약 여러 파일들의 이름을 바꾸고 싶다면 어떻게 하면 될까요?
실행 명령을 mv로 바꾸고 옵션을 비우고 Input/output type을 첫 번 째 것으로 바꾸면 되겠죠? 그러면

mv old.001 new.001
mv old.002 new.002
mv old.003 new.003

과 같이 실행할 수 있겠죠. 하지만, 더 좋은 방법이 있습니다. ‘rename’ 명령을 사용하면 됩니다.

rename old new pattern

을 실행하면 세번째 argument의 pattern(glob pattern)에 해당하는 파일들을 모두 찾아 old라는 문자열을 new라는 문자열로 바꿔줍니다. 위의 경우에는

rename old new old.*

과 같이 실행할 수 있겠죠.

SConstruct basic (SCons)

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
myEnv=Environment(ENV=os.environ,CFLAGS='-O3',
    FORTRANFLAGS='-O1')

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와 실행 결과를 살펴볼까요?

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

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하는지 살펴보겠습니다.

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

Program('main.e',['main.f','sub1.f','sub2.f'])

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

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

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 이름을 만들어 줍니다. 하지만 경험상 target file 이름을 적어주는 것이 좋더군요.

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

오늘은 가장 기초적인 사용법을 알아보았습니다. 다음에는 scons에 대해 좀 더 자세히 살펴보겠습니다.

SCons Homepage

Rakefile basic

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은 다음과 같고,

01: # target: dependency list
02: # [tab] command
03: F77=gfortran
04:
05: all: main
06:
07: main: main.o sub1.o sub2.o
08:         $(F77) -O2 -o main main.o sub1.o sub2.o
09: main.o: main.f
10:         $(F77) -O2 -c main.f
11: sub1.o: sub1.f
12:         $(F77) -O2 -c sub1.f
13: sub2.o: sub2.f
14:         $(F77) -O2 -c sub2.f
15: clean:
16:         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를 실행하면 CLOBBER와 CLEAN에 지정된 파일들을 모두 지워줍니다. 위에서 볼 수 있는 것처럼, 최종 결과 파일만 CLOBBER에 포함시키고 중간에 생성되는 파일들은 CLEAN에 포함시키면 편리하게 사용할 수 있습니다.

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

01: # $^ : dependency list
02: # $@ : target
03:
04: F77=ifort
05: FFLAG=-assume byterecl -O2
06: TARGET=main
07: OBJECTS=main.o sub1.o sub2.o
08:
09: all: $(TARGET)
10:
11: $(TARGET): $(OBJECTS)
12:         $(F77) -o $@ $^
13:
14: .SUFFIXES: .o .f
15: %.o: %.f
16:         $(F77) ${FFLAG} -c $^
17:
18: clean:
19:         rm $(TARGET) $(OBJECTS)

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들을 참고하세요.
http://rake.rubyforge.org/
http://docs.rubyrake.org/

Next Page »