Regular Expression을 실제 작업에 이용하기에 이은 두번째 글입니다. 이번 글에서는 지난 글에서 소개하지 못했던 자바 소스에 대해서 간략히 설명을 할까 합니다. 그리고는 고수님들의 조언을 받아서 리팩토링도 하고, 또 다른 언어로 바꾸어볼까도 생각합니다. 지난 글에서 밝혔듯이 처음에 작성하고 섣불리 팀내에 배포했다가 몇가지 패턴을 인식못하고 또 잘못 바꾸어주는 덕에 테스트의 중요성을 실감한 사건이기도 했습니다. 정말이지 테스트는 무지 중요합니다. ^^
먼저 소스 코드입니다. 너무 짧은 코드라서 설명할 거리가 별로 없기도 합니다.
전체 소스 코드 보기
[LogRemover.java]
import java.io.*;
import java.util.*;
public class LogRemover {
public final String NEWLINE="\r\n";
boolean isAddPreStatement = false;
boolean isRecoveryMode = false;
boolean isVerbose = false;
boolean isApplyIndentation = false;
int totalFiles;
int failureFiles;
long processedEvents;
public LogRemover()
{
totalFiles = 0;
failureFiles = 0;
processedEvents= 0;
}
public void setAddPreStatement(boolean sw)
{
isAddPreStatement = sw;
}
public void setRecoveryMode(boolean sw)
{
isRecoveryMode = sw;
}
public void setVerbose(boolean sw)
{
isVerbose = sw;
}
public void setApplyIndentation(boolean sw)
{
isApplyIndentation = sw;
}
public void process(File dir)
{
if( isRecoveryMode ) isAddPreStatement = false;
if( !dir.exists())
{
if( isVerbose ) System.out.println("[Error] File Not Found : " + dir.getName());
return;
}
if( dir.isDirectory() )
traverse(dir);
else
doJob(dir);
}
private void traverse(File dir)
{
File[] list = dir.listFiles();
for( File file : list)
{
if( file.isDirectory() ) traverse( file);
if( file.getName().toLowerCase().endsWith(".c")) doJob(file);
}
}
public void doJob(File srcFile)
{
totalFiles ++;
if( isVerbose ) System.out.print("["+totalFiles + "]" + srcFile.getName());
int replaced = 0;
String replaceRegExp = "(event_log\\s*\\(\\s*[a-zA-Z_0-9\\[\\]]+\\s*),.*";
String replaceRegExp2 = "$1\\);";
try{
File tempFile = new File("temp_"+srcFile.getName());
BufferedReader br = new BufferedReader(new FileReader(srcFile));
BufferedWriter bw = new BufferedWriter(new FileWriter(tempFile));
String line;
boolean isIfDefStarted = false;
while( (line = br.readLine()) != null)
{
if( line.matches("^.*#ifdef EVENT_DEBUG.*"))
{
isIfDefStarted = true;
}
boolean isLongEventStatement = line.matches("^\\s*event_log.*,.*;.*");
String indentationStr ="";
if( isLongEventStatement )
{
indentationStr = line.substring(0, line.indexOf("event_log") );
}
if( isIfDefStarted )
{
/*
* 이미 #ifdef EVENT_DEBUG 로 시작된 경우에는
* AddPreStatement 모드인 경우는 그대로 출력하고
* 만약 AddPreStatement 모드가 아닌 경우, 즉
* #ifdef 없이 단순히 EventLog를 삭제하는 경우는
* #ifdef 가 정의된 부분을 함께 삭제하도록 한다.
*/
if( isAddPreStatement )
{
bw.write(line + NEWLINE);
} else {
if( isLongEventStatement )
{
if(isRecoveryMode)
bw.write( line + NEWLINE);
else
bw.write( line.replaceFirst(replaceRegExp,replaceRegExp2) + NEWLINE);
replaced ++;
}
}
} else { /* IfDef 블럭이 아닌 경우 */
if( isLongEventStatement )
{
if( isAddPreStatement )
{
if(isApplyIndentation) bw.write(indentationStr);
bw.write("#ifdef EVENT_DEBUG" + NEWLINE);
bw. write(line + NEWLINE);
if(isApplyIndentation) bw.write(indentationStr);
bw.write("#else" + NEWLINE);
bw.write( line.replaceFirst(replaceRegExp,replaceRegExp2) + NEWLINE);
if(isApplyIndentation) bw.write(indentationStr);
bw.write("#endif" + NEWLINE);
replaced++;
} else {
if( isRecoveryMode )
{
bw.write(line + NEWLINE);
}else{
bw.write( line.replaceFirst(replaceRegExp,replaceRegExp2) + NEWLINE);
replaced++;
}
}
} else {
bw.write(line + NEWLINE);
}
}
if( line.matches("^.*#endif.*"))
{
isIfDefStarted = false;
}
}
bw.close();
br.close();
if( replaced > 0 )
{
String destFileName = srcFile.getAbsolutePath();
srcFile.delete();
tempFile.renameTo(new File(destFileName));
} else {
tempFile.delete();
}
if( isVerbose ) System.out.println( " ..." + replaced + " replaced. Success");
}catch(Exception e)
{
e.printStackTrace();
failureFiles++;
if( isVerbose ) System.out.println( "FAILED!!");
}
processedEvents += replaced;
}
public static void showUsage()
{
System.out.println("Event Log Remover (build 003)");
System.out.println("Usage:");
System.out.println(" options file file ...\n");
System.out.println("[OPTIONS]");
System.out.println(" -v verbose mode");
System.out.println(" -a add preprocessing statements");
System.out.println(" -d delete event log message");
System.out.println(" -r restore original event log");
System.out.println(" -i apply indentation");
System.out.println("");
System.out.println("2008.12.15\nHyun-Kyu Shin\n");
}
/**
* @param args
*/
public static void main(String[] args) {
Vector<String> fileNameList = new Vector<String>();
LogRemover lr = new LogRemover();
lr.setRecoveryMode(false);
lr.setVerbose(false);
lr.setApplyIndentation(false);
boolean isModeDefined = false;
for( String arg : args)
{
if(arg.equals("-a"))
{
lr.setAddPreStatement(true);
isModeDefined = true;
}else if( arg.equals("-d"))
{
lr.setAddPreStatement(false);
isModeDefined = true;
}else if( arg.equals("-r"))
{
lr.setRecoveryMode(true);
isModeDefined = true;
}else if( arg.equals("-v"))
{
lr.setVerbose(true);
}else if( arg.equals("-i"))
{
lr.setApplyIndentation(true);
}else fileNameList.add(arg);
}
if( !isModeDefined || fileNameList.size()==0)
{
showUsage();
System.exit(0);
}
long duration = System.currentTimeMillis();
for( String fileName : fileNameList)
{
try{
lr.process(new File(fileName));
}catch(Exception e)
{
e.printStackTrace();
}
}
duration = System.currentTimeMillis() - duration;
System.out.println("\n\n>>Processing Summary");
System.out.println("Total Files: " + lr.totalFiles);
System.out.println("Failed Files: " + lr.failureFiles);
System.out.println("ProcessedEvents: " + lr.processedEvents);
System.out.println("Elapsed Time: " + duration + " msec");
}
}
배포했다가 버그를 발견하면서 Regular Expression에 대해 좀더 많은 고려를 해야 한다는 것을 느끼게 되었습니다. 다른 글을 통해
Greedy와 Ungreedy에 대한 짧은 글도 썼는데, 정말이지 Regular Expression의 특성은 꼭 알고 가셔야 합니다. 그래야 저처럼 실수 하지 않겠지요.. ^^
제가 작성한 프로그램과 똑같은 일을 수행하는 코드를 Regular Expression을 이용하지 않고 단순히 indexOf 만으로도 충분히 구현이 가능하고 오히려 버그의 개입 여지가 더 없을 수도 있으리라고 생각합니다. 하지만 Regular Expression을 실제 업무에 사용하는 것이 이 글의 목적이기때문에 이에 대한 이해해 주시기 바랍니다.
public static void showUsage()
{
System.out.println("Event Log Remover (build 003)");
System.out.println("Usage:");
System.out.println(" options file file ...\n");
System.out.println("[OPTIONS]");
System.out.println(" -v verbose mode");
System.out.println(" -a add preprocessing statements");
System.out.println(" -d delete event log message");
System.out.println(" -r restore original event log");
System.out.println(" -i apply indentation");
System.out.println("");
System.out.println("2008.12.15\nHyun-Kyu Shin\n");
}
위의 코드는 프로그램의 사용법을 나타내주는 부분입니다. 이것을 먼저 보여드리는 이유는 이 프로그램이 무엇을 하는지를 먼저 보여드리기 위해서입니다. 앞의 글에서 설명했듯이 C 프로그램내에 존재하는 event_log에 대해서 원하는 작업을 수행하는 것이 목적입니다. LogRemover는 모두 세개의 모드를 지원합니다. #ifdef~#else~#endif 를 새롭게 적용하는 -a 모드, 모든 이벤트 메시지를 제거하는 -d 모드, #ifdef~#else~#endif 구문을 다시 제거하는 -r 모드. 그리고 -v 는 처리 과정을 표시하고,-i는 들여쓰기를 지원합니다. 처리 대상이 되는 파일은 동시에 여러개를 입력할 수 있고, 디렉토리인 경우는 하위 디렉토리에 대해서 모두 처리를 수행하게 됩니다.
프로그램의 다른 부분은 별도의 설명이 필요없을 정도로 단순하므로 처리의 핵심인 void doJob(File srcFile) 부분에 대해서만 살펴보도록 하겠습니다.
public void doJob(File srcFile)
{
totalFiles ++;
if( isVerbose ) System.out.print("["+totalFiles + "]" + srcFile.getName());
int replaced = 0;
String replaceRegExp = "(event_log\\s*\\(\\s*[a-zA-Z_0-9\\[\\]]+\\s*),.*";
String replaceRegExp2 = "$1\\);";
try{
File tempFile = new File("temp_"+srcFile.getName());
BufferedReader br = new BufferedReader(new FileReader(srcFile));
BufferedWriter bw = new BufferedWriter(new FileWriter(tempFile));
String line;
boolean isIfDefStarted = false;
while( (line = br.readLine()) != null)
{
if( line.matches("^.*#ifdef EVENT_DEBUG.*"))
{
isIfDefStarted = true;
}
boolean isLongEventStatement = line.matches("^\\s*event_log.*,.*;.*");
String indentationStr ="";
if( isLongEventStatement )
{
indentationStr = line.substring(0, line.indexOf("event_log") );
}
if( isIfDefStarted )
{
/*
* 이미 #ifdef EVENT_DEBUG 로 시작된 경우에는
* AddPreStatement 모드인 경우는 그대로 출력하고
* 만약 AddPreStatement 모드가 아닌 경우, 즉
* #ifdef 없이 단순히 EventLog를 삭제하는 경우는
* #ifdef 가 정의된 부분을 함께 삭제하도록 한다.
*/
if( isAddPreStatement )
{
bw.write(line + NEWLINE);
} else {
if( isLongEventStatement )
{
if(isRecoveryMode)
bw.write( line + NEWLINE);
else
bw.write( line.replaceFirst(replaceRegExp,replaceRegExp2) + NEWLINE);
replaced ++;
}
}
} else { /* IfDef 블럭이 아닌 경우 */
if( isLongEventStatement )
{
if( isAddPreStatement )
{
if(isApplyIndentation) bw.write(indentationStr);
bw.write("#ifdef EVENT_DEBUG" + NEWLINE);
bw. write(line + NEWLINE);
if(isApplyIndentation) bw.write(indentationStr);
bw.write("#else" + NEWLINE);
bw.write( line.replaceFirst(replaceRegExp,replaceRegExp2) + NEWLINE);
if(isApplyIndentation) bw.write(indentationStr);
bw.write("#endif" + NEWLINE);
replaced++;
} else {
if( isRecoveryMode )
{
bw.write(line + NEWLINE);
}else{
bw.write( line.replaceFirst(replaceRegExp,replaceRegExp2) + NEWLINE);
replaced++;
}
}
} else {
bw.write(line + NEWLINE);
}
}
if( line.matches("^.*#endif.*"))
{
isIfDefStarted = false;
}
}
bw.close();
br.close();
if( replaced > 0 )
{
String destFileName = srcFile.getAbsolutePath();
srcFile.delete();
tempFile.renameTo(new File(destFileName));
} else {
tempFile.delete();
}
if( isVerbose ) System.out.println( " ..." + replaced + " replaced. Success");
}catch(Exception e)
{
e.printStackTrace();
failureFiles++;
if( isVerbose ) System.out.println( "FAILED!!");
}
processedEvents += replaced;
}
위의 코드에서 가장 문제가 많았던 부분이 바로
String replaceRegExp = "(event_log\\s*\\(\\s*[a-zA-Z_0-9\\[\\]]+\\s*),.*";
String replaceRegExp2 = "$1\\);";
이 부분입니다. 이벤트 메시지 부분을 제거하고 이벤트 아이디만을 남기로독 하는 부분인데, 이게 메시지 부분에 여러가지 변형사항이 있다보니 이상한 결과들을 내놓는 경우가 있었습니다. 위의 코드에서 replaceRegExp 는 검색을 하는 데 사용되고, replaceRegExp2는 그 결과를 이용해서 치환할 때 사용되는 식입니다. $1 는 replaceRegExp를 통해 검색된 내용 중 ,(comma)의 앞부분을 의미하게 됩니다.
만약 처리하는 파일에 다음과 같은 라인이 있다고 하면,
event_log ( EVENT_01 , "Hello Event" );
이에 대한 처리 결과는
event_log( EVENT_01 );
이 되도록 하는 것입니다.
그런데, Regular Expression을 잘못 쓰게 되면
event_log( EVENT_02 , "Hello, Sir");
은
event_log( EVENT_02, "Hello );
와 같은 현상이 나타나기도 했습니다.
몇 번의 수정을 통해 위와 같은 Regular Expression을 쓰게 되었고, 다행히 지금까지는 별다른 에러 케이스를 찾지 못했습니다. ^^
if( isIfDefStarted )
{
/*
* 이미 #ifdef EVENT_DEBUG 로 시작된 경우에는
* AddPreStatement 모드인 경우는 그대로 출력하고
* 만약 AddPreStatement 모드가 아닌 경우, 즉
* #ifdef 없이 단순히 EventLog를 삭제하는 경우는
* #ifdef 가 정의된 부분을 함께 삭제하도록 한다.
*/
if( isAddPreStatement )
{
bw.write(line + NEWLINE);
} else {
if( isLongEventStatement )
{
if(isRecoveryMode)
bw.write( line + NEWLINE);
else
bw.write( line.replaceFirst(replaceRegExp,replaceRegExp2) + NEWLINE);
replaced ++;
}
}
}
이 부분은 #ifdef EVENT_DEBUG 블럭 안쪽일 경우의 처리를 나타냅니다. 만약 -a 모드인 경우는 있는 그대로 표현하면 됩니다. 어차피 하고자 하는 일이 #ifdef ~ #else ~ #endif 를 추가하는 것이기 때문에 이미 있는 부분은 그대로 유지하고자 하는 것입니다. 만약 -a 모드가 아니라면 기본적으로는 해당 부분을 삭제하게 됩니다. 다만 이벤트 메시지를 포함한 부분인 경우에 대해서만 추가적으로 처리하게 되는데, -r 모드인 경우는 이벤트 메시지를 포함한 전체 문장으로, -d 모드인 경우는 이벤트 메시지가 삭제된 형태로 출력하는 것입니다.
} else { /* IfDef 블럭이 아닌 경우 */
if( isLongEventStatement )
{
if( isAddPreStatement )
{
if(isApplyIndentation) bw.write(indentationStr);
bw.write("#ifdef EVENT_DEBUG" + NEWLINE);
bw.write(line + NEWLINE);
if(isApplyIndentation) bw.write(indentationStr);
bw.write("#else" + NEWLINE);
bw.write( line.replaceFirst(replaceRegExp,replaceRegExp2) + NEWLINE);
if(isApplyIndentation) bw.write(indentationStr);
bw.write("#endif" + NEWLINE);
replaced++;
} else {
if( isRecoveryMode )
{
bw.write(line + NEWLINE);
}else{
bw.write( line.replaceFirst(replaceRegExp,replaceRegExp2) + NEWLINE);
replaced++;
}
}
} else {
bw.write(line + NEWLINE);
}
}
#ifdef EVENT_DEBUG 블럭이 아닌 경우에는 이벤트 메시지를 포함하는 문장이 나오는 경우에 대해서만 추가 처리를 해주면 됩니다. -a 모드라면 #ifdef~#else~#endif 를 적용하고, -r 모드라면 원본 문장 그대로, -d 모드의 경우에는 이벤트 메시지를 삭제한 형태가 출력됩니다.
설명하고 나니 무척이나 간단합니다. 이처럼 간단한 프로그램에서 왜 그리 실수가 많았는지 모르겠네요 ^^
소스 코드에 대한 조언은 언제든지 환영입니다. 오히려 시간 내어 봐주시는 분들께 제가 더 큰 감사를 드려야 겠지요. (근데, 여기 오셔서 보시는 분들이 몇분이나 될 지는 의문입니다 ^^)
좋은 하루 되세요.
댓글을 달아 주세요