GoでFlagパッケージを使用する方法

前書き

コマンドラインユーティリティは、追加設定なしですぐに使用できることはほとんどありません。 適切なデフォルトは重要ですが、有用なユーティリティはユーザーからの設定を受け入れる必要があります。 ほとんどのプラットフォームでは、コマンドラインユーティリティはフラグを受け入れてコマンドの実行をカスタマイズします。 フラグは、コマンド名の後に追加されるキーと値で区切られた文字列です。 Goを使用すると、標準ライブラリのflagパッケージを使用して、フラグを受け入れるコマンドラインユーティリティを作成できます。

このチュートリアルでは、flagパッケージを使用してさまざまな種類のコマンドラインユーティリティを構築するさまざまな方法について説明します。 フラグを使用してプログラム出力を制御し、フラグと他のデータを混在させる位置引数を導入してから、サブコマンドを実装します。

フラグを使用してプログラムの動作を変更する

flagパッケージの使用には、3つのステップが含まれます。最初にdefine variablesでフラグ値をキャプチャし、次にGoアプリケーションが使用するフラグを定義し、最後に実行時にアプリケーションに提供されるフラグを解析します。 flagパッケージ内のほとんどの関数は、フラグの定義と、定義した変数へのフラグのバインドに関係しています。 解析フェーズは、Parse()関数によって処理されます。

説明のために、標準出力に出力されるメッセージを変更するBooleanフラグを定義するプログラムを作成します。 -colorフラグが指定されている場合、プログラムはメッセージを青色で出力します。 フラグが指定されていない場合、メッセージは色なしで印刷されます。

boolean.goという名前の新しいファイルを作成します。

nano boolean.go

次のコードをファイルに追加して、プログラムを作成します。

boolean.go

package main

import (
    "flag"
    "fmt"
)

type Color string

const (
    ColorBlack  Color = "\u001b[30m"
    ColorRed          = "\u001b[31m"
    ColorGreen        = "\u001b[32m"
    ColorYellow       = "\u001b[33m"
    ColorBlue         = "\u001b[34m"
    ColorReset        = "\u001b[0m"
)

func colorize(color Color, message string) {
    fmt.Println(string(color), message, string(ColorReset))
}

func main() {
    useColor := flag.Bool("color", false, "display colorized output")
    flag.Parse()

    if *useColor {
        colorize(ColorBlue, "Hello, DigitalOcean!")
        return
    }
    fmt.Println("Hello, DigitalOcean!")
}

この例では、ANSI Escape Sequencesを使用して、色付きの出力を表示するように端末に指示します。 これらは文字の特殊なシーケンスであるため、新しいタイプを定義することは理にかなっています。 この例では、そのタイプをColorと呼び、タイプをstringとして定義しました。 次に、次のconstブロックで使用する色のパレットを定義します。 constブロックの後に定義されたcolorize関数は、これらのColor定数の1つと、メッセージの色付けのためのstring変数を受け入れます。 次に、最初に要求された色のエスケープシーケンスを印刷して色を変更するよう端末に指示し、次にメッセージを印刷し、最後に特別な色リセットシーケンスを印刷して端末に色をリセットするように要求します。

main内で、flag.Bool関数を使用して、colorと呼ばれるブールフラグを定義します。 この関数の2番目のパラメーターfalseは、このフラグが指定されていない場合のデフォルト値を設定します。 予想に反して、これをtrueに設定しても、フラグを指定するとfalseになるような動作は反転しません。 したがって、このパラメーターの値は、ほとんどの場合、ブールフラグを使用したfalseです。

最後のパラメーターは、使用法メッセージとして印刷できるドキュメントの文字列です。 この関数から返される値は、boolへのポインターです。 次の行のflag.Parse関数は、このポインターを使用して、ユーザーから渡されたフラグに基づいてbool変数を設定します。 次に、ポインターを逆参照することにより、このboolポインターの値を確認できます。 ポインタ変数の詳細については、tutorial on pointersを参照してください。 このブール値を使用すると、-colorフラグが設定されている場合はcolorizeを呼び出し、フラグがない場合はfmt.Println変数を呼び出すことができます。

ファイルを保存し、フラグなしでプログラムを実行します。

go run boolean.go

次の出力が表示されます。

OutputHello, DigitalOcean!

ここで、-colorフラグを使用してこのプログラムを再度実行します。

go run boolean.go -color

出力は同じテキストになりますが、今回は青色です。

コマンドに渡される値はフラグだけではありません。 ファイル名またはその他のデータを送信することもできます。

位置引数の使用

