章 17. 常規表示 (Regular Expression)

除了應用程式 (Application), GNUstep 還可產生程式庫, bundle 及工具 (command-line tool). 在這介紹製做這些功能的方法. 在這裡先撰寫一個常規表示的程式庫, 再寫個工具來使用這個程式庫.

程式庫其是就是類別的集合. 大般的 Unix 系統都有內建的常規表示程式庫, 在這裡只是用 Objective-C 加以包裝. 首先是 GNUmakefile.

GNUmakefile

include $(GNUSTEP_MAKEFILES)/common.make

LIBRARY_NAME = libRegEx

libRegEx_OBJC_FILES = RegEx.m 

libRegEx_HEADERS = RegEx.h 

# The Headers that are to be installed
libRegEx_HEADER_FILES = RegEx.h 

libRegEx_HEADER_FILES_DIR = .
libRegEx_HEADER_FILES_INSTALL_DIR = RegEx

include $(GNUSTEP_MAKEFILES)/library.make

與應用程式的 GNUmakefile 大同小異. 這裡使用 LIBRARY_NAME 指定程式庫名稱. libRegEx_HEADER_FILES 是要安裝的檔頭 (header), libRegEx_HEADER_FILES_DIR 是這些檔頭所在的目錄, libRegEx_HEADER_FILES_INSTALL_DIR 是要安裝的目錄. 在這個範例中即是 GNUstep/Local/Library/Headers/RegEx/. 有關 GNUmakefile, 可參考 document for GNUstep Makefiles Package

接下來是程式庫的內容:

RegEx.h

#ifndef _RegEx_H_
#define _RegEx_H_

#include <Foundation/Foundation.h>
#include <regex.h>

@interface RegExPattern : NSObject
{
  regex_t *preg;
  /* Mask could be regex options. For example: REG_ICASE, REG_NEWLINE*/
  unsigned int _mask;
}

+ (RegExPattern *) regexPattern: (NSString *) pattern;

- (id) initWithPattern: (NSString *) pattern options: (unsigned int) mask;
- (regex_t *) pattern;

@end

@interface RegExParser: NSObject
{
}

/* The return range is related to the whole string.
 * Not related to the given range.
 */
+ (NSRange) rangeOfString:(NSString *) pattern
                 inString: (NSString *) string;

+ (NSRange) rangeOfPattern: (RegExPattern *) pattern
                  inString: (NSString *) string;

+ (NSRange) rangeOfString: (NSString *) pattern
                 inString: (NSString *) string
                    range: (NSRange) range;

+ (NSRange) rangeOfPattern: (RegExPattern *) pattern
                  inString: (NSString *) string
                     range: (NSRange) range;
@end

#endif /* _RegEx_H_ */

RegEx.m

#include "RegEx.h"

@implementation RegExPattern

+ (RegExPattern *) regexPattern: (NSString *) pattern
{
  id object = [[RegExPattern alloc] initWithPattern: pattern
                                            options: REG_EXTENDED];

  return AUTORELEASE(object);
}

- (void) dealloc
{
  regfree(preg);
  free(preg); /* Not sure about this */
  [super dealloc];
}

- (id) initWithPattern: (NSString *) pattern options: (unsigned int) mask
{
  int result;
  char errbuf[255];
  _mask = mask;

  preg = malloc(sizeof(regex_t));
  result = regcomp(preg, [pattern cString], mask);

  if (result != 0)
    {
      regerror(result, preg, errbuf, 255);
      NSLog(@"RegEx Error: Couldn't compile regex %@: %s", pattern, errbuf);

      regfree(preg);
      return nil;
    }

  self =  [super init];
  return self;
}

- (regex_t *) pattern
{
  return preg;
}
@end

static  regmatch_t pmatch[1];
static  char errbuf[255];

@implementation RegExParser

+ (NSRange) rangeOfString:(NSString *) pattern
                 inString: (NSString *) string
{
  return [RegExParser rangeOfString: pattern
                           inString: string
                              range: NSMakeRange(0, [string length])];
}

