共通テーマ: 実例でわかる awk: 第 3 回

ストリング関数と...小切手帳?

awk シリーズの最終回では、Daniel 氏が awk の重要なストリング関数を紹介し、完全な小切手帳残高計算プログラムを最初から作成する方法を示します。この記事を読み進むにつれ、独自の関数を作成する方法や awk の多次元配列の使い方の理解が深まっていくことと思います。そしてこの記事を終える頃には、皆さんの awk についての知識はさらに高まり、より強力なスクリプトを作成できるようになるでしょう。

Daniel Robbins, President/CEO, Gentoo Technologies, Inc.

ニューメキシコ州アルバカーキに住む Daniel Robbins 氏は、Gentoo Technologies, Inc. の社長兼 CEO であり、PC 拡張版 Linux である Gentoo Linux、および Linux 版次世代ポート・システムである Portage の作成者です。また、Macmillan 書籍の Caldera OpenLinux Unleashed、SuSE Linux Unleashed、および Samba Unleashed の共同執筆者でもあります。彼は、小学 2年のとき初めて Logo プログラム言語や、潜在的に危険な Pac Man に出会って以来、何らかの形でコンピューターに関係してきています。これで、彼がなぜ SONY Electronic Publishing/Psygnosis でリード・グラフィック・アーチストを務めていたかが分かるでしょう。彼は妻 Mary さんや生まれたばかりの娘 Hadassah さんと過ごす時間を大切にしています。



2001年 4月 01日

出力のフォーマット

awk の print ステートメントは、多くの場合、所期の目的を果しますが、不十分の場合もあります。このような場合は、awk と類似の機能を持つprintf() とsprintf() という関数が役に立ちます。もちろん、これらの関数は、他の多くの awk 部品と同様、対応する C 言語の関数と同一です。printf() はフォーマット・ストリングを stdout に出力するのに対し、sprintf() は変数に代入できるフォーマット・ストリングを戻します。printf() と sprintf() のことをよく知らない方は、初心者向けの C 言語の参考書に目を通せば、この 2つの重要な出力関数についてすぐ理解できます。Linux システムでは、"man 3 printf" と入力すると、printf() の man ページが表示されます。

次に、awk の sprintf() と printf() のコード例を示します。お分かりのように、すべて C とほとんど同じです。

x=1
b="foo"
printf("%s got a %d on the last test\n","Jim",83)
myout=("%s-%d",b,x)
print
myout

このコードでは、以下が出力されます。

Jim got a 83 on the last test
foo-1

ストリング関数

awk にはストリング関数が大量にあります。これは大いに役に立ちます。awk を使う場合、実際にストリング関数が必要になります。なぜなら、C、C++、Python などのほかの言語のように、ストリングを文字の配列として扱うことができないからです。たとえば、以下のコードを実行した場合、

mystring="How are you doing today?"
print mystring[3]

次のようなエラーが発生します。

awk: string.gawk:59: fatal: attempt to use scalar as array

そうです。Python のシーケンス型ほど便利ではありませんが、awk のストリング関数も仕事をうまくこなしてくれます。では、説明しましょう。

まず、ストリングの長さを戻す基本的なlength() 関数があります。次に、この使い方を示します。

print length(mystring)

このコードでは、以下が出力されます。

24

先に進みましょう。次のストリング関数は index と言います。別のストリング内で検出したサブストリングの位置を戻します。ストリングが見つからない場合は 0 を戻します。mystring の場合は、このように呼び出すことができます。

print index(mystring,"you")

以下が出力されます。

9

次に、簡単な関数をもう 2つ説明しましょう。tolower() と toupper() です。お察しのとおり、これらの関数は、すべての文字をそれぞれ小文字または大文字に変換してストリングを戻します。tolower() と toupper() は新しいストリングを戻すのであって、オリジナルを変更するのではないことに注意してください。以下のコードでは、

print tolower(mystring)
print toupper(mystring)
print mystring

以下が出力されます。

how are you doing today?
HOW ARE YOU DOING TODAY?
How are you doing today?

