0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoでCLIアプリを開発する!Flagパッケージの活用術

Posted at

Group1551.png

Leapcell: The Next-Gen Serverless Platform for Web Hosting

はじめに

flag はコマンドラインオプションを解析するために使用されます。Unix 系システムを使用する経験のある人は、コマンドラインオプションに馴染んでいるはずです。たとえば、ls -al というコマンドは、現在のディレクトリ内のすべてのファイルとディレクトリの詳細情報を一覧表示します。ここで、-al がコマンドラインオプションです。

コマンドラインオプションは、実際の開発でよく使われ、特にツールを書く際には欠かせません。

設定ファイルのパスを指定することができます。たとえば、postgres -D /usr/local/pgsql/data は、指定されたデータディレクトリで PostgreSQL サーバーを起動します。
特定のパラメータをカスタマイズすることもできます。たとえば、python -m SimpleHTTPServer 8080 は、ポート 8080 でリスニングする HTTP サーバーを起動します。指定しない場合は、デフォルトでポート 8000 でリスニングします。

クイックスタート

ライブラリを学ぶ最初のステップは、もちろんそれを使うことです。まず、flag ライブラリの基本的な使い方を見てみましょう:

package main

import (
  "fmt"
  "flag"
)

var (
  intflag int
  boolflag bool
  stringflag string
)

func init() {
  flag.IntVar(&intflag, "intflag", 0, "int flag value")
  flag.BoolVar(&boolflag, "boolflag", false, "bool flag value")
  flag.StringVar(&stringflag, "stringflag", "default", "string flag value")
}

func main() {
  flag.Parse()

  fmt.Println("int flag:", intflag)
  fmt.Println("bool flag:", boolflag)
  fmt.Println("string flag:", stringflag)
}

まず、プログラムをコンパイルしてから実行します(私は macOS を使用しています):

$ go build -o main main.go
$ ./main -intflag 12 -boolflag 1 -stringflag test

出力:

int flag: 12
bool flag: true
string flag: test

特定のオプションを設定しない場合、対応する変数はデフォルト値を取ります:

$ ./main -intflag 12 -boolflag 1

出力:

int flag: 12
bool flag: true
string flag: default

設定しなかったオプション stringflag がデフォルト値 default を持つことがわかります。

また、直接 go run を使うこともできます。このコマンドは、まずプログラムをコンパイルして実行可能ファイルを生成し、その後そのファイルを実行し、コマンドライン内の他のオプションをこのプログラムに渡します。

$ go run main.go -intflag 12 -boolflag 1

-h を使ってオプションのヘルプ情報を表示することができます:

$ ./main -h
Usage of /path/to/main:
  -boolflag
        bool flag value
  -intflag int
        int flag value
  -stringflag string
        string flag value (default "default")

要約すると、flag ライブラリを使う一般的なステップは以下の通りです:

  1. オプションの値を格納するためのいくつかのグローバル変数を定義します。たとえば、ここでは intflagboolflagstringflag です。
  2. init メソッド内で flag.TypeVar メソッドを使ってオプションを定義します。ここで、TypeIntUintFloat64Bool などの基本型でもよく、時間間隔の time.Duration でもよいです。定義する際には、変数のアドレス、オプション名、デフォルト値、ヘルプ情報を渡します。
  3. main メソッド内で flag.Parse を呼び出して、os.Args[1:] からオプションを解析します。os.Args[0] は実行可能プログラムのパスなので、除外されます。

注意点

flag.Parse メソッドは、すべてのオプションが定義された後に呼び出さなければなりません。また、flag.Parse が呼び出された後に新しいオプションを定義することはできません。前述のステップに従えば、基本的に問題は発生しません。

init はすべてのコードの前に実行されるため、すべてのオプション定義を init に入れることで、main 関数内で flag.Parse が実行されるときにはすべてのオプションがすでに定義されています。

オプションの形式

flag ライブラリは 3 つのコマンドラインオプション形式をサポートしています。

-flag
-flag=x
-flag x

--- のどちらを使っても構いません。それらは同じ機能を持ちます。いくつかのライブラリでは - をショートオプション、-- をロングオプションとして表すこともあります。比較的に、flag は使いやすいです。

最初の形式は、ブール型オプションのみをサポートしています。もし現れれば true となり、現れなければデフォルト値を取ります。

