Slide 1

Slide 1 text

WHAT I TALK ABOUT
 WHEN I TALK ABOUT WRITING CLI TOOL BY GOLANG

Slide 2

Slide 2 text

TAICHI NAKASHIMA @deeeet @tcnksm

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

WHAT I TALK ABOUT
 WHEN I TALK ABOUT WRITING CLI TOOL BY GOLANG

Slide 8

Slide 8 text

THINK AGAIN Why I choose Golang for CLI tool ?

Slide 9

Slide 9 text

Easy to Cross-Compile Easy to Distribute Performance

Slide 10

Slide 10 text

Cross-Compile Compile to windows exe from OS X $ GOOS=windows GOARCH=amd64 go build -o hello.exe

Slide 11

Slide 11 text

$ curl -L https://github.com/docker/machine/releases/download/ v0.3.0/docker-machine_darwin-amd64 \ > /usr/local/bin/docker-machine $ chmod +x /usr/local/bin/docker-machine Easy to Distribute e.g., docker/docker-machine

Slide 12

Slide 12 text

$ time heroku apps >/dev/null real 0m3.830s $ time hk apps >/dev/null real 0m0.785s e.g., heroku/hk Performance

Slide 13

Slide 13 text

Easy to Cross-Compile Easy to Distribute Performance

Slide 14

Slide 14 text

What is GOOD CLI tool ?

Slide 15

Slide 15 text

Do ONE Thing Well Intuitive UI/UX Play with Others Helpful Configurable Painless Installation Maintainable

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

How to do that by Golang ?

Slide 18

Slide 18 text

Do ONE Thing Well Intuitive UI/UX Play with Others Helpful Configurable Painless Installation Maintainable

Slide 19

Slide 19 text

