Goでフラグパッケージを使用する方法
序章
コマンドラインユーティリティは、追加の構成なしで、箱から出してすぐに役立つことはめったにありません。 適切なデフォルトは重要ですが、便利なユーティリティはユーザーからの構成を受け入れる必要があります。 ほとんどのプラットフォームでは、コマンドラインユーティリティはフラグを受け入れてコマンドの実行をカスタマイズします。 フラグは、コマンド名の後に追加されるキーと値で区切られた文字列です。 Goを使用すると、標準ライブラリのflag
パッケージを使用して、フラグを受け入れるコマンドラインユーティリティを作成できます。
このチュートリアルでは、flag
パッケージを使用して、さまざまな種類のコマンドラインユーティリティを構築するさまざまな方法について説明します。 フラグを使用してプログラム出力を制御し、フラグと他のデータを混合する位置引数を導入してから、サブコマンドを実装します。
フラグを使用してプログラムの動作を変更する
flag
パッケージの使用には、次の3つのステップが含まれます。最初に変数を定義してフラグ値をキャプチャし、次に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エスケープシーケンスを使用して、色付きの出力を表示するように端末に指示します。 これらは特殊な文字シーケンスであるため、新しいタイプを定義することは理にかなっています。 この例では、そのタイプをColor
と呼び、タイプをstring
として定義しました。 次に、次のconst
ブロックで使用する色のパレットを定義します。 const
ブロックの後に定義されたcolorize
関数は、これらのColor
定数の1つと、メッセージの色付けのためのstring
変数を受け入れます。 次に、要求された色のエスケープシーケンスを最初に印刷して色を変更するように端末に指示し、次にメッセージを印刷し、最後に特別な色リセットシーケンスを印刷して端末に色のリセットを要求します。
main
内で、flag.Bool
関数を使用して、color
というブールフラグを定義します。 この関数の2番目のパラメーターfalse
は、このフラグが指定されていない場合のデフォルト値を設定します。 予想に反して、これをtrue
に設定しても、フラグを指定するとfalseになるような動作は反転しません。 したがって、このパラメーターの値は、ほとんどの場合、ブールフラグ付きのfalse
です。
最後のパラメーターは、使用法メッセージとして印刷できる一連のドキュメントです。 この関数から返される値は、bool
へのポインターです。 次の行のflag.Parse
関数は、このポインターを使用して、ユーザーから渡されたフラグに基づいてbool
変数を設定します。 次に、ポインターを逆参照することにより、このbool
ポインターの値を確認できます。 ポインター変数の詳細については、ポインターに関するチュートリアルを参照してください。 このブール値を使用すると、-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
変数を定義して、プログラムがファイルから読み取る必要のある行数を保持します。 次に、flag.IntVar
を使用して-n
フラグを定義し、元のhead
プログラムの動作を反映します。 この関数を使用すると、Var
サフィックスがないflag
関数とは対照的に、独自のポインターを変数に渡すことができます。 この違いを除けば、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ループを使用して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
これにより、パッケージステートメントのみが出力されます。
Outputpackage main
最後に、プログラムは位置引数が指定されていないことを検出すると、head
と同様に、標準入力から入力を読み取ります。 次のコマンドを実行してみてください。
echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3
次の出力が表示されます。
Outputfish lobsters sharks
これまでに見たflag
関数の動作は、コマンド呼び出し全体を調べることに限定されていました。 特にサブコマンドをサポートするコマンドラインツールを作成している場合は、この動作が常に必要になるとは限りません。
FlagSetを使用したサブコマンドの実装
最近のコマンドラインアプリケーションは、多くの場合、「サブコマンド」を実装して、一連のツールを1つのコマンドにバンドルします。 このパターンを使用する最もよく知られているツールはgit
です。 git init
のようなコマンドを調べる場合、git
はコマンドであり、init
はgit
のサブコマンドです。 サブコマンドの注目すべき機能の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
関数は、コマンドから返されたエラーを処理します。 関数がエラーを返す場合、if
ステートメントはそれをキャッチし、エラーを出力し、プログラムは1
のステータスコードで終了し、エラーを示します。オペレーティングシステムの残りの部分に発生しました。 main
内で、プログラムが呼び出されたすべての引数をroot
に渡します。 最初にos.Args
をスライスして、プログラムの名前である最初の引数(前の例では./subcommand
)を削除します。
root
関数は、[]Runner
を定義します。ここで、すべてのサブコマンドが定義されます。 Runner
は、root
がName()
を使用してサブコマンドの名前を取得し、その内容と比較できるようにするサブコマンドのインターフェイスです。 X166X]変数。 cmds
変数を反復処理した後、正しいサブコマンドが見つかったら、残りの引数でサブコマンドを初期化し、そのコマンドのRun()
メソッドを呼び出します。
このフレームワークでは他のサブコマンドを簡単に作成できますが、定義するサブコマンドは1つだけです。 GreetCommand
は、NewGreetCommand
を使用してインスタンス化され、flag.NewFlagSet
を使用して新しい*flag.FlagSet
を作成します。 flag.NewFlagSet
は、フラグセットの名前と、解析エラーを報告するための戦略の2つの引数を取ります。 *flag.FlagSet
の名前には、flag.(*FlagSet).Name
メソッドを使用してアクセスできます。 これを(*GreetCommand).Name()
メソッドで使用するため、サブコマンドの名前は*flag.FlagSet
に付けた名前と一致します。 NewGreetCommand
も、前の例と同様の方法で-name
フラグを定義しますが、代わりに、*GreetCommand
の*flag.FlagSet
フィールドからのメソッドとしてこれを呼び出します。 gc.fs
。 root
が*GreetCommand
のInit()
メソッドを呼び出すとき、*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プログラミング言語の詳細については、完全なGoシリーズのコーディング方法をご覧ください。