第 3 の形式は、ブール型オプションをサポートしていません。この形式のブール型オプションは、Unix 系システムで予期せぬ振る舞いをする可能性があるからです。以下のコマンドを考えてみましょう:

cmd -x *

ここで、* はシェルのワイルドカードです。0false という名前のファイルが存在する場合、ブール型オプション -x は値 false を取ります。そうでなければ、ブール型オプション -x は値 true を取ります。そして、このオプションは 1 つの引数を消費します。

ブール型オプションを明示的に false に設定したい場合は、-flag=false という形式を使うしかありません。

最初の非オプション引数(すなわち - または -- で始まらない引数)または終端記号 -- に遭遇すると、解析は停止します。以下のプログラムを実行してみましょう:

$ ./main noflag -intflag 12

出力は以下の通りになります:

int flag: 0
bool flag: false
string flag: default

noflag に遭遇したときに解析が停止し、その後のオプション -intflag は解析されません。そのため、すべてのオプションはデフォルト値を取ります。

以下のプログラムを実行してみましょう:

$ ./main -intflag 12 -- -boolflag=true

出力は以下の通りになります:

int flag: 12
bool flag: false
string flag: default

まず、オプション intflag が解析され、その値が 12 に設定されます。-- に遭遇した後、解析は停止し、その後の --boolflag=true は解析されないので、boolflag オプションはデフォルト値 false を取ります。

解析が停止した後、コマンドライン引数がまだ存在する場合、flag ライブラリはそれらを保存し、flag.Args メソッドを通じてこれらの引数のスライスを取得することができます。

flag.NArg メソッドを使って未解析の引数の数を取得することができ、flag.Arg(i) を使って位置 i(0 から始まる)の引数にアクセスすることができます。

オプションの数も、flag.NFlag メソッドを呼び出すことで取得することができます。

上記のプログラムを少し変更します:

func main() {
  flag.Parse()
    
  fmt.Println(flag.Args())
  fmt.Println("Non-Flag Argument Count:", flag.NArg())
  for i := 0; i < flag.NArg(); i++ {
    fmt.Printf("Argument %d: %s\n", i, flag.Arg(i))
  }
  
  fmt.Println("Flag Count:", flag.NFlag())
}

このプログラムをコンパイルして実行します:

$ go build -o main main.go
$ ./main -intflag 12 -- -stringflag test

出力:

[-stringflag test]
Non-Flag Argument Count: 2
Argument 0: -stringflag
Argument 1: test

-- に遭遇して解析が停止した後、残りの引数 -stringflag testflag に保存され、ArgsNArgArg などのメソッドを通じてアクセスすることができます。

整数型のオプション値は、1234(10 進数)、0664(8 進数)、0x1234(16 進数)のような形式を受け付けることができ、負の値でも構いません。実際、flag は内部で strconv.ParseInt メソッドを使って文字列を int に解析します。

そのため、理論的には ParseInt が受け付けるあらゆる形式でも構いません。

ブール型のオプション値は以下のようになります:

  • true の値:1tTtrueTRUETrue
  • false の値:0fFfalseFALSEFalse

オプションを定義する別の方法

上では、flag.TypeVar を使ってオプションを定義する方法を紹介しました。この方法では、まず変数を定義し、その後変数のアドレスを渡す必要があります。

もう 1 つの方法があります。flag.Type(ここで TypeIntUintBoolFloat64StringDuration など)を呼び出すと、自動的に変数を割り当ててくれ、その変数のアドレスを返します。使い方は以前の方法と似ています:

package main

import (
  "fmt"
  "flag"
)

var (
  intflag *int
  boolflag *bool
  stringflag *string
)

func init() {
  intflag = flag.Int("intflag", 0, "int flag value")
  boolflag = flag.Bool("boolflag", false, "bool flag value")
  stringflag = flag.String("stringflag", "default", "string flag value")
}

func main() {
  flag.Parse()
    
  fmt.Println("int flag:", *intflag)
  fmt.Println("bool flag:", *boolflag)
  fmt.Println("string flag:", *stringflag)
}

このプログラムをコンパイルして実行します:

$ go build -o main main.go
$ ./main -intflag 12

出力は以下の通りになります:

int flag: 12
bool flag: false
string flag: default

使う際には参照解除が必要な点を除けば、基本的に以前の方法と同じです。

高度な使い方

ショートオプションを定義する

flag ライブラリは明示的にショートオプションをサポートしていませんが、同じ変数に異なるオプションを設定することで実現することができます。すなわち、2 つのオプションが同じ変数を共有するわけです。