Do ONE thing well Don’t worry, your tool will be more and more complex (e.g., tcnksm/ghr) flags.StringVar(&githubAPIOpts.OwnerName, []string{"u", "-username"}, "", "") flags.StringVar(&githubAPIOpts.RepoName, []string{"r", "-repository"}, "", "") flags.StringVar(&githubAPIOpts.Token, []string{"t", "-token"}, "", "") flags.StringVar(&githubAPIOpts.Commitish, []string{"c", "-commitish"}, "", "") flags.BoolVar(&githubAPIOpts.Draft, []string{"-draft"}, false, "") flags.BoolVar(&githubAPIOpts.Prerelease, []string{"-prerelease"}, false, "") flags.IntVar(&ghrOpts.Parallel, []string{"p", "-parallel"}, -1, "") flags.BoolVar(&ghrOpts.Replace, []string{"-replace"}, false, "") flags.BoolVar(&ghrOpts.Delete, []string{"-delete"}, false, "") flags.BoolVar(&stat, []string{"-stat"}, false, “") version := flags.Bool([]string{"v", "-version"}, false, "") debug := flags.Bool([]string{"-debug"}, false, "")

Slide 20

Slide 20 text

Do ONE thing well `ls` has different 37 flags ls [-ABCFGHLOPRSTUW@abcdefghiklmnopqrstuwx1] [file ...]

Slide 21

Slide 21 text

Do ONE Thing Well Intuitive UI/UX Play with Others Helpful Configurable Painless Installation Maintainable

Slide 22

Slide 22 text

LONG HISTORY Should follow its convention/standard

Slide 23

Slide 23 text

Intuitive UI/UX What is CLI UI convention/standard ? # Executable + flags + args pattern $ grep —i -C 4 "some string" /tmp # Executable + commands + args pattern $ git --no-pager push -v origin master

Slide 24

Slide 24 text

FLAG PATTERN flag docker/mflag ogier/pflag jessevdk/go-flags

Slide 25

Slide 25 text

FLAG PATTERN flag docker/mflag ogier/pflag jessevdk/go-flags

Slide 26

Slide 26 text

Intuitive UI/UX e.g., docker/mflag // Easy to provide short option and long option version := mflag.Bool([]string{"v", "-version"}, false, "")

Slide 27

Slide 27 text

COMMANDS PATTERN codegangsta/cli … docker/xxx, cloudfoudry/cli mitchellh/cli … hashicorp/xxx, tcnksm/gcli spf13/cobra … hugo docopt/docopt.go

Slide 28

Slide 28 text

COMMANDS PATTERN codegangsta/cli … docker/xxx, cloudfoudry/cli mitchellh/cli … hashicorp/xxx, tcnksm/gcli spf13/cobra … hugo docopt/docopt.go

Slide 29

Slide 29 text

Intuitive UI/UX e.g., mitchellh/cli type Command interface { // Help should return long-form help text that includes // the command-line usage Help() string // Synopsis should return a one-line, short synopsis // of the command. Synopsis() string // Run should run the actual command with the given CLI // instance and command-line arguments. Run(args []string) int }

Slide 30

Slide 30 text

Exit Code Stdin, Stdout, Stderr

Slide 31

Slide 31 text

Do ONE Thing Well Intuitive UI/UX Play with Others Helpful Configurable Painless Installation Maintainable

Slide 32

Slide 32 text

Exit Code Stdin, Stdout, Stderr

Slide 33

Slide 33 text

Play with Others Other systems (softwares) may check error without parting the output (e.g., appc/spec) $ actool validate ./image.json $ echo $? 1 # e.g., CI system can detect something wrong and notify

Slide 34

Slide 34 text

Play with Others We can use os.Exit() // Exit causes the current program to exit with // the given status code. // Conventionally, code zero indicates success, // non-zero an error. // The program terminates immediately; deferred functions are // not run. func Exit(code int) { syscall.Exit(code) }

Slide 35

Slide 35 text

Play with Others When something wrong, call os.Exist with non zero number func main() { filePath := args[1] out, err := somethingCool(filepath) if err != nil { fmt.Printf("Error: %s\n", err.Error()) os.Exit(1) } fmt.Printf("Out: %s\n", out) os.Exit(0) }

Slide 36

Slide 36 text

Play with Others We may need defer for some cleaning up func main() { filePath := args[1] out, err := somethingCool(filepath) if err != nil { fmt.Printf("Error: %s\n", err.Error()) os.Exit(1) } defer cleanup() fmt.Printf("Out: %s\n", out) os.Exit(0) }

Slide 37

Slide 37 text

Play with Others But deferred functions are not run if we call os.Exit() // Exit causes the current program to exit with // the given status code. // Conventionally, code zero indicates success, // non-zero an error. // The program terminates immediately; deferred functions are // not run. func Exit(code int) { syscall.Exit(code) }

Slide 38

Slide 38 text

Play with Others Call os.Exit only on main() and create function return exit code func main() { os.Exit(Run(os.Args[1:])) } func Run(args []string) int { filePath := args[0] out, err := somethingCool(filePath) if err != nil { fmt.Printf("Error: %s\n", err.Error()) return 1 } defer cleanup() fmt.Printf("Out: %s\n", out) return 0 }

Slide 39

Slide 39 text

Exit Code Stdin, Stdout, Stderr

Slide 40

Slide 40 text

Exit Code Stdin, Stdout, Stderr

Slide 41

Slide 41 text

Play with Others “ Write programs to handle text streams, because that is a universal interface. - Doug McIlroy func main() { os.Exit(Run(os.Args[1:])) } func Run(args []string) int { filePath := args[0] out, err := somethingCool(filePath) if err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) return 1 } defer cleanup() fmt.Fprintf(os.Stdout,"Out: %s\n”, out) return 0 }

Slide 42

Slide 42 text

Play with Others “ Write programs to handle text streams, because that is a universal interface. - Doug McIlroy // os.Stdout/os.Stderr is io.Writer // So mostly configurable from other function // flag pacakge flag.SetOutput(os.Stderr) // log package log.SetOutput(os.Stderr)

Slide 43

Slide 43 text

Do ONE Thing Well Intuitive UI/UX Play with Others Helpful Configurable Painless Installation Maintainable

Slide 44

Slide 44 text

README.md Usage Man page

Slide 45

Slide 45 text

README.md Usage Man page

Slide 46

Slide 46 text

Helpful Standard flag package output is … $ ghr -h Usage of ghr: -p=0: Parallelization factor -debug=false: Run as debug mode -username=“": Github username -quiet=false: be quieter in some situations -version=false: Print version information and quit

Slide 47

Slide 47 text

Helpful Write by yourself, more clear usage flag.Usage = func() { fmt.Fprint(os.Stderr, helpText) } var helpText = ` Usage: ghr [options] args Command ghr is … Options: -debug Run as Debug mode… `

Slide 48

Slide 48 text

Helpful Go1.5, help out will be a little bit better $ ghr -h Usage of call: -p int Parallelization factor -debug run as debug mode -username string GitHub user name -quiet be quieter in some situations -version Print version information and quit.

Slide 49

Slide 49 text

Do ONE Thing Well Intuitive UI/UX Play with Others Helpful Configurable Painless Installation Maintainable

Slide 50

Slide 50 text

json yaml toml hashicorp/hcl

Slide 51

Slide 51 text

NO STRONG OPINION Just think about playing with others Thinks about your team preference

Slide 52

Slide 52 text

USE ONE CENTRAL CONF Keep it in one place

Slide 53

Slide 53 text

Configurable Central one place will be mostly $HOME directory, and you can use user.Current() var defaultCfgName = ".xxxrc" func main() { userInfo, err := user.Current() if err != nil { panic(err) } cfg := filepath.Join(userInfo.HomeDir, defaultCfgName) // … }

Slide 54

Slide 54 text

Configurable It depends on cgo … // src/os/user/lookup_unix.go u := &User{ Uid: strconv.Itoa(int(pwd.pw_uid)), Gid: strconv.Itoa(int(pwd.pw_gid)), Username: C.GoString(pwd.pw_name), Name: C.GoString(pwd.pw_gecos), HomeDir: C.GoString(pwd.pw_dir), }

Slide 55

Slide 55 text

Configurable Provide platform agnostic configuration file, use mitchellh/home-dir import "github.com/mitchellh/go-homedir" var defaultCfgName = ".xxxrc" func main() { home, err := homedir.Dir() if err != nil { panic(err) } cfg := filepath.Join(home, defaultCfgName) // … }

Slide 56

Slide 56 text

Do ONE Thing Well Intuitive UI/UX Play with Others Helpful Configurable Painless Installation Maintainable

Slide 57

Slide 57 text

Painless Installation Provide Copy & Paste -able one line command (e.g., on README.md) $ curl -L https://github.com/docker/machine/releases/download/ v0.3.0/docker-machine_darwin-amd64 \ > /usr/local/bin/docker-machine $ chmod +x /usr/local/bin/docker-machine

Slide 58

Slide 58 text

Painless Installation Write homebrew formula, it’s easy class Ghr < Formula homepage "https://github.com/tcnksm/ghr" version 'v0.4.0' url "https://github.com/tcnksm/ghr/releases/download/v0.4.0/ ghr_v0.4.0_darwin_amd64.zip" sha1 “1aa90dde58c3b15dfd1e2f90cbaa317ee4752855” end

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

Painless Installation Upload cross-compiled binaries in parallel on Github Releases $ ghr v0.1.0 pkg/ --> Uploading: pkg/0.1.0_SHASUMS --> Uploading: pkg/ghr_0.1.0_darwin_386.zip --> Uploading: pkg/ghr_0.1.0_darwin_amd64.zip --> Uploading: pkg/ghr_0.1.0_linux_386.zip --> Uploading: pkg/ghr_0.1.0_linux_amd64.zip --> Uploading: pkg/ghr_0.1.0_windows_386.zip --> Uploading: pkg/ghr_0.1.0_windows_amd64.zip

Slide 61

Slide 61 text

git push hook compile ghr dev

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

Do ONE Thing Well Intuitive UI/UX Play with Others Helpful Configurable Painless Installation Maintainable

Slide 64

Slide 64 text

Testing Updating

Slide 65

Slide 65 text

Testing Updating

Slide 66

Slide 66 text

Testing is important for fixing bug and adding new feature Maintainable func main() { os.Exit(Run(os.Args[1:])) } func Run(args []string) int { filePath := args[0] out, err := somethingCool(filePath) if err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) return 1 } defer cleanup() fmt.Fprintf(os.Stdout,"Out: %s\n”, out) return 0 }

Slide 67

Slide 67 text

os.Exit() os.Args() main() Run() user APIs user os.Stdout os.Stderr [] string int

Slide 68

Slide 68 text

os.Exit() os.Args() main() Run() user APIs user os.Stdout os.Stderr [] string int

Slide 69

Slide 69 text

os.Exit() os.Args() main() Run() user APIs user os.Stdout os.Stderr [] string int

Slide 70

Slide 70 text

Maintainable func TestRun(t *testing.T) { // test console output and status code by argument } Test for console output and status code by arguments

Slide 71

Slide 71 text

os.Exit() os.Args() main() Run() user APIs user os.Stdout os.Stderr [] string int

Slide 72

Slide 72 text

os.Args() os.Stdout os.Stderr main() Run() user [] string io.Writer APIs int os.Exit()

Slide 73

Slide 73 text

[]string bytes.Buffer Run() user [] string io.Writer APIs int TestRun()

Slide 74

Slide 74 text

Use io.Writer type cli struct { outWriter, errWriter io.Writer } func (c *cli) Run(args []string) int { file := args[0] out, err := somethingCool(file) if err != nil { fmt.Fprintf(c.errWriter, "Error: %s\n", err.Error()) return 1 } defer cleanup() fmt.Fprintf(c.outWriter,"Out: %s\n", out) return 0 } Maintainable

Slide 75

Slide 75 text

Use io.Writer func main() { cli := &cli{outWriter: os.Stdout, errWriter: os.Stderr} os.Exit(cli.Run(os.Args[1:])) } Maintainable

Slide 76

Slide 76 text

[]string bytes.Buffer Run() user [] string io.Writer APIs int TestRun()

Slide 77

Slide 77 text

Use bytes.Buffer func TestRun(t *testing.T) { outWriter, errWriter := new(bytes.Buffer), new(bytes.Buffer) cli := &cli{outWriter: outWriter, errWriter: errWriter} args := strings.Split("./tool xxx.txt", " ") status := cli.Run(args) if status != 0 { t.Errorf("expected %d to eq %d", status, 0) } expected := "cool" if !strings.Contains(outWriter.String(), expected) { t.Errorf("expected %q to eq %q", outWriter.String(), expected) } } Maintainable

Slide 78

Slide 78 text

Testing Updating

Slide 79

Slide 79 text

Testing Updating

Slide 80

Slide 80 text

UPDATE IS IMPORTANT More features & more performance (vulnerability…)

Slide 81

Slide 81 text

DIFFICULT TO ENCOURAGE USER Once users install it, they may not update… Same as iOS or Android Application

Slide 82

Slide 82 text

TCNKSM/GO-LATEST Simple way to check version is latest or not

Slide 83

Slide 83 text

Maintainable Encourage user to upgrade to latest version // Simplest way is using tags on GitHub githubTag := &latest.GithubTag{ Owner: "tcnksm", Repository: "ghr", } res, _ := latest.Check(githubTag, "0.1.0") if res.Outdated { fmt.Printf("0.1.0 is not latest, you should upgrade to %s”, res.Current) }

Slide 84

Slide 84 text

Maintainable Encourage user to upgrade to latest version $ ghr --version gar version v0.3.1, build aded5ca Your version is out of date! The latest version is v0.4.0

Slide 85

Slide 85 text

Do ONE Thing Well Intuitive UI/UX Play with Others Helpful Configurable Painless Installation Maintainable

Slide 86

Slide 86 text

OK, how to start ?

Slide 87

Slide 87 text

TCNKSM/GCLI Generates the codes and its directory structure you need to start building CLI tool right out of the box. (Formerly `cli-init`, re-wrote everything from scratch)

Slide 88

Slide 88 text

TCNKSM/GCLI Now you can choose your favorite cli framework # Example, todo CLI application which has add, list # and delete command with mitchellh/cli framework, $ gcli new -F mitchellh_cli -c add -c list -c delete todo

Slide 89

Slide 89 text

TCNKSM/GCLI Now you can choose your favorite cli framework $ tree todo ├── CHANGELOG.md ├── README.md ├── cli.go ├── command │ ├── add.go │ ├── add_test.go │ ├── delete.go │ ├── delete_test.go │ ├── list.go │ ├── list_test.go │ └── meta.go ├── commands.go ├── main.go └── version.go

Slide 90

Slide 90 text

Do ONE Thing Well Intuitive UI/UX Play with Others Helpful Configurable Painless Installation Maintainable

Slide 91

Slide 91 text

@deeeet