標準愚痴出力

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

Goの構造体タグによるコマンドラインオプション・設定ファイル・環境変数の一元管理

いわゆるクリーンアーキテクチャコマンドラインツールを作る時、標準の "flag" パッケージのオプションを全部構造体へ転記するのが面倒です。

そこで、次のようにタグにオプション名と Usage を書いておけば、flag.Parse が構造体へ直接フラグ値を代入してくれるというパッケージ: struct2flag をでっちあげました。

(巷にはそういうライブラリも既にありますが、ちょっと多機能すぎるんで…)

type Env struct {
    B bool   `flag:"b,This is a boolean flag"`
    N int    `flag:"n,This is an integer flag"`
    S string `flag:"s,this is a string flag"`
}

使う際は、次のように flag.Parse を呼ぶ前に BindDefault 関数を呼び出すだけで OK です。

    var env Env
    struct2flag.BindDefault(&env)
    flag.Parse()

全ソース

構造体フィールドに記載するタグは、他の JSONXML のタグと共存可能なので

  • デフォルト値は設定ファイルから
  • デフォルトから変更する指定はコマンドラインオプションで

ということも可能です。

type Env struct {
    B bool   `flag:"b,This is a boolean flag"  json:"b"`
    N int    `flag:"n,This is an integer flag" json:"n"`
    S string `flag:"s,this is a string flag"   json:"s"`
}
    var env Env

    if data, err := os.ReadFile("example3.json"); err == nil {
        err = json.Unmarshal(data, &env)
    }
    struct2flag.BindDefault(&env)
    flag.Parse()

全ソース

さらにテキストエディターの設定みたいに、環境変数も参照できると便利ですよね。同じようなインターフェイス環境変数から値をひっぱってくる struct2env というパッケージも作ってみました。

package main

import (
    "os"
    "fmt"
    "encoding/json"
    "flag"

    "github.com/hymkor/struct2env"
    "github.com/hymkor/struct2flag"
)

type Env struct {
    Editor string `env:"EDITOR" flag:"e,specify text editor" "json:"editor"`
}

func main() {
    var env Env

    if jsonText, err := os.ReadFile("example2.json"); err == nil {
        if err := json.Unmarshal(jsonText, &env); err != nil {
            fmt.Fprintln(os.Stderr, err.Error())
            return
        }
    }
    struct2env.Bind(&env)
    struct2flag.BindDefault(&env)
    flag.Parse()

    fmt.Printf("EDITOR=%#v\n", env.Editor)
}

全ソース

タグだけで、設定ファイル(JSON)、環境変数コマンドラインオプションを全部参照するようになりました。やりすぎですね、すみません。
(こうやって仕組みは肥大化し当初の理念が行方不明となりがちになるわけですが、幸い、参照する機能はどれも別のパッケージです。必要な機能のみ取捨選択すればよいでしょう)

See Also


  1. なお、struct2flag は "flag" の部分ラッパーに過ぎないので、従来の flag.String など従来の flag 関数も併用できる

Unicode の曖昧幅の文字は2セルか1セルか?

Unicode では各コードに対応する文字がターミナルで、どれだけの幅をとるかという補助情報を扱う「East Asian Width」というプロパティを定義しています。

意味 表示幅
F (Fullwidth) 全角文字 2 桁
H (Halfwidth) 半角文字 1 桁
W (Wide) 通常全角扱い 2 桁
Na (Narrow) 通常半角扱い 1 桁
A (Ambiguous) 曖昧 環境依存
N (Neutral) 幅の概念がない 通常 0

Ambiguous とされる文字の扱いが環境依存となっていて、これを確定しないと TUI アプリケーションでは画面のレイアウトが狂ってしまいます。

Ambiguous Width が 1桁か、2桁かの判断するには、いろんな方法があります。

(1)端末・コードページ・ロケールの種類を見る

