📑

サクッと作るteratermマクロ入門

に公開

はじめに

  • Tera Term(テラターム)は、Windowsで動作するオープンソースのターミナルエミュレータで、SSH接続を通じてサーバーやネットワーク機器を遠隔操作するためのソフトウェアです。

  • Tera Termマクロ(テラタームマクロ)とは、Tera Termでの操作を自動化するためのスクリプト機能で、繰り返しのログインやコマンド投入、ログ保存などを効率化できる仕組みです。

  • Tera Termマクロの仕組みはとても良いのですが、問題はWindowsでしか動かないことです。

  • どうせであれば Mac や Linuxで動かしたい。

  • とても良いことに Tera Term マクロはヘルプが非常に充実していて、サクッとTera Term マクロを作るのに十分な情報が記載されています。

  • このページでは、Antlr4を使ってサクッとTera Term マクロを作っていきたいと思いまーす。

どんな人向け?

  • Python や Java でサクッとTera Term マクロを作りたい人。
  • Antlr4 を使ってコンパイラコンパイラを作りたい人。

最低限知っておきたいこと

  • 以下に記載するTeratermマクロを作成する際に必要最低限知っておくべきことを記載。
  • そんな解説よりマクロを作りたい!って方は飛ばしてもらって構いません。

Antlr4

  • Antlr4(アントラー4)は、プログラミング言語やデータフォーマットの構文解析器(パーサー)を自動生成するためのツールで、コンパイラ、コード解析ツールの開発に広く使われています。

Python, Java などの言語

  • Antlr4 では以下の言語を出力することができるので、お好きな言語を使用してサクッとTera Term マクロを作ることができます。
    • Java
    • C# (CSharp)
    • Python3
    • JavaScript
    • TypeScript
    • Go
    • C++ (Cpp)
    • Swift
    • Dart
    • PHP

どうやってサクッとTera Term マクロを作るのか?

  • まずは Antlr4 の公式サイトからantlr-4.x.x-complete.jarファイルをダウンロードし、Java環境にセットアップするのが基本です。
  • 私は以下のコマンドでサクッとTera Termマクロを作りました
$ export CLASSPATH=".:/usr/local/lib/antlr-4.13.2-complete.jar:$CLASSPATH"
$ alias antlr4='java -jar /usr/local/lib/antlr-4.13.2-complete.jar'
$ cd 作成したファイルのフォルダ
$ antlr4 -visitor -Dlanguage=Python3 ttl_parse.g4
  • ここで ttl_parser.g4 は サクッとTeraterm Macroを作るための Lexer(字句解析器)とParser(構文解析器)です。中身はテキストです。
  • 以降は、サクッとTeraterm Macroを作るための ttl_parser.g4 の中身について説明していきます。

定数の形式

  • Teraterm マクロの公式サイトでは、定数の形式 を定めています。それに合わせて書くとこんな感じになるでしょう。
  1. 整数型定数は10進数または "$" で始まる16進数で表現する。 浮動小数点は未サポート
NUMBER: [-+]? [0-9]+;
NUMBER_16: '$' [-+]? [a-fA-F0-9]+;
  1. 文字列型定数
  • 値となる文字列の両端を ' か " で囲む(両端とも同じ文字で)。文字列値を構成する文字は表示可能で囲み文字と異なる文字ならば何でもよい
  • 1文字を ASCII (または JIS ローマ字、Shift-JIS) コード(10進数または$で始まる16進数)で表現し、先頭に "#" をつける。 ASCII コード 0 の文字 (NUL) は文字列定数に含めることができない。
  • 上記は組み合わせることが可能。
