標準愚痴出力

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

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 に置き換えた。

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

n番煎じの powershell 向け shebang !

PowerShellスクリプトを実行しようとすると、実行ポリシーを一時的に変えるため

powershell -ExecutionPolicy RemoteSigned -file HOGE.ps1

と長い起動コマンドラインになってしまいがちです。

ということで、HOGE.ps1 を実行するために、別途 HOGE.cmd などのバッチファイルを用意したりしますが、ファイルが増えると、配布する手間、説明する手間(「二つを同じフォルダーに置いて実行してください!」とか)も増え、しかも使ってもらえなくなったりして、なかなか難儀です。

そこでバッチファイルの中に PowerShell スクリプトを組み込んで、1ファイル化しようということになります。過去、PowerShell をバッチファイルに組み込む方法は、過去いろいろ記事がありました。

そんな中、先日見つけた記事:

これはかなり決定打のように思われました。echo がバッチコマンド、PowerShell 両方にある(echo = Write-Outputエイリアス)ことを利用するなど、よく考えられたものです。ただ、惜しまれるのはバッチとの共用行が長くて、覚えづらいということでしょうか。

そこで、自分なりにアレンジしてみました。

@set "args=%*"
@powershell "iex((@('')*3+(cat '%~f0'|select -skip 3))-join[char]10)"
@exit /b %ERRORLEVEL%

Write-Output $env:args

# vim:set ft=ps1:
  • プログラム的にバッチファイルの先頭3行を空行に差し替えて、Invoke-Expression(iex)で処理するようにした(3行を差し替えずに、そのまま消してもよかったのだが、エラーの行番号がズレさせたくなかったため)
  • 改行を表す「\"`n\"」を [char]10 などに置き換えた(エディターのシンタックスハイライトに優しい)
  • 引数は残念ながら $args[] ではなく、$env:args を使う(とほほ)

しかし、こういうのも、多分、どこかで既出なんでしょうね

GoのEnum表現は、go:generate + stringer の出番?

主旨が違うかもしれないんだけど、そういうのは go:generate + stringer の出番と違うかなと思った。

package main

import "fmt"

type SkuNameEnum int

//go:generate stringer -type=SkuNameEnum

const (
    Free SkuNameEnum = iota
    PerNode
    Premium
    Standalone
    Standard
    Unlimited
)

func main(){
    for i := Free ; i <= Unlimited ; i++ {
        fmt.Println( i )
    }
}
$ go generate
$ go build
$ ls
foo.go                skuname.exe*          skunameenum_string.go
$ skuname.exe
Free
PerNode
Premium
Standalone
Standard
Unlimited
$