メインコンテンツにスキップ
モジュール 10 · レッスン 4 / 5コース内の 49/50~18 min
モジュールのレッスン (4/5)

ミニプロジェクト: フラグ付きの CLI

前のモジュールの構成要素を組み合わせて、flag パッケージを使用したフラグ解析、位置引数の処理、os.Stderr でのエラー、および適切な終了コードなどの小さな慣用的な CLI を構築しましょう。

flag パッケージ

Go
package main

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

func main() {
    n := flag.Int("n", 1, "number of repetitions")
    upper := flag.Bool("u", false, "print in uppercase")
    flag.Parse()

    args := flag.Args() // remaining positional arguments
    if len(args) < 1 {
        fmt.Fprintln(os.Stderr, "usage: echo [-n N] [-u] <text>")
        os.Exit(2)
    }

    text := args[0]
    if *upper {
        text = strings.ToUpper(text)
    }
    for i := 0; i < *n; i++ {
        fmt.Println(text)
    }
}

重要なポイント:

  • flag.Int、flag.String、flag.Bool、flag.Duration (など) は ポインターを返します。*n、*upper を使用します。
  • flag.Parse() は、値を読み取る前、flag.Args() の前に 1 回呼び出す必要があります。
  • flag.Args() は 位置 引数 (-- の後または最後のフラグの後の引数) を返します。
  • 受け入れられるフォーム: -n 3、-n=3、--n=3。ブール値: -u (true)、-u=false。

従来の終了コード

Go
os.Exit(0) // success
os.Exit(1) // generic error
os.Exit(2) // usage error (incorrect flag usage)

シェルとスクリプトはこれらのコードを想定しています。一貫して使用してください。 決して os.Exit を使用してメイン以外の goroutine を終了しないでください。log のフラッシュを含むすべての defer がスキップされます。

標準誤差対標準出力

古くて神聖な Unix ルール:

  • os.Stdout → 便利な出力、パイプ可能 (cli | grep ...)。
  • os.Stderr → 診断、エラー、進行状況バー、使用状況。
Go
fmt.Println("result: 42")                   // stdout
fmt.Fprintln(os.Stderr, "error: ...")       // stderr

このように、cli 2>/dev/null は結果のみを表示し、cli >out.txt は結果のみをリダイレクトし、エラーは表示されたままになります。

log 対 fmt

  • fmt ユーザー出力用。
  • log 診断用: タイムスタンプを追加し、デフォルトで os.Stderr に書き込みます。log.Fatal (ログ + os.Exit(1)) および log.Panic があります。
Go
log.SetFlags(log.LstdFlags | log.Lshortfile) // timestamp + file:line
log.Fatal("boom") // prints and exits with 1

最新のアプリ (Go 1.21 以降) では、構造化ログ (JSON またはテキスト、レベル、型付きフィールド) に log/slog を使用します。

アーキテクチャ: main をロジックから分離する

Go
func main() {
    if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func run(args []string, stdout, stderr io.Writer) error {
    // here use a new flag.FlagSet instead of the global flag.CommandLine
    fs := flag.NewFlagSet("echo", flag.ContinueOnError)
    fs.SetOutput(stderr)
    n := fs.Int("n", 1, "repetitions")
    if err := fs.Parse(args); err != nil {
        return err
    }
    // ...
    return nil
}

利点:

  • テスト可能: bytes.Buffer を stdout/stderr として渡し、出力を検証します。
  • グローバル状態なし: 各 FlagSet は分離されています。
  • main は小さいままです。IO とデリゲートを準備します。

サブコマンド

サブコマンド (mycli serve、mycli migrate) を含む CLI の場合、外部依存関係のないスキーム:

Go
func main() {
    if len(os.Args) < 2 {
        usage()
        os.Exit(2)
    }
    switch os.Args[1] {
    case "serve":
        serveCmd(os.Args[2:])
    case "migrate":
        migrateCmd(os.Args[2:])
    default:
        usage()
        os.Exit(2)
    }
}

各サブコマンドには独自の flag.NewFlagSet があります。大規模なプロジェクトの場合、cobra や urfave/cli などのライブラリが、自動生成されたヘルプを使用してこのスキームを構造化します。

演習

運動#go.m10.l4.e1
試行回数: 0読み込み中…

デフォルトの 1 のフラグを定義し、「リペティツィオーニ」の説明、フラグの値を解析します。Parse() の値を確認します。

エディターを読み込み中…

3 回の試行後に解決策が利用可能になります

運動#go.m10.l4.e2
試行回数: 0読み込み中…

フラグを設定する必要はありません。解析、スタンパ 'manca argomento' su Stderr e termina con os.Exit(1)。

エディターを読み込み中…

3 回の試行後に解決策が利用可能になります

Quiz#go.m10.l4.e3
準備完了

CLI でのエラー ストリーム スタンピ メッセージの使用法は Unix フレンドリーですか?

Go
fmt.Fprintln(???, "uso: prog ...")
回答の選択肢