STRING1: ('\'' ~[']* '\'' | '"' ~["]* '"' | '#' '$'? [a-fA-F0-9]+)+;
strContext: STRING1;
  1. 名前の形式
  • 1文字目は、アルファベット (A-Z, a-z) か ""、2文字目以降はアルファベットか "" か数字 (0-9)。
KEYWORD: [_a-zA-Z][_a-zA-Z0-9]*;
label: ':' KEYWORD ;
  1. 整数配列/文字列配列
  • Teraterm Macroは以下のように配列を受け付けます。
keyword: KEYWORD (LEFT_KAKKO (intExpression| strExpression) RIGHT_KAKKO)?;
LEFT_KAKKO: '[';
RIGHT_KAKKO: ']';

文字列の形式

  • 文字列は、変数と文字の組み合わせです
strExpression: strContext| keyword;

数式の形式

  • Teraterm マクロの公式サイトでは、式と演算子 を定めています。それに合わせて書くとこんな感じになるでしょう。
intContext
    : NUMBER
    | NUMBER_16
    ;
intExpression: intContext | keyword | '(' p11Expression ')';
p1Expression
    : intExpression
    | 'not' intExpression
    | '~' intExpression
    | '!' intExpression
    ;
p2Expression
    : p1Expression
    | p2Expression '*' p1Expression
    | p2Expression '/' p1Expression
    | p2Expression '%' p1Expression
    ;
※中略
p10Expression
    : p9Expression
    | p10Expression '&&' p9Expression
    ;
p11Expression
    : p10Expression
    | p11Expression '||' p10Expression
    ;

リターンコード

  • リターンコードを規定します。改行コードの前に ; が付いている場合はドキュメントなのでそれらは省きます。
RN: '\r'? '\n'
    | ';' ~[\n]* '\n'
    ;

ホワイトスペース

  • TeraTermマクロの資料にはないですが、ホワイトスペースを規定します。
  • ホワイトスペースは文字と文字の間の区切りとしてスキップされます。
WS1 : [ \t]+ -> skip;
WS2 : '/*' ~[/]* '*/'-> skip;

コマンド

  • Tera Termマクロに合わせてコマンドを並べていきます。
command
    : 'bplusrecv'
    | 'bplussend' strExpression
    | 'callmenu' p11Expression
    | 'changedir' strExpression
    | 'clearscreen' strExpression
※中略
    | 'assert' p11Expression (strExpression|p11Expression)*
    ;

ラベル

  • ラベルは先頭に : があって次にキーワードが来ます。簡単ですね。
label: ':' KEYWORD ;

コマンドライン

  • Teratermマクロの場合、テキストは複数のコマンドラインと EOF から成り立ちます。
statement: commandline+ EOF;
  • コマンドラインは 普通のコマンド、if文、white文などから成り立ちます。
  • このあたりが サクッとteratermマクロを作るにあたり一番難しいあたりでしょう。
commandline
    : input RN
    | if2
    | if1
    | forNext RN
    | whileEndwhile RN
    | label RN
    | command RN
    | untilEnduntil RN
    | doLoop RN
    | RN
    ;
  • if文などのコマンドラインの中身は、Teraterm マクロのヘルプを見ながらサクッと作っていきます。
forNext
    : 'for' keyword p11Expression p11Expression RN commandline+ 'next';

whileEndwhile
    : 'while' p11Expression RN commandline+ 'endwhile';

untilEnduntil
    : 'until' p11Expression RN commandline+ 'enduntil';

doLoop
    : 'do' p11Expression? RN commandline+ 'loop' p11Expression?;

if1
    : 'if' p11Expression commandline;

if2
    : 'if' p11Expression 'then' RN commandline+ (elseif)* (else)? 'endif';

elseif:'elseif' p11Expression 'then' RN commandline+;

else: 'else' RN commandline+;

早速サクッと作ってみる

作成のポイント(Antlr4の起動)

  • まずはAntlr4の出力を使ってTtlParserLexerで字句解析、TtlParserParserで構文解析後、TtlParseTreeVisitorでJsonに変換します。
    def include_data(self, data: str):
        """Jsonで結果を得る"""
        if self.end_flag:
            return
        #
        input_stream = InputStream(data + "\n")
        lexer = TtlParserLexer(input_stream)
        lexer.removeErrorListeners()
        lexer.addErrorListener(ThrowingErrorListener())
        token_stream = CommonTokenStream(lexer)
        parser = TtlParserParser(token_stream)
        tree = parser.statement()
        visitor = TtlParseTreeVisitor()
        return tree.accept(visitor)
  • Vistorはこんな感じです。Jsonに変換します。Javaとかであればクラスをそのまま使って処理していく方が楽だと思います、Pythonの場合はJsonの方がなにかと楽なので変換しています。
  • 工夫点といえば 本来必要のない line も取得していることでしょう。これによりエラーがでたときにエラー行数を簡単に出力することができます。
class TtlParseTreeVisitor(ParseTreeVisitor):
    def visit(self, tree):
        return tree.accept(self)

    def visitChildren(self, node):
        result = {}
        result["name"] = node.__class__.__name__
        line_number = node.start.line
        result["line"] = line_number
        #
        n = node.getChildCount()
        if n == 0:
            return result
        worker_list = []
        for i in range(n):
            c = node.getChild(i)
            childResult = c.accept(self)
            if childResult is not None:
                worker_list.append(childResult)
        if worker_list != 0:
            result["child"] = worker_list
        return result

    def visitTerminal(self, node):
        type = node.getSymbol().type
        # print(f"visitErrorNode type={type} text={node.getText()}")
        if type < 0:
            return None
        x = TtlParserLexer.ruleNames[type - 1]
        if x == "RN" or x == "WS":
            return None
        return node.getText()

    def visitErrorNode(self, node):
        x = node.getSymbol().type
        x = TtlParserLexer.ruleNames[x - 1]
        y = node.getText()
        line_number = node.getSymbol().line
        raise TypeError(f"### l={str(line_number)} visitErrorNode type={x} text={y}")

作成のポイント(continue, break)

  • 作成のポイントとして次にあげられるのはfor文などで使われる continue, breakでしょう。
  • サクッとteratermマクロを作るのにあたっては、for文などの途中でcontinueが出たとしてもどこに戻れば良いかなどの実装は大変困難です。
  • 幸いにして、Pythonでは try/except の機構があるので、それをそのまま使うのが一番手っ取り早いと思われます。
  • まずは、breakやcontinue用の例外クラスを作ります(同様にReturn用なども作っておきます)
class TtlContinueFlagException(Exception):
    """continue 用の例外"""

    def __init__(self, message):
        """コンストラクタ"""
        super().__init__(message)


class TtlBreakFlagException(Exception):
    """break 用の例外"""

    def __init__(self, message):
        """コンストラクタ"""
        super().__init__(message)
  • for文などを使うシーンでこれらの例外クラスをexceptすることで簡単にcontinueやbreakを実装することができます。
    def whileEndwhileContext(self, data_list):
        while self.getDataInt(data_list[1]) != 0:
            try:
                #
                self.execute_result(data_list[2:-1])
                #
                if self.end_flag:
                    break
            except TtlContinueFlagException:
                pass
            except TtlBreakFlagException:
                break
  • breakを発生させる例
                elif "break" == command_name:
                    raise TtlBreakFlagException(command_name)

おわりに

  • 後はconnectの中身をparamikoで実装するなどすれば完成です。
  • 簡単でしょ。ぜひ他の言語でも実装してみると面白いでしょう。
  • Teraterm マクロの文化が世界に広がっていけば幸いです。

Discussion