初期化の順序は不確定なので、同じデフォルト値を持つようにする必要があります。そうしないと、このオプションが渡されないときには、振る舞いが不確定になります。

package main

import (
  "fmt"
  "flag"
)

var logLevel string

func init() {
  const (
    defaultLogLevel = "DEBUG"
    usage = "set log level value"
  )
  
  flag.StringVar(&logLevel, "log_type", defaultLogLevel, usage)
  flag.StringVar(&logLevel, "l", defaultLogLevel, usage + "(shorthand)")
}

func main() {
  flag.Parse()

  fmt.Println("log level:", logLevel)
}

このプログラムをコンパイルして実行します:

$ go build -o main main.go
$ ./main -log_type WARNING
$ ./main -l WARNING是继续翻译的内容:

ロングオプションとショートオプションのどちらを使っても、以下のように出力されます:

log level: WARNING

このオプションを渡さない場合、デフォルト値が出力されます:

$ ./main
log level: DEBUG

時間間隔を解析する

基本型をオプションとして使うほか、flag ライブラリは time.Duration 型、すなわち時間間隔もサポートしています。時間間隔でサポートされる形式は非常に多様で、たとえば "300ms""-1.5h""2h45m" などです。

時間単位は nsusmssmhday などがあります。実際、flag は内部で time.ParseDuration を呼び出します。具体的にサポートされる形式は、time ライブラリのドキュメントを参照することができます。

package main

import (
  "flag"
  "fmt"
  "time"
)

var (
  period time.Duration
)

func init() {
  flag.DurationVar(&period, "period", 1*time.Second, "sleep period")
}

func main() {
  flag.Parse()
  fmt.Printf("Sleeping for %v...", period)
  time.Sleep(period)
  fmt.Println()
}

渡されたコマンドラインオプション period に応じて、プログラムは対応する時間だけスリープします。デフォルトは 1 秒です。このプログラムをコンパイルして実行します:

$ go build -o main main.go
$ ./main
Sleeping for 1s...

$ ./main -period 1m30s
Sleeping for 1m30s...

オプションをカスタマイズする

flag ライブラリが提供するオプション型を使うほか、自分たちでオプション型をカスタマイズすることもできます。標準ライブラリに提供されている例を解析してみましょう:

package main

import (
  "errors"
  "flag"
  "fmt"
  "strings"
  "time"
)

type interval []time.Duration

func (i *interval) String() string {
  return fmt.Sprint(*i)
}

func (i *interval) Set(value string) error {
  if len(*i) > 0 {
    return errors.New("interval flag already set")
  }
  for _, dt := range strings.Split(value, ",") {
    duration, err := time.ParseDuration(dt)
    if err != nil {
      return err
    }
    *i = append(*i, duration)
  }
  return nil
}

var (
  intervalFlag interval
)

func init() {
  flag.Var(&intervalFlag, "deltaT", "comma-seperated list of intervals to use between events")
}

func main() {
  flag.Parse()

  fmt.Println(intervalFlag)
}

まず、新しい型を定義します。ここでは、interval 型が定義されています。

新しい型は、flag.Value インターフェイスを実装しなければなりません:

// src/flag/flag.go
type Value interface {
  String() string
  Set(string) error
}

String メソッドは、この型の値を形式づけます。flag.Parse メソッドが実行され、カスタム型のオプションに遭遇したときには、この型の変数の Set メソッドが、オプション値をパラメータとして呼び出されます。

ここでは、, で区切られた時間間隔を解析して、スライスに保存しています。

カスタム型のオプションの定義には、flag.Var メソッドを使う必要があります。

このプログラムをコンパイルして実行します:

$ go build -o main main.go
$ ./main -deltaT 30s
[30s]
$ ./main -deltaT 30s,1m,1m30s
[30s 1m0s 1m30s]

指定されたオプション値が不正な場合、Set メソッドは error 型の値を返し、Parse の実行は停止し、エラーと使用方法のヘルプが表示されます。

$ ./main -deltaT 30x
invalid value "30x" for flag -deltaT: time: unknown unit x in duration 30x
Usage of /path/to/main:
  -deltaT value
        comma-seperated list of intervals to use between events

プログラム内で文字列を解析する

時々、オプションはコマンドラインを通じて渡されません。たとえば、設定テーブルから読み取られたり、プログラムによって生成されたりする場合です。このような場合、flag.FlagSet 構造体の関連メソッドを使って、これらのオプションを解析することができます。

