Upgrade to Pro — share decks privately, control downloads, hide ads and more …

それ CLI フレームワークがなくてもできるよ / Building CLI Tools Wi...

それ CLI フレームワークがなくてもできるよ / Building CLI Tools Without Frameworks

『GopherのためのCLIツール開発』最新事情 LT
https://findy.connpass.com/event/362163/

Avatar for Kuniwak

Kuniwak PRO

July 25, 2025
Tweet

More Decks by Kuniwak

Other Decks in Programming

Transcript

  1. 15 type InOut struct { Stdin io.Reader Stdout io.Writer Stderr

    io.Writer } func NewInOut() *InOut { return &InOut{ Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr, } } type Command func(args []string, inout *InOut) int func Run(c Command) { args := os.Args[1:] exitStatus := c(args, NewInOut()) os.Exit(exitStatus) } ඪ४ೖग़ྗΛ·ͱΊͨߏ଄ମɻ JP3FBE$MPTFS΍JP8SJUF$MPTFSͰ΋Α͍ɻ ςετ࣌ʹ͸ద౰ͳِ෺ͱࠩ͠ସ͑Δ ຊ෺ͷඪ४ೖग़ྗΛ࡞Δؔ਺ $PNNBOEͷ࣮ߦؔ਺ $-*ϓϩάϥϜຊମͷܕ ຊ ମ ࣮ ૷
  2. func TestMainCommand(t *testing.T) { stdin := strings.NewReader("") stdout := &bytes.Buffer{}

    stderr := &bytes.Buffer{} exitStatus := MainCommand([]string{}, &cli.InOut{ Stdin: stdin, Stdout: stdout, Stderr: stderr, }) if exitStatus != 0 { t.Errorf("unexpected exit status: %d", exitStatus) } expected := "Hello, World!" if stdout.String() != expected { t.Errorf("want %q, got %q", expected, stdout.String()) } } $-*ϓϩάϥϜͷςετɻ ͔ͳΓ͋ͬ͞Γॻ͚Δ ς ε τ
  3. 18 func TestMainCommand(t *testing.T) { stdin := strings.NewReader("") stdout :=

    &bytes.Buffer{} stderr := &bytes.Buffer{} exitStatus := MainCommand([]string{"foo", "bar"}, &cli.InOut{ Stdin: stdin, Stdout: stdout, Stderr: stderr, }) if exitStatus != 0 { t.Errorf("unexpected exit status: %d", exitStatus) } expected := "Hello, foo!\nHello, bar!\n" if stdout.String() != expected { t.Errorf("want %q, got %q", expected, stdout.String()) } } .$1αʔόʔͳͲର࿩తͳ $-*ϓϩάϥϜͰ΋೉ͳ͘ ςετΛॻ͚Δɻ ͜Ε͸ඪ४ೖྗʹೖྗ͞Εͨ ໊લʹ)FMMPΛ͚ͭͯฦ͢ྫɻ ς ε τ
  4. 20 type Options struct { Foo string Bar string Help

    bool } func ParseOptions(args []string, inout *cli.InOut) (*Options, error) { flags := flag.NewFlagSet("recipe2", flag.ContinueOnError) flags.SetOutput(inout.Stderr) options := &Options{} flags.StringVar(&options.Foo, "foo", "", "Foo") flags.StringVar(&options.Bar, "bar", "", "Bar") if err := flags.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { options.Help = true return options, nil } return nil, err } return options, nil } $-*ϓϩάϥϜͷΦϓγϣϯߏ଄ମ Φϓγϣϯղੳؔ਺ ຊ ମ ࣮ ૷
  5. func TestParseOptions(t *testing.T) { testCases := []struct { Input []string

    Expected *Options }{ { Input: []string{"-foo", "foo", "-bar", "bar"}, Expected: &Options{ Foo: "foo", Bar: "bar", }, }, { Input: []string{"-h"}, Expected: &Options{ Help: true, }, }, } Φϓγϣϯղੳؔ਺ͷςετ ೖྗͱͳΔҾ਺Λఆٛͯ͠ ظ଴͢ΔΦϓγϣϯߏ଄ମΛॻ͘ ς ε τ
  6. 22 for _, testCase := range testCases { stdout :=

    &bytes.Buffer{} stderr := &bytes.Buffer{} opts, err := ParseOptions(testCase.Input, &cli.InOut{ Stdin: strings.NewReader(""), Stdout: stdout, Stderr: stderr, }) if err != nil { t.Fatalf("failed to parse options: %v", err) } if !reflect.DeepEqual(opts, testCase.Expected) { t.Error(cmp.Diff(opts, testCase.Expected)) } } } ςʔϒϧۦಈͰςετ͢Δ ς ε τ
  7. 24 type Options struct { SomethingRequired string Help bool }

    func ParseOptions(args []string, inout *cli.InOut) (*Options, error) { flags := flag.NewFlagSet("recipe3", flag.ContinueOnError) flags.SetOutput(inout.Stderr) options := &Options{} flags.StringVar(&options.SomethingRequired, "something-required", "", "Something required") if err := flags.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { options.Help = true return options, nil } return nil, err } if options.SomethingRequired == "" { return nil, errors.New("something-required is required") } return options, nil } $-*ϓϩάϥϜͷΦϓγϣϯߏ଄ମ Φϓγϣϯղੳؔ਺Λ࣮૷͢Δ όϦσʔγϣϯ͢Δ ຊ ମ ࣮ ૷
  8. 25 func TestParseOptions_Error(t *testing.T) { testCases := []struct { Input

    []string }{ { Input: []string{}, }, } for _, testCase := range testCases { stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} _, err := ParseOptions(testCase.Input, &cli.InOut{ Stdin: strings.NewReader(""), Stdout: stdout, Stderr: stderr, }) if err == nil { t.Fatalf("expected error, got nil") } } } ςʔϒϧۦಈͰςετ͢Δ ඞਢͷҾ਺͕ͳ͚Ε͹ ΤϥʔʹͳΔ͜ͱΛظ଴͢Δ ς ε τ
  9. 27 func ParseOptions(args []string, inout *cli.InOut) (*Options, error) { flags

    := flag.NewFlagSet("recipe4", flag.ContinueOnError) flags.SetOutput(inout.Stderr) options := &Options{} flags.StringVar(&options.Foo, "foo", "", "foo") flags.StringVar(&options.Bar, "bar", "", "bar") flags.Usage = func() { inout.Stderr.Write([]byte("Usage: recipe4 [options]\n")) inout.Stderr.Write([]byte("OPTIONS\n")) flags.PrintDefaults() } if err := flags.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { options.Help = true return options, nil } return nil, err } return options, nil } ຊ ମ ࣮ ૷ Φϓγϣϯղੳؔ਺ ϔϧϓͷදࣔॲཧ
  10. 28 func TestFlagUsage(t *testing.T) { stdin := strings.NewReader("") stdout :=

    &bytes.Buffer{} stderr := &bytes.Buffer{} _, err := ParseOptions([]string{"-h"}, &cli.InOut{ Stdin: stdin, Stdout: stdout, Stderr: stderr, }) if err != nil { t.Fatalf("failed to parse options: %v", err) } expected := `Usage: recipe4 [options] OPTIONS -bar string bar -foo string foo ` if stderr.String() != expected { t.Errorf("want %q, got %q", expected, stderr.String()) } } ς ε τ ϔϧϓΛදࣔͤ͞Δ ظ଴ͱͷҰகΛ֬ೝ
  11. 30 type SubCommand struct { Name string Description string Run

    Command } ຊ ମ ࣮ ૷ αϒίϚϯυͷ໊લ αϒίϚϯυߏ଄ମ αϒίϚϯυͷઆ໌ αϒίϚϯυͷॲཧ
  12. 31 Φϓγϣϯͷղੳ αϒίϚϯυͷ͋ΔίϚϯυΛએݴ αϒίϚϯυͷىಈ ϔϧϓͷ४උ func NewCommand(name string, cmds []SubCommand)

    Command { return func(args []string, inout *InOut) int { flags := flag.NewFlagSet(name, flag.ContinueOnError) flags.SetOutput(inout.Stderr) flags.Usage = func() { fmt.Fprintf(inout.Stderr, "Usage: %s [command]\n\n", name) fmt.Fprintf(inout.Stderr, "COMMANDS\n") for _, cmd := range cmds { fmt.Fprintf(inout.Stderr, " %s\n \t%s\n", cmd.Name, cmd.Description) } } if err := flags.Parse(args); err != nil { if err == flag.ErrHelp { return 0 } return 1 } if flags.NArg() == 0 { fmt.Fprintf(inout.Stderr, "error: no command provided\n") flags.Usage() return 1 } for _, cmd := range cmds { if cmd.Name == flags.Arg(0) { return cmd.Run(flags.Args()[1:], inout) } } fmt.Fprintf(inout.Stderr, "error: unknown command: %s\n", flags.Arg(0)) flags.Usage() return 1 } } ຊ ମ ࣮ ૷
  13. 32 func TestNewCommand(t *testing.T) { s1 := SubCommand{ Name: "one",

    Description: "1st subcommand", Run: func(args []string, inout *ProcInout) int { fmt.Fprintln(inout.Stdout, "1") return 0 }, } s2 := SubCommand{ Name: "two", Description: "2nd subcommand", Run: func(args []string, inout *ProcInout) int { fmt.Fprintln(inout.Stdout, "2") return 0 }, } ς ε τ ِͷαϒίϚϯυΛ༻ҙ ِͷαϒίϚϯυΛ༻ҙ
  14. 33 stdin := strings.NewReader("") stdout := &bytes.Buffer{} stderr := &bytes.Buffer{}

    cmd := NewCommand("test", []SubCommand{s1, s2}) exitStatus := cmd([]string{"two"}, &ProcInout{ Stdin: stdin, Stdout: stdout, Stderr: stderr, }) if exitStatus != 0 { t.Errorf("expected exit status 0, got %d", exitStatus) } if stdout.String() != "2\n" { t.Errorf("expected output '2', got %q", stdout.String()) } } ς ε τ ͋ͱ͸͍ͭ΋௨Γςετ
  15. 39 ࢖༻Ͱ͖ΔϥΠϒϥϦʢOPUϑϨʔϜϫʔΫʣɿ w KFTTFWELHP fl BHT w BMFY fl JOUHPBSH

    w ʜ ࢖͍ํΛ஌Δʹ͸ͦΕͧΕͷ3&"%.&Λࢀরͯ͠΄͍͠ɻ ൃ ද Ͱ ͸ ׂ Ѫ