小鳥遊アイトの挑戦記

アイトの挑戦記

プログラミングで色々なことに挑戦していくブログ

コマンドプロンプトに憧れて...

はじめに

WindowsLinuxMacはそれぞれコマンドを実行できる環境がデフォルトで入っている。CUIは、慣れると操作がとても速くなるし使いやすい。何よりコンピュータを操作している感じに浸れるのが嬉しい。C言語JavaRubyなどはコンソールアプリケーションを作ることができる。それに対してVBAはイミディエイトウィンドウに出力できるもののコンソールアプリケーションとは少し違うような...。そんなこんなでVBAを使ってコマンドを実行できるものを作ってみた。役に立つかはわかりないが、あくまで私の「憧れ」を形にしたものなのであしからず。まずは以下の動画をご覧あれ。

コンソールを作る

Excelでは(私の知る限りでは)そのままコンソール入出力を扱うことができない。例外としてコマンドプロンプトを呼び出してコマンドを入力させることができるが、Excel上で処理をさせようと考えた場合、少し分が悪い。そこでまずは入出力可能なコンソールから作るとする。以下のようなフォームを作成してほしい。表記してあるのはプロパティを変える部分だけで、後は変更の必要はない。

f:id:Kaburanet:20200427142517p:plain
コンソール用のフォームのコントロール一覧

まずは起動時の初期処理から書いていく。コマンドプロンプトに似せるためにそれっぽく書いてみる。ユーザーフォームの起動時のイベントに書くとしよう。

Private Sub UserForm_Initialize()
    'バージョン情報の表示
    Dim VerInfo As String
    VerInfo = "Microsoft Excel [Version " + Application.Version + "]" + vbCrLf + _
              "(c) 2019 Microsoft Corporation. All rights reserved." + vbCrLf + vbCrLf
    
    Console.Text = VerInfo
    
End Sub

なお、Excelのバージョン情報は以下のコードで取得できる。

Application.Version


コマンドの受け付けは「Console_Input」が受け持つ。入力された文字列はそのままコマンド文字列として使うので何もいじらず、そのままコンソールに表示する。Enterで実行したいので入力されたキーを判別する必要がある。そこでKeyDownイベントにコードを記述する。

Private Sub Console_Input_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
    If KeyCode = vbKeyReturn And Console_Input.Text <> "" Then
        'もしexitと入力された場合は終了する
        If LCase(Console_Input.Text) = "exit" Then
            Unload Me
            Exit Sub
        End If
        'Enterキーが押された場合、処理をCommandクラスに投げる(コマンドクラスを後で作る)
        Console_Write_Command (Console_Input.Text)    '(この後で実装する)
        'コマンドの実行
        Run_Command (Console_Input.Text)    '(この後で実装する)
        
        '入力されたコマンドをクリア
        Console_Input.Text = ""
        
        'スクロール対策
        Console.SetFocus    'フォーカスをコンソールにあてる
        Console.SelStart = Len(Console.Text)    '最後に移動
        Console_Input.SetFocus    '入力用のテキストボックスにフォーカスを戻す
        
    End If
End Sub

ここで大事なのが、スクロール対策のところで、Excelで用意されているテキストボックスはなぜかホイールでスクロールできない。一度フォーカスを当ててキャレットを移動した後、再び入力用のテキストボックスにフォーカスを戻してやる必要がある。

次に、コマンドを表示するところを書く。処理自体は一行で終わるほど簡単なもの。

'入力されたコマンドを表示する
Private Sub Console_Write_Command(ByVal CommandStr As String)
    Console.Text = Console.Text + ">>> " + CommandStr + vbCrLf
End Sub


今度はコマンドを実行するクラスを呼び出す処理を書く。ログ出力もできるようにしたほうが利便性高いかな?

'ログ出力用
Private Sub Console_Write_Log(ByVal LogMessage As String)
    Console.Text = Console.Text + LogMessage + vbCrLf + vbCrLf
End Sub
'コマンドを実行するクラスを呼び出す
Private Sub Run_Command(ByVal CommandStr As String)
    Dim Command As New Command
    
    Dim Result As String
    'Commandクラスに処理を投げる
    Result = Command.Run(CommandStr)
    'ログ出力用
    Console_Write_Log (Result)
    
End Sub



フォームの処理は全部書き終わったので本命のコマンド解析&実行するクラスを書いていくよ。

コマンド解析&実行するクラスを作る

初めに言っておくが、これがすごく面倒くさい。簡単そうに見えるけど、実はそう簡単なものでもなかった。実装してみて初めて気が付いた。 まずは、コマンドを解析する処理から書いていく。新しくクラスを追加し、名前を「Command」に変える。