Go言語の文字幅算出のデファクトスタンダートライブラリの mattn/go-runewidth は次のようなロジックを採用しています (ちょっと端折っています)

  1. 環境変数 WT_SESSION などが定義されている
    Windows Terminal
    Ambiguous Width は1桁
  2. Windows 環境でコードページが 932, 51932, 936, 949, 950
    UNIX 系環境で、locale が ja, ko, zh で始まっている
    Ambiguous Width は2桁
  3. Ambiguous Width は 1桁

(2)実測する

Ambiguous Width な文字を実際に出力してカーソル移動量から実装します。

  1. 復帰文字(\r) + Ambiguous Width な文字(等) を表示
  2. ESC[6n というシーケンスを出力する
  3. 端末から ESC[行位置;桁位置R という形式でカーソル位置が入力される
    ( Enter 入力がないため、端末を RAW モードにする必要がある )
  4. 桁位置が3 なら2桁、2なら1桁となる
    ( 桁位置は 1 スタートなので -1 している )

しかしながら、稀に日本語も表示できないような非GUI系のターミナルなど ESC[6n というシーケンスを認識しない環境もあります。この場合、ユーザから見ると、ずっとキー入力待ちで動作が止ってるように見えます。

これの回避策はないのですが、次善の策として、\r∇\rESC[6n を表示する前に

"\r   Calibrating terminal... (press any key to skip)\r"

と表示しておいて、ユーザが気付いたら手動でスキップしてもらうようにするのがよいでしょう。この場合なにひとつ情報が得られませんが、そもそも ESC[6n を認識しない端末は Unicode 自体表示できない場合が多いので、もういらんでしょう。英数字だけしか表示しない環境だとみなすのがよいかも

(3)環境変数で判別する

最終手段としてはユーザに環境変数やオプションなどで明示してもらうのもありです。

mattn/go-runewidth では RUNEWIDTH_EASTASIAN という環境変数を見ています。 これが

RUNEWIDTH_EASTASIAN Ambiguous Width
未定義 or "" 指定なし
"1" 2
"1"以外で1文字以上 1

さて、最適解は?

Go言語で開発している場合は mattn/go-runewidth の判断をそのまま採用すればよいでしょう。さらに一歩進めて、未知のターミナルでの文字幅も適切に対応したいという場合は

  1. RUNEWIDTH_EASTASIAN が定義されていなければ、(2) を実行して、桁数を算出する
  2. さもなければ、go-runewidth の判断をそのまま採用する

がよいかもしれません( 拙作の hymkor/csvi で採用している方法です )

環境変数から得る端末情報おぼえがき

昔は TERMCAP/TERMINFO でターミナルで使うべき制御コードが得られた。現在の端末はほぼ VT100 互換なので、その役割は薄れている。

一方、まだ自動では判別できない端末情報があり、一部のライブラリ・アプリケーションは次の環境変数からそれを取得している。

$RUNEWIDTH_EASTASIAN

Unicode の 「曖昧幅 (Ambiguous Width)」文字を、2セル幅として扱うか1セル幅として扱うかを指定する。

$RUNEWIDTH_EASTASIAN 意味
"1" 2セル幅
"1" 以外の1文字以上 1セル幅
未定義もしくは "" 指定なし

$NO_COLOR

色付きターミナル出力を無効化する指定

$NO_COLOR 意味
未定義もしくは "" 指定なし
1文字以上指定 無効化する

$COLORFGBG

「文字色:背景色」という値が GNOME Terminal や Konsole など、一部の X11 端末で自動設定される場合がある。例:

  • COLORFGBG="15;0" - 前景=白 (15), 背景=黒 (0)
  • COLORFGBG="0;15" - 前景=黒 (0), 背景=白 (15)

ただし、普通に端末のデフォルトの前景色、背景色を使うだけならば、ESC[39mESC[49m を使えばよいので、前景色・背景色以外の色を決める時の参考情報くらいしか使えない。

端末種類を判別する環境変数

  • VSCODE_PID - 定義されていたら Visual Studio Code 内シェル
  • WT_SESSION とWT_PROFILE_ID - 定義されていたら Windows Terminal
  • TERM_PROGRAM - WezTerm なら WezTerm
  • TERMINAL_NAME - contour なら contour

5DB x Go言語 "database/sql" プレースホルダーまとめ

  • (2025-10-25更新) SQLite3, MySQL でのバインド変数の利用について追記
DB プレースホルダーor バインド変数
SQLite3 ? $VARNAME, :VARNAME, @VARNAME
SQL Server @VARNAME
Oracle :VARNAME
PostgreSQL $1 $2...
MySQL ? (@VARNAME)

プレースホルダー・バインド変数に与える値は ExecContext 関数の args という可変長パラメータに与えるが、作法に少し差異がある。

func (c *Conn) ExecContext(ctx context.Context, sql string, args ...any) (Result, error)

SQLite3

args として、プレースホルダ? を使った順番に値を与えればよい

SQL

UPDATE  emp
   SET  ENAME = ?
 WHERE  EMPNO = ?  AND  ENAME = ?  AND  JOB = ?  AND  MGR = ?  AND  date(HIREDATE) = date(?)  AND  SAL = ?  AND  COMM is NULL  AND  DEPTNO = ?

args

(1) "ahaha4" (2) 8002 (3) "ahaha3" (4) "ihihi2" (5) 9003 (6) time.Date(1972, time.May, 23, 0, 0, 0, 0, time.Location("")) (7) "" (8) 30

? 以外にも $VARNAME, :VARNAME, @VARNAME 形式も使用可能 (公式は $VARNAME を推奨)

SQL Server

値そのものを並べるのではなく、sql.Named を使ってバインド変数名と値をペアでならべてやらなければいけない

SQL

UPDATE  testtbl
   SET  DTTM = @v1
 WHERE  TESTNO = @v2  AND  DTTM = @v3  AND  DT = @v4  AND  TM = @v5  AND  SDTTM = @v6  AND  DTTM2 = @v7

args

(v1) time.Date(2024, time.May, 25, 13, 45, 34, 0, time.Location("")) (v2) 10 (v3) time.Date(2024, time.May, 25, 13, 45, 33, 0, time.Location("")) (v4) "2024-05-26 00:00:00 +00:00" (v5) "0001-01-01 14:53:26 +00:00" (v6) time.Date(2024, time.May, 25, 14, 53, 0, 0, time.Location("")) (v7) time.Date(2024, time.May, 25, 14, 52, 13, 133000000, time.Location(""))

values という []any 型配列に値が格納されている場合、次のように args を用意する。

args := make([]any,0,len(values))
for i, v := range values {
    args = append(args, sql.Named(fmt.Sprintf("v%d", i+1), v))
}

Oracle

バインド変数を使うが、sql.Named の使用は必須ではない。使った順で並べてもよい ( SQL-Bless では処理を共通化させるため、SQL Server と同様に sql.Named を使っている )

PosgreSQL

$1,$2.. に割り当てる値そのものを順番に args として与えればよい。

MySQL

プレースホルダーとして ?@VARNAME が使用できる。

ただし、Go言語用ドライバー"github.com/go-sql-driver/mysql"? 形式しかサポートしていない。使用すると mysql: driver does not support the use of Named Parameters という実行時エラーが発生する。

余談

SQL-Bless ではデータベースごとの差異は

https://github.com/hymkor/sqlbless/tree/master/dialect

以下で管理している。

以上

PowerShell で (Go言語のように) リダイレクトされてたら、標準出力を UTF8 化する

注意:PowerShell 7 でしか検証してません

$saveEncode = $null
if ([Console]::IsOutputRedirected) {
    $saveEncode = [System.Console]::OutputEncoding
    [System.Console]::OutputEncoding=[System.Text.Encoding]::UTF8
}

# ... 中略 ...

if ( $saveEncode -ne $null ){
    [System.Console]::OutputEncoding=$saveEncode
}

なお、タイトルでは「(Go言語のように)」と述べているが、正確にいうと Go言語では「リダイレクトされてたらそのまま出力、ターミナルだったら UTF16 化」なので、PowerShell のそれとはちょっと違う。

例:

Go言語で、コンパイラのバージョンによって import 先を切り替える

"log/slog" の出力をS式化するアドオン sxlog-go というパッケージを開発しています。

"log/slog" は Go 1.21 で実装された標準ライブラリですが、ちょうど 1.21 で Windows 7, 8, Server 2008 R2 へのサポートは切られてしまいました。古い WindowsMicrosoft もサポートをきっていますが、世の中にはまだこっそり使われてる場所もあると思われます。

ですが、幸いにして "log/slog" の前身の "golang.org/x/exp/slog" はまだ今も更新されているようです。ということで、Go コンパイラのバージョンを見て、適切な側へ import 先を切り変えるようにしてみました。

(1) go.mod のバージョンを 1.20 におとす

go.mod

--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,7 @@
 module github.com/hymkor/sxlog-go

-go 1.24.5
+go 1.20

(2)バージョンごとにソースを分ける

Go 1.21+ 向け

go121.go

//go:build go1.21

package sxlog

import (
    "log/slog"
)

Go 1.20 まで

go120.go

//go:build !go1.21

package sxlog

import (
    // This version is compatible with Go 1.20.
    // Only versions up to v0.0.0-20240904232852-e7e105dedf7e are supported.
    "golang.org/x/exp/slog"
)

(3)go get する

Go 1.20.15 は go1.20.15(.exe) という実行ファイル名で参照できるようになっているとすると、

> go1.20.14 get golang.org/x/exp/slog

でよいはずだが、最新版は既に「要 Go 1.24」になっており、次のようなエラーが発生する。

golang.org/x/exp/slog imports
        golang.org/x/exp/slices imports
        cmp: package cmp is not in GOROOT (C:\Users\hymko\sdk\go1.20.14\src\cmp)
note: imported by a module that requires go 1.24
golang.org/x/exp/slog imports
        golang.org/x/exp/slices imports
        slices: package slices is not in GOROOT (C:\Users\hymko\sdk\go1.20.14\src\slices)
note: imported by a module that requires go 1.24

そこで Go 1.20 をサポートする最後のバージョンを go get する。

> go1.20.14 get golang.org/x/exp/slog@v0.0.0-20240904232852-e7e105dedf7e

( …Go 1.20 という4世代前のバージョンを使うのは、いろいろと無理があることは否めない )

(4)型エイリアスで違いを吸収する

メインロジックのソースで import してしまうと、全ソースを 1.20 までと1.21 以降で複写しなくてはいけないので、型エイリアスを使ってメインロジックでは import せずに slog パッケージの型を参照できるようにする。

の双方で以下を記述

type slogLevel = slog.Level

type slogRecord = slog.Record

type slogAttr = slog.Attr

type slogHandler = slog.Handler

".../slog" を import していない メインロジック では、slog の型を直接使わず、これらの型 slogXXXXX を使うようにすればよい

(5)サンプルを2バージョン用意

以上

Windows 版 goawk は LF 改行を扱えない

Go言語でツールを書いていると、Windows でも LF 改行のテキストファイルを扱うことがめずらしくない。だが、goawk で加工しようとすると、LF が CRLF に変換されてしまう。これを行なわないモードを追加する修正を作成したので、プルリクエストを送ろうかと思った。

が、今の書き方だと修正行は少ないが、goawk の設計ポリシーとは合わない可能性が高い。改行コード変換のフラグをグローバルに持たせるのではなく、インタプリタインスタンスのフィールドで保持すべきかもしれない。だが、それは変更行が増えるので、これはこれで嫌がられるかもしれない。

というわけで、issue をたてて、お伺いをたててみたわけだが:

どうも、Windows の改行コードについては長年の懸案らしく、ずっと検討が続いており、簡単に結論を出せる状態ではないようだ。残念