Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
それ CLI フレームワークがなくてもできるよ / Building CLI Tools Wi...
Search
Kuniwak
PRO
July 25, 2025
Programming
4.7k
18
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
それ CLI フレームワークがなくてもできるよ / Building CLI Tools Without Frameworks
『GopherのためのCLIツール開発』最新事情 LT
https://findy.connpass.com/event/362163/
Kuniwak
PRO
July 25, 2025
More Decks by Kuniwak
See All by Kuniwak
AIベース静的検査器の偽陽性率を抑える工夫3選
orgachem
PRO
6
950
仕様漏れ実装漏れをなくすトレーサビリティAI基盤のご紹介
orgachem
PRO
9
6.4k
要求定義・仕様記述・設計・検証の手引き - 理論から学ぶ明確で統一された成果物定義
orgachem
PRO
31
16k
DeNA での思い出 / Memories at DeNA
orgachem
PRO
7
3.6k
状態遷移図を書こう / Sequence Chart vs State Diagram
orgachem
PRO
4
740
テストケースの名前はどうつけるべきか?
orgachem
PRO
2
900
欠陥を早期に発見するための Software Engineer in Test とその重要性 / What is Software Engineer in Test and How they works
orgachem
PRO
21
5k
住宅を WebXR で評価しよう / Evaluating My Home by WebXR
orgachem
PRO
0
240
HOME VR
orgachem
PRO
1
870
Other Decks in Programming
See All in Programming
Make SRE Operations Easier with Azure SRE Agent
kkamegawa
0
7.8k
決定論的オーケストレーションの設計と実装 / Design and Implementation of Deterministic Orchestration
nrslib
4
1.5k
LaravelLive Japan の裏方のすべて — 第188回 PHP勉強会@東京 (2026-06-24)
suguruooki
2
120
TSKaigi Night Talks 2026_TypeScriptでサプライチェーンの整合性を型に閉じ込める
geekplus_tech
0
400
その問い、本当に正しいですか?AI時代のエンジニアに必要な哲学と認知科学 / ai-philosophy-cognitive-science
minodriven
13
6.2k
Oxcを導入して開発体験が向上した話
yug1224
4
340
脅威をエンジニアリングの糧にして――現場編 / Turning Threats into Engineering Fuel — Field Edition
nrslib
0
300
Developing with AI Agents — Codex, Claude Code & Cowork Practical Guide
x5gtrn
PRO
0
1.3k
AIだと陥りがちなJakarta EE最新技術への移行時の落とし穴と解決策
tnagao7
0
120
Lemonade + Foundry Toolkit でお手軽アプリ開発
seosoft
1
370
ADKを使って簡単にAIエージェントを作ってみよう
k1mu21
0
280
Vite+ Unified Toolchain for the Web
naokihaba
0
340
Featured
See All Featured
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
16
2k
Automating Front-end Workflow
addyosmani
1370
210k
What’s in a name? Adding method to the madness
productmarketing
PRO
24
4.1k
How to Align SEO within the Product Triangle To Get Buy-In & Support - #RIMC
aleyda
2
1.6k
How Software Deployment tools have changed in the past 20 years
geshan
0
34k
Ethics towards AI in product and experience design
skipperchong
2
310
Collaborative Software Design: How to facilitate domain modelling decisions
baasie
1
250
What Being in a Rock Band Can Teach Us About Real World SEO
427marketing
0
1k
Designing for Timeless Needs
cassininazir
1
260
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
31
10k
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
5.9k
Transcript
ͦΕ$-*ϑϨʔϜϫʔΫ͕ ͳͯ͘Ͱ͖ΔΑ 'JOEZ5&$)#"50/ʮ(PQIFSͷͨΊͷ$-*πʔϧ։ൃʯ࠷৽ࣄ-5,VOJXBL
͍͑ͨ͜ͱ 2 w $-*πʔϧΛͭ͘Δͱ͖ϑϨʔϜϫʔΫΛΘͳ͍ બࢶʹΛ͚Α͏ w গ͠ଥڠ͢Εɺඪ४ϥΠϒϥϦͱ͍ίʔυ͚ͩ ʢߦ͙Β͍ʣͰ͍͍ͨͯͷ͜ͱ࣮ݱͰ͖Δ w ϑϨʔϜϫʔΫΛΘ͍ͣίʔυͰ࣮ݱͰ͖Ε
୯ҐͰͷϝϯςφϯεϑϦʔʹͰ͖Δ
աڈ࣮ w ࢲͷͭ͘ΔϑϨʔϜϫʔΫΛΘͳ͍$-*πʔϧ ͯ͢୯ҐͰϝϯςφϯεϑϦʔͰಈ࡞ w %FQFOEBCPUͳͲ͔Β੬ऑੑͷࢦఠΛड͚Δ ͜ͱ͔ͳΓ͍͠ 3 ͨͱ͑%F/"VOJUZNFUBDIFDLػೳՃΛআ͚طଘػೳۙ͘ϝϯς͠ͳ͘ͱ ੬ऑੑߋ৽ͳ͘ϢʔεέʔεΛຬ͍ͨͤͯΔ
3FOPWBUFͳͲΛ͑Ξοϓσʔτ͋ΔఔলྗԽͰ͖Δ͕ɺࢲͷ߹লྗԽͲ͜Ζ͔ ·͍ͬͨͬͯ͘ͳ͍ͷͰθϩ
༻ޠͷఆٛ 4 ·ͣ
5 ϑϨʔϜϫʔΫ ෦ͷن֨ΛܾΊΔͷɻϢʔβʔن֨ʹԊͬͨ෦ΛΈೖΕΔ ͜ͱͰػೳΛ࣮ݱ͢Δɻͨͱ͑VSGBWFDMJTQGDPCSBͳͲɻ ϥΠϒϥϦ ෦ͦͷͷɻϢʔβʔ෦Λ͖ʹΈ߹ΘͤͯػೳΛ࣮ݱ͢Δɻ ྫ͑KFTTFWELHP fl BHTɻಛʹϥΠϒϥϦͷ͏ͪඪ४ϥΠϒϥϦͱ ݴޠʹΈೖΕΒΕ͍ͯΔͷΛࢦ͢ɻྫ͑
fl BHɻ
6 ϑϨʔϜϫʔΫͱϥΠϒϥϦΛݟ͚Δʹɺ ͦͷίϯϙʔωϯτͷఏڙ͢Δܕ͕ࣗͷ࣮͢Δ ίϯϙʔωϯτʹͲͷఔସෆՄೳͳͷͱͯ͠ ొ͢Δ͔ΛଌΔͱΑ͍ɻ ଟ͚ΕϑϨʔϜϫʔΫతɺগͳ͚ΕϥΠϒϥϦతɻ ϑϨʔϜϫʔΫͱϥΠϒϥϦͷதؒతͳͷ͋Γ͑Δɻͨͱ͑3FBDUυϝΠϯ͔Β ϥΠϒϥϦͷΑ͏ʹΈ͑ɺ7JFX͔ΒϑϨʔϜϫʔΫͷΑ͏ʹΈ͑Δ
ϝϯςϑϦʔʹ͢Δίπ 7 ͕͜͜ࠓճͷϙΠϯτ
ͳͯ͋͘·ΓࠔΒͳ͍ػೳΛଥڠͯ͠อकੑΛͱΔ 8 อकੑ ػೳͷଟ͞ ͭ͘Δπʔϧͷอकੑͱ ػೳͷଟ͓͓͞Αͦ ൺྫ͢Δ
ͳͯ͋͘·ΓࠔΒͳ͍ػೳΛଥڠͯ͠อकੑΛͱΔ 9 อकੑ ػೳͷଟ͞ ʹཱͨͳ͍ อक͕େม ͜͜Λࢦ͢
ͳͯ͋͘·ΓࠔΒͳ͍ػೳΛଥڠͯ͠อकੑΛͱΔ 10 อकੑ ػೳͷଟ͞ ͔͜͜Βग़ൃͯ͠ ͜͜Λࢦ͢
ͳͯ͋͘·ΓࠔΒͳ͍ػೳΛଥڠͯ͠อकੑΛͱΔ 11 อकੑ ػೳͷଟ͞ ϑϨʔϜϫʔΫΛ͏ͱ͜͜ʹͳΓ͕ͪ
ඪ४ϥΠϒϥϦ͚ͩͷϨγϐ 12 ϝϯςϑϦʔΛ࣮ݱ͢ΔͨΊͷ
αϯϓϧίʔυ(JU)VCʹ͋Γ·͢ 13
ΤϯτϦϙΠϯτͷॻ͖ํ 14 Ϩγϐ
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$MPTFSJP8SJUF$MPTFSͰΑ͍ɻ ςετ࣌ʹదͳِͱࠩ͠ସ͑Δ ຊͷඪ४ೖग़ྗΛ࡞Δؔ $PNNBOEͷ࣮ߦؔ $-*ϓϩάϥϜຊମͷܕɻςετΛ ͘͢͢͠ΔͨΊʹ͏ ຊ ମ ࣮
16 $-*ϓϩάϥϜຊମ func MainCommand(args []string, inout *cli.InOut) int { fmt.Fprintln(inout.Stdout,
"Hello, World!") return 0 } ຊ ମ ࣮
17 NBJOؔ͜Ε͚ͩ func main() { cli.Run(MainCommand) } ຊ ମ ࣮
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()) } } $-*ϓϩάϥϜͷςετɻ ͔ͳΓ͋ͬ͞Γॻ͚Δ ς ε τ
19 func TestMainCommand(t *testing.T) { stdin := strings.NewReader("foo\nbar\n") 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, foo!\nHello, bar!\n" if stdout.String() != expected { t.Errorf("want %q, got %q", expected, stdout.String()) } } .$1αʔόʔͳͲରతͳ $-*ϓϩάϥϜͰͳ͘ ςετΛॻ͚Δɻ ͜Εඪ४ೖྗʹೖྗ͞Εͨ ໊લʹ)FMMPΛ͚ͭͯฦ͢ྫɻ ς ε τ
ϑϥάͷॻ͖ํ 20 Ϩγϐ
21 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 } $-*ϓϩάϥϜͷΦϓγϣϯߏମ Φϓγϣϯղੳؔ ຊ ମ ࣮
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, }, }, } Φϓγϣϯղੳؔͷςετ ೖྗͱͳΔҾΛఆٛͯ͠ ظ͢ΔΦϓγϣϯߏମΛॻ͘ ς ε τ
23 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)) } } } ςʔϒϧۦಈͰςετ͢Δ ς ε τ
ϑϥάͷόϦσʔγϣϯͷॻ͖ํ 24 Ϩγϐ
25 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 } $-*ϓϩάϥϜͷΦϓγϣϯߏମ ΦϓγϣϯղੳؔΛ࣮͢Δ όϦσʔγϣϯ͢Δ ຊ ମ ࣮
26 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") } } } ςʔϒϧۦಈͰςετ͢Δ ඞਢͷҾ͕ͳ͚Ε ΤϥʔʹͳΔ͜ͱΛظ͢Δ ς ε τ
ϔϧϓͷॻ͖ํ 27 Ϩγϐ
28 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 } ຊ ମ ࣮ Φϓγϣϯղੳؔ ϔϧϓͷදࣔॲཧ
29 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()) } } ς ε τ ϔϧϓΛදࣔͤ͞Δ ظͱͷҰகΛ֬ೝ
αϒίϚϯυͷॻ͖ํ 30 Ϩγϐ
31 type SubCommand struct { Name string Description string Run
Command } ຊ ମ ࣮ αϒίϚϯυͷ໊લ αϒίϚϯυߏମ αϒίϚϯυͷઆ໌ αϒίϚϯυͷॲཧ
32 Φϓγϣϯͷղੳ αϒίϚϯυͷ͋ΔίϚϯυΛએݴ αϒίϚϯυͷىಈ ϔϧϓͷ४උ 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 } } ຊ ମ ࣮
33 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 }, } ς ε τ ِͷαϒίϚϯυΛ༻ҙ ِͷαϒίϚϯυΛ༻ҙ
34 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()) } } ς ε τ ͋ͱ͍ͭ௨Γςετ
͍͑ͨ͜ͱ 35 w $-*πʔϧΛͭ͘Δͱ͖ϑϨʔϜϫʔΫΛΘͳ͍ બࢶʹΛ͚Α͏ w গ͠ଥڠ͢Εɺඪ४ϥΠϒϥϦͱ͍ίʔυ͚ͩ ʢߦ͙Β͍ʣͰ͍͍ͨͯͷ͜ͱ࣮ݱͰ͖Δ w ϑϨʔϜϫʔΫΛΘ͍ͣίʔυͰ࣮ݱͰ͖Ε
୯ҐͰͷϝϯςφϯεϑϦʔʹͰ͖Δ
αʔυύʔςΟϥΠϒϥϦͷϨγϐ 36 Ͳ͏ͯ͠ଥڠͰ͖ͳ͍ͱ͖ͷͨΊͷ ൃ ද Ͱ ׂ Ѫ
37 ͜Ε͔Βհ͢ΔϨγϐɺඪ४ϥΠϒϥϦ͚ͩͩͱ ίʔυ͕͘ͳͬͯ͠·͏ͨΊंྠͷ࠶ൃ໌ͷײ͕ ग़͖ͯͯ͠·͏ͷʹͳ͍ͬͯΔɻ ൃ ද Ͱ ׂ Ѫ
γΣϧิͷॻ͖ํ 38 Ϩγϐ ൃ ද Ͱ ׂ Ѫ
39 ඪ४ϥΠϒϥϦ͚ͩͰ࣮ݱ͢Δͱߦ΄Ͳ͔͔Δ ʢαϯϓϧίʔυͷSFDJQFΛࢀরʣɻ ସͱͯ͑͠ΔϥΠϒϥϦʢOPUϑϨʔϜϫʔΫʣɿ w KFTTFWELHP fl BHT w TBHP
fl BHDNQM w ʜ ͍ํΛΔʹͦΕͧΕͷ3&"%.&Λࢀরͯ͠΄͍͠ɻ ൃ ද Ͱ ׂ Ѫ
ಉ͡Φϓγϣϯͷෳճͷग़ݱͷରԠ 40 Ϩγϐ ൃ ද Ͱ ׂ Ѫ
41 ༻Ͱ͖ΔϥΠϒϥϦʢOPUϑϨʔϜϫʔΫʣɿ w KFTTFWELHP fl BHT w BMFY fl JOUHPBSH
w ʜ ͍ํΛΔʹͦΕͧΕͷ3&"%.&Λࢀরͯ͠΄͍͠ɻ ൃ ද Ͱ ׂ Ѫ