今回考慮することは以下の通り。

  • 空白で引数の区切りとする
  • ダブルクォーテーションの中に空白があった場合は無視


実装すると以下のようになる。結構複雑なところもある(わかる人はすぐわかると思う)のでわからなければTwitterでDMくれて聞いてもらって構わない(回答に困らない質問の仕方にしてね)。
コマンドが空白じゃない場合は処理を継続することにする。本当はもっとちゃんとエラーチェックすべきだろうけど、そこは気にしたら負けだ。うん。

Public Function GetCommandLineArgs(ByVal CommandStr As String) As String()
    '----------------------------------------------------------------
    '@Name
    '   GetCommandLineArgs
    '
    '@Param
    '   CommandStr          : コマンド文字列。
    '
    '@Return
    '   CommandLineArgs     : コマンドを分割して順に入れた配列。
    '
    '@Description
    '   コマンドを分割して配列で返す。
    '
    '@Note
    '   なし。
    '
    '----------------------------------------------------------------
    
    '戻り値
    Dim CommandLineArgs() As String
    
    
    If CommandStr = "" Or CommandStr = " " Then     '文字列が空白だった場合
    
        ReDim CommandLineArgs(0) As String
        CommandLineArgs(0) = ""
        
    Else                        '文字列が空白でなかった場合
    
        '入力された文字列を分割する。
        Dim Counter As Long: Counter = 0                'ダブルクォーテーションの数をカウントする。
        Dim Char As String                              '一文字切り出して入れておく
        
        Dim ArrayCounter As Long: ArrayCounter = 0      '配列用のカウンタ
        Dim StartPos As Long: StartPos = 1              '分割の始まりの位置
        
        
        Dim i As Long: For i = 1 To Len(CommandStr) + 1
            
            '一文字切り出す。
            Char = Mid(CommandStr, i, 1)
            
            '切り出した文字がダブルクォーテーションの場合。
            If Char = Chr(34) Then
                Counter = Counter + 1   'ダブルクォーテーションの数を1増やす。
            End If
            
            'ダブルクォーテーションの数が偶数の場合のみ空白で分割する。
            If (Counter Mod 2 = 0 And Char = " ") Or (i = Len(CommandStr) + 1) Then
                Dim tmp As String: tmp = Mid(CommandStr, StartPos, i - StartPos)
                
                'オプションの「/」のみのものは追加しない。
                If tmp <> "/" Then
                    ReDim Preserve CommandLineArgs(ArrayCounter) As String
                    CommandLineArgs(ArrayCounter) = tmp
                    
                    ArrayCounter = ArrayCounter + 1
                End If
                
                StartPos = i + 1
                
            End If
            
        Next i
        
    End If
    
    
    '置換処理
    Dim j As Long: For j = 0 To UBound(CommandLineArgs)
        'ダブルクォーテーションを一括削除
        CommandLineArgs(j) = Replace(CommandLineArgs(j), Chr(34), "")
    Next j
    
    GetCommandLineArgs = CommandLineArgs
    
End Function


次はコマンドを実行する関数を作っていくよ。VBAには関数を文字列として指定できる「CallByName」関数があるのでそれを有効活用させてもらおう。なお、上で作った、コマンド解析の関数を呼び出してる。それから、これが一番大事なことで、「コマンド名」=「関数名」という関係(大文字と小文字は区別しない)。これが成り立たないと呼び出せない!!

Public Function Run(ByVal CommandStr As Variant) As String
    '----------------------------------------------------------------
    '@Name
    '   Run
    '
    '@Param
    '   CommandStr          : コマンド文字列。
    '
    '@Return
    '   Result              : 処理結果を返す文字列。
    '
    '@Description
    '   コマンドを実行する。
    '
    '@Note
    '   CallByNameを使用。
    '
    '----------------------------------------------------------------
    On Error GoTo ERROR
    Dim Result As Variant    '処理結果
    
    Dim Args() As String: Args = GetCommandLineArgs(CommandStr) '引数を取得
    '配列の最初にコマンド名が入っている。配列としては渡せないのでコマンドとして渡してコマンドプロセスで引数判断をしてもらう。
    Result = CallByName(Me, Args(0), VbMethod, CommandStr)
    
    Run = Result
    
    Exit Function
    

ERROR:
    Run = "'" + CommandStr + "'" + "は、有効なコマンドではありません。"

End Function

コマンドを作る

よっしゃ!!ようやくここまで来たぜ。コマンドを作ってくぞ!今回は最も簡単な例として一番最初の動画にも使った、文字列反転のコマンドにする。引き続きCommandクラスに書いていくぞ。こちらもオプション解析が非常に面倒くさい。ここでの実装は私が何も見ずに思いつきで書いたものなので実際のコマンドプロンプトにおけるコマンド処理とは全然違うかもしれない。興味ある人は自分で調べてみてもいいだろう。解説するとすごく長い記事になっちゃうのでソースコードだけ載せておくので、これも不明な点があればTwitterのDMで聞いてくださいな(回答に困らない質問の仕方にしてね)。