実際、私たちが以前に呼び出した flag ライブラリのメソッドはすべて、間接的に FlagSet 構造体のメソッドを呼び出しています。flag ライブラリは、コマンドラインオプションを解析するために、FlagSet 型のグローバル変数 CommandLine を特別に定義しています。

私たちが以前に呼び出した flag ライブラリのメソッドは、単に便利のためのもので、内部的にはすべて CommandLine の対応するメソッドを呼び出しています:

// src/flag/flag.go
var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

func Parse() {
  CommandLine.Parse(os.Args[1:])
}

func IntVar(p *int, name string, value int, usage string) {
  CommandLine.Var(newIntValue(value, p), name, usage)
}

func Int(name string, value int, usage string) *int {
  return CommandLine.Int(name, value, usage)
}

func NFlag() int { return len(CommandLine.actual) }

func Arg(i int) string {
  return CommandLine.Arg(i)
}

func NArg() int { return len(CommandLine.args) }

同様に、私たちも自分たちの FlagSet 型の変数を作成して、オプションを解析することができます。

package main

import (
  "flag"
  "fmt"
)

func main() {
  args := []string{"-intflag", "12", "-stringflag", "test"}

  var intflag int
  var boolflag bool
  var stringflag string

  fs := flag.NewFlagSet("MyFlagSet", flag.ContinueOnError)
  fs.IntVar(&intflag, "intflag", 0, "int flag value")
  fs.BoolVar(&boolflag, "boolflag", false, "bool flag value")
  fs.StringVar(&stringflag, "stringflag", "default", "string flag value")

  fs.Parse(args)
  
  fmt.Println("int flag:", intflag)
  fmt.Println("bool flag:", boolflag)
  fmt.Println("string flag:", stringflag)
}

NewFlagSet メソッドには 2 つのパラメータがあります。最初のパラメータはプログラム名で、ヘルプを出力するときやエラーが発生したときに表示されます。2 番目のパラメータは、解析中のエラーをどのように処理するかを示すもので、いくつかのオプションがあります:

  • ContinueOnError:エラーが発生した後も解析を続行します。CommandLine はこのオプションを使っています。
  • ExitOnError:エラーが発生したときに os.Exit(2) を呼び出してプログラムを終了します。
  • PanicOnError:エラーが発生したときに panic を起こします。

flag ライブラリ内の関連コードをすぐに見てみましょう:

// src/flag/flag.go
func (f *FlagSet) Parse(arguments []string) error {
  f.parsed = true
  f.args = arguments
  for {
    seen, err := f.parseOne()
    if seen {
      continue
    }
    if err == nil {
      break
    }
    switch f.errorHandling {
    case ContinueOnError:
      return err
    case ExitOnError:
      os.Exit(2)
    case PanicOnError:
      panic(err)
    }
  }
  return nil
}

これは、直接 flag ライブラリのメソッドを使う場合と少し異なります。FlagSetParse メソッドを呼び出すときには、明示的に文字列のスライスをパラメータとして渡す必要があります。なぜなら、flag.Parse は内部で CommandLine.Parse(os.Args[1:]) を呼び出すからです。

Leapcell: The Next-Gen Serverless Platform for Web Hosting

最後に、Go サービスのデプロイに最適なプラットフォームをおすすめします:Leapcell

barndpic.png

1. 多言語対応

  • JavaScript、Python、Go、または Rust で開発できます。

2. 無制限のプロジェクトを無料でデプロイ

  • 使用分のみ課金 — リクエストがなければ、請求されません。

3. 抜群のコスト効率

  • 使った分だけ課金で、アイドル時の課金はありません。
  • 例:平均応答時間 60ms で 694 万回のリクエストに対応するのに 25 ドルです。

4. 合理化された開発者体験

  • 直感的な UI で簡単にセットアップできます。
  • 完全自動化された CI/CD パイプラインと GitOps 統合。
  • アクション可能なインサイトを得るためのリアルタイム メトリクスとログ。

5. 簡単なスケーラビリティと高性能

  • 高い同時実行性を簡単に処理するための自動スケーリング。
  • ゼロの運用オーバーヘッド — 構築に集中するだけです。

Frame3-withpadding2x.png

ドキュメントでさらに詳しく調べる!

Leapcell Twitter: https://x.com/LeapcellHQ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?