+ (NSRange) rangeOfPattern: (RegExPattern *) pattern
                  inString: (NSString *) string
{
  return [RegExParser rangeOfPattern: pattern
                            inString: string
                               range: NSMakeRange(0, [string length])];
}

+ (NSRange) rangeOfString: (NSString *) pattern
                 inString: (NSString *) string
                    range: (NSRange) range;
{
  return [RegExParser rangeOfPattern: [RegExPattern regexPattern: pattern]
                            inString: string
                               range: range];
}

+ (NSRange) rangeOfPattern: (RegExPattern *) pattern
                  inString: (NSString *) string
                     range: (NSRange) range
{
  int result;
  int location, length;
  int mask = 0;

  /* Considering the situation of beginning line */
  if (range.location != 0)
    mask = mask | REG_NOTBOL;
  if ((range.location + range.length) != [string length])
    mask = mask | REG_NOTEOL;
   
  result = regexec([pattern pattern], 
                   [[string substringWithRange: range] cString],
                   1, pmatch, mask);
  if (result != 0)
    {
      if (result != REG_NOMATCH) 
        {
          regerror(result, [pattern pattern], errbuf, 255);
          NSLog(@"RegEx Error: Couldn't match RegEx %s", errbuf);
        }
      return NSMakeRange(NSNotFound, 0);
    }

  location = range.location + pmatch->rm_so;
  length = pmatch->rm_eo - pmatch->rm_so;

  return NSMakeRange(location, length);
}

@end

這個程式庫的內容其實不重要. 編譯及安裝完之後便可以使用這個程式庫了.

在這裡寫個工具來測試這個程式庫. 工具名稱為 regex_test

GNUmakefile

include $(GNUSTEP_MAKEFILES)/common.make

TOOL_NAME = regex_test

regex_test_OBJC_FILES = \
        main.m

regex_HEADERS =

ADDITIONAL_TOOL_LIBS += -lRegEx

include $(GNUSTEP_MAKEFILES)/tool.make

使用 TOOL_NAME 來指定工具名稱. ADDITIONAL_TOOL_LIBS 用來指定所需的 libRegEx 程式庫.

main.m

#include <Foundation/Foundation.h>
#include <RegEx/RegEx.h>

int main (int argc, const char **argv)
{
  NSRange range;
  NSAutoreleasePool *pool = [NSAutoreleasePool new];

  range = [RegExParser rangeOfString: @"middle"
                            inString: @"head middle end"];
  NSLog(@"%@", NSStringFromRange(range));

  RELEASE(pool);
  return 0;
}

GNUstep 的工具其實就是 Unix 的程式. 因此可直接在其所在的目錄執行, 或是設好路徑, 或是使用 opentool 這個 GNUstep 附帶的程式來執行工具, 以避免設定路徑的麻煩.

在這裡可以把程式庫及工具都放在同一個專案目錄中, 避免程式碼到處分散. 這時可以使用 GNUmakefile 的子專案的功能.

在這裡專案目錄假設為 ~/foo/. 工具的部份放在 ~/foo/ 中, 程式庫的部份則放在 ~/foo/RegEx/, 做為工具的子專案. 工具的 GNUmakefile 如下:

GNUmakefile

include $(GNUSTEP_MAKEFILES)/common.make

SUBPROJECTS = RegEx

TOOL_NAME = regex_test

regex_test_OBJC_FILES = \
        main.m

regex_HEADERS =

ADDITIONAL_TOOL_LIBS += -lRegEx
ADDITIONAL_LIB_DIRS += -LRegEx/$(GNUSTEP_OBJ_DIR)

include $(GNUSTEP_MAKEFILES)/aggregate.make
include $(GNUSTEP_MAKEFILES)/tool.make