通常、コマンドは、コマンドのフォーカスの主題として機能する多くの引数を取ります。 たとえば、ファイルの最初の行を出力するheadコマンドは、多くの場合、head example.txtとして呼び出されます。 ファイルexample.txtは、headコマンドの呼び出しにおける位置引数です。

Parse()関数は、フラグ以外の引数を検出するまで、検出したフラグを解析し続けます。 flagパッケージは、これらをArgs()およびArg()関数を介して利用できるようにします。

これを説明するために、headコマンドの簡略化された再実装を作成します。これにより、特定のファイルの最初の数行が表示されます。

head.goという名前の新しいファイルを作成し、次のコードを追加します。

head.go

package main

import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "os"
)

func main() {
    var count int
    flag.IntVar(&count, "n", 5, "number of lines to read from the file")
    flag.Parse()

    var in io.Reader
    if filename := flag.Arg(0); filename != "" {
        f, err := os.Open(filename)
        if err != nil {
            fmt.Println("error opening file: err:", err)
            os.Exit(1)
        }
        defer f.Close()

        in = f
    } else {
        in = os.Stdin
    }

    buf := bufio.NewScanner(in)

    for i := 0; i < count; i++ {
        if !buf.Scan() {
            break
        }
        fmt.Println(buf.Text())
    }

    if err := buf.Err(); err != nil {
        fmt.Fprintln(os.Stderr, "error reading: err:", err)
    }
}

まず、プログラムがファイルから読み取る必要のある行数を保持するために、count変数を定義します。 次に、元のheadプログラムの動作を反映して、flag.IntVarを使用して-nフラグを定義します。 この関数を使用すると、Varサフィックスがないflag関数とは対照的に、独自のpointerを変数に渡すことができます。 この違いは別として、flag.IntVarの残りのパラメーターは、対応するflag.Intのフラグ名、デフォルト値、および説明に従います。 前の例のように、次にflag.Parse()を呼び出してユーザーの入力を処理します。

次のセクションでファイルを読み取ります。 最初に、ユーザーが要求したファイル、またはプログラムに渡される標準入力のいずれかに設定されるio.Reader変数を定義します。 ifステートメント内で、flag.Arg関数を使用して、すべてのフラグの後の最初の位置引数にアクセスします。 ユーザーがファイル名を指定した場合、これが設定されます。 それ以外の場合は、空の文字列("")になります。 ファイル名が存在する場合、os.Open関数を使用してそのファイルを開き、前に定義したio.Readerをそのファイルに設定します。 それ以外の場合は、os.Stdinを使用して標準入力から読み取ります。

最後のセクションでは、bufio.NewScannerで作成された*bufio.Scannerを使用して、io.Reader変数inから行を読み取ります。 for loopを使用してcountの値まで反復し、buf.Scanで行をスキャンするとfalse値が生成される場合は、breakを呼び出します。行数がユーザーの要求数より少ない。

このプログラムを実行し、ファイル引数としてhead.goを使用して、作成したファイルの内容を表示します。

go run head.go -- head.go

--セパレーターは、flagパッケージによって認識される特別なフラグであり、これ以上フラグ引数が続かないことを示します。 このコマンドを実行すると、次の出力が表示されます。

Outputpackage main

