KEN's .NET

[第8回] Consoleアプリケーションでフィルタープログラム4 - LineNumbererクラス

ホーム > KEN's .NET > [第8回] Consoleアプリケーションでフィルタープログラム4 - LineNumbererクラス

前回はコンソールアプリの雛形となるクラスConsoleApplicationクラスの概要とソースコードの解説を行いました。 今回はCommandLineSwitchクラス、ConsoleErrorクラスの概要とLineNumbererクラスのソースコードを解説します。
※私はVS.NETやVB.NET Standardなどを持っておらず、本プログラム作成時点で使用しているのは.NET Framework SDK(SP1)です。

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メソッドの処理の流れ

詳細な流れは以下の通りです。
  1. コマンドライン引数の入った配列CmdArgsから(空白で区切られた)コマンドライン1要素を列挙する
    (コマンドが"LN /figure:4 /d::"なら、CmdArgsは"/figure:4"、"/d::"の配列になっている)
  2. その1要素がどのスイッチ(またはスイッチではなくファイルのパス)に該当するかをチェック
    (最初にコマンドライン引数の頭に「/」が付いているかどうかでスイッチかファイルのパスかを分ける)

    ■コマンドライン引数をスイッチと判断した場合
    • スイッチの入ったswitchesからスイッチ1要素を列挙する。
    • コマンドライン引数1要素がそのスイッチに該当するか?をチェック。該当すれば次の処理へ、該当しなければスイッチの次の要素を列挙する。
    • 該当するスイッチがあれば、そのスイッチ用のプロパティに値を設定する。エラーがある場合は例外を発生させる。
    • 上記処理を繰り返して最終的に、コマンドライン引数1要素に該当するスイッチがなかった場合、無効なスイッチの指定だったということで例外を発生させる。

    ■コマンドライン引数をファイルのパスと判断した場合
    • 以前のコマンドライン引数1要素で、すでにファイルの指定があった場合は複数のファイル指定があるとして例外を発生させる。
    • 指定のコマンドライン引数をファイルのパスと考えて、そのパスのファイルが存在するかチェック。ファイルがなければ例外を発生させる。

  3. もし前の処理で例外が発生した場合は、Try〜CatchのCatchで例外を拾って、適切なエラーメッセージをConsoleErrorクラスのインスタンスに設定する。
  4. ここまでの処理を繰り返し、その中でエラーがあった場合は、エラーメッセージを表示して、戻り値Falseでメソッドを抜ける。
  5. エラーがなく、ファイルの指定がないときは戻り値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構文などを説明しました。 行番号付加プログラムを使った説明は今回で終わりです。次回からはまた違うネタを用意したいと思います。

ホーム > KEN's .NET > [第8回] Consoleアプリケーションでフィルタープログラム4 - LineNumbererクラス

[e-mail] yone_ken00@hotmail.com