[インデックス 14800] ファイルの概要
このコミットは、Go言語のツールチェインにおけるコマンドライン引数解析のメカニズムを刷新するものです。具体的には、リンカ(5l
, 6l
, 8l
)とコンパイラ(cc
, gc
)が、Go言語標準ライブラリのflag
パッケージと互換性のある新しいフラグパーサーを使用するように変更されます。これにより、従来のUnixスタイルのフラグ解析とは異なる挙動が導入され、コマンドライン引数の指定方法に一部変更が生じます。
コミット
commit cbbc6a102d5e7e5e2bb685a0c661d1f731c88344
Author: Russ Cox <rsc@golang.org>
Date: Sun Jan 6 15:24:47 2013 -0500
cmd/5l, cmd/6l, cmd/8l, cmd/cc, cmd/gc: new flag parsing
This CL adds a flag parser that matches the semantics of Go's
package flag. It also changes the linkers and compilers to use
the new flag parser.
Command lines that used to work, like
8c -FVw
6c -Dfoo
5g -I/foo/bar
now need to be split into separate arguments:
8c -F -V -w
6c -D foo
5g -I /foo/bar
The new spacing will work with both old and new tools.
The new parser also allows = for arguments, as in
6c -D=foo
5g -I=/foo/bar
but that syntax will not work with the old tools.
In addition to matching standard Go binary flag parsing,
the new flag parser generates more detailed usage messages
and opens the door to long flag names.
The recently added gc flag -= has been renamed -complete.
R=remyoudompheng, daniel.morsing, minux.ma, iant
CC=golang-dev
https://golang.org/cl/7035043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cbbc6a102d5e7e5e2bb685a0c661d1f731c88344
元コミット内容
このコミットは、Go言語のリンカ(5l
, 6l
, 8l
)とコンパイラ(cc
, gc
)におけるコマンドラインフラグの解析方法を、Go標準ライブラリのflag
パッケージのセマンティクスに合わせるように変更します。
主な変更点は以下の通りです。
- フラグと引数の分離: 従来のツールでは
-FVw
のように複数のフラグを連結したり、-Dfoo
や-I/foo/bar
のようにフラグと引数を連結して指定できましたが、新しいパーサーでは-F -V -w
や-D foo
、-I /foo/bar
のように、フラグと引数を個別の引数として分離して指定する必要があります。ただし、新しい形式は古いツールでも動作します。 =
による引数指定のサポート: 新しいパーサーでは-D=foo
や-I=/foo/bar
のように、フラグと引数を=
で連結して指定することが可能になります。この構文は古いツールでは動作しません。- 詳細な使用法メッセージ: 新しいフラグパーサーは、より詳細な使用法メッセージを生成します。
- ロングフラグ名の可能性: 将来的にロングフラグ名(例:
--verbose
)をサポートするための道を開きます。 gc
フラグの名称変更: 最近追加されたgc
フラグの-=
が-complete
に名称変更されました。
変更の背景
この変更の背景には、Goツールチェイン全体のコマンドラインインターフェースの一貫性を向上させるという目的があります。Go言語の標準ライブラリには、コマンドラインフラグを解析するためのflag
パッケージが提供されており、Goアプリケーション開発において広く利用されています。しかし、Goツールチェインのコンパイラやリンカは、伝統的なUnixスタイルのフラグ解析を採用しており、flag
パッケージのセマンティクスとは異なる挙動をしていました。
この不一致は、Goツールチェインを直接利用するスクリプトやビルドシステムにおいて、混乱や予期せぬ挙動を引き起こす可能性がありました。例えば、Goアプリケーションでは-flag=value
という形式が一般的であるにもかかわらず、ツールチェインでは-flag value
という形式が推奨される、といった状況です。
このコミットは、ツールチェインのフラグ解析をflag
パッケージのセマンティクスに合わせることで、以下のようなメリットをもたらします。
- 一貫性の向上: GoツールチェインとGoアプリケーションのフラグ解析挙動が統一され、開発者にとってより直感的になります。
- スクリプトの簡素化: ツールチェインのフラグ解析が予測可能になることで、ビルドスクリプトや自動化スクリプトの記述が容易になります。
- 機能拡張の容易性:
flag
パッケージのセマンティクスに準拠することで、より詳細な使用法メッセージの生成や、ロングフラグ名のサポートなど、将来的な機能拡張が容易になります。
特に、gc
フラグの -=
が -complete
に変更されたのは、この新しいフラグ解析の導入に伴い、より明確で分かりやすいフラグ名に統一するためと考えられます。
前提知識の解説
コマンドライン引数解析
コマンドライン引数解析とは、プログラムが起動される際に、ユーザーがコマンドラインで指定したオプションや引数を解釈し、プログラム内部で利用可能な形式に変換する処理のことです。
- フラグ (Flags/Options): プログラムの挙動を変更するための設定項目です。通常、ハイフン (
-
) またはダブルハイフン (--
) で始まります。例:-v
(verbose),--help
。 - 引数 (Arguments): プログラムが処理する対象となるデータです。例:
go run main.go
のmain.go
。
Unixスタイルのフラグ解析
伝統的なUnixシステムでは、コマンドライン引数解析に関していくつかの慣習があります。
- 単一ハイフンと単一文字フラグ:
-a
,-b
,-c
のように、単一のハイフンに続く単一の文字でフラグを表します。 - フラグの連結: 複数の単一文字フラグを連結して指定できます。例:
-abc
は-a -b -c
と同じ意味になります。 - 引数を伴うフラグ: フラグによっては、その後に値を伴うものがあります。例:
-o output.txt
。この場合、フラグと引数の間にスペースを入れるのが一般的ですが、一部のツールではスペースなしで連結することも許容されます(例:-ooutput.txt
)。 - ダブルハイフン:
--
は、それ以降の引数をすべてファイル名などの非フラグ引数として扱うことを示す慣習です。
Go言語のflag
パッケージ
Go言語の標準ライブラリflag
パッケージは、Goプログラム内でコマンドラインフラグを簡単に定義・解析するための機能を提供します。flag
パッケージの主な特徴は以下の通りです。
flag.StringVar
,flag.IntVar
など: 特定の型のフラグ変数を定義するための関数です。flag.Parse()
: コマンドライン引数を解析し、定義されたフラグ変数に値を設定します。flag.Args()
:flag.Parse()
の後に、フラグとして解析されなかった残りの非フラグ引数を取得します。=
による引数指定:flag
パッケージは、-flag=value
の形式をサポートします。これは、フラグと引数を明確に区別し、曖昧さを避けるのに役立ちます。- 詳細な使用法メッセージ:
flag
パッケージは、定義されたフラグに基づいて自動的に詳細な使用法メッセージを生成する機能を持っています。
このコミットは、Goツールチェインのフラグ解析を、このflag
パッケージのセマンティクスに近づけることを目指しています。
Goツールチェインのコンパイラとリンカ
Go言語のビルドプロセスは、複数のツールによって構成されています。
- コンパイラ (
gc
,5c
,6c
,8c
): Goソースコードをアセンブリコードにコンパイルします。gc
はGo言語のコンパイラであり、5c
,6c
,8c
はそれぞれPlan 9アーキテクチャ(ARM, AMD64, x86)向けのCコンパイラです。Goのランタイムや一部の標準ライブラリはC言語で書かれているため、これらのCコンパイラもGoツールチェインの一部として利用されます。 - リンカ (
ld
,5l
,6l
,8l
): コンパイルされたオブジェクトファイルやライブラリを結合し、実行可能ファイルを生成します。ld
はGo言語のリンカであり、5l
,6l
,8l
はそれぞれPlan 9アーキテクチャ向けのリンカです。
これらのツールは、それぞれ独自のコマンドライン引数を受け取り、ビルドプロセスを制御します。本コミットは、これらのツールのフラグ解析ロジックを変更するものです。
技術的詳細
このコミットの主要な技術的変更は、Goツールチェインのコンパイラとリンカが、新しいフラグ解析ライブラリ src/lib9/flag.c
を使用するように移行した点です。
src/lib9/flag.c
の導入
src/lib9/flag.c
は、Go言語のflag
パッケージのセマンティクスをC言語で実装した新しいライブラリです。このファイルは、以下の主要な関数を提供します。
flagcount(char *name, char *desc, int *p)
: カウンターフラグ(例:-v
のように複数回指定するとカウントが増えるフラグ)を定義します。flagint32(char *name, char *desc, int32 *p)
: 32ビット整数値を引数にとるフラグを定義します。flagint64(char *name, char *desc, int64 *p)
: 64ビット整数値を引数にとるフラグを定義します。flagstr(char *name, char *desc, char **p)
: 文字列を引数にとるフラグを定義します。flagfn0(char *name, char *desc, void (*fn)(void))
: 引数を取らない関数を呼び出すフラグを定義します。flagfn1(char *name, char *desc, void (*fn)(char*))
: 文字列引数を取る関数を呼び出すフラグを定義します。flagfn2(char *name, char *desc, void (*fn)(char*, char*))
: 2つの文字列引数を取る関数を呼び出すフラグを定義します。flagparse(int *argcp, char ***argvp, void (*usage)(void))
: コマンドライン引数を解析します。flagprint(int fd)
: 定義されたフラグの使用法メッセージを標準出力または指定されたファイルディスクリプタに出力します。
このライブラリは、内部でハッシュテーブル (fhash
) を使用してフラグを管理し、FNV-1ハッシュ関数を用いて効率的なルックアップを実現しています。また、フラグの定義時に説明 (desc
) を指定することで、flagprint
関数が詳細な使用法メッセージを生成できるようになっています。
既存ツールの変更
各リンカ (5l
, 6l
, 8l
) とコンパイラ (cc
, gc
) の obj.c
または lex.c
ファイルにおいて、従来の ARGBEGIN
/ARGEND
マクロを使用した手動の引数解析ロジックが削除され、代わりに flagcount
, flagstr
, flagfn0
などの新しい flag
ライブラリの関数群を用いてフラグが定義されています。そして、main
関数内で flagparse(&argc, &argv, usage)
が呼び出され、引数解析が新しいライブラリに委譲されています。
これにより、以下のような挙動の変化がもたらされます。
- フラグと引数の分離の強制:
flag
パッケージのセマンティクスに従い、-Dfoo
のような連結形式は認識されず、-D foo
のようにスペースで区切るか、-D=foo
のように=
で連結する必要があります。 =
による引数指定のサポート: 新しいパーサーは-flag=value
形式をネイティブにサポートします。- 詳細な使用法メッセージ: 各ツールは、
flagprint
関数を呼び出すことで、自動生成された詳細な使用法メッセージを表示できるようになります。 debug
配列の型変更: 従来のchar debug[128]
やchar debug[256]
のようなchar
配列でデバッグフラグの状態を管理していた箇所が、int debug[128]
やint debug[256]
のようにint
配列に変更されています。これは、flagcount
関数がint*
を引数として受け取るためです。
src/cmd/go/build.go
の変更
src/cmd/go/build.go
は、go
コマンドのビルドロジックを管理するファイルです。このファイルでは、コンパイラやリンカを呼び出す際の引数生成ロジックが変更されています。
-=
から-complete
への変更:gcToolchain.gc
関数内で、Goパッケージが完全にGo言語で書かれておりCやアセンブリを含まない場合に渡されていた-=
フラグが-complete
に変更されています。これは、gc
コンパイラのフラグ名称変更に対応したものです。-D
フラグの引数分離:gcToolchain.asm
およびgcToolchain.cc
関数内で、-DGOOS_
や-DGOARCH_
のような定義フラグの指定方法が、従来の-DGOOS_
から-D
,GOOS_
のようにフラグと引数を分離する形式に変更されています。これは、新しいフラグ解析のセマンティクスに合わせたものです。同様の変更がgccgcToolchain
のasm
とcc
関数にも適用されています。
これらの変更により、go
コマンドが内部でコンパイラやリンカを呼び出す際にも、新しいフラグ解析ルールに準拠した引数が渡されるようになります。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルに集中しています。
src/lib9/flag.c
: 新しいフラグ解析ライブラリの追加。これが新しいフラグ解析の基盤となります。src/cmd/*/obj.c
(リンカ):src/cmd/5l/obj.c
src/cmd/6l/obj.c
src/cmd/8l/obj.c
これらのファイルでは、従来のARGBEGIN
/ARGEND
マクロを使用した手動の引数解析ロジックが削除され、flagcount
,flagstr
,flagint32
,flagint64
,flagfn0
,flagfn1
,flagfn2
などの新しいflag
ライブラリの関数群を用いたフラグ定義と、flagparse
関数による解析に置き換えられています。また、usage
関数もflagprint
を利用するように変更されています。
src/cmd/cc/lex.c
(Cコンパイラ):- 従来の引数解析ロジックが削除され、
flag
ライブラリの関数群を用いたフラグ定義とflagparse
関数による解析に置き換えられています。 dodef
関数(-D
フラグの処理)やdospim
関数(-l
フラグの処理)が、新しいフラグ解析フレームワークに統合されています。
- 従来の引数解析ロジックが削除され、
src/cmd/gc/lex.c
(Goコンパイラ):- 従来の引数解析ロジックが削除され、
flag
ライブラリの関数群を用いたフラグ定義とflagparse
関数による解析に置き換えられています。 doversion
関数(-V
フラグの処理)が追加され、flagfn0
を介して呼び出されるようになっています。debug
配列の型がchar
からint
に変更されています。
- 従来の引数解析ロジックが削除され、
src/cmd/go/build.go
:gcToolchain.gc
関数で、-=
フラグが-complete
に変更されています。gcToolchain.asm
,gcToolchain.cc
,gccgcToolchain.asm
,gccgcToolchain.cc
関数で、-D
フラグの引数指定方法が新しいフラグ解析のセマンティクスに合わせて変更されています。
include/libc.h
: 新しいflag
ライブラリの関数群のプロトタイプ宣言が追加されています。src/cmd/*/l.h
(リンカのヘッダ):src/cmd/5l/l.h
src/cmd/6l/l.h
src/cmd/8l/l.h
これらのファイルでは、debug
配列の型がchar
からint
に変更されています。また、6l/l.h
ではINITTEXT
とINITDAT
の型がvlong
からint64
に変更されています。
src/cmd/cc/cc.h
およびsrc/cmd/gc/go.h
:debug
配列の型がchar
からint
に変更されています。doc/go1.1.html
: Go 1.1のリリースノートに、コマンドラインフラグ解析の変更に関する説明が追加されています。
これらの変更は、Goツールチェインのコマンドライン引数処理の根本的な変更を反映しており、新しいflag
ライブラリへの移行が広範囲にわたって行われたことを示しています。
コアとなるコードの解説
ここでは、主要な変更箇所である src/lib9/flag.c
と、リンカ/コンパイラの main
関数におけるフラグ解析の変更に焦点を当てて解説します。
src/lib9/flag.c
の解説
このファイルは、Go言語のflag
パッケージの機能をC言語で再現したものです。
-
Flag
構造体:typedef struct Flag Flag; struct Flag { char *name; int namelen; char *desc; int iscount; void (*set)(char*, void*); void (*set2)(char*, char*, void*); void *arg; Flag *next; Flag *allnext; };
Flag
構造体は、個々のコマンドラインフラグの情報を保持します。name
: フラグ名(例: "o" for -o)namelen
: フラグ名の長さdesc
: フラグの説明(使用法メッセージ用)iscount
: カウンターフラグかどうか(例:-v
のように複数回指定でカウントが増える)set
: フラグの値を設定するための関数ポインタ(引数が1つの場合)set2
: フラグの値を設定するための関数ポインタ(引数が2つの場合、例:-X name value
)arg
:set
またはset2
関数に渡される引数(通常はフラグの値が格納される変数のポインタ)next
: ハッシュテーブル内の次のフラグへのポインタ(衝突解決用)allnext
: 全てのフラグを連結したリストの次のフラグへのポインタ(flagprint
で使用)
-
fhash
とfirst
,last
:static Flag *fhash[512];
は、フラグを効率的に検索するためのハッシュテーブルです。static Flag *first, *last;
は、定義された全てのフラグを順序付けて保持するためのリンクリストの先頭と末尾のポインタです。これはflagprint
関数がフラグを定義順に表示するために使用されます。 -
fnv
関数:static uint32 fnv(char *p, int n) { uint32 h; h = 2166136261U; while(n-- > 0) h = (h*16777619) ^ (uchar)*p++; return h; }
FNV-1ハッシュアルゴリズムを実装しており、フラグ名をハッシュ値に変換してハッシュテーブルのインデックスを計算します。
-
lookflag
関数: フラグ名に基づいてFlag
構造体を検索または作成します。既に存在するフラグを再定義しようとするとsysfatal
でエラーを発生させます。 -
flagcount
,flagint32
,flagint64
,flagstr
,flagfn0
,flagfn1
,flagfn2
: これらの関数は、特定の型のフラグを定義するためのAPIです。それぞれlookflag
を呼び出してFlag
構造体を作成し、desc
、iscount
、set
(またはset2
)、arg
フィールドを設定します。set
関数には、引数を解析して対応する変数に格納するロジックが実装されています。例えば、flagstr
はstring
関数をset
に設定し、string
関数は引数文字列をchar**
型のarg
に格納します。 -
flagparse
関数:void flagparse(int *argcp, char ***argvp, void (*usage)(void)) { // ... (引数処理ロジック) ... while(argc > 0) { p = *argv; // ... (フラグの識別、=による引数分離など) ... f = lookflag(name, namelen, 0); // フラグを検索 if(f == nil) { // ... (不明なフラグの場合のエラー処理) ... } curflag = f; if(!f->iscount && q == nil) { // カウンターフラグでなく、=がない場合 // ... (次の引数を値として取得) ... } if(f->set2 != nil) { // 引数が2つのフラグの場合 // ... (2つ目の引数を取得し、set2を呼び出す) ... } f->set(q, f->arg); // フラグの値を設定 } // ... (残りの引数を更新) ... }
この関数が実際のコマンドライン引数解析を実行します。
argc
とargv
を受け取り、ハイフンで始まる引数をフラグとして解釈します。=
で引数が指定されている場合や、次の引数がフラグの値である場合を適切に処理し、対応するFlag
構造体のset
またはset2
関数を呼び出して値を設定します。不明なフラグや引数不足の場合にはエラーを発生させます。 -
flagprint
関数:first
ポインタから始まるリンクリストを辿り、各フラグのname
とdesc
を整形して使用法メッセージとして出力します。desc
に ": " が含まれる場合は、フラグ名と説明を異なる行に表示するなどの整形を行います。
リンカ/コンパイラの main
関数の変更例 (src/cmd/5l/obj.c
の main
関数)
変更前は、ARGBEGIN
/ARGEND
マクロを使用して手動で引数を解析していました。
void
main(int argc, char *argv[])
{
int c;
char *p, *name, *val;
// ... 初期化 ...
ARGBEGIN {
default:
c = ARGC();
if(c == 'l')
usage();
if(c >= 0 && c < sizeof(debug))
debug[c]++;
break;
case 'o':
outfile = EARGF(usage());
break;
// ... 他のフラグ処理 ...
} ARGEND
// ... 後続処理 ...
}
変更後は、flag
ライブラリの関数群を用いてフラグを定義し、flagparse
を呼び出す形に変わっています。
void
main(int argc, char *argv[])
{
char *p;
// ... 初期化 ...
flagcount("1", "use alternate profiling code", &debug['1']);
flagfn1("B", "info: define ELF NT_GNU_BUILD_ID note", addbuildinfo);
flagstr("E", "sym: entry symbol", &INITENTRY);
flagint32("D", "addr: data address", &INITDAT);
// ... 他のフラグ定義 ...
flagparse(&argc, &argv, usage); // ここで引数解析が実行される
if(argc != 1) // 残りの引数が1つ(入力ファイル)であることを確認
usage();
// ... 後続処理 ...
}
この変更により、各ツールの main
関数はフラグ解析の具体的なロジックから解放され、フラグの定義と flagparse
の呼び出しに集中できるようになりました。これにより、コードの可読性と保守性が向上し、将来的なフラグの追加や変更が容易になります。また、flag
ライブラリが提供する統一された解析セマンティクスと使用法メッセージ生成機能の恩恵を受けることができます。
関連リンク
- Go言語
flag
パッケージのドキュメント: https://pkg.go.dev/flag - Go言語のビルドプロセスに関する公式ドキュメント(該当するバージョンがあれば)
- Plan 9 from Bell Labs: https://9p.io/plan9/ (GoツールチェインのルーツであるPlan 9のCコンパイラ/リンカに関する情報)
参考にした情報源リンク
- Go言語の公式ドキュメント
flag
パッケージのソースコード- Unixコマンドライン引数の慣習に関する一般的な情報源
- FNVハッシュアルゴリズムに関する情報源 (例: http://isthe.com/chongo/tech/comp/fnv/)
- Go言語のツールチェインの歴史と進化に関する記事や議論(特にRuss Cox氏のブログやGo開発メーリングリストのアーカイブ)
[インデックス 14800] ファイルの概要
このコミットは、Go言語のツールチェインにおけるコマンドライン引数解析のメカニズムを刷新するものです。具体的には、リンカ(5l
, 6l
, 8l
)とコンパイラ(cc
, gc
)が、Go言語標準ライブラリのflag
パッケージと互換性のある新しいフラグパーサーを使用するように変更されます。これにより、従来のUnixスタイルのフラグ解析とは異なる挙動が導入され、コマンドライン引数の指定方法に一部変更が生じます。
コミット
commit cbbc6a102d5e7e5e2bb685a0c661d1f731c88344
Author: Russ Cox <rsc@golang.org>
Date: Sun Jan 6 15:24:47 2013 -0500
cmd/5l, cmd/6l, cmd/8l, cmd/cc, cmd/gc: new flag parsing
This CL adds a flag parser that matches the semantics of Go's
package flag. It also changes the linkers and compilers to use
the new flag parser.
Command lines that used to work, like
8c -FVw
6c -Dfoo
5g -I/foo/bar
now need to be split into separate arguments:
8c -F -V -w
6c -D foo
5g -I /foo/bar
The new spacing will work with both old and new tools.
The new parser also allows = for arguments, as in
6c -D=foo
5g -I=/foo/bar
but that syntax will not work with the old tools.
In addition to matching standard Go binary flag parsing,
the new flag parser generates more detailed usage messages
and opens the door to long flag names.
The recently added gc flag -= has been renamed -complete.
R=remyoudompheng, daniel.morsing, minux.ma, iant
CC=golang-dev
https://golang.org/cl/7035043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cbbc6a102d5e7e5e2bb685a0c661d1f731c88344
元コミット内容
このコミットは、Go言語のリンカ(5l
, 6l
, 8l
)とコンパイラ(cc
, gc
)におけるコマンドラインフラグの解析方法を、Go標準ライブラリのflag
パッケージのセマンティクスに合わせるように変更します。
主な変更点は以下の通りです。
- フラグと引数の分離: 従来のツールでは
-FVw
のように複数のフラグを連結したり、-Dfoo
や-I/foo/bar
のようにフラグと引数を連結して指定できましたが、新しいパーサーでは-F -V -w
や-D foo
、-I /foo/bar
のように、フラグと引数を個別の引数として分離して指定する必要があります。ただし、新しい形式は古いツールでも動作します。 =
による引数指定のサポート: 新しいパーサーでは-D=foo
や-I=/foo/bar
のように、フラグと引数を=
で連結して指定することが可能になります。この構文は古いツールでは動作しません。- 詳細な使用法メッセージ: 新しいフラグパーサーは、より詳細な使用法メッセージを生成します。
- ロングフラグ名の可能性: 将来的にロングフラグ名(例:
--verbose
)をサポートするための道を開きます。 gc
フラグの名称変更: 最近追加されたgc
フラグの-=
が-complete
に名称変更されました。
変更の背景
この変更の背景には、Goツールチェイン全体のコマンドラインインターフェースの一貫性を向上させるという目的があります。Go言語の標準ライブラリには、コマンドラインフラグを解析するためのflag
パッケージが提供されており、Goアプリケーション開発において広く利用されています。しかし、Goツールチェインのコンパイラやリンカは、伝統的なUnixスタイルのフラグ解析を採用しており、flag
パッケージのセマンティクスとは異なる挙動をしていました。
この不一致は、Goツールチェインを直接利用するスクリプトやビルドシステムにおいて、混乱や予期せぬ挙動を引き起こす可能性がありました。例えば、Goアプリケーションでは-flag=value
という形式が一般的であるにもかかわらず、ツールチェインでは-flag value
という形式が推奨される、といった状況です。
このコミットは、ツールチェインのフラグ解析をflag
パッケージのセマンティクスに合わせることで、以下のようなメリットをもたらします。
- 一貫性の向上: GoツールチェインとGoアプリケーションのフラグ解析挙動が統一され、開発者にとってより直感的になります。
- スクリプトの簡素化: ツールチェインのフラグ解析が予測可能になることで、ビルドスクリプトや自動化スクリプトの記述が容易になります。
- 機能拡張の容易性:
flag
パッケージのセマンティクスに準拠することで、より詳細な使用法メッセージの生成や、ロングフラグ名のサポートなど、将来的な機能拡張が容易になります。
特に、gc
フラグの -=
が -complete
に変更されたのは、この新しいフラグ解析の導入に伴い、より明確で分かりやすいフラグ名に統一するためと考えられます。
前提知識の解説
コマンドライン引数解析
コマンドライン引数解析とは、プログラムが起動される際に、ユーザーがコマンドラインで指定したオプションや引数を解釈し、プログラム内部で利用可能な形式に変換する処理のことです。
- フラグ (Flags/Options): プログラムの挙動を変更するための設定項目です。通常、ハイフン (
-
) またはダブルハイフン (--
) で始まります。例:-v
(verbose),--help
。 - 引数 (Arguments): プログラムが処理する対象となるデータです。例:
go run main.go
のmain.go
。
Unixスタイルのフラグ解析
伝統的なUnixシステムでは、コマンドライン引数解析に関していくつかの慣習があります。
- ショートオプション:
-a
,-b
,-c
のように、単一のハイフンに続く単一の文字でオプションを表します。 - オプションのグループ化: 複数のショートオプションが引数を取らない場合、
-abc
のように単一のハイフンに続けてグループ化できます。これは-a -b -c
と同等です。 - 引数を伴うオプション: オプションによっては、その後に値を伴うものがあります。例:
-o output.txt
。この場合、オプションと引数の間にスペースを入れるのが一般的ですが、一部のツールではスペースなしで連結することも許容されます(例:-ooutput.txt
)。 - オプションの終了マーカー (
--
): ダブルハイフン (--
) は、それ以降の引数をすべて非オプション引数(例: ファイル名)として扱うことを示す慣習です。これにより、ハイフンで始まるファイル名などをオプションとして誤って解釈されることを防ぎます。
Go言語のflag
パッケージ
Go言語の標準ライブラリflag
パッケージは、Goプログラム内でコマンドラインフラグを簡単に定義・解析するための機能を提供します。flag
パッケージの主な特徴は以下の通りです。
- フラグの定義:
flag.String()
,flag.Bool()
,flag.Int()
などの関数を使用して、フラグの名前、デフォルト値、使用法文字列(説明)を指定してフラグを定義します。これらの関数は、フラグの値が格納される変数へのポインタを返します。既存の変数にフラグをバインドするには、flag.StringVar()
などのVar
関数を使用します。 - フラグの解析: 全てのフラグを定義した後、
flag.Parse()
を呼び出す必要があります。この関数はコマンドライン引数を読み込み、フラグの値を抽出し、定義された変数に格納します。 - フラグ値へのアクセス:
flag.String()
などで定義されたフラグの値にアクセスするには、返されたポインタを逆参照します(例:*wordPtr
)。flag.StringVar()
を使用した場合、値は直接変数に格納されます。 - デフォルト値: コマンドラインでフラグが指定されない場合、定義時に指定されたデフォルト値が自動的に使用されます。
- コマンドライン構文:
flag
パッケージは、-flag=x
や-flag x
(非ブール型フラグのみ) の形式をサポートします。ブール型フラグをfalse
に設定するには、-flag=false
の形式を使用する必要があります。 - 位置引数:
flag.Parse()
の後、フラグとして解析されなかった引数は位置引数として利用できます。これらはflag.Args()
でスライスとして、またはflag.Arg(i)
で個別にアクセスできます。フラグの解析は、最初の非フラグ引数の直前、または--
ターミネータの後に停止します。 - カスタムフラグ型: カスタム型やバリデーションのために、
flag.Value
インターフェース(String()
とSet(string)
メソッドが必要)を実装し、flag.Var()
を使用してカスタムフラグを登録できます。 - サブコマンド:
flag.NewFlagSet()
を使用して、独自のフラグセットを持つサブコマンドを作成することもサポートしています。 - ヘルプメッセージ: フラグ定義時に提供される使用法文字列は、
-h
または--help
でプログラムを実行したときに表示されるヘルプテキストの生成に使用されます。 - エラーハンドリング: 未定義のフラグが提供された場合、プログラムはエラーメッセージを出力し、ヘルプテキストを表示します。
このコミットは、Goツールチェインのフラグ解析を、このflag
パッケージのセマンティクスに近づけることを目指しています。
Goツールチェインのコンパイラとリンカ
Go言語のビルドプロセスは、複数のツールによって構成されています。
- コンパイラ (
go tool compile
): Goはコンパイル型言語であり、Goソースコードは実行前にバイナリ形式に変換される必要があります。go build
コマンドは内部的にGoコンパイラを呼び出します。コンパイラのプロセスには、字句解析、構文解析(ASTの構築)、コード生成が含まれます。 - リンカ (
go tool link
): リンカの役割は、コンパイラによって生成されたオブジェクトファイルと、Goランタイムや標準ライブラリを含む必要な全ての依存関係を結合し、単一の実行可能バイナリを作成することです。Goリンカは、シンボルテーブルのようなランタイムデータ構造の構築や、カスタムオブジェクトファイル形式の使用など、Go独自の要件に合わせて設計されています。また、実行可能ファイル内のシンボルの最終的なメモリアドレスを計算する再配置も処理します。 go
コマンド: このコマンドは、Goツールチェインと対話するための主要なインターフェースとして機能します。コンパイルからリンクまでのビルドプロセス全体を調整し、go run
、go install
、go test
、go fmt
などの様々なサブコマンドを提供します。Go 1.21以降では、go
コマンドは異なるGoツールチェインのバージョンも管理し、go.mod
やgo.work
ファイル、またはGOTOOLCHAIN
環境変数で定義されたプロジェクト固有の要件に基づいて柔軟性を提供します。
このコミットで言及されている 5l
, 6l
, 8l
(リンカ) および 5c
, 6c
, 8c
(Cコンパイラ) は、Goツールチェインの初期段階でPlan 9アーキテクチャ(ARM, AMD64, x86)向けに存在していたツールであり、Goのランタイムや一部の標準ライブラリがC言語で書かれていたため、Goツールチェインの一部として利用されていました。本コミットは、これらのツールのフラグ解析ロジックを変更するものです。
技術的詳細
このコミットの主要な技術的変更は、Goツールチェインのコンパイラとリンカが、新しいフラグ解析ライブラリ src/lib9/flag.c
を使用するように移行した点です。
src/lib9/flag.c
の導入
src/lib9/flag.c
は、Go言語のflag
パッケージのセマンティクスをC言語で実装した新しいライブラリです。このファイルは、以下の主要な関数を提供します。
flagcount(char *name, char *desc, int *p)
: カウンターフラグ(例:-v
のように複数回指定するとカウントが増えるフラグ)を定義します。flagint32(char *name, char *desc, int32 *p)
: 32ビット整数値を引数にとるフラグを定義します。flagint64(char *name, char *desc, int64 *p)
: 64ビット整数値を引数にとるフラグを定義します。flagstr(char *name, char *desc, char **p)
: 文字列を引数にとるフラグを定義します。flagfn0(char *name, char *desc, void (*fn)(void))
: 引数を取らない関数を呼び出すフラグを定義します。flagfn1(char *name, char *desc, void (*fn)(char*))
: 文字列引数を取る関数を呼び出すフラグを定義します。flagfn2(char *name, char *desc, void (*fn)(char*, char*))
: 2つの文字列引数を取る関数を呼び出すフラグを定義します。flagparse(int *argcp, char ***argvp, void (*usage)(void))
: コマンドライン引数を解析します。flagprint(int fd)
: 定義されたフラグの使用法メッセージを標準出力または指定されたファイルディスクリプタに出力します。
このライブラリは、内部でハッシュテーブル (fhash
) を使用してフラグを管理し、FNV-1ハッシュ関数を用いて効率的なルックアップを実現しています。また、フラグの定義時に説明 (desc
) を指定することで、flagprint
関数が詳細な使用法メッセージを生成できるようになっています。
既存ツールの変更
各リンカ (5l
, 6l
, 8l
) とコンパイラ (cc
, gc
) の obj.c
または lex.c
ファイルにおいて、従来の ARGBEGIN
/ARGEND
マクロを使用した手動の引数解析ロジックが削除され、代わりに flagcount
, flagstr
, flagfn0
などの新しい flag
ライブラリの関数群を用いてフラグが定義されています。そして、main
関数内で flagparse(&argc, &argv, usage)
が呼び出され、引数解析が新しいライブラリに委譲されています。
これにより、以下のような挙動の変化がもたらされます。
- フラグと引数の分離の強制:
flag
パッケージのセマンティクスに従い、-Dfoo
のような連結形式は認識されず、-D foo
のようにスペースで区切るか、-D=foo
のように=
で連結する必要があります。 =
による引数指定のサポート: 新しいパーサーは-flag=value
形式をネイティブにサポートします。- 詳細な使用法メッセージ: 各ツールは、
flagprint
関数を呼び出すことで、自動生成された詳細な使用法メッセージを表示できるようになります。 debug
配列の型変更: 従来のchar debug[128]
やchar debug[256]
のようなchar
配列でデバッグフラグの状態を管理していた箇所が、int debug[128]
やint debug[256]
のようにint
配列に変更されています。これは、flagcount
関数がint*
を引数として受け取るためです。
src/cmd/go/build.go
の変更
src/cmd/go/build.go
は、go
コマンドのビルドロジックを管理するファイルです。このファイルでは、コンパイラやリンカを呼び出す際の引数生成ロジックが変更されています。
-=
から-complete
への変更:gcToolchain.gc
関数内で、Goパッケージが完全にGo言語で書かれておりCやアセンブリを含まない場合に渡されていた-=
フラグが-complete
に変更されています。これは、gc
コンパイラのフラグ名称変更に対応したものです。-D
フラグの引数分離:gcToolchain.asm
およびgcToolchain.cc
関数内で、-DGOOS_
や-DGOARCH_
のような定義フラグの指定方法が、従来の-DGOOS_
から-D
,GOOS_
のようにフラグと引数を分離する形式に変更されています。これは、新しいフラグ解析のセマンティクスに合わせたものです。同様の変更がgccgcToolchain
のasm
とcc
関数にも適用されています。
これらの変更により、go
コマンドが内部でコンパイラやリンカを呼び出す際にも、新しいフラグ解析ルールに準拠した引数が渡されるようになります。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルに集中しています。
src/lib9/flag.c
: 新しいフラグ解析ライブラリの追加。これが新しいフラグ解析の基盤となります。src/cmd/*/obj.c
(リンカ):src/cmd/5l/obj.c
src/cmd/6l/obj.c
src/cmd/8l/obj.c
これらのファイルでは、従来のARGBEGIN
/ARGEND
マクロを使用した手動の引数解析ロジックが削除され、flagcount
,flagstr
,flagint32
,flagint64
,flagfn0
,flagfn1
,flagfn2
などの新しいflag
ライブラリの関数群を用いたフラグ定義と、flagparse
関数による解析に置き換えられています。また、usage
関数もflagprint
を利用するように変更されています。
src/cmd/cc/lex.c
(Cコンパイラ):- 従来の引数解析ロジックが削除され、
flag
ライブラリの関数群を用いたフラグ定義とflagparse
関数による解析に置き換えられています。 dodef
関数(-D
フラグの処理)やdospim
関数(-l
フラグの処理)が、新しいフラグ解析フレームワークに統合されています。
- 従来の引数解析ロジックが削除され、
src/cmd/gc/lex.c
(Goコンパイラ):- 従来の引数解析ロジックが削除され、
flag
ライブラリの関数群を用いたフラグ定義とflagparse
関数による解析に置き換えられています。 doversion
関数(-V
フラグの処理)が追加され、flagfn0
を介して呼び出されるようになっています。debug
配列の型がchar
からint
に変更されています。
- 従来の引数解析ロジックが削除され、
src/cmd/go/build.go
:gcToolchain.gc
関数で、-=
フラグが-complete
に変更されています。gcToolchain.asm
,gcToolchain.cc
,gccgcToolchain.asm
,gccgcToolchain.cc
関数で、-D
フラグの引数指定方法が新しいフラグ解析のセマンティクスに合わせて変更されています。
include/libc.h
: 新しいflag
ライブラリの関数群のプロトタイプ宣言が追加されています。src/cmd/*/l.h
(リンカのヘッダ):src/cmd/5l/l.h
src/cmd/6l/l.h
src/cmd/8l/l.h
これらのファイルでは、debug
配列の型がchar
からint
に変更されています。また、6l/l.h
ではINITTEXT
とINITDAT
の型がvlong
からint64
に変更されています。
src/cmd/cc/cc.h
およびsrc/cmd/gc/go.h
:debug
配列の型がchar
からint
に変更されています。doc/go1.1.html
: Go 1.1のリリースノートに、コマンドラインフラグ解析の変更に関する説明が追加されています。
これらの変更は、Goツールチェインのコマンドライン引数処理の根本的な変更を反映しており、新しいflag
ライブラリへの移行が広範囲にわたって行われたことを示しています。
コアとなるコードの解説
ここでは、主要な変更箇所である src/lib9/flag.c
と、リンカ/コンパイラの main
関数におけるフラグ解析の変更に焦点を当てて解説します。
src/lib9/flag.c
の解説
このファイルは、Go言語のflag
パッケージの機能をC言語で再現したものです。
-
Flag
構造体:typedef struct Flag Flag; struct Flag { char *name; int namelen; char *desc; int iscount; void (*set)(char*, void*); void (*set2)(char*, char*, void*); void *arg; Flag *next; Flag *allnext; };
Flag
構造体は、個々のコマンドラインフラグの情報を保持します。name
: フラグ名(例: "o" for -o)namelen
: フラグ名の長さdesc
: フラグの説明(使用法メッセージ用)iscount
: カウンターフラグかどうか(例:-v
のように複数回指定でカウントが増える)set
: フラグの値を設定するための関数ポインタ(引数が1つの場合)set2
: フラグの値を設定するための関数ポインタ(引数が2つの場合、例:-X name value
)arg
:set
またはset2
関数に渡される引数(通常はフラグの値が格納される変数のポインタ)next
: ハッシュテーブル内の次のフラグへのポインタ(衝突解決用)allnext
: 全てのフラグを連結したリストの次のフラグへのポインタ(flagprint
で使用)
-
fhash
とfirst
,last
:static Flag *fhash[512];
は、フラグを効率的に検索するためのハッシュテーブルです。static Flag *first, *last;
は、定義された全てのフラグを順序付けて保持するためのリンクリストの先頭と末尾のポインタです。これはflagprint
関数がフラグを定義順に表示するために使用されます。 -
fnv
関数:static uint32 fnv(char *p, int n) { uint32 h; h = 2166136261U; while(n-- > 0) h = (h*16777619) ^ (uchar)*p++; return h; }
FNV-1ハッシュアルゴリズムを実装しており、フラグ名をハッシュ値に変換してハッシュテーブルのインデックスを計算します。
-
lookflag
関数: フラグ名に基づいてFlag
構造体を検索または作成します。既に存在するフラグを再定義しようとするとsysfatal
でエラーを発生させます。 -
flagcount
,flagint32
,flagint64
,flagstr
,flagfn0
,flagfn1
,flagfn2
: これらの関数は、特定の型のフラグを定義するためのAPIです。それぞれlookflag
を呼び出してFlag
構造体を作成し、desc
、iscount
、set
(またはset2
)、arg
フィールドを設定します。set
関数には、引数を解析して対応する変数に格納するロジックが実装されています。例えば、flagstr
はstring
関数をset
に設定し、string
関数は引数文字列をchar**
型のarg
に格納します。 -
flagparse
関数:void flagparse(int *argcp, char ***argvp, void (*usage)(void)) { // ... (引数処理ロジック) ... while(argc > 0) { p = *argv; // ... (フラグの識別、=による引数分離など) ... f = lookflag(name, namelen, 0); // フラグを検索 if(f == nil) { // ... (不明なフラグの場合のエラー処理) ... } curflag = f; if(!f->iscount && q == nil) { // カウンターフラグでなく、=がない場合 // ... (次の引数を値として取得) ... } if(f->set2 != nil) { // 引数が2つのフラグの場合 // ... (2つ目の引数を取得し、set2を呼び出す) ... } f->set(q, f->arg); // フラグの値を設定 } // ... (残りの引数を更新) ... }
この関数が実際のコマンドライン引数解析を実行します。
argc
とargv
を受け取り、ハイフンで始まる引数をフラグとして解釈します。=
で引数が指定されている場合や、次の引数がフラグの値である場合を適切に処理し、対応するFlag
構造体のset
またはset2
関数を呼び出して値を設定します。不明なフラグや引数不足の場合にはエラーを発生させます。 -
flagprint
関数:first
ポインタから始まるリンクリストを辿り、各フラグのname
とdesc
を整形して使用法メッセージとして出力します。desc
に ": " が含まれる場合は、フラグ名と説明を異なる行に表示するなどの整形を行います。
リンカ/コンパイラの main
関数の変更例 (src/cmd/5l/obj.c
の main
関数)
変更前は、ARGBEGIN
/ARGEND
マクロを使用して手動で引数を解析していました。
void
main(int argc, char *argv[])
{
int c;
char *p, *name, *val;
// ... 初期化 ...
ARGBEGIN {
default:
c = ARGC();
if(c == 'l')
usage();
if(c >= 0 && c < sizeof(debug))
debug[c]++;
break;
case 'o':
outfile = EARGF(usage());
break;
// ... 他のフラグ処理 ...
} ARGEND
// ... 後続処理 ...
}
変更後は、flag
ライブラリの関数群を用いてフラグを定義し、flagparse
を呼び出す形に変わっています。
void
main(int argc, char *argv[])
{
char *p;
// ... 初期化 ...
flagcount("1", "use alternate profiling code", &debug['1']);
flagfn1("B", "info: define ELF NT_GNU_BUILD_ID note", addbuildinfo);
flagstr("E", "sym: entry symbol", &INITENTRY);
flagint32("D", "addr: data address", &INITDAT);
// ... 他のフラグ定義 ...
flagparse(&argc, &argv, usage); // ここで引数解析が実行される
if(argc != 1) // 残りの引数が1つ(入力ファイル)であることを確認
usage();
// ... 後続処理 ...
}
この変更により、各ツールの main
関数はフラグ解析の具体的なロジックから解放され、フラグの定義と flagparse
の呼び出しに集中できるようになりました。これにより、コードの可読性と保守性が向上し、将来的なフラグの追加や変更が容易になります。また、flag
ライブラリが提供する統一された解析セマンティクスと使用法メッセージ生成機能の恩恵を受けることができます。
関連リンク
- Go言語
flag
パッケージのドキュメント: https://pkg.go.dev/flag - Go言語のビルドプロセスに関する公式ドキュメント(該当するバージョンがあれば)
- Plan 9 from Bell Labs: https://9p.io/plan9/ (GoツールチェインのルーツであるPlan 9のCコンパイラ/リンカに関する情報)
参考にした情報源リンク
- Go言語の公式ドキュメント
flag
パッケージのソースコード- Unixコマンドライン引数の慣習に関する一般的な情報源
- FNVハッシュアルゴリズムに関する情報源 (例: http://isthe.com/chongo/tech/comp/fnv/)
- Go言語のツールチェインの歴史と進化に関する記事や議論(特にRuss Cox氏のブログやGo開発メーリングリストのアーカイブ)