うっかり、オレオレ grep (seek)を書いてしまった。

先日、while( <> ){ } 的なものの Go 版ライブラリ argfを書いてみたわけだけれども、そのサンプルとして 簡単な grep (seek)を書いてみた。

最初は必要最小限にしていたんだけど、jvgrep みたいに「検索文字列の部分をハイライト表示」できないかなと思って頑張ってみたら、Go言語のライブラリが充実していることもあって、案外簡単に出来てしまった。

で、調子にのって、自分が grep に望む機能を実装してみた。

  • UTF8 と ANSI(現在のコードページマルチバイト文字列:日本なら SJIS~CP932)の行単位での簡易自動判定
    • UTF8 と解釈できなければ、ANSI と判断するという安直判定
    • 前は ShiftJIS の時は Windows 標準の findstr.exe、UTF8 なら jvgrep を使っていたが、いちいち使い分けるのは面倒くさかった。
    • jvgrep はテキストファイルチェックが厳しくて、すぐバイナリファイル扱いにしてしまう。
    • git で SJIS テキストの差分を見る時、git の UTF8 と、本文の SJIS が混ざってしまう。両方とも化けずに見たい場合があるので、行単位で別々の文字コード判定にしたかった。

本当に自分が必要とした機能しか入れていないので、以下のような制限がある

  • オプションは -i (大文字・小文字を区別しない)、-r再帰的にファイルを検索する)だけ
  • 速度はあまり重視しておらず、工夫なし
  • 常にカラー表示。「リダイレクトしていたらエスケープシーケンスを抑制」とか別にしない
  • 色使いは jvgrepパクリ 互換

貴殿も、オレオレ grep を作ってみませんか?

Lua 5.1 向け shebang for Windows

@lua5.1.exe -e "_,_,b=io.input([[%~f0]]):read('*l','*l','*a');assert(loadstring('\n'..b,[[%~f0]]))()"
@exit /b %ERRORLEVEL%

print('ahaha')
x

5行目にわざと Lua としては不適切な文字 x を入れている。これを実行すると

lua: (command line):1: [string "C:\Users\USERNAME\foo.cmd"]:5: syntax error near <
eof>
stack traceback:
        [C]: in function 'assert'
        (command line):1: in main chunk
        [C]: in ?
exit status 1

5行目がおかしいというエラーがちゃんと出る。x を消すと

$ foo
ahaha

とちゃんと実行される。

while( <> ){…} みたいなことを Go で

UNIX風のテキストファイルフィルターを書くのに便利なように Perl には

while( <> ){
   print $_
}

という構文がある。これと同じような手軽さで Go でもフィルターを書くためのライブラリ argf を書いてみた。

package main

import (
    "fmt"
    "os"

    "github.com/zetamatta/experimental/argf"
)

func main() {
    r := argf.New()
    for r.Scan() {
        fmt.Println(r.Text())
    }
    if err := r.Err(); err != nil {
        fmt.Fprintln(os.Stderr, err.Error())
    }
}
  • 基本インターフェイスbufio.Scanner に準拠した(=ほぼ同じメソッド構成)
  • len(os.Args) < 2 の時は、標準入力から読み取るが、そうでない時は os.Args[1:] のファイルを順に読み込んでゆく
  • argf.New()インスタンスには、AWK の組み込み変数と同じ名前のメソッドを実装している
    • NR() … 読み込みレコード数(行数)
    • Filename() … 現在読み込んでいるファイルの名前
    • FNR() … 現在のファイルで読み込んだレコード数(行数)

かんたんな grep もどきも書いてみた。

package main

import (
    "errors"
    "fmt"
    "github.com/zetamatta/experimental/argf"
    "os"
    "regexp"
)

func main1() error {
    if len(os.Args) < 2 {
        return errors.New("Usage: grep.exe REGEXP Files...")
    }
    rx, err := regexp.Compile(os.Args[1])
    if err != nil {
        return err
    }
    r := argf.NewFiles(os.Args[2:])
    for r.Scan() {
        if rx.MatchString(r.Text()) {
            fmt.Printf("%s(%d): %s\n", r.Filename(), r.FNR(), r.Text())
        }
    }
    return r.Err()
}

func main() {
    if err := main1(); err != nil {
        fmt.Fprintln(os.Stderr, err.Error())
        os.Exit(1)
    }
    os.Exit(0)
}

argf.New()os.Args[1:] を全て読み込み対象としてしまうが、grep もどきでは os.Args[1] は検索文字列となるので、読み込み対象から除きたい。そのために argf.NewFiles(args []string) という、読み込み対象のファイル名を、引数で与えるタイプのコンストラクタも用意した。

今週の zetamatta/experimental は以上です。

Go言語についての3つくらいの誤解

以下、自分の思い込みもあるとは思うが、あまり気にしないで書いてみる。

Goは短く書くための言語ではない

短く書けるようにした結果、落とし穴が発生したり、 読みにくくなったり誤解が生じる余地があるならば、 そのような書き方は排除されている。

貴方が書きたいコードは、誰にでも読みやすいコードなのか、 誤解の余地がまったくないコードなのか、そのあたりをよく考えてほしい。

サポートされていない機能は未来永劫サポートしないとは限らない

例外は将来的にもサポートされないだろうが、 ジェネリクスは優先度が低いとされているだけで、 別に未来永劫サポートしないわけではない。

ただ、複雑性が増すなど課題が多いのと、 ジェネリクスがなくても済ませられるケースが多いため、 後回しにされているだけである。

Goはあらゆる問題を解決する言語ではないし、誰にでも使ってもらおう等と譲歩した言語でもない

RubyJava のように、あらゆる課題を解決しようなどと思い上がった言語ではない。 不得意分野は不得意分野と割り切っている。

