標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

2ペインテキストビューア、いろいろ作ってます。

20年くらい前にOS/2eff , ell という「テキストビューア」群を作ってましたが、それと同じような感じのものできないかなーと思って、Go言語で簡単なフレームワーク go-twopane を作ってみました。

  • コンソール画面を2分割して、上に件名一覧、下に詳細を表示する
  • go-twopane には配列を渡す。配列の要素は「件名」を得る Title() メソッドと、「詳細」を得る Contents() というメソッドを定義してあれば OK
  • カーソル上移動(,k)、下移動(,j)、詳細次ページ(SPACE)、終了(ESC,C-q)程度は標準で実装。他のキーに対するアクションを定義したい場合はコールバック関数を渡せば良い

という感じです。ね、簡単でしょ?

そして、Windows でも Linux でも動きます(ここ重要)

実装例1:git のログビューア (gitview

件名一覧は git log の結果。詳細は git show の結果です。git show の方は、git の出すカラーを維持し、かつ UTF8 と解釈できない行は ANSI → UTF8 変換を試みるので、文字化けがかなり少ないはずです。(ファイルの中身は ShiftJIS だが、git.exe がファイル名を Unicode で出すので、出力全部の文字コードが一貫しないことが Windows ではよくあるのですわ…)

実装例2: Subversion のビューア

こちらはソースだけ。 なぜなら、スクリーンショットが業務情報のカタマリのやつしかないから…。

git と違って git show みたいなやつはないんですが、svn log --xml でそこそこ情報が取れるので、詳細情報はログ全部+変更ファイル一覧みたいなものをかわりに載せてます。

実装例3:twitter client(tmt view

いわゆる「人間性はありません」(tmt.exe)という名前の謎ツイッタークライアントが正体がコレです。もともと tmt post で投稿、tmt timeline でタイムラインを標準出力に出すといった twtyパクったようなインスパイアした ツールなんですが(だから自分の趣味のサブコマンドしかない)、これに tmt view というビューアを起動するサブコマンドを追加しています。

twitter は文字種が多いので、go-twopane のすごい耐久テストができました。ANSI エスケープシーケンスだけでやっているので、うっかりすると行の桁数が想定よりオーバーフローして、画面がくずれてしまいます。なので、以下のような対策を go-twopane 側でとっています。

  • Windows の端末はサロゲートペアな Unicode が表示できないので、それらは &#xNNNN; といった形にエンコードする(豆腐にしてもよかったんですけどね)
  • 文字幅があいまい(全角か半角かというやつです)とされている文字については、あふれないように端末のマスを常に2セル消費することを前提にしてしまう

(注意)

tmt はソースそのままではビルドできません。twitter の ConsumerKey , ConsumerSecretKey がカラだからです。これらは twitter に開発申請を出してゲットしてから、secret/secret.go.sample にある変数名を埋めないといけません(埋めたあとファイル名から .sample を除くのも忘れないように)

なお、バイナリについては、リリースタブのところからダウンロードできます

利用技術

うむ、また mattn 先生のライブラリにおんぶにだっこだな! でも、この組み合わせだと、Windows/Linux 両方サポートが簡単なんだよ

痛い話など

  • 最初レポジトリを作った時、twopain と命名していました。これは痛い…

  • マインクラフトばっかりやってて GitHub の砂漠化が進んでいましたが、こいつを作るようになって、ちょっと緑が回復しました。

  • ブラウザで見るより twitter 目立たないかなーと思ってたんですが、黒いコンソールの中に日本語がみっちり入ってると意外と目立ちますね(おまえは何を懸念しているんだ

以上です

(続々) std::vector / std::set 両方からデータを取れる関数をあまりテンプレートにしたくない

(半日も経ってませんが、(続) std::vector / std::set 両方からデータを取れる関数をあまりテンプレートにしたくない - 標準愚痴出力からの続きです)

前回 std::function を使って、コールバック関数を関数オブジェクトにし、呼び出しを簡素にしてみたわけですが、まだ型パラメータが長めでした。それをなんとか一つまでに減らしてみました。

#include <iostream>
#include <vector>
#include <functional>
#include <set>

template <typename T>
class enumerator {
    typename T::iterator m_cursor,m_end;
public:
    enumerator(typename T::iterator begin,typename T::iterator end)
        : m_cursor(begin) , m_end(end) {}
    bool operator() (typename T::value_type &store) {
        if( m_cursor == m_end ){
            return false;
        }
        store = *m_cursor;
        m_cursor++;
        return true;
    };
};


void put(const std::function<bool(std::string&)> &each)
{
    std::string value;
    while( each(value) ){
        std::cout << value << std::endl;
    }

int main()
{
    std::vector<std::string> v;
    v.push_back( "a" );
    v.push_back( "b" );
    v.push_back( "c" );

    put( enumerator<std::vector<std::string>>(v.begin(),v.end()) );

    std::set<std::string> s;
    s.insert( "a" );
    s.insert( "b" );
    s.insert( "c" );

    put( enumerator<std::set<std::string>>(s.begin(),s.end()) );
}

よしよし

(続) std::vector / std::set 両方からデータを取れる関数をあまりテンプレートにしたくない

関数オブジェクトで、少し改善できました。

#include <iostream>
#include <vector>
#include <functional>
#include <set>

template <class E,class T>
class enumerator {
    E m_cursor;
    E m_end;
public:
    enumerator(const E &begin,const E &end) : m_cursor(begin) , m_end(end) {}
    bool operator() (T &store) {
        if( m_cursor == m_end ){
            return false;
        }
        store = *m_cursor;
        m_cursor++;
        return true;
    };
};

void put(const std::function<bool(std::string&)> &each)
{
    std::string value;
    while( each(value) ){
        std::cout << value << std::endl;
    }
}

int main()
{
    std::vector<std::string> v;
    v.push_back( "a" );
    v.push_back( "b" );
    v.push_back( "c" );

    put( enumerator<std::vector<std::string>::iterator,std::string>(v.begin(),v.end()) );

    std::set<std::string> s;
    s.insert( "a" );
    s.insert( "b" );
    s.insert( "c" );

    put( enumerator<std::set<std::string>::iterator,std::string>(s.begin(),s.end()) );
}

しかし、enumerator<std::set<std::string>::iterator,std::string>(…) は長いすぎる。クラス名くらいは一つにしたい…

std::vector / std::set 両方からデータを取れる関数をあまりテンプレートにしたくない

というような主旨のことを述べたら、boost::any_range をオススメされました。

が、弊社では外部ライブラリの導入がめんどくさいので、boost といえど、あまり使いたくない(未だに Visual Studio 2010 がメインに使われてるようなところだからね)

とすると、コールバック関数かなぁ

#include <iostream>
#include <vector>
#include <functional>
#include <set>

void put(std::function<bool(std::string&)> each)
{
    std::string value;
    while( each(value) ){
        std::cout << value << std::endl;
    }
}

int main()
{
    std::vector<std::string> v;
    v.push_back( "a" );
    v.push_back( "b" );
    v.push_back( "c" );

    auto p=v.begin();
    put( [&v,&p](std::string &value){ if( p == v.end() ){ return false; } value = *p ; p++ ; return true; } );

    std::set<std::string> s;
    s.insert( "a" );
    s.insert( "b" );
    s.insert( "c" );

    auto q=s.begin();
    put( [&s,&q](std::string &value){ if( q == s.end() ){ return false; } value = *q ; q++ ; return true; } );
}
$ gcc -std=c++14 it.cpp -lstdc++
$ a
a
b
c
a
b
c

うーん、呼び出しがすごく面倒くさい。

標準ライブラリに std::vector , std::set の既定クラス的な std::collection とか std::enumerable とかあればよかったのに。

本件、たぶん、続きます。

ExcelVBA のソースを .xls ファイルから抽出する JScript を書いた

Excel VBA の保守をしなければいけないんだけども…

  • そのアプリは 開いただけで自動的に VBA が自動的に起動してしまう
  • xlt ファイルなので、名前を変えないまま保存できず、毎回自分自身を選択しなくてはいけない

こういう場合、Ariawase という Windows Scripting Host のツールで VBA のソースをエクスポート / インポートするのが定石です。が、ちょっと User Interface が好みではない。

ということで、同ツールのソースから必要最小限の部分をパクって 参考にして 、エクスポートするコードを JScript で書いてみました (本当は Go でいきなり書きたかったけど、COM の構成を理解するために、まず JScript でプロトタイプを書いた)

exportvba.js

var args = WScript.Arguments;

if( args.length < 1 ){
    WScript.Echo( "cscript exportvbs.js XLSNAME" );
    WScript.Quit();
}
var fsObj = new ActiveXObject("Scripting.FileSystemObject");
var excel = new ActiveXObject("Excel.Application");
try{
    excel.Visible = true;
    var targetPath = fsObj.GetAbsolutePathName(args(0));
    WScript.Echo( "extract: " + targetPath );
    var book = excel.WorkBooks.Open(targetPath);
    for( var p = new Enumerator(book.VBProject.VBComponents) ; ! p.atEnd() ; p.moveNext() ){
        var obj = p.item();
        var name = obj.Name;
        switch( obj.Type ){
            case 1: name += ".bas"; break;
            case 2: name += ".cls"; break;
            case 3: name += ".frm"; break;
            case 100: name += ".dcm";break;
        }
        var fullpath = fsObj.GetAbsolutePathName( name );
        WScript.Echo( fullpath );
        obj.Export( fullpath );
    }
}finally{
    if ( excel != null ){
        excel.Quit();
    }
}

cscript exportvba.js foo.xls とすると、カレントディレクトリに foo.xls 内の VBA ソースを展開するという本当にシンプルな動作です。

さて、次のステップは

  • インポート版を作る
  • Go 言語で作り直す

かな。これ、いつになったら出来るかな!?

nyagos 4.4.5_0 を公開しました。

4.4.4_4 は欠番になりました。今回の修正は、パッチリリースというには、少し修正が多めになりましたので。

今回の修正は、~/.nyagos の簡素化がメインです。

  • Lua関数: nyagos.dirname() を実装

これは UNIX の dirname 相当を行うための関数です。

$ lua_e "print(nyagos.dirname('C:/User/hymko/Share'))"
C:\User\hymko

みたいに使います。実は nyagos.dirname('A')nyagos.pathjoin('A','..') と等価なのは秘密です。

  • C-o で複数ファイル選択をサポート(Space,BackSpace,Shift-H/J/K/L,Ctrl-Left/Right/Down/Up)

ごらんのとおり、複数ファイル選択ができるようになりました。

  • Alt-Y(引用符つきペースト)で、改行前後に引用符を置くようにした

これは git status でリストされた修正ファイルをマウス操作でコマンドラインにペーストする時、空白が含まれたファイル名も扱えるようにするための修正です。 (Visual Studio だと、My Project なんていう名前のフォルダーがあって、これが難儀なんですよね)

  • C-o で表示される選択肢がディレクトリの時、末尾に \ (Linux では /) をつけるようにした。

これで選択後に TAB を1回打鍵するのが省けます。

  • nyagos.envadd("ENVNAME","DIR") と nyagos.envdel("ENVNAME","PATTERN") を実装

.nyagos での%PATH% の追加/削除の支援関数です。こんな感じで使います。

nyagos.envdel("PATH",
    "Oracle","Lenovo","Skype",
    "chocolatey","TypeScript","WindowsApps",
    "Wbem","dotnet")

これは %PATH% から OracleLenovo といったキーワードを含むフォルダーを削除します。長過ぎる %PATH% は誤動作を引き起こすこともあるので、いつも Lua 関数を作って短くしていたのですが、いい加減定番コードなので本体にツール関数として内蔵させた方がよいのではないかということで実装してみました。

nyagos.envadd("PATH",
    "C:\\go\\bin",
    "C:\\TDM-GCC-64\\bin",
    "%ProgramFiles%\\Git\\bin",
    "%ProgramFiles%\\Git\\cmd",
    "%ProgramFiles%\\Git\\cmd",
    "%ProgramFiles%\\Git\\usr\\bin",
    "%ProgramFiles(x86)%\\Git\\bin",
    "%ProgramFiles(x86)%\\Git\\cmd",
    "%ProgramFiles(x86)%\\Git\\usr\\bin",
    "%ProgramFiles%\\Subversion\\bin",
    "%ProgramFiles(x86)%\\Subversion\\bin",
    "~\\Share\\Program Files\\idmanager",
    "%VBOX_MSI_INSTALL_PATH%",
    "~\\Share\\bin",
    "~\\Share\\cmds")

こちらは逆に %PATH% にフォルダーを追加します。こちらは少々機能追加してありまして、%~% というワードを環境変数、~\ をホームディレクトリ名に展開するといったことを行います。また、ディレクトリとして存在しない場合は追加を行いませんので、.nyagos を DropBox などで共有化して、各PCでありえるケースをとにかく書くといったことも可能になります。

  • nyagos.pathjoin() で %ENVNAME% と ~\,~/ を展開するようにした

これは文字通りです。いちいち nyagos.env.ENVNAME とかいちいち書くの面倒なので。

以上になります。

strings.EqualFold はどの程度の速度か

strings.EqualFold について言及がなかったので、測ってみました。

追加行:

func isDevNull4(name string) bool {
    return strings.EqualFold(name,"nul")
}
func BenchmarkS4(b *testing.B) {
    test(b, isDevNull4)
}

結果:

$ go test -bench .
goos: windows
goarch: amd64
BenchmarkS1-4           50000000                29.5 ns/op
BenchmarkS2-4           50000000                29.1 ns/op
BenchmarkS3-4             200000              9844 ns/op
BenchmarkS4-4           20000000               101 ns/op

自分的には「なんか、まぁまぁ速かったけど、速すぎるので、何かミスってる可能性大」と思っていたんですが、「そんなもんだと思います」とのコメントをいただきました。

まぁ、考えてみれば、S3 で使われている strings.ToLower(name) == "nul" は文字列の領域確保とインスタンス作成が入っているため、この結果は当然かもしれません。

mattn さん、どうもありがとうございました。