SUBPROJECTS 表示有個 RegEx 子專案目錄. ADDITIONAL_LIB_DIRS 則表示編譯好的程式庫所在, 如此工具便可以在程式庫未安裝時找到正確的程式庫. aggregate.make 指示 gmake 編譯子專案的方法.

現在所有的程式碼都在同一專案目錄 ~/foo/ 中. 程式碼在此:RegEx-1-src.tar.gz

除了程式庫, GNUstep 還支援 bundle. Bundle 算是可動態載入的程式庫, 或是 Plug-in. Bundle 可以在任意時間載入, 程式庫要在編譯時就指定好.

在這裡將程式庫改成 Bundle.

GNUmakefile

include $(GNUSTEP_MAKEFILES)/common.make

BUNDLE_NAME = RegEx
BUNDLE_EXTENSION = .bundle
BUNDLE_INSTALL_DIR = $(GNUSTEP_INSTALLATION_DIR)/Library/Bundles

RegEx_OBJC_FILES = RegEx.m 

RegEx_HEADERS = RegEx.h 

RegEx_PRINCIPAL_CLASS = RegExParser

include $(GNUSTEP_MAKEFILES)/bundle.make

只是將 LIBRARY 換成 BUNDLE. 最重要的是 RegEx_PRINCIPAL_CLASS. 如此可以從 bundle 中取出最主要的類別而不需要知道該類別的名稱. 如此便完成了 bundle.

使用 bundle 的方法是找出其路徑, 載入, 取出特定名稱的類別或是主類別. 這裡是使用 bundle 的工具.

GNUmakefile

include $(GNUSTEP_MAKEFILES)/common.make

SUBPROJECTS = RegEx

TOOL_NAME = regex_test

regex_test_OBJC_FILES = main.m

regex_HEADERS =

include $(GNUSTEP_MAKEFILES)/aggregate.make
include $(GNUSTEP_MAKEFILES)/tool.make

main.m

#include <Foundation/Foundation.h>
#include <RegEx/RegEx.h>

int main (int argc, const char **argv)
{
  NSRange range;
  NSAutoreleasePool *pool;
  NSArray *paths;
  NSFileManager *fileManager;
  NSString *path;
  NSBundle *bundle;
  Class RegExClass;
  int i;

  pool = [NSAutoreleasePool new];

搜尋 bundle 的路徑.

  fileManager = [NSFileManager defaultManager];

  /* Search for the bundles */
  paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
                                              NSLocalDomainMask, YES);

  for (i = 0; i < [paths count]; i++)
    {
      path = [[paths objectAtIndex: i] stringByAppendingPathComponent: @"Bundles/RegEx.bundle"];
      if ([fileManager fileExistsAtPath: path])
        break;
    }

根據路徑取得 bundle, 並從 bundle 中取出類別.

  bundle = [NSBundle bundleWithPath: path];
  RegExClass = [bundle principalClass];

取得類別後即可直接使用, 如同使用程式庫一般.

  range = [RegExClass rangeOfString: @"middle"
                           inString: @"head middle end"];

  NSLog(@"%@", NSStringFromRange(range));
  
  RELEASE(pool);
  return 0;
}

程式碼在此: RegEx-2-src.tar.gz

使用 bundle 的好處在放如果不到 bundle, 可以在程式中直接取消相關功能. 換言之, 一個程式可以根據使用者所安裝的 bundle 提供不同的服務. Bundle 也可以當做 plug-in 使用. 例如一個程式可以有很多的 bundle 來處理不同的檔案格式. 隨檔案格式的不同, 找到可以處理其格式的 bundle 來處理該檔案. 這些 bundle 中的類別只要使用相同的介面即可, 這可例用 protocol 或是繼承自相同 superclass 來達成. 如果想要安裝 bundle 的檔頭, 可以比照程式庫的用法, 在 GNUmakefile 中使用 _HEADER_FILES 指定要安裝的檔頭及路徑.

Bundle 是相當有用的功能, 值得花點時間研究一下. Bundle 亦可攜帶如影像或聲音等資源.