前書き
コマンドラインユーティリティは、追加設定なしですぐに使用できることはほとんどありません。 適切なデフォルトは重要ですが、有用なユーティリティはユーザーからの設定を受け入れる必要があります。 ほとんどのプラットフォームでは、コマンドラインユーティリティはフラグを受け入れてコマンドの実行をカスタマイズします。 フラグは、コマンド名の後に追加されるキーと値で区切られた文字列です。 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
はコマンドであり、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
関数は、コマンドから返されたエラーを処理します。 関数がerrorを返す場合、if
ステートメントはそれをキャッチしてエラーを出力し、プログラムは1
のステータスコードで終了し、残りにエラーが発生したことを示しますオペレーティングシステムの。 main
内で、プログラムが呼び出されたすべての引数をroot
に渡します。 最初にos.Args
をスライスして、プログラムの名前である最初の引数(前の例では./subcommand
)を削除します。
root
関数は[]Runner
を定義し、すべてのサブコマンドが定義されます。 Runner
は、サブコマンドのinterfaceであり、root
がName()
を使用してサブコマンドの名前を取得し、それを内容subcommand
変数と比較できるようにします。 。 cmds
変数を反復処理した後、正しいサブコマンドが見つかったら、残りの引数でサブコマンドを初期化し、そのコマンドのRun()
メソッドを呼び出します。
サブフレームを1つだけ定義しますが、このフレームワークでは他のサブコマンドを簡単に作成できます。 GreetCommand
はNewGreetCommand
を使用してインスタンス化され、flag.NewFlagSet
を使用して新しい*flag.FlagSet
を作成します。 flag.NewFlagSet
は、フラグセットの名前と、解析エラーを報告するための戦略の2つの引数を取ります。 *flag.FlagSet
の名前には、flag.(*FlagSet).Name
メソッドを使用してアクセスできます。 これを(*GreetCommand).Name()
メソッドで使用するため、サブコマンドの名前は*flag.FlagSet
に付けた名前と一致します。 NewGreetCommand
も前の例と同様の方法で-name
フラグを定義しますが、代わりにこれを*GreetCommand
、gc.fs
の*flag.FlagSet
フィールドから離れたメソッドとして呼び出します。 s。 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プログラミング言語の詳細については、完全なHow To Code in Go seriesを確認してください。