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
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
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
2.3k
仕様漏れ実装漏れをなくすトレーサビリティAI基盤のご紹介
orgachem
PRO
13
9.1k
要求定義・仕様記述・設計・検証の手引き - 理論から学ぶ明確で統一された成果物定義
orgachem
PRO
35
21k
DeNA での思い出 / Memories at DeNA
orgachem
PRO
7
3.6k
状態遷移図を書こう / Sequence Chart vs State Diagram
orgachem
PRO
4
750
テストケースの名前はどうつけるべきか?
orgachem
PRO
2
910
欠陥を早期に発見するための 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
Lessons from Spec-Driven Development
simas
PRO
0
220
Semantic Version 単位で戦略を柔軟に変えて、パッケージアップデートを自動化する
daitasu
1
310
AI駆動開発を妨げる技術的負債の解消アプローチ / ai-refactoring-approach
minodriven
15
7.3k
AI 輔助遺留系統現代化的經驗分享
jame2408
1
1k
Make SRE Operations Easier with Azure SRE Agent
kkamegawa
0
8.4k
Hunting Vulnerabilities in Symfony with LLMs
vinceamstoutz
0
560
The NotImplementedError Problem in Ruby
koic
1
960
Honoでのサプライチェーン侵害対策 〜 3つのライブラリに学ぶ
yusukebe
7
1.5k
Javaの型とAI時代に型が大事な理由 / java types and type in AI era
kishida
2
150
不変条件と整合性境界—ビジネスが決める設計判断と実現パターン / Invariants and Consistency Boundaries
nrslib
14
5.9k
ローカルLLMを使ってB2Bサービスを作っていての学び
yaotti
0
220
AIで効率化できた業務・日常
ochtum
0
150
Featured
See All Featured
The World Runs on Bad Software
bkeepers
PRO
72
12k
Color Theory Basics | Prateek | Gurzu
gurzu
0
370
Noah Learner - AI + Me: how we built a GSC Bulk Export data pipeline
techseoconnect
PRO
0
200
My Coaching Mixtape
mlcsv
0
160
Making Projects Easy
brettharned
120
6.7k
Done Done
chrislema
186
16k
DevOps and Value Stream Thinking: Enabling flow, efficiency and business value
helenjbeal
1
250
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
38
2.9k
Producing Creativity
orderedlist
PRO
348
40k
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
350
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
330
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.5k
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&"%.&Λࢀরͯ͠΄͍͠ɻ ൃ ද Ͱ ׂ Ѫ