また、誰にでも使ってもらうなどと考えている言語でもない。 気に入らないのなら、選択肢は他に幾らでもあるだろう。

以上、ソースはないよ。コメントもらっても回答しないかも

PowerShell で書く、Go言語プログラムのリリース管理的なアレ(3)バージョン文字列の設定

バッチファイルの話もするとたいへんなので、以下 PowerShell だけという方向で:

Go ソースへの埋め込み

これは簡単で、

go build "-o" nyagos.exe -ldflags "$ldflags -X main.version=$version"

だけでよい。これで main パッケージの version という変数に $version の内容が設定される。 Go ソース側で受け皿となる変数の宣言を忘れないように。

var version string

Windows プロパティーへの埋め込み

ちょっとめんどう。goversioninfo というツールを使う。 (make.cmd では Make-SysO という関数内で行っている)

    .\goversioninfo.exe `
        "-file-version=$version" `
        "-product-version=$version" `
        "-icon=mains\nyagos.ico" `
        ("-ver-major=" + $v[0]) `
        ("-ver-minor=" + $v[1]) `
        ("-ver-patch=" + $v[2]) `
        ("-ver-build=" + $v[3]) `
        ("-product-ver-major=" + $v[0]) `
        ("-product-ver-minor=" + $v[1]) `
        ("-product-ver-patch=" + $v[2]) `
        ("-product-ver-build=" + $v[3]) `
        "-o" nyagos.syso `
        versioninfo.json

(PowerShell で逆クォートは、UNIX系のバックスラッシュみたいな役割を担っている)

基本の情報は versioninfo.json に記しておいて、変更がよく発生するところだけ、オプションで変更する。 このコマンドで生成されたnyagos.sysoというファイルを go build するフォルダーにおいておくだけで、 これらの情報が実行ファイルに埋め込まれる。

Windows にはバージョン番号のプロパティーが複数ある。

  • 製品バージョン(product-version)
  • ファイルバージョン(file-version)

NYAGOS では両者は同じ扱いにしているが、大きなパッケージソフトウェアでは、パッケージ全体のバージョンを product-version とし、個別ファイルのバージョンはファイルバージョンとするのが普通のようだ。

さて、これらのバージョンには「バージョン文字列」と4つの数値:「メジャーバージョン」「マイナーバージョン」「ビルドナンバー」「パッチナンバー」をそれぞれに持っている。

Windowsエクスプローラで EXE ファイルのプロパティー→詳細タブを開くと、バージョン情報が表示されるが

  • 製品バージョンは、バージョン文字列(-product-version=)がそのまま表示される
  • ファイルバージョンは、「メジャーバージョン(-ver-major=)」「マイナーバージョン(-ver-minor=)」「パッチナンバー(-ver-patch=)」「ビルドナンバー(-ver-build=)」をピリオドで連結したテキストが表示される。

ようになっているようだ。

なお、古い goversioninfo.exe には「-product-ver-*****」という引数はない (わしがプルリク送った)。

最新版をゲットしてくれ…といいたいところだが、make.cmd には最新版の goversioninfo.exe を自動で入手してくれる機能がある(続く)

PowerShell で書く、Go言語プログラムのリリース管理的なアレ(2)バージョン文字列の取得

方針としては

  • リリースビルドの時は、テキストファイル(Misc\version.txt)に記載のテキスト
  • スナップショットの時は、git describe --tags の結果

を使うことにしている。これはバッチファイルでも比較的簡単だ。 リリースビルドなら

for /F %%I in (%~dp0Misc\version.txt) do set "VERSION=%%I"

だし、スナップショットなら

for /F %%I in ('git describe --tags') do set "X_VERSION=-X main.version=%%I"

だ(若干変数が違うがスルーしていただきたい)。

これが PowerShell だと、もっと簡単で、リリースビルド:

Build  (Get-Content Misc\version.txt)  ""

となる。Build はビルドするための関数で第一引数のバージョン文字列を取る形になっている。スナップショットは

 Build (git describe --tags) ""

だけで済む。PowerShell では括弧が逆クォート的な役割を担っている(文法的には式を展開する的な意味)。

うん、つまらないところなので、早くも飽きてきた(おい)。 次はもう少し面白いはずの、バージョンを設定するところを書こう。

PowerShell で書く、Go言語プログラムのリリース管理的なアレ(1)目次

NYAGOS は 4.2.1 まではバッチファイルで、以下の作業を行っていた。

  • 実行ファイルの作成
    • バージョン文字列の取得
      • リリース(make release)の時はテキストファイル(Misc\version.txt)
      • スナップショット(引数なし make)の時は git describe --tags を使用
    • バージョン文字列・アイコンの埋め込み
      • Go言語のソースへのバージョン文字列埋め込み
      • Windows のプロパティとしてのバージョン文字列・アイコンの埋め込み
    • nyagos.d 以下のファイルの EXE ファイル内への埋め込み(実は nyagos.d が存在しない場合にデフォルトとして利用するようになっている)
    • 必要最小限の go fmtDOSファイルシステムアーカイブビットを利用している)
    • 作成CPUアーキテクチャの判断
      • goarch.txt があれば、その中身を %GOARCH% に設定する
      • goarch.txt がなければ、go version の結果から %GOARCH% を設定(不要だが、後の処理を合わせるため)
    • 必要に応じて go generate の実行
  • リリース用 ZIP ファイル作成(make package)
  • C言語定数の Go ソース化(make constgcc)
  • make clean
  • make get(サブディレクトリ全部に対する go get
  • 実行ファイルのバージョン・アーキテクチャの確認(make status

NYAGOS 4.2.2 では、これらをだいたい PowerShell に置き換えた。

気が向いたら、順次説明してゆきたい