|
1.はじめに
このアプリの中心のクラスでConsoleApplicationクラスを継承したLineNumbererクラスを紹介します。
LineNumbererクラスの中では自作のConsoleErrorクラス、CommandLineSwitchクラスを利用しますので、
これら2つのクラスについては実装しているプロパティ、メソッドを紹介します。これらの内部の実装はごく単純なものなので解説しません。
2.LineNumbererクラスで利用する自作クラス
2.1.CommandLineSwitchクラス概要
| プロパティ・メソッド | 内容 | 例(VB.NETのコンパイラのtargetスイッチでの例) |
| Nameプロパティ | コマンドラインスイッチを識別するための名称。 | targetまたはt |
| GroupIDプロパティ | コマンドラインスイッチの属するグループを識別する文字列。 | target(使用する他のコマンドラインスイッチのGroupIDとかぶらなければ何でも構わない) targetスイッチのtとtargetのように複数のNameを持ちたい場合のみ指定必須。 |
| Prefixプロパティ | コマンドラインスイッチを構成する接頭辞の文字列。既定値は/。 | / |
| Delimiterプロパティ | パラメータ付きのコマンドラインスイッチの場合にスイッチとパラメータを区切る文字列。既定値は:(コロン)。 | :(コロン) |
| Switchプロパティ | コマンドラインスイッチの全体の文字列。 | /targetまたは/t |
| IsSwitchOfメソッド | 指定の文字列が、インスタンスが示すコマンドラインスイッチか判定する。 | 指定文字列が/target、/t、または、パラメータ付きの場合はスイッチ+区切り文字であればTrueを返す。 |
| HasParameterメソッド | 指定の文字列が、パラメータ付きのスイッチかを判定する。※HasParameterを使用する場合、事前のIsSwitchOfでのチェックが必要です。(この部分に関してメソッド仕様に改善の余地ありかも) | /t:winexeならTrueを返す。 |
| GetParameterメソッド | 指定の文字列からパラメータ部分を返す。パラメータがない場合は""を返す | /t:winexeなら"winexe"を返す。 |
2.2.ConsoleErrorクラス概要
| プロパティ・メソッド | 内容 |
| Addメソッド | エラーメッセージを追加する。追加するエラーメッセージには改行が付加される。 |
| Clearメソッド | エラーメッセージをクリアする。 |
| GetMessageメソッド | エラーメッセージを取得する。 |
| Showメソッド | エラーメッセージを標準エラー出力に表示する。 |
| Existsメソッド | エラーメッセージの有無を返す。 |
3.LineNumbererクラス
3.1.概要
| プロパティ・メソッド | 内容 |
| Figureプロパティ | 行番号の桁数を指定するプロパティ。 |
| Paddingプロパティ | 行番号の桁数に満たない行番号のときに残った部分を埋める文字を指定するプロパティ。 |
| Delimiterプロパティ | 行番号と本文の間の区切り文字列を指定するプロパティ。 |
| Autoプロパティ | 行番号の桁数指定を入力データから自動的に判断するかどうかのプロパティ。 |
| Linesプロパティ(ConsoleApplicationクラスより継承) | 入力行データのコレクションプロパティ。 |
| AddNumberメソッド | Linesプロパティのデータに行番号を付加する。 |
| AnalyzeArgumentメソッド(ConsoleApplicationクラスより継承) | オーバーライド。コマンドライン引数を解析し、引数の整合性をチェックし、各プロパティに取り込む。 |
| ShowUsageメソッド(ConsoleApplicationクラスより継承) | オーバーライド。使用方法を表示する。 |
| ReadLinesメソッド(ConsoleApplicationクラスより継承) | 標準入力から全データを読み込む。 |
| WriteLinesメソッド(ConsoleApplicationクラスより継承) | 標準出力に全データを書き込む。 |
3.2.ソースコードと解説
※下記のソースコードの編集には、自作ツール3つを利用しています。
タブを空白に置き換えるTabExpander(未公開)、VBソースをHTMLに変換するVB2HTML(未公開)、今回の行番号付加プログラムの3つです。
1:Imports System
2:Imports System.IO
3:Imports System.Text
4:Imports System.Collections
5:
System.IO名前空間からはFileInfoクラス、StreamReaderクラスを、
System.Text名前空間からはEncodingクラスを、
System.Collections名前空間からはArrayListを利用しています。そのために各名前空間をインポートしています。
6:
7:Public Class LineNumberer
8: Inherits ConsoleApplication
9:
10:
11: Private mFigure As Integer = 4
12: Private mPadding As Char = " "C
13: Private mDelimiter As String = ":"
14: Private mAuto As Boolean = False
15: Private mInputFile As FileInfo = Nothing
16:
- [ 変数の宣言と初期化、文字型の型指定子 ]
-
ここではこのクラスのプロパティの内部変数を宣言し、宣言と同時に初期値を与えています。
宣言と同時に初期化できるのはVB.NETからの機能です。
12行目の末尾の「C」は文字型を表す型指定子です。1文字のスペースで初期化したいのですが、
単に「" "」と指定した場合、これは文字列型(String)のスペースを指定したことになるので、
VBで厳密にChar型の定数を記述する場合は、後ろに「C」を付ける必要があります。Option Strict On指定の場合は、これが必須です。
※C#なら、文字型は「' '」のようにシングルクォートで囲んで表せるので、文字列型とはっきり区別できます。
VBの場合、コメントの記号としてシングルクォートが割り当てられているため、この案を取れなかったのでしょう。
17:
18: Private Const SwFigure As String = "figure"
19: Private Const SwPadding As String = "padding"
20: Private Const SwDelimiter As String = "delimiter"
21: Private Const SwAuto As String = "auto"
22: Private Const SwHelp As String = "help"
23:
24:
25: Private switchFigure1 As CommandLineSwitch = New CommandLineSwitch("figure" ,SwFigure)
26: Private switchFigure2 As CommandLineSwitch = New CommandLineSwitch("f" ,SwFigure)
27: Private switchPadding1 As CommandLineSwitch = New CommandLineSwitch("padding" ,SwPadding)
28: Private switchPadding2 As CommandLineSwitch = New CommandLineSwitch("p" ,SwPadding)
29: Private switchDelimiter1 As CommandLineSwitch = New CommandLineSwitch("delimiter",SwDelimiter)
30: Private switchDelimiter2 As CommandLineSwitch = New CommandLineSwitch("d" ,SwDelimiter)
31: Private switchAuto1 As CommandLineSwitch = New CommandLineSwitch("auto" ,SwAuto)
32: Private switchAuto2 As CommandLineSwitch = New CommandLineSwitch("a" ,SwAuto)
33: Private switchHelp As CommandLineSwitch = New CommandLineSwitch("?" ,SwHelp)
34:
35: Private switches As ArrayList = New ArrayList( _
36: New CommandLineSwitch(){switchFigure1 , switchFigure2, _
37: switchPadding1 , switchPadding2, _
38: switchDelimiter1, switchDelimiter2, _
39: switchAuto1 , switchAuto2, switchHelp} _
40: )
41:
- [ CommandLineSwitchクラスの利用 ]
-
例えば、25行目、26行目でコマンドラインスイッチfigureの準備をしています。
スイッチfigureはスイッチの名称としてfigureと略称のfを使用できるため、この2行で2種類のスイッチを用意し、
そのときのグループIDにグループとしての名称figure(定数SwFigure)を指定しています。
後で出てくるコマンドラインを解析するAnalyzeArgumentメソッド内でこれらをぐるぐるループさせて、
コマンドライン引数がスイッチであることを判断するため、ここでコレクションArrayListに各スイッチを入れています。
※今考えるとここは単にCommandLineSwitchクラスの配列で十分です。ArrayListとして他の機能を用いてないので。
以下で各プロパティを定義しています。
42:
43: Public Property Figure() As Integer
44: Get
45: Return mFigure
46: End Get
47: Set(ByVal value As Integer)
48: if value < 0 Then Throw New OverflowException("桁数の指定に負数が指定されています。")
49: mFigure = value
50: End Set
51: End Property
52:
- [ OverflowExceptionクラス ]
-
Figureプロパティは桁数を意味するので、負の数はありえません。なので、プロパティに負の数を与えられたときは、
例外を発生させるようにしました。プロパティの許容する範囲外を与えた場合のエラーなので、System.OverflowExceptionクラスを利用しています。
53:
54: Public Property Padding() As Char
55: Get
56: Return mPadding
57: End Get
58: Set(ByVal value As Char)
59: mPadding = value
60: End Set
61: End Property
62:
63:
64: Public Property Delimiter() As String
65: Get
66: Return mDelimiter
67: End Get
68: Set(ByVal value As String)
69: mDelimiter = value
70: End Set
71: End Property
72:
73:
74: Public Property [Auto]() As Boolean
75: Get
76: Return mAuto
77: End Get
78: Set(ByVal value As Boolean)
79: mAuto = value
80: End Set
81: End Property
82:
- [ [ ]で予約語を括る ]
-
Autoプロパティの定義の74行目で[Auto]とありますが、AutoがVBの予約語のためそのままでは使えないので、[ ]で括っています。
こうすることで予約語でも使うことができます。
プロパティとしてAutoを使う場合には「インスタンス変数.Auto」という記述になりAuto単独ではないので、[ ]で括る必要はありません。
つまり、宣言時は[Auto]としないといけませんが、このクラスを使う側のコードを書く人は予約語かどうかを気にする必要はありません。
83:
84: Public Shared Sub Main(ByVal CmdArgs() As String)
85: Dim aLineNumberer As LineNumberer = New LineNumberer()
86:
87: If aLineNumberer.AnalyzeArgument(CmdArgs) Then
88: aLineNumberer.ReadLines()
89: aLineNumberer.AddNumber()
90: aLineNumberer.WriteLines()
91: End If
92: End Sub
93:
ここが実際の行番号付加のメイン処理です。LineNumbererクラスのインスタンスを生成し、
AnalyzeArgumentメソッドでコマンドライン引数を解析しOKなら、
データ読み取り、行番号付加、データ書き込みを行います。
94:
95: Public Sub AddNumber()
96: If Me.Auto = True Then Me.Figure = lines.Count.ToString().Length
97:
98: Dim count As Integer
99: For count = 1 To Me.Lines.Count
100: Dim lineNumber As String = count.ToString().PadLeft(Me.Figure, Me.Padding) + Me.Delimiter
101: Me.Lines(count - 1) = lineNumber + Me.Lines(count - 1).ToString()
102: Next
103: End Sub
104:
- [ Object.ToStringメソッドとString.Lengthプロパティ ]
-
96行目で、Auto指定が有効な場合に、行数(lines.Count)を文字列化(ToString())しその長さ(Length)を桁数としてFigureプロパティに設定しています。
つまり、入力データの行数が1234行だった場合、行番号に必要な桁が4桁ということを設定しているだけです。
- [ String.PadLeftメソッド ]
-
98行目〜102行目で行番号を付加しているわけですが、100行目で行番号用の文字列を作成しています。
StringクラスのPadLeftメソッド(String.PadLeft(Int32, Char))を利用しており、
4桁指定で行番号12なら、" 12"という文字列(空白x2と12)を作成します。
105:
106: Overrides Public Function AnalyzeArgument(ByVal CmdArgs() As String) As Boolean
107: Dim arg As String
108: Dim result As Boolean = True
109: Dim aConsoleError As ConsoleError = New ConsoleError()
110: Dim argumentKind As String = ""
111: Dim aCommandLineSwitch As CommandLineSwitch
112:
113: aConsoleError.Clear()
114: For Each arg In CmdArgs
115: Try
116: If arg Like "/*" Then
117: argumentKind = ""
118:
119: For Each aCommandLineSwitch In switches
120:
121: If aCommandLineSwitch.IsSwitchOf(arg) Then
122: argumentKind = aCommandLineSwitch.GroupID
123:
124: Select Case argumentKind
125: Case SwFigure
126: Me.Figure = Integer.Parse( aCommandLineSwitch.GetParameter(arg) )
127: Case SwPadding
128: Me.Padding = Char.Parse( aCommandLineSwitch.GetParameter(arg) )
129: Case SwDelimiter
130: Me.Delimiter = aCommandLineSwitch.GetParameter(arg)
131: Case SwAuto
132: If aCommandLineSwitch.HasParameter(arg) Then Throw New FormatException()
133: Me.Auto = True
134: Case SwHelp
135: ShowUsage()
136: Return False
137: End Select
138: Exit For
139: End If
140:
141: Next
142:
143: If argumentKind = "" Then Throw New ArgumentException()
144:
145: Else
146: If mInputFile Is Nothing Then
147: mInputFile = New FileInfo(arg)
148: If Not mInputFile.Exists() Then
149: Throw New FileNotFoundException("ファイル'" + arg + "
150: End If
151: Else
152: Throw New ApplicationException("ファイルの指定が複数あります。")
153: End If
154: End If
155:
156: Catch ane As ArgumentNullException
157: Select Case argumentKind
158: Case SwFigure : aConsoleError.Add("桁数の指定に有効な値が指定されていません。")
159: Case SwPadding : aConsoleError.Add("桁数の不足分を埋める文字の指定に有効な値が指定されていません。")
160: Case SwDelimiter: aConsoleError.Add("区切りの指定に有効な値が指定されていません。")
161: End Select
162: Catch fe As FormatException
163: Select Case argumentKind
164: Case SwFigure : aConsoleError.Add("桁数の指定に数値以外の値が指定されています。")
165: Case SwPadding : aConsoleError.Add("桁数に足りない分を埋める文字、1文字を指定して下さい。")
166: Case SwAuto : aConsoleError.Add("スイッチ/autoには引数指定はありません。")
167: End Select
168: Catch ofe As OverflowException
169: aConsoleError.Add("桁数の指定が範囲外です。")
170: Catch ae As ArgumentException
171: aConsoleError.Add("無効なスイッチ'"+ arg +"
172: Catch fnfe As FileNotFoundException
173: aConsoleError.Add(fnfe.Message)
174: Catch appex As ApplicationException
175: aConsoleError.Add(appex.Message)
176: Catch ex As Exception
177: aConsoleError.Add(ex.Message)
178: End Try
179: Next
180:
181:
182: If aConsoleError.Exists() Then
183: result = False
184: aConsoleError.Show()
185: Else
186:
187: If Not mInputFile Is Nothing Then
188: Console.SetIN(New StreamReader(mInputFile.OpenRead(), Encoding.GetEncoding("Shift_JIS")))
189: End If
190: End If
191:
192: Return result
193: End Function
194:
- [ Overridesキーワード ]
-
コマンドライン引数の解析部で、ConsoleApplicationクラスの抽象メソッドAnalyzeArgumentをオーバーライドしています。
オーバーライドとは、継承元クラス(ベース(基底)クラス、スーパークラスといいます)で定義されているメソッド等を、
継承先クラス(派生クラス、サブクラスといいます)で同名のメソッドで上書きしてしまうことです。
キーワードOverridesを利用します。Override((命令などを)無視する。AがBに優先する)+s(三人称単数現在のs)です。
- [ コマンドライン引数解析処理の流れ ]
-
大まかな流れは以下の図のようになります。
図1 AnalyzeArgumentメソッドの処理の流れ
詳細な流れは以下の通りです。
- コマンドライン引数の入った配列CmdArgsから(空白で区切られた)コマンドライン1要素を列挙する
(コマンドが"LN /figure:4 /d::"なら、CmdArgsは"/figure:4"、"/d::"の配列になっている)
- その1要素がどのスイッチ(またはスイッチではなくファイルのパス)に該当するかをチェック
(最初にコマンドライン引数の頭に「/」が付いているかどうかでスイッチかファイルのパスかを分ける)
■コマンドライン引数をスイッチと判断した場合
- スイッチの入ったswitchesからスイッチ1要素を列挙する。
- コマンドライン引数1要素がそのスイッチに該当するか?をチェック。該当すれば次の処理へ、該当しなければスイッチの次の要素を列挙する。
- 該当するスイッチがあれば、そのスイッチ用のプロパティに値を設定する。エラーがある場合は例外を発生させる。
- 上記処理を繰り返して最終的に、コマンドライン引数1要素に該当するスイッチがなかった場合、無効なスイッチの指定だったということで例外を発生させる。
■コマンドライン引数をファイルのパスと判断した場合
- 以前のコマンドライン引数1要素で、すでにファイルの指定があった場合は複数のファイル指定があるとして例外を発生させる。
- 指定のコマンドライン引数をファイルのパスと考えて、そのパスのファイルが存在するかチェック。ファイルがなければ例外を発生させる。
- もし前の処理で例外が発生した場合は、Try〜CatchのCatchで例外を拾って、適切なエラーメッセージをConsoleErrorクラスのインスタンスに設定する。
- ここまでの処理を繰り返し、その中でエラーがあった場合は、エラーメッセージを表示して、戻り値Falseでメソッドを抜ける。
- エラーがなく、ファイルの指定がないときは戻り値Trueでメソッドを抜ける。ファイルの指定があった場合は、
そのファイルのストリームを開き、標準入力をそのファイルストリームと結びつける。戻り値Trueでメソッドを抜ける。
- [ 例外処理 Try 〜 Catch 〜 End Try ]
-
例外処理の構文は以下の通りです。
Try
(A)
Catch XXXex As XXXException (B)
(C)
Finally
(D)
End Try
図2 Try 〜 Catch構文
図2の(A)の部分に例外が発生する可能性のあるコードを書きます。そして、例外が発生したときの処理を(B)(C)の箇所に書きます。
(B)にはどの例外に対する処理するかを指定します。この例であればXXXExceptionの例外が発生したときに(C)が実行されます。
Finally句の(D)には例外が発生する/しないに関わらず処理したい内容を記述します。例外が発生しても/しなくても処理されるなら、
Finally句の意味がないように思われますが、(A)の処理と関係のある処理のみここに書くことで(A)〜(D)までが一つのまとまった処理だということが明示できます。
AnalyzeArgumentメソッドの処理では特に必要ないのでFinally句は省いています。
195:
196: Overrides Overloads Public Sub ShowUsage(ByVal out As TextWriter)
197: out.WriteLine("LN - 標準入力からの入力に行番号を付加して標準出力に出力する。")
198: out.WriteLine("LN [/a[uto]] [/d[elimiter]:区切り文字列] [/f[igure]:桁数] [/p[adding]:桁揃え用の文字]
[[ドライブ:][パス]ファイル名]")
199: End Sub
200:End Class
201:
使用方法を表示するメソッドShowUsageをオーバーライドしているだけです。
4.まとめ
今回は、残っていたCommandSwitchクラス、ConsoleErrorクラスの概要を掲載し、
本アプリの中心であるLineNumbererクラスについてはソースコードを順をおって説明しました。
技術的な部分で大きなところでは、オーバーライドや構造化されたエラー処理であるTry〜Catch構文などを説明しました。
行番号付加プログラムを使った説明は今回で終わりです。次回からはまた違うネタを用意したいと思います。
|