青木 峰郎 SBクリエイティブ 2017-09-22
こんにちは。
最近、ふつうのLinuxプログラミング 第2版を読んでいるので知識の定着のために学んだ内容を要約したメモを書きます。
このエントリは完全な個人のメモです。
お勉強のためにこの本を読んでいるので、内容を覚えるために以前Summarizing(サマライジング)を行います。
7章の内容はこちら。
amistad06-a.hatenablog.com
第8章 grepコマンドを作る
この章で説明すること
grepコマンドの作成を通して以下のようなことを説明します。
・正規表現
・Linuxにおける日本語処理
・文字コード
など
基本的な正規表現
正規表現についての説明が書いてありました。
*や.や?など簡単なメタキャラクタの説明やエスケープシーケンスの説明がありましたが、ここでは割愛します。
libcの正規表現API
regcompやregfree、regexec、regerrorの簡単な説明がありました。
ここでは説明を割愛しします。
詳しく見たい方はman regexを参照してください。
grepコマンドの実装
以下にソースコードを記載します。
本の中に書いてあるソースをそのまま写経しています。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <regex.h>
static void do_grep(regex_t * pat, FILE *f);
int main(int argc, char *argv[]){
// 正規表現に使う構造体メンバを宣言
regex_t pat;
int err;
int i;
// grep対象文字列が指定されていない場合プログラムを終了
if(argc < 2){
fputs("no pattern\n", stderr);
exit(1);
}
// 正規表現をコンパイルしてregexecの準備をする
// flagsの意味は以下
// REG_EXTENDED : POSIX拡張正規表現を使用する
// REG_NOSUB : マッチの場所を報告しない
// REG_NEWLINE : 改行をマッチさせない
// REG_ICASE : 大文字小文字の違いを無視する
err = regcomp(&pat, argv[1], REG_EXTENDED | REG_NOSUB | REG_NEWLINE);
if(err != 0){
// 正規表現のコンパイルに失敗したら
// エラーログを出してプログラムを終了
char buf[1024];
// エラーコードからエラーメッセージを取得
regerror(err, &pat, buf, sizeof buf);
puts(buf);
exit(1);
}
if(argc == 2){
// コマンドライン引数が2つの場合、標準入力からgrep
do_grep(&pat, stdin);
}else{
// コマンドライン引数が2つ以上の場合、
// ストリームから文字列を取得してgrep
for(i = 2; i < argc; i++){
FILE *f;
f = fopen(argv[i], "r");
if(!f){
perror(argv[i]);
exit(1);
}
do_grep(&pat, f);
fclose(f);
}
}
// regcomp()によって割り当てられたメモリを開放する
// regcomp()を起動したら必ずregfree()も行うこと
regfree(&pat);
exit(0);
}
static void do_grep(regex_t *pat, FILE *src){
char buf[4096];
while(fgets(buf, sizeof buf, src)){
// パターンマッチ実行
if(regexec(pat, buf, 0, NULL, 0) == 0){
// パターンににマッチした文字列を出力
fputs(buf, stdout);
}
}
}
プログラムの説明はプログラム内のコメント部分を参照。
文字コード
文字コードには複数種類がある。
日本語を扱う文字コードで、代表的なのは以下。
- ECU-JP
- shift JIS
- ISO-2022-JP
- UTF-8(Unicode)
- UTF-16(Unicode)
それぞれの文字コードやUnicodeについての説明は割愛。
文字コードを理解するには、符号化文字集合とエンコーディングについて理解する必要がある。
符号化文字集合
符号化文字集合(CCS : Coded Character Set)というのは、文字コードで表現可能な文字の範囲のこと。
日本語で使われる符号化文字集合は主に以下の2つ。
- JIS X 0201 + 0208 + 0212
- UCS(ISO-10646)
JIS Xはshift JISやECU-JP、ISO-2022-JPの符号化文字集合。
UCSはUnicodeの符号化文字集合。
※つまりどの文字コードでどの文字が使用可能か決めたもののこと?
エンコーディング
符号化文字集合に含まれる文字には、それぞれ番号が振られている。
その番号を実際のバイト列に変換する計算式がエンコーディング(CES : Character Encoding Scheme)。
ECU-JPやUTF-8は正確にはこのエンコーディングのことを指す。
エンコーディングは大まかに以下の2つに分類される。
- 全ての文字の使用バイト数が固定のワイドキャラクタ
- 文字の種類によって使用バイト数が異なるマルチバイトキャラクタ
文字列の保存や転送にはマルチバイトキャラクタがよく使われるが、プロセス内でデータを処理するときにはワイドキャラクタもよく使われる(処理が簡単だから)。
プログラムの多言語化、地域化、国際化
プログラムで扱える言語を増やすことを多言語化という。
プログラムの動作を特定の地域の習慣に合わせることを地域化という。
実行時に対応地域を切り替えられるようにすることを国際化という。
国際化のことをI18N(internationalizationの最初と最後IとNの間に18文字あるから)、多言語化のことをM17N(multilingalization)、地域課(localization)のことをL10Nという。
豆知識。
多言語処理と国際化のためのライブラリ
libcのロケール機構
libcのワイドキャラクタ関連ルーチン(wchar)
iconv
gettext
PCRE
鬼車
など。
詳細は割愛。
演習問題
1. 本物のgrepコマンドにある-iオプションを、本章で作ったgrepコマンドに追加しなさい。
サポートサイトを見る前に書いたソースコードを載せます。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <regex.h>
#include <unistd.h>
static void do_grep(regex_t * pat, FILE *f);
int main(int argc, char *argv[]){
// 正規表現に使う構造体メンバを宣言
regex_t pat;
int err;
int i;
int opt;
int eflags = 0;
// optionの解析
while((opt = getopt(argc, argv, "i")) != -1){
switch(opt){
case 'i':
// iオプションが指定された場合、大文字小文字の違いを無視する。
eflags = REG_ICASE;
break;
case '?':
fprintf(stdout, "invalid option. ignore that.\n");
break;
default:
break;
}
}
// for debug
//printf("optind = %d\n", optind);
//printf("argc = %d\n", argc);
// grep対象文字列が指定されていない場合プログラムを終了
if(argc == optind){
fputs("no pattern\n", stderr);
exit(1);
}
// 正規表現をコンパイルしてregexecの準備をする
// flagsの意味は以下
// REG_EXTENDED : POSIX拡張正規表現を使用する
// REG_NOSUB : マッチの場所を報告しない
// REG_NEWLINE : 改行をマッチさせない
// REG_ICASE : 大文字小文字の違いを無視する
err = regcomp(&pat, argv[optind], (REG_EXTENDED | REG_NOSUB | REG_NEWLINE | eflags));
if(err != 0){
// 正規表現のコンパイルに失敗したら
// エラーログを出してプログラムを終了
char buf[1024];
// エラーコードからエラーメッセージを取得
regerror(err, &pat, buf, sizeof buf);
puts(buf);
exit(1);
}
if(optind + 1 == argc){
// コマンドライン引数でファイルが
// 指定されていない場合、標準入力からgrep
do_grep(&pat, stdin);
}else{
// コマンドライン引数が2つ以上の場合、
// ストリームから文字列を取得してgrep
for(i = optind + 1; i < argc; i++){
FILE *f;
f = fopen(argv[i], "r");
if(!f){
perror(argv[i]);
exit(1);
}
do_grep(&pat, f);
fclose(f);
}
}
// regcomp()によって割り当てられたメモリを開放する
// regcomp()を起動したら必ずregfree()も行うこと
regfree(&pat);
exit(0);
}
static void do_grep(regex_t *pat, FILE *src){
char buf[4096];
while(fgets(buf, sizeof buf, src)){
// パターンマッチ実行
if(regexec(pat, buf, 0, NULL, 0) == 0){
// パターンににマッチした文字列を出力
fputs(buf, stdout);
}
}
}
※会社の先輩からのコメント
int eflags = 0;
じゃなくて
int eflags = REG_EXTENDED | REG_NOSUB | REG_NEWLINE;
で初期化した方がスッキリする。
2.正規表現に適合した行でなく、適合した部分文字列を出力するコマンドsliceを書きなさい。regexecの第3引数と第4引数を使う必要があるのでマニュアルを調べましょう
サポートサイトを見る前に書いたソースコードを載せます。
regexのmanが読みづらくて結構ググりました。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <regex.h>
static void do_grep(regex_t * pat, FILE *f);
int main(int argc, char *argv[]){
// 正規表現に使う構造体メンバを宣言
regex_t pat;
int err;
int i;
// grep対象文字列が指定されていない場合プログラムを終了
if(argc < 2){
fputs("no pattern\n", stderr);
exit(1);
}
// 正規表現をコンパイルしてregexecの準備をする
// flagsの意味は以下
// REG_EXTENDED : POSIX拡張正規表現を使用する
// REG_NOSUB : マッチの場所を報告しない
// REG_NEWLINE : 改行をマッチさせない
// REG_ICASE : 大文字小文字の違いを無視する
err = regcomp(&pat, argv[1], REG_EXTENDED | REG_NEWLINE);
if(err != 0){
// 正規表現のコンパイルに失敗したら
// エラーログを出してプログラムを終了
char buf[1024];
// エラーコードからエラーメッセージを取得
regerror(err, &pat, buf, sizeof buf);
puts(buf);
exit(1);
}
if(argc == 2){
// コマンドライン引数が2つの場合、標準入力からgrep
do_grep(&pat, stdin);
}else{
// コマンドライン引数が2つ以上の場合、
// ストリームから文字列を取得してgrep
for(i = 2; i < argc; i++){
FILE *f;
f = fopen(argv[i], "r");
if(!f){
perror(argv[i]);
exit(1);
}
do_grep(&pat, f);
fclose(f);
}
}
// regcomp()によって割り当てられたメモリを開放する
// regcomp()を起動したら必ずregfree()も行うこと
regfree(&pat);
exit(0);
}
static void do_grep(regex_t *pat, FILE *src){
int i;
char buf[4096];
// 1行のうち、最初にマッチした文字列しか見ない
size_t nmatch = 1;
// マッチした文字列のオフセットが入る構造体
regmatch_t pmatch[nmatch];
while(fgets(buf, sizeof buf, src)){
// パターンマッチ実行
if(regexec(pat, buf, nmatch, pmatch, 0) == 0){
// パターンににマッチした文字列を出力
// regmatch_t.rm_so : マッチした文字列の最初の文字のオフセット
// regmatch_t.rm_eo : マッチした文字列の最後の文字のオフセット
// 複数マッチさせたい場合はnmatchの数を増やす
// マッチした文字列のそれぞれのオフセットがpmatchに格納される
for(i = pmatch[0].rm_so; i < pmatch[0].rm_eo; i++ ){
putchar(buf[i]);
}
fprintf(stdout, "\n");
}
}
}
まとめ
第8章の内容は大体こんな感じです。
第9章の内容はこちら。
amistad06-a.hatenablog.com
おわり。
青木 峰郎 SBクリエイティブ 2017-09-22