Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.gomain.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_ のようにフラグと引数を分離する形式に変更されています。これは、新しいフラグ解析のセマンティクスに合わせたものです。同様の変更が gccgcToolchainasmcc 関数にも適用されています。

これらの変更により、goコマンドが内部でコンパイラやリンカを呼び出す際にも、新しいフラグ解析ルールに準拠した引数が渡されるようになります。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルに集中しています。

  1. src/lib9/flag.c: 新しいフラグ解析ライブラリの追加。これが新しいフラグ解析の基盤となります。
  2. 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 を利用するように変更されています。
  3. src/cmd/cc/lex.c (Cコンパイラ):
    • 従来の引数解析ロジックが削除され、flagライブラリの関数群を用いたフラグ定義と flagparse 関数による解析に置き換えられています。
    • dodef 関数(-D フラグの処理)や dospim 関数(-l フラグの処理)が、新しいフラグ解析フレームワークに統合されています。
  4. src/cmd/gc/lex.c (Goコンパイラ):
    • 従来の引数解析ロジックが削除され、flagライブラリの関数群を用いたフラグ定義と flagparse 関数による解析に置き換えられています。
    • doversion 関数(-V フラグの処理)が追加され、flagfn0 を介して呼び出されるようになっています。
    • debug配列の型が char から int に変更されています。
  5. src/cmd/go/build.go:
    • gcToolchain.gc 関数で、-= フラグが -complete に変更されています。
    • gcToolchain.asm, gcToolchain.cc, gccgcToolchain.asm, gccgcToolchain.cc 関数で、-D フラグの引数指定方法が新しいフラグ解析のセマンティクスに合わせて変更されています。
  6. include/libc.h: 新しい flag ライブラリの関数群のプロトタイプ宣言が追加されています。
  7. 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 では INITTEXTINITDAT の型が vlong から int64 に変更されています。
  8. src/cmd/cc/cc.h および src/cmd/gc/go.h: debug配列の型が char から int に変更されています。
  9. 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 で使用)
  • fhashfirst, 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 構造体を作成し、desciscountset(または set2)、arg フィールドを設定します。set 関数には、引数を解析して対応する変数に格納するロジックが実装されています。例えば、flagstrstring 関数を 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); // フラグの値を設定
        }
        // ... (残りの引数を更新) ...
    }
    

    この関数が実際のコマンドライン引数解析を実行します。argcargv を受け取り、ハイフンで始まる引数をフラグとして解釈します。= で引数が指定されている場合や、次の引数がフラグの値である場合を適切に処理し、対応する Flag 構造体の set または set2 関数を呼び出して値を設定します。不明なフラグや引数不足の場合にはエラーを発生させます。

  • flagprint 関数: first ポインタから始まるリンクリストを辿り、各フラグの namedesc を整形して使用法メッセージとして出力します。desc に ": " が含まれる場合は、フラグ名と説明を異なる行に表示するなどの整形を行います。

リンカ/コンパイラの main 関数の変更例 (src/cmd/5l/obj.cmain 関数)

変更前は、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.gomain.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 rungo installgo testgo fmt などの様々なサブコマンドを提供します。Go 1.21以降では、goコマンドは異なるGoツールチェインのバージョンも管理し、go.modgo.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_ のようにフラグと引数を分離する形式に変更されています。これは、新しいフラグ解析のセマンティクスに合わせたものです。同様の変更が gccgcToolchainasmcc 関数にも適用されています。

これらの変更により、goコマンドが内部でコンパイラやリンカを呼び出す際にも、新しいフラグ解析ルールに準拠した引数が渡されるようになります。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルに集中しています。

  1. src/lib9/flag.c: 新しいフラグ解析ライブラリの追加。これが新しいフラグ解析の基盤となります。
  2. 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 を利用するように変更されています。
  3. src/cmd/cc/lex.c (Cコンパイラ):
    • 従来の引数解析ロジックが削除され、flagライブラリの関数群を用いたフラグ定義と flagparse 関数による解析に置き換えられています。
    • dodef 関数(-D フラグの処理)や dospim 関数(-l フラグの処理)が、新しいフラグ解析フレームワークに統合されています。
  4. src/cmd/gc/lex.c (Goコンパイラ):
    • 従来の引数解析ロジックが削除され、flagライブラリの関数群を用いたフラグ定義と flagparse 関数による解析に置き換えられています。
    • doversion 関数(-V フラグの処理)が追加され、flagfn0 を介して呼び出されるようになっています。
    • debug配列の型が char から int に変更されています。
  5. src/cmd/go/build.go:
    • gcToolchain.gc 関数で、-= フラグが -complete に変更されています。
    • gcToolchain.asm, gcToolchain.cc, gccgcToolchain.asm, gccgcToolchain.cc 関数で、-D フラグの引数指定方法が新しいフラグ解析のセマンティクスに合わせて変更されています。
  6. include/libc.h: 新しい flag ライブラリの関数群のプロトタイプ宣言が追加されています。
  7. 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 では INITTEXTINITDAT の型が vlong から int64 に変更されています。
  8. src/cmd/cc/cc.h および src/cmd/gc/go.h: debug配列の型が char から int に変更されています。
  9. 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 で使用)
  • fhashfirst, 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 構造体を作成し、desciscountset(または set2)、arg フィールドを設定します。set 関数には、引数を解析して対応する変数に格納するロジックが実装されています。例えば、flagstrstring 関数を 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); // フラグの値を設定
        }
        // ... (残りの引数を更新) ...
    }
    

    この関数が実際のコマンドライン引数解析を実行します。argcargv を受け取り、ハイフンで始まる引数をフラグとして解釈します。= で引数が指定されている場合や、次の引数がフラグの値である場合を適切に処理し、対応する Flag 構造体の set または set2 関数を呼び出して値を設定します。不明なフラグや引数不足の場合にはエラーを発生させます。

  • flagprint 関数: first ポインタから始まるリンクリストを辿り、各フラグの namedesc を整形して使用法メッセージとして出力します。desc に ": " が含まれる場合は、フラグ名と説明を異なる行に表示するなどの整形を行います。

リンカ/コンパイラの main 関数の変更例 (src/cmd/5l/obj.cmain 関数)

変更前は、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開発メーリングリストのアーカイブ)