Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 74 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ monitoring-forgeのMackerel pluginで広く利用している [go-flags](https:/
- 引数が必要かどうかの判定
- エラー時の終了コード返却(UNKNOWN)

また、用途に応じて以下の3種類のインターフェースを提供します。

- `Runner[T]` — 汎用的な `(メッセージ, 終了コード)` を返す形式
- `Checker` — [mackerelio/checkers](https://github.com/mackerelio/checkers) の `*checkers.Checker` を返す形式
- `Shipper` — 何も返さず、副作用でメトリクスなどを送信する形式

## インストール

```bash
Expand All @@ -19,15 +25,16 @@ go get github.com/monitoring-forge/flagrun

## 使い方

`Runner` インターフェースを実装した構造体を `flagrun.Go` に渡します。
### `Runner[T]` — 汎用的な実行

`Runner[T]` インターフェースを実装した構造体を `flagrun.Go` に渡します。

`Run` メソッドの戻り値は `(メッセージ, 終了コード)` です。終了コードが `OK` の場合、メッセージは標準出力へ出力されます。`OK` 以外の場合は標準エラー出力へ出力されます。終了コードは `os.Exit` に渡されます。

```go
package main

import (
_ "github.com/jessevdk/go-flags"
"github.com/monitoring-forge/flagrun"
)

Expand All @@ -47,21 +54,83 @@ func main() {
os.Exit(flagrun.Go(
opt,
flagrun.Version(version),
flagrun.Commit(commit),
))
}
```

### `Checker` — mackerelio/checkers を使う

`Checker` インターフェースを実装した構造体を `flagrun.Check` に渡します。

`Run` メソッドの戻り値は `*checkers.Checker` です。`Checker.String()` の結果を標準出力へ出力し、`Checker.Status` を終了コードとして返します。

```go
package main

import (
"github.com/mackerelio/checkers"
"github.com/monitoring-forge/flagrun"
)

type Opt struct {
Host string `short:"H" long:"host" default:"localhost" description:"Target host"`
Version bool `short:"v" long:"version" description:"Show version"`
}

func (p *Opt) Run(args []string) *checkers.Checker {
return checkers.Ok("service is reachable")
}
Comment thread
kazeburo marked this conversation as resolved.

func main() {
opt := &Opt{}
os.Exit(flagrun.Check(
opt,
flagrun.Version(version),
))
}
```

### `Shipper` — 副作用だけで実行

`Shipper` インターフェースを実装した構造体を `flagrun.Ship` に渡します。

`Run` メソッドは戻り値を持ちません。メトリクスの送信など、副作用だけを行いたい場合に使います。終了コードは常に `OK` を返します。

```go
package main

import (
"github.com/monitoring-forge/flagrun"
)

type Opt struct {
Host string `short:"H" long:"host" default:"localhost" description:"Target host"`
Version bool `short:"v" long:"version" description:"Show version"`
}

func (p *Opt) Run(args []string) {
// 副作用でメトリクスを送信
}

func main() {
opt := &Opt{}
os.Exit(flagrun.Ship(
opt,
flagrun.Version(version),
))
}
```

## オプション

`flagrun.Go` では、以下の関数を使って動作をカスタマイズできます。
| `flagrun.Go` / `flagrun.Check` / `flagrun.Ship` では、以下の関数を使って動作をカスタマイズできます。

| 関数 | 説明 |
|------|------|
| `flagrun.Version(version string)` | バージョン表示に使用する文字列を指定します。 |
| `flagrun.Commit(commit string)` | コミットハッシュなどを指定します(デフォルト: `dev`)。 |
| `flagrun.ArgsRequired()` | コマンドライン引数を必須にします。引数がない場合は UNKNOWN で終了します。 |
| `flagrun.AlwaysStdout()` | `Run` の戻り値を、終了コードに関係なく標準出力へ出力します。 |
| `flagrun.AlwaysStdout()` | `Run` の戻り値を、終了コードに関係なく標準出力へ出力します。`flagrun.Check` では常に標準出力へ出力されるため、このオプションは不要です。 |

## 終了コード

Expand Down
125 changes: 98 additions & 27 deletions flagrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"github.com/jessevdk/go-flags"
"github.com/mackerelio/checkers"
)

const (
Expand All @@ -19,9 +20,19 @@ const (
UNKNOWN
)

type Runner interface {
type Runner[T any] interface {
// Run executes the command with the provided flags and arguments.
Run([]string) (string, int)
Run([]string) (T, int)
}

type Checker interface {
// Check executes the command with the provided flags and arguments.
Run([]string) *checkers.Checker
}

type Shipper interface {
// Run executes the command with the provided flags and arguments.
Run([]string)
}

type Flagrun struct {
Expand Down Expand Up @@ -72,20 +83,8 @@ func printLine(w io.Writer, s string) error {
return err
}

func Go(opt Runner, options ...FlagrunOptions) int {
f, msg, code := internalGo(os.Args[1:], os.Stdout, os.Stderr, opt, options...)
if msg != "" {
if code == OK || f.AlwaysStdout {
_ = printLine(os.Stdout, msg)
} else {
_ = printLine(os.Stderr, msg)
}
}
return code
}

// hasBooleanVersionField checks if the struct has a Version field of type bool and its value is true
func hasBooleanVersionField(opt Runner) bool {
func hasBooleanVersionField(opt any) bool {
if opt == nil {
return false
}
Expand Down Expand Up @@ -134,21 +133,22 @@ func buildCommitHash() string {
return commit
}

func internalGo(
argv []string,
stdout io.Writer,
stderr io.Writer,
opt Runner,
options ...FlagrunOptions,
) (*Flagrun, string, int) {
func buildFlagrun(options ...FlagrunOptions) *Flagrun {
f := &Flagrun{
Commit: buildCommitHash(),
Version: "unknown",
}
for _, option := range options {
option(f)
}
return f
}

func nullint(i int) *int {
return &i
}

func (f *Flagrun) parseArgs(argv []string, stdout, stderr io.Writer, opt any) ([]string, *int) {
psr := flags.NewParser(opt, flags.HelpFlag|flags.PassDoubleDash)
if f.ArgsRequired {
psr.Usage = "[OPTIONS] -- command [args...]"
Expand All @@ -165,18 +165,89 @@ func internalGo(
runtime.GOARCH,
runtime.Version(),
f.Commit)
return f, "", OK
return nil, nullint(OK)
} else if flags.WroteHelp(err) {
fmt.Fprintf(stdout, "%v\n", err)
return f, "", OK
return nil, nullint(OK)
} else if err != nil {
fmt.Fprintf(stderr, "%v\n", err)
return f, "", UNKNOWN
return nil, nullint(UNKNOWN)
} else if f.ArgsRequired && len(args) == 0 {
fmt.Fprintf(stderr, "command is required\n")
psr.WriteHelp(stderr)
return f, "", UNKNOWN
return nil, nullint(UNKNOWN)
}
return args, nil
}

func internalGo[T any](
f *Flagrun,
argv []string,
stdout io.Writer,
stderr io.Writer,
opt Runner[T],
) (string, int) {
args, c := f.parseArgs(argv, stdout, stderr, opt)
if c != nil {
return "", *c
}
msg, code := opt.Run(args)
return f, msg, code
return fmt.Sprintf("%v", msg), code
}

// Checker return *checkers.Checker
func (f *Flagrun) internalChecker(
argv []string,
stdout io.Writer,
stderr io.Writer,
opt Checker,
) (string, int) {
args, c := f.parseArgs(argv, stdout, stderr, opt)
if c != nil {
return "", *c
}
f.AlwaysStdout = true
chk := opt.Run(args)
return chk.String(), int(chk.Status)
Comment thread
kazeburo marked this conversation as resolved.
Outdated
Comment thread
kazeburo marked this conversation as resolved.
Outdated
}

func (f *Flagrun) internalShipper(
argv []string,
stdout io.Writer,
stderr io.Writer,
opt Shipper,
) {
args, c := f.parseArgs(argv, stdout, stderr, opt)
if c != nil {
return
}
opt.Run(args)
}

func Go[T any](opt Runner[T], options ...FlagrunOptions) int {
f := buildFlagrun(options...)
msg, code := internalGo(f, os.Args[1:], os.Stdout, os.Stderr, opt)
if msg != "" {
if code == OK || f.AlwaysStdout {
_ = printLine(os.Stdout, msg)
} else {
_ = printLine(os.Stderr, msg)
}
}
return code
}

func Check(opt Checker, options ...FlagrunOptions) int {
f := buildFlagrun(options...)
msg, code := f.internalChecker(os.Args[1:], os.Stdout, os.Stderr, opt)
if msg != "" {
_ = printLine(os.Stdout, msg)
}
return code
}

func Ship(opt Shipper, options ...FlagrunOptions) int {
f := buildFlagrun(options...)
f.internalShipper(os.Args[1:], os.Stdout, os.Stderr, opt)
return OK
Comment thread
kazeburo marked this conversation as resolved.
Outdated
}
Comment thread
kazeburo marked this conversation as resolved.
Loading