package main import ( "fmt" "io" "io/ioutil" "log" "net/http" "os" "strings" kingpin "gopkg.in/alecthomas/kingpin.v2" ) // VERSION The version release const VERSION = "0.0.4" // AUTHOR The author name const AUTHOR = "Elia el Lazkani" // Create a list of constants to figure out // the command line flags and translate them // into uri flags. const ( METRIC = 1 << iota USCS METERSECOND ZERO ONE TWO IGNOREUSERAGENT FOLLOWLINK NARROW QUIET SUPERQUIET NOCOLORS ADDFRAME MIDTRANSPARENCY FORMAT LOCATION LANGUAGE PNG VTWO ) // Create a list of constants to define // uri switches by flag. const ( MetricSwitch = "m" UscsSwitch = "u" MeterSecondSwitch = "M" ZeroSwitch = "0" OneSwitch = "1" TwoSwitch = "2" IgnoreUserAgentSwitch = "A" FollowLinkSwitch = "F" NarrowSwitch = "n" QuietSwitch = "q" SuperQuietSwitch = "Q" NoColorsSwitch = "T" AddFrameSwitch = "p" MidTransparencySwitch = "T" ) // We create a weather struct with the url type weather struct { url string } // `weather` method to handle creating new requests with proper headers func (w weather) newRequest(headers map[string]string) *http.Request { req, err := http.NewRequest("GET", w.url, nil) if err != nil { log.Fatal("Error reading request. ", err) } return w.formatHeader(req, headers) } // `weather` method to format the headers and inject required ones func (w weather) formatHeader(req *http.Request, headers map[string]string) *http.Request { req.Header.Set("Content-Type", "text/plain; charset=utf-8") // I had to read the wttr.in code to figure this one out. // It wasn't Golang, it was 2 wasted hours that I won't be getting back. req.Header.Set("User-Agent", "curl") for key, value := range headers { req.Header.Add(key, value) } return req } // `weather` method to create a client and get a weather response // This method handles printing or downloading the weather func (w weather) get(req *http.Request, download bool) { client := &http.Client{} client.Jar = nil resp, err := client.Do(req) if err != nil { log.Fatal("Error reading response. ", err) } defer resp.Body.Close() if download { w.download(resp) } else { w.print(resp) } } // `weather` method to print the weather func (w weather) print(resp *http.Response) { body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatal("Error reading body. ", err) } fmt.Printf("%s\n", body) } // `weather` method to download the weather func (w weather) download(resp *http.Response) { splitURL := strings.Split(w.url, "/") filename := splitURL[len(splitURL)-1] f, err := os.Create(filename) if err != nil { return } defer f.Close() _, err = io.Copy(f, resp.Body) } // Wraper function around the weather struct func getWheather(url string, headers map[string]string, download bool) { w := weather{url} req := w.newRequest(headers) w.get(req, download) } // Define a command parameters struct. // This will hold all the configurations // set by the command line parameters provided. type cmdParams struct { Flags int Transparency int Format string Location string Language string } // Create a method to figure out the command parameters // provided and translate them into uri parameters. // This will figure out if we need to download the PNG image // as well. func (cmdP *cmdParams) generateParamFormat() (string, bool, error) { var params []string var affix []string var download bool affix = append(affix, "?") // If --format is specified, let's not bother and simply return it if cmdP.Flags&FORMAT == FORMAT { return cmdP.Format, false, nil } if cmdP.Flags&METRIC == METRIC { params = append(params, MetricSwitch) } if cmdP.Flags&USCS == USCS { params = append(params, UscsSwitch) } if cmdP.Flags&METERSECOND == METERSECOND { params = append(params, MeterSecondSwitch) } if cmdP.Flags&ZERO == ZERO { params = append(params, ZeroSwitch) } if cmdP.Flags&ONE == ONE { params = append(params, OneSwitch) } if cmdP.Flags&TWO == TWO { params = append(params, TwoSwitch) } if cmdP.Flags&IGNOREUSERAGENT == IGNOREUSERAGENT { params = append(params, IgnoreUserAgentSwitch) } if cmdP.Flags&FOLLOWLINK == FOLLOWLINK { params = append(params, FollowLinkSwitch) } if cmdP.Flags&NARROW == NARROW { params = append(params, NarrowSwitch) } if cmdP.Flags&QUIET == QUIET { params = append(params, QuietSwitch) } if cmdP.Flags&SUPERQUIET == SUPERQUIET { params = append(params, SuperQuietSwitch) } if cmdP.Flags&NOCOLORS == NOCOLORS { params = append(params, NoColorsSwitch) } if cmdP.Flags&ADDFRAME == ADDFRAME { params = append(params, AddFrameSwitch) } if cmdP.Flags&PNG == PNG { affix[0] = "_" if cmdP.Flags&MIDTRANSPARENCY == MIDTRANSPARENCY { params = append(params, MidTransparencySwitch) } if cmdP.Transparency >= 0 && cmdP.Transparency <= 100 { params = append(params, strings.Join([]string{"_transparency=", string(cmdP.Transparency)}, "")) } params = append(params, ".png") download = true } params = append(affix, strings.Join(params, "")) return strings.Join(params, ""), download, nil } // Defining the command-line interface var ( app = kingpin.New("go-cmw", "A small terminal wrapper around the wttr.in weather endpoint.") metric = app.Flag("metric", "Display weather in metric").Short('m').Default("false").Bool() uscs = app.Flag("uscs", "Display weather in imperial").Short('u').Default("false").Bool() meterSecond = app.Flag("meter-second", "Display wind in m/s").Short('M').Default("false").Bool() zero = app.Flag("zero", "Show the weather now").Short('z').Default("false").Bool() one = app.Flag("one", "Show the weather for one day").Short('o').Default("false").Bool() two = app.Flag("two", "Show the weather for two days").Short('w').Default("false").Bool() ignoreUserAgent = app.Flag("ignore-user-agent", "Request ignoring the user agent").Short('A').Default("false").Bool() followLink = app.Flag("follow-link", "Follow link redirect").Short('F').Default("true").Bool() narrow = app.Flag("narrow", "Display weather in narrow view").Short('n').Default("false").Bool() quiet = app.Flag("quiet", "Add the quiet flag").Short('q').Default("false").Bool() superQuiet = app.Flag("super-quiet", "Add the super quiet flag").Short('Q').Default("false").Bool() noColors = app.Flag("no-colors", "Disable displaying colors (always enabled on windows").Short('N').Default("false").Bool() addFrame = app.Flag("add-frame", "Add a frame to the output").Short('p').Default("false").Bool() midTransparency = app.Flag("mid-transparency", "Enable mid-transparency (PNG only)").Short('T').Default("false").Bool() png = app.Flag("png", "Download a weather PNG image").Short('P').Default("false").Bool() v2 = app.Flag("v2", "Use the v2 endpoint").Default("false").Bool() transparency = app.Flag("transparency", "Set transparency level (0-100) (PNG only)").Short('t').Default("0").Int() format = app.Flag("format", "Query format (overrides everything else)").Short('f').OverrideDefaultFromEnvar("GO_CMW_FORMAT").String() location = app.Flag("location", "Specify explicite location").Short('L').OverrideDefaultFromEnvar("GO_CMW_LOCATION").String() language = app.Flag("language", "Specify explicite language").Short('l').OverrideDefaultFromEnvar("GO_CMW_LANGUAGE").String() extraInformation = app.Flag("extra-information", "Print extra information").Default("false").Bool() ) // Special help menu func printExtraInformation() { eInfo := ` Supported Location Types ------------------------ City name: Paris Unicode name: Москва Airport code (3 letters): muc Domain name: @stackoverflow.com Area code: 94107 GPS coordinates: -78.46,106.79 Special Location ---------------- Moon phase (add ,+US or ,+France for these cities): moon Moon phase for a date: moon@2016-10-25 Supported languages ------------------- Supported: af da de el et fr fa hu id it nb nl pl pt-br ro ru tr uk vi ` println(eInfo) } // Create a function to parse all the command line parameters // provided and save them in the parameter struct. func flagParser(cmdP *cmdParams) { app.Version(VERSION) app.Author(AUTHOR) kingpin.MustParse(app.Parse(os.Args[1:])) if *extraInformation { printExtraInformation() os.Exit(0) } // Windows does not have color encoding // so let's make sure windows users are happy term := os.Getenv("TERM") if term == "" { *noColors = true } if *metric { cmdP.Flags += METRIC } if *uscs { cmdP.Flags += USCS } if *meterSecond { cmdP.Flags += METERSECOND } if *zero { cmdP.Flags += ZERO } if *one { cmdP.Flags += ONE } if *two { cmdP.Flags += TWO } if *ignoreUserAgent { cmdP.Flags += IGNOREUSERAGENT } if *followLink { cmdP.Flags += FOLLOWLINK } if *narrow { cmdP.Flags += NARROW } if *quiet { cmdP.Flags += QUIET } if *superQuiet { cmdP.Flags += SUPERQUIET } if *noColors { cmdP.Flags += NOCOLORS } if *addFrame { cmdP.Flags += ADDFRAME } if *midTransparency { cmdP.Flags += MIDTRANSPARENCY } if *transparency >= 0 && *transparency <= 100 { cmdP.Transparency = *transparency } if *png { cmdP.Flags += PNG } if *v2 { cmdP.Flags += VTWO } if *format != "" { cmdP.Flags += FORMAT cmdP.Format = *format } if *location != "" { cmdP.Flags += LOCATION cmdP.Location = *location } if *language != "" { cmdP.Flags += LANGUAGE cmdP.Language = *language } } // Create a function to generate the url that we'll be calling shortly. func generateURL(domain string, v2 bool, location string, lang string, affix string) ([]string, map[string]string) { var link []string var headers = make(map[string]string) link = append(link, "https://") if v2 { link = append(link, "v2.") } link = append(link, domain, "/") if location != "" { link = append(link, location) } if affix != "" { link = append(link, affix) } if lang != "" { headers["Accept-Language"] = lang } return link, headers } // This is the main function that glues everything together. func main() { var params cmdParams = cmdParams{} var domain string = "wttr.in" flagParser(¶ms) affix, download, _ := params.generateParamFormat() link, headers := generateURL(domain, params.Flags&VTWO == VTWO, params.Location, params.Language, affix) getWheather(strings.Join(link, ""), headers, download) }