これまでのところは問題ありませんが、ストリングからサブストリングや単一の文字を正確にはどのように選択しているのでしょうか。ここで登場するのが substr() です。次に、substr() の呼び出し方を示します。

mysub=substr(mystring,startpos,maxlen)

mystring は、サブストリングを取り出したいストリング変数またはリテラル・ストリングのいずれかでなければなりません。startpos は、先頭の文字位置に設定し、maxlen では、取り出したいストリングの最大長さを指定します。指定するのは最大長さであることに注意してください。length(mystring) が startpos + maxlen よりも短いと、結果が切り捨てられてしまいます。substr() はオリジナルのストリングを変更するのではなく、代わりにサブストリングを戻します。以下に例を示します。

print substr(mystring,9,3)

以下が出力されます。

you

配列索引を使用してストリングの一部にアクセスする言語で日頃プログラミングしている場合 (しない人がいるでしょうか) は、substr() は awk ではこの配列索引に代わるものであることを覚えておきましょう。この関数を使用しなければ、単一の文字やサブストリングを取り出すことはできません。awk はストリング・ベースの言語なので、この関数をよく使用することになります。

では、もっと中心的な関数の説明に移りましょう。最初の関数は match() です。match() は、index() のようにサブストリングを検索するのではなく、正規表現を検索する点を除き、index() と非常によく似ています。match() 関数は、一致するものの先頭位置を戻し、一致するものがない場合は 0 を戻します。さらに、match() は RSTART と RLENGTH をいう 2つの変数を設定します。RSTART には、戻り値 (最初に一致するものの場所) が入り、RLENGTH では、文字内におけるその長さが指定されます (一致がない場合は -1)。RSTART、RLENGTH、substr()、および小さいループを使用することで、簡単にストリング内のすべての一致の検索を反復することができます。以下に、match() 呼び出しの例を示します。

print match(mystring,/you/), RSTART, RLENGTH

以下が出力されます。

9 9 3

ストリングの置換

では、ストリング置換関数 sub() と gsub() について説明しましょう。これらの関数は、これまでに説明してきた関数とは若干異なり、実際にオリジナルのストリングを変更します。以下は、sub() の呼び出し方を示すテンプレートです。

sub(regexp,replstring,mystring)

sub() を呼び出すと、sub() は mystring 内で regexp と一致する文字の最初のシーケンスを探し、そのシーケンスを replstring と置換します。sub() と gsub() は同一の引数を取ります。これらの関数の唯一の相違点は、sub() は最初に regexp と一致するもの (存在する場合) を置換するのに対し、gsub() はグローバル置換を実行し、ストリング内で一致するすべてのものを置き換えます。以下に、sub() と gsub() 呼び出しの例を示します。

sub(/o/,"O",mystring)
print mystring
mystring="How are you doing today?"
gsub(/o/,"O",mystring)
print
mystring

最初の sub() 呼び出しにより mystring が直接変更されたので、mystring をオリジナルの値にリセットしなければなりません。このコードを実行した場合、出力は以下のようになります。

HOw are you doing today?
HOw are yOu dOing tOday?

もちろん、もっと複雑な正規表現も可能です。もっと複雑な regexp のテストは、皆さんに任せることにしましょう。

ストリング関数の説明の最後に、split() という関数を紹介します。split() の仕事はストリングを「分割」して、各部分を整数索引付けされた配列に入れることです。以下に、split() 呼び出しの例を示します。

numelements=split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",mymonths,",")

split() を呼び出す場合、最初の引数では、分割するリテラル・ストリングまたはストリング変数を指定します。2つ目の引数では、split() が分割したそれぞれの部分を入れる配列の名前を指定します。3つ目の要素では、ストリングを分割する場合に使用する区切り文字を指定します。split() は戻す際に、分割したストリング要素の数を戻します。split() は、各要素を 1 から始まる配列索引に割り当てるので、コードは以下のようになります。

print mymonths[1],mymonths[numelements]

以下が出力されます。

Jan Dec

特殊なストリング・フォーム

