DEV Community

loading...
Cover image for สร้าง CLI app ด้วย spf13/cobra

สร้าง CLI app ด้วย spf13/cobra

Atthavit Wannasakwong
・3 min read

CLI app แบบง่ายๆด้วย spf13/cobra คำสั่งหลักๆที่จะมีคือ

  • cli-covid19 today - แสดงข้อมูลวันนี้
  • cli-covid19 plot (confirmed|deaths|...) - วาดกราฟ 30 วันที่ผ่านมา โดยรับข้อมูลที่เราสนใจ เช่น confirmed, deaths, recovered


ส่วนประกอบ

  • spf13/cobra v1.0.0 - เป็น lib หลักในการทำ cli
  • guptarohit/asciigraph v0.4.2 - ใช้ในการสร้าง ascii graph ให้เราดูกราฟแบบเป็น text ได้
  • Thai Covid-19 API - Covid-19 api ของไทย ใช้ในการดึงข้อมูล

โครงสร้าง

เราจะทำตามใน README ของ cobra เลย คือ

  ▾ cli-covid19/
    ▾ cmd/
        root.go
        plot.go
        today.go
    ▾ covid19/
        covid19.go
      main.go

จริงๆ cobra มี tool ที่สามารถ generate โค้ดขึ้นมาได้ แต่ในโพสนี้เราจะสร้างไฟล์ขึ้นมาเอง ไม่ได้ใช้ code generator ใครสนใจอ่านเพิ่มเติมที่นี่

ดึงข้อมูล

ส่วนนี้ไม่ใช่ส่วนสำคัญของโพสนี้ อ่านผ่านๆก็ได้ครับ เป็น function ให้ CLI เราใช้ในการดึงข้อมูลจาก API เฉยๆ

สร้างไฟล์ covid19/covid19.go

package covid19

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strings"
    "time"

    "github.com/guptarohit/asciigraph"
)

type Type string

var (
    Confirmed    Type = "confirmed"
    Deaths       Type = "deaths"
    Hospitalized Type = "hospitalized"
    Recovered    Type = "recovered"
    Types             = []Type{
        Confirmed,
        Deaths,
        Hospitalized,
        Recovered,
    }
    URL = "https://covid19.th-stat.com/api/open/"
)

type Data struct {
    Confirmed       int `json:"Confirmed"`
    Deaths          int `json:"Deaths"`
    Hospitalized    int `json:"Hospitalized"`
    Recovered       int `json:"Recovered"`
    NewConfirmed    int `json:"NewConfirmed"`
    NewRecovered    int `json:"NewRecovered"`
    NewHospitalized int `json:"NewHospitalized"`
    NewDeaths       int `json:"NewDeaths"`
}

func PrintToday() error {
    resp, err := http.Get(URL + "today")
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    var data Data
    if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
        return err
    }
    fmt.Printf(
        "%s - New Confirmed: %d, New Deaths: %d, New Recovered: %d, Total Confirmed: %d\n",
        time.Now().Format("Jan 2 2006"),
        data.NewConfirmed,
        data.NewDeaths,
        data.NewRecovered,
        data.Confirmed,
    )
    return nil
}

func Plot(t Type) error {
    resp, err := http.Get(URL + "timeline")
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    var data struct {
        List []Data `json:"Data"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
        return err
    }

    days := 30
    points := make([]float64, days)
    length := len(data.List)
    for i := 0; i < days; i++ {
        var p int
        d := data.List[length-days+i]
        switch t {
        case Confirmed:
            p = d.Confirmed
        case Deaths:
            p = d.Deaths
        case Hospitalized:
            p = d.Hospitalized
        case Recovered:
            p = d.Recovered
        }
        points[i] = float64(p)
    }
    graph := asciigraph.Plot(points, asciigraph.Height(10))
    fmt.Println(strings.ToUpper(string(t)))
    fmt.Println(graph)
    return nil
}
  • มี 2 function ตาม command ที่เราออกแบบไว้เลยคือ
    • PrintToday ใช้แสดงข้อมูลของวันนี้
    • Plot(Type) ใช้วาดกราฟของข้อมูลย้อนหลัง 30 วัน
  • มี struct ไว้ใช้ unmarshal json ที่ได้จาก API
  • มีค่าคงที่ต่างๆที่เดี๋ยวจะใช้ตอนทำ cli

CLI

เริ่มจากสร้าง cmd/root.go เพื่อเป็น command หลักของเรา

package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use: "cli-covid19",
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}
  • อันนี้ไม่มีอะไรมาก แค่ Use เป็นชื่อของ command เรา และมี Execute() เป็น function ไว้รัน command root

สร้าง main.go เพื่อเรียก function Execute ที่สร้างไว้ก่อนหน้า

package main

import "github.com/atthavit/myblog/cli-covid19/cmd"

func main() {
    cmd.Execute()
}

สร้างไฟล์ cmd/today.go เพื่อเป็นคำสั่งลูกแบบง่ายๆอันแรก

package cmd

import (
    "github.com/atthavit/myblog/cli-covid19/covid19"
    "github.com/spf13/cobra"
)

func init() {
    rootCmd.AddCommand(todayCmd)
}

var todayCmd = &cobra.Command{
    Use:   "today",
    Short: "Print today's stats",
    RunE: func(cmd *cobra.Command, args []string) error {
        return covid19.PrintToday()
    },
}
  • RunE เป็น function ที่จะถูก execute ตอนเรารัน command นั้นๆ
  • E ท้าย RunE หมายถึง function เราจะ return error ถ้าไม่อยากให้ return อยาก log เองก็ไม่ต้องใช้ E ก็ได้

สร้างไฟล์ cmd/plot.go เพื่อวาดกราฟ โดยใช้ functionที่เราได้เขียนไว้แล้ว

package cmd

import (
    "github.com/atthavit/myblog/cli-covid19/covid19"
    "github.com/spf13/cobra"
)

var selectedType covid19.Type

func init() {
    rootCmd.AddCommand(plotCmd)

    for _, t := range covid19.Types {
        t := t
        plotCmd.AddCommand(&cobra.Command{
            Use: string(t),
            Run: func(cmd *cobra.Command, args []string) {
                selectedType = t
            },
        })
    }
}

var plotCmd = &cobra.Command{
    Use:   "plot",
    Short: "Plot a 30-day graph",
    PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
        return covid19.Plot(selectedType)
    },
}
  • ใน init() เรา loop สร้าง subcommand ให้ plotCmd หน้าที่ของมันมีแค่เซตค่าตัวแปรเฉยๆ จริงๆตรงนี้ถ้าใช้เป็น Arguments น่าจะง่ายกว่า
  • ต้องมี t := t เพราะเราใช้ t ใน function Run และค่าของ t จะเปลี่ยนไปเรื่อยๆในแต่ละ loop อ่านเพิ่มเติมที่นี่ ตรงนี้ต้องระวังดีๆ
  • ใช้ PersistentPostRunE แทน RunE เพราะว่าเราต้องให้ subcommand รันก่อน เพื่อเซตค่าของตัวแปรที่เราต้องใช้ แล้วค่อยรัน function นี้
  • ตรงนี้ไม่ได้ใช้ RunE เหมือน today เพราะเรามี subcommand ถ้าเรารัน cli-covid19 plot confirmed RunE ในนี้จะไม่ถูกรัน

Build

หลังจากเขียนโค้ดเสร็จหมดแล้ว เราก็ build ด้วยคำสั่ง

go build

เราก็จะได้ไฟล์ cli-covid19 ออกมา
Alt Text

ดูโค้ดเต็มๆที่นี่

Discussion (0)