import (
        "bufio"
        "flag"

定義した-nフラグを使用して、出力量を調整します。

go run head.go -n 1 head.go

これは、packageステートメントのみを出力します。

Outputpackage main

最後に、プログラムは、位置引数が指定されていないことを検出すると、headと同様に、標準入力から入力を読み取ります。 このコマンドを実行してみてください:

echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3

出力が表示されます。

Outputfish
lobsters
sharks

これまでに見たflag関数の動作は、コマンド呼び出し全体の調査に限定されていました。 特にサブコマンドをサポートするコマンドラインツールを作成している場合は、常にこの動作が必要なわけではありません。

FlagSetを使用してサブコマンドを実装する

現代のコマンドラインアプリケーションは、多くの場合「サブコマンド」を実装して、単一のコマンドの下に一連のツールをバンドルします。 このパターンを使用する最もよく知られているツールはgitです。 git initのようなコマンドを調べる場合、gitはコマンドであり、initgitのサブコマンドです。 サブコマンドの注目すべき機能の1つは、各サブコマンドが独自のフラグのコレクションを持つことができることです。

Goアプリケーションは、flag.(*FlagSet)タイプを使用して、独自のフラグセットを持つサブコマンドをサポートできます。 これを説明するために、異なるフラグを持つ2つのサブコマンドを使用してコマンドを実装するプログラムを作成します。

subcommand.goという名前の新しいファイルを作成し、次のコンテンツをファイルに追加します。

package main

import (
    "errors"
    "flag"
    "fmt"
    "os"
)

func NewGreetCommand() *GreetCommand {
    gc := &GreetCommand{
        fs: flag.NewFlagSet("greet", flag.ContinueOnError),
    }

    gc.fs.StringVar(&gc.name, "name", "World", "name of the person to be greeted")

    return gc
}

type GreetCommand struct {
    fs *flag.FlagSet

    name string
}

func (g *GreetCommand) Name() string {
    return g.fs.Name()
}

func (g *GreetCommand) Init(args []string) error {
    return g.fs.Parse(args)
}

func (g *GreetCommand) Run() error {
    fmt.Println("Hello", g.name, "!")
    return nil
}

type Runner interface {
    Init([]string) error
    Run() error
    Name() string
}

func root(args []string) error {
    if len(args) < 1 {
        return errors.New("You must pass a sub-command")
    }

    cmds := []Runner{
        NewGreetCommand(),
    }

    subcommand := os.Args[1]

    for _, cmd := range cmds {
        if cmd.Name() == subcommand {
            cmd.Init(os.Args[2:])
            return cmd.Run()
        }
    }

    return fmt.Errorf("Unknown subcommand: %s", subcommand)
}

func main() {
    if err := root(os.Args[1:]); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

このプログラムは、main関数、root関数、およびサブコマンドを実装するための個々の関数のいくつかの部分に分かれています。 main関数は、コマンドから返されたエラーを処理します。 関数がerrorを返す場合、ifステートメントはそれをキャッチしてエラーを出力し、プログラムは1のステータスコードで終了し、残りにエラーが発生したことを示しますオペレーティングシステムの。 main内で、プログラムが呼び出されたすべての引数をrootに渡します。 最初にos.Argsをスライスして、プログラムの名前である最初の引数(前の例では./subcommand)を削除します。

root関数は[]Runnerを定義し、すべてのサブコマンドが定義されます。 Runnerは、サブコマンドのinterfaceであり、rootName()を使用してサブコマンドの名前を取得し、それを内容subcommand変数と比較できるようにします。 。 cmds変数を反復処理した後、正しいサブコマンドが見つかったら、残りの引数でサブコマンドを初期化し、そのコマンドのRun()メソッドを呼び出します。

サブフレームを1つだけ定義しますが、このフレームワークでは他のサブコマンドを簡単に作成できます。 GreetCommandNewGreetCommandを使用してインスタンス化され、flag.NewFlagSetを使用して新しい*flag.FlagSetを作成します。 flag.NewFlagSetは、フラグセットの名前と、解析エラーを報告するための戦略の2つの引数を取ります。 *flag.FlagSetの名前には、flag.(*FlagSet).Nameメソッドを使用してアクセスできます。 これを(*GreetCommand).Name()メソッドで使用するため、サブコマンドの名前は*flag.FlagSetに付けた名前と一致します。 NewGreetCommandも前の例と同様の方法で-nameフラグを定義しますが、代わりにこれを*GreetCommandgc.fs*flag.FlagSetフィールドから離れたメソッドとして呼び出します。 s。 root*GreetCommandInit()メソッドを呼び出すとき、提供された引数を*flag.FlagSetフィールドのParseメソッドに渡します。

このプログラムをビルドして実行すると、サブコマンドが見やすくなります。 プログラムをビルドします。

go build subcommand.go

次に、引数なしでプログラムを実行します。

./subcommand

次の出力が表示されます。

OutputYou must pass a sub-command

次に、greetサブコマンドを使用してプログラムを実行します。

./subcommand greet

これにより、以下の出力が生成されます。

OutputHello World !

次に、-nameフラグとgreetを使用して、名前を指定します。

./subcommand greet -name Sammy

プログラムから次の出力が表示されます。

OutputHello Sammy !

この例は、Goでより大きなコマンドラインアプリケーションを構築する方法の背後にあるいくつかの原則を示しています。 `FlagSet`は、フラグ解析ロジックによってフラグが処理される場所と方法を開発者がより細かく制御できるように設計されています。

結論

フラグを使用すると、ユーザーはプログラムの実行方法を制御できるため、より多くのコンテキストでアプリケーションがより便利になります。 ユーザーに便利なデフォルトを提供することは重要ですが、ユーザーの状況に合わない設定を上書きする機会をユーザーに与える必要があります。 flagパッケージは、構成オプションをユーザーに提示するための柔軟な選択肢を提供することを確認しました。 いくつかの単純なフラグを選択するか、サブコマンドの拡張可能なスイートを構築できます。 いずれの場合も、flagパッケージを使用すると、柔軟でスクリプト可能なコマンドラインツールの長い歴史のスタイルでユーティリティを構築するのに役立ちます。

Goプログラミング言語の詳細については、完全なHow To Code in Go seriesを確認してください。