Public Function StringReverse(ByVal CommandStr As String) As Variant
    '----------------------------------------------------------------
    '@Name
    '   StringReverse
    '
    '@Param
    '   CommandStr          : コマンド文字列。
    '
    '@Return
    '   Result              : 戻り値。
    '
    '@Description
    '   任意の文字列を反転して返す。
    '
    '@Note
    '   オプション一覧
    '       /a              選択範囲に限定。
    '       /l              すべて大文字にして反転。
    '       /s              すべて小文字にして反転
    '
    '   文法
    '       StringReverse {/aまたは反転する文字列} (/l) (/s)
    '----------------------------------------------------------------
    Dim Result As String
    
    Dim Args() As String: Args = GetCommandLineArgs(CommandStr)
    
    '引数の確認
    If UBound(Args) = 0 Then
        Result = "コマンドの構文が間違っています。"
    Else
        '------------------------------------------オプションのためのスイッチ----------------------------------------------
        Dim OPTION_SELECTION As Boolean: OPTION_SELECTION = False
        Dim OPTION_LARGE As Boolean: OPTION_LARGE = False
        Dim OPTION_SMALL As Boolean: OPTION_SMALL = False
        '------------------------------------------------------------------------------------------------------------------
        
        'オプションの確認
        Dim i As Long: For i = 1 To UBound(Args)
            '一番最初が「/」で始まっているものはオプションとみなす
            If Args(i) Like "/*" = True Then
                If Args(i) Like "/a" = True Then
                    OPTION_SELECTION = True
                ElseIf Args(i) = "/l" Then
                    OPTION_LARGE = True
                ElseIf Args(i) = "/s" Then
                    OPTION_SMALL = True
                End If
            End If
        Next i
        
        '反対のオプションがあった場合はエラーとして処理
        If OPTION_LARGE = True And OPTION_SMALL = True Then
            GoTo ERROR
        End If
        
        
        Dim tmp As String
        
        '読み込み範囲を選択範囲に限定するかどうか
        If OPTION_SELECTION = False Then
            '範囲読み込みでない場合は第一引数に反転させる文字を持ってくる
            tmp = Args(1)
            
            '大文字にするかどうか
            If OPTION_LARGE = True Then
                tmp = UCase(tmp)
            End If
            
            '小文字にするかどうか
            If OPTION_SMALL = True Then
                tmp = LCase(tmp)
            End If
            Result = StrReverse(tmp)
            
        Else
            Dim r As Range: Set r = Selection
            Dim val As Range
            '出力先範囲が指定されていない場合
            For Each val In r
                tmp = CStr(val.Value)
                '大文字にするかどうか
                If OPTION_LARGE = True Then
                    tmp = UCase(tmp)
                End If
                
                '小文字にするかどうか
                If OPTION_SMALL = True Then
                    tmp = LCase(tmp)
                End If
                
                val.Value = StrReverse(tmp)
            Next
            
            Result = "指定された範囲の文字列の反転を開始しています...完了"
        End If
        
        
    End If
    
    
    
    '戻り値
    StringReverse = Result
    
    Exit Function
    

ERROR:
    StringReverse = "不正なオプションを使用しているか、コマンドの文法が間違っています。"
    
End Function




以上、Excel上で動くコマンドプロンプトもどきでした!コマンドプロンプトに憧れて...ってタイトル付けたけど実際私が憧れてるのはbashなどのLinuxシェルの方です(笑)。何かの役に立てば私は嬉しいかな。自分でいろいろコマンドを追加して遊んでみてくださいね!!

はじめに

はじめまして。

私は小学生の頃からExcel VBAを触ってきました。なぜほかのプログラミング言語じゃないのか、というツッコミはなしにしてくださいね(笑)。まだ周りには興味を持ってくれる友達もいない中、寂しくも一生懸命にプログラミングに励んでいました。

時がたつこと早6年、Twitterを始めた私は軽い気持ちで作ったソフトの動画を公開しました。すると予想以上に好評でした。しかしながら、裏にはたくさんの苦労があります。一つのツールやソフトを作るために様々なサイトを調べましたが、変わった使い方を紹介しているサイトは少なく困ってしまいました。今後私と似たような境遇の人が少しでも多く情報を手に入れることができるように情報を公開したいと思いブログを開設しました。

 

現時点(2019 / 9 / 16)において、私は高校生ですから更新頻度は不定期です。更新したらぜひ見てくださいね。

これからよろしくお願いします!