クイック・メモ -- length()、sub()、または gsub() を呼び出す場合には、最後の引数を省略できます。awk は、関数呼び出しを $0 (現行の行全体) に適用します。各行の長さをファイルに出力するには、以下の awk スクリプトを使用します。

{
	print length()
}

財政管理の楽しみ

数週間前、わたしは awk で自分独自の小切手帳残高計算プログラムを作成することにしました。最新の預金額と引き出し額を入力できる、簡単なタブで区切ったテキスト・ファイル作成したいと思いました。つまり、このデータを awk スクリプトに渡して、そこで自動的にすべての金額を加算し、残高を算出するという考えでした。以下に、わたしのすべての取引を "ASCII 小切手帳" にどのように記録することにしたかを示します。

23 Aug 2000	food	-	-	Y	Jimmy's Buffet		30.25

このファイル内の各フィールドは、1つ以上のタブで区切っています。日付 (フィールド1、$1) の後には、「支出カテゴリー」と「収入カテゴリー」という 2つのフィールドがあります。上記のような行に支出を入力する場合は、支出フィールドに 4 文字のフィールド名を入力し、収入フィールドに "-" (ブランクのエントリー) を入力します。これは、この特定の項目が「食費」であることを示すものです。以下に、預金の場合はどのようになるかを示します。

23 Aug 2000	-	inco	-	Y	Boss Man		2001.00

この場合、支出カテゴリーに "-" (ブランク) を入力し、収入カテゴリーに "inco" を入力します。"inco" は一般的な収入 (給料スタイル) のフィールド名です。カテゴリーのニックネームを使用することで、収入と支出のカテゴリー別の分類を生成することができます。残りのレコードに関する限り、他のフィールドはすべて一目で分かるようになっています。分かりやすい (?) フィールド ("Y" または "N") は、取引を口座に記入したかどうかを記録するものです。そのほかに、取引の説明と、プラスのドル金額があります。

現在の残高の計算に使用しているアルゴリズムはそれほど難しくはありません。awk は、単に各行を 1 行ずつ読み取ることを必要としているだけです。支出カテゴリーはリストされているけれども収入カテゴリーがない (つまり "-" の) 場合、この項目は借方です。収入カテゴリーはリストされているけれども支出カテゴリーがない (つまり "-" の) 場合、そのドル金額は貸方です。そして、支出カテゴリーと収入カテゴリーの両方がリストされている場合、この金額は「カテゴリー振替」です。つまり、ドル金額は支出カテゴリーから減算され、収入カテゴリーに追加されます。繰り返しますが、これらのカテゴリーはすべて仮想上のものですが、収支を追跡するだけでなく、予算管理にも非常に役立ちます。


コード

次は、コードについての説明です。1 行目から見ていきましょう。BEGIN ブロックと関数定義です。

balance、第 1 部
#!/usr/bin/env awk -f
BEGIN {
	FS="\t+"
	months="Jan Feb Mar Apr May Jun
Jul Aug Sep Oct Nov Dec"
}
function monthdigit(mymonth) {
	return (index(months,mymonth)+3)/4
}

1 行目の "#!..." を任意の awk スクリプトに追加すると、最初に "chmod +x myscript" を実行していれば、シェルから直接実行することができます。残りの行では、BEGIN ブロックを定義します。このブロックは、awk が小切手帳ファイルの処理を始める前に実行されます。FS (フィールド区切り文字) を "\t+" に設定すると、フィールドを 1つ以上のタブで区切ることが awk に伝えられます。さらに、次の行では、monthdigit() 関数で使用する months というストリングを定義します。

最後の 3 行は、独自の awk 関数の定義方法を示しています。フォーマットは簡単です。"function" と入力してから関数名を入力し、パラメーターをコンマで区切って括弧で囲って入力します。この後、"{ }" コード・ブロックには、この関数を実行したいコードが入ります。関数はすべて (months 変数のような) グローバル変数にアクセスすることができます。さらに、awk には、関数に値を戻させることができるのと同時に、C、Python などの言語の "return" と同じような動作をする "return" 文も用意されています。この特定の関数は、3 文字のストリング・フォーマットである月の名前を等価の数値に変換します。たとえば、以下の場合、

print monthdigit("Mar")

以下が出力されます。

3

では、その他の関数の説明に移りましょう。


会計用関数

次に、記帳を実行する 3つの関数を紹介します。次に示すメイン・コード・ブロックは、小切手帳ファイルの各行を順次処理し、これらの関数の 1つを呼び出して、適切な取引が awk 配列に記録されるようにします。取引には、基本的に 3 種類あります。貸方 (doincome)、借方 (doexpense)、そして振替 (dotransfer) です。3つの関数はすべて mybalance という 1つの引数を取ることに注意してください。mybalance は、引数として渡すことになる 2 次元配列のプレースホルダーです。これまで、2 次元配列を扱ったことはありませんでしたが、以下で分かるように、構文は極めて簡単です。各次元をコンマで区切るだけで、終わりです。

以下のように、情報を "mybalance" に記録します。配列の 1つ目の次元の範囲は 0?12 で、月 (または、年全体を表す場合は0) を指定します。2つ目の次元は、"food"、"inco" などの 4 文字のカテゴリーです。これは、処理する実際のカテゴリーです。したがって、food カテゴリーの年全体の残高を求める場合は、mybalance[0,"food"] となります。6 月の収入を求める場合は、mybalance[6,"inco"] となります。

balance、第 2 部
function doincome(mybalance) {
	mybalance[curmonth,$3] += amount
	mybalance[0,$3]
+= amount	
}
function doexpense(mybalance) {
	mybalance[curmonth,$2] -= amount
	mybalance[0,$2]
-= amount	
}
function dotransfer(mybalance) {
	mybalance[0,$2] -= amount
	mybalance[curmonth,$2]
-= amount
	mybalance[0,$3] += amount
	mybalance[curmonth,$3] += amount
}

doincome() などの任意の関数が呼び出されると、取引は 2つの場所 mybalance[0,category] と mybalance[curmonth, category]、すなわちそれぞれ年全体のカテゴリー残高と現在の月のカテゴリー残高に記録されます。これで、後で収入/支出の年次または月次の内訳を簡単に生成することができます。

これらの関数の場合、mybalance で参照された配列は参照に渡されることに注意してください。さらに、現在のレコードの月の数値を保持する curmonth、$2 (支出カテゴリー)、$3 (収入カテゴリー)、amount ($7、ドル金額) といった、いくつかのグローバル変数も参照します。doincome() などの関数が呼び出されたとき、これらの変数は、現在のレコード (行) が処理されるようにすでに正しく設定されています。


メイン・ブロック

次に、入力データの各行を解析するコードを含むメイン・コード・ブロックを示します。FS を正しく設定したので、1つ目のフィールドを $1、2つ目のフィールドを $2 として参照できることを思い出しましょう。doincome() などの関数が呼び出されると、関数の内側から curmonth、$2、$3、および amount の現在の値にアクセスすることができます。コードの後、また説明を続けます。

balance、第 3 部
{
	curmonth=monthdigit(substr($1,4,3))
	amount=$7
	
	#record all the categories encountered
	if ( $2 != "-" )
		globcat[$2]="yes"
	if
( $3 != "-" )
		globcat[$3]="yes"
	#tally up the transaction properly
	if ( $2 == "-" ) {
		if ( $3 == "-"
) {
			print "Error: inc and exp fields are both blank!"
			exit 1
		}
else {
			#this is income
			doincome(balance)
			if ( $5 == "Y" )
				doincome(balance2)
		}
	} else if ( $3 == "-" ) {
		#this is an expense
		doexpense(balance)
		if
( $5 == "Y" )
			doexpense(balance2)
	} else {
		#this is a transfer
		dotransfer(balance)
		if
( $5 == "Y" )
			dotransfer(balance2)
	}			
}

メイン・ブロック内の最初の 2 行は、curmonth を 1?12 の間の整数に設定し、amount をフィールド 7 に設定します (コードを分かりやすくするためです)。次に、おもしろい 4 行が続きます。ここでは、値を globcat という配列に書き込みます。globcat、すなわちグローバル・カテゴリー配列を使用して、ファイル内にあるすべてのカテゴリー ("inco"、"misc"、"food"、"util" など) を記録します。たとえば、$2 == "inco" の場合、globcat["inco"] を "yes" に設定します。後で、簡単な "for (globcat ではx)" ループでカテゴリーのリスト内を反復することができます。

次の 20 数行では、フィールド $2 と $3 を分析し、取引を適切に記録します。$2=="-" および $3!="-" の場合、収入があるので、doincome() を呼び出します。逆の状況の場合は、doexpense() を呼び出します。$2 と $3 の両方にカテゴリーが含まれている場合は、dotransfer() を呼び出します。"balance" 配列には適切なデータが記録されるように、その都度、この配列をこれらの関数に渡します。

また、「( $5 == "Y" ) の場合、同じ取引を balance2 に記録する」という行があることにも注目してください。ここでは、正確には何をしているのでしょうか。$5 には "Y" または "N" のいずれかが含まれており、取引が口座に記入されたかどうかが記録されていることを思い出しましょう。取引が記入された場合のみ取引を balance2 に記録するので、balance2 には実際の口座残高が入るのに対し、"balance" には、取引の記入の有無にかかわらず、すべての取引が入ります。データ入力を確認する場合は (銀行に従って現在の口座残高と一致しているはずなので) balance2 を使用し、口座から借り越していないことを確認する場合は (まだ現金化されていない作成済みの小切手も勘定に入れているので) "balance" を使用することができます。


レポートの生成

メイン・ブロックが各入力レコードを繰り返し処理した後、カテゴリー別と月別に分類されたかなり総合的な借方と貸方のレコードができあがります。後は、レポートを生成する END ブロック (この場合には、あまり大きくありません) を定義するだけです。

balance、第 4 部
END {
	bal=0
	bal2=0	
	for (x in globcat) {
		bal=bal+balance[0,x]
		bal2=bal2+balance2[0,x]
    	}
    	printf("Your available funds: %10.2f\n", bal)
    	printf("Your
account balance: %10.2f\n", bal2)	
}

このレポートは、以下のようなサマリーを出力します。

Your available funds:    1174.22
Your account balance:    2399.33

END ブロックでは、"for (globcat では x)" 構造体を使用して、すべてのカテゴリー内を反復し、記録されているすべての取引に基づくマスター残高を計算しました。実際には、利用可能な資本の残高と、口座残高の残高の 2つの残高を計算しています。プログラムを実行して、"mycheckbook.txt" というファイルに入力した会計情報を処理するには、前述のコードすべてを "balance" というテキスト・ファイルに入れ、"chmod +x balance" を実行して、"./balance mycheckbook.txt" と入力します。balance スクリプトにより、すべての取引が加算され、2 行の残高サマリーが自動的に出力されます。


アップグレード

私個人、およびビジネスの会計には、このプログラムのもっと高度なバージョンを使用しています。わたしのバージョン (ここでは、スペースの制限上、紹介できませんでした) は、年間総計、純収入などを含め、収入と支出の月次内訳を出力します。さらに、データは HTML 形式で出力されるので、Web ブラウザーで表示することができます。このプログラムが役立つとお思いになる場合は、是非これらの機能をこのスクリプトに追加してください。追加情報を記録するために構成する必要はありません。必要なすべての情報は既に balance と balance2 に含まれています。あとは、END ブロックをアップグレードするだけです。

このシリーズをお楽しみいただけましたでしょうか。awk の詳細については、以下の参考文献を参照してください。

参考文献

  • Read Daniel による awk シリーズの前の回: developerWorks の実例でわかる awk第 1 回および第 2 回
  • 古典的な本がお好の方には、O'Reilly 社から出版されている sed & awk, 2nd Edition をお勧めします。
  • comp.lang.awk FAQ もぜひ調べてください。ここにも、さらにたくさんの awk リンクがあります。
  • Patrick Hartigan 氏のawk tutorial には、便利な awk スクリプトがまとめられています。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=226646
ArticleTitle=共通テーマ: 実例でわかる awk: 第 3 回
publish-date=04012001