序章

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

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

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

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

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

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

  1. 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変数を呼び出すことができます。

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

  1. go run boolean.go

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

Output
Hello, DigitalOcean!

次に、-colorフラグを指定してこのプログラムを再度実行します。

  1. 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を使用して、作成したファイルの内容を表示します。

  1. go run head.go -- head.go

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

Output
package main import ( "bufio" "flag"

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

  1. go run head.go -n 1 head.go

これにより、パッケージステートメントのみが出力されます。

Output
package main

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

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

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

Output
fish lobsters sharks

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

FlagSetを使用したサブコマンドの実装

最近のコマンドラインアプリケーションは、多くの場合、「サブコマンド」を実装して、一連のツールを1つのコマンドにバンドルします。 このパターンを使用する最もよく知られているツールは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関数は、コマンドから返されたエラーを処理します。 関数がエラーを返す場合、ifステートメントはそれをキャッチし、エラーを出力し、プログラムは1のステータスコードで終了し、エラーを示します。オペレーティングシステムの残りの部分に発生しました。 main内で、プログラムが呼び出されたすべての引数をrootに渡します。 最初にos.Argsをスライスして、プログラムの名前である最初の引数(前の例では./subcommand)を削除します。

root関数は、[]Runnerを定義します。ここで、すべてのサブコマンドが定義されます。 Runnerは、rootName()を使用してサブコマンドの名前を取得し、その内容と比較できるようにするサブコマンドのインターフェイスです。 X166X]