How to implement OptionFlag with urfave/cli - go

Is there a way to define an OptionFlag with urfave/cli?
I'm looking for something that will look like this.
mycli --format json
mycli --format xml
I know I can use the StringFlag, but it would be great if I could have the --help show what are the valid options/values for this flag, so it is transparent for the end-user of mycli.
This way the Flag could also be validated against the options to inform the user he has provided an invalid value for this flag for example, Which ofcourse can also be done with the StringFlag, but would rather have something more sofisticated that does all of this.
I also filed an issue on the Github repository. Maybe it is a missing feature, which I would be happy to contribute with some guidance.
https://github.com/urfave/cli/issues/1154

I think you want the StringSliceFlag It allows you to define valid/default values for a flag.
package main
import (
"fmt"
"log"
"os"
"github.com/urfave/cli/v2"
)
func main() {
validMeetings := []string{"standup", "postmortem", "jourfix"}
meetings := cli.NewStringSlice(validMeetings...)
app := &cli.App{
Flags: []cli.Flag{
&cli.StringSliceFlag{
Value: meetings,
Name: "meeting",
Usage: "use one of the default values"},
},
Action: func(c *cli.Context) error {
m := c.StringSlice("meeting")
ok := false
for _, selected := range m {
for _, valid := range validMeetings {
if selected == valid {
ok = true
}
}
}
if !ok {
return fmt.Errorf("you must use one of %v", validMeetings)
}
fmt.Printf("%s\n", c.String("meeting"))
return nil
},
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
https://github.com/urfave/cli/blob/4f74020d9f07911f0fdb8facbc1f557a12cd2a93/app_test.go#L760

Related

Go-github not retrieving tag names correctly

I have the following simple golang code which retrieves tags from terraform repository:
import (
"github.com/google/go-github/v48/github"
"context"
)
func main() {
client := github.NewClient(nil)
tags, _, _ := client.Repositories.ListTags(context.Background(), "hashicorp", "terraform", nil)
if len(tags) > 0 {
latestTag := tags[0]
fmt.Println(latestTag.Name)
} else {
fmt.Printf("No tags yet")
}
}
Which returns a strange hexadecimal value:
0x1400035c4a0
And I would want to return:
v1.4.0-alpha20221207
Following the official docs, the function ListTags should return the name encoded into a struct:
https://pkg.go.dev/github.com/google/go-github/github#RepositoriesService.ListTags
Many thanks
I did try to execute a simple GET request https://api.github.com/repos/hashicorp/terraform/tags and I can see that the github api returns the tags correctly
IDK why, but I realize the latestTag.Name is a pointer and what you're printing is the address of the memory: 0x1400035c4a0.
You just need to dereference it:
fmt.Println(*latestTag.Name)
Bonus, check error with if condition that is returned by the function call to avoid having to go something like this:
tags, response, err := client.Repositories.ListTags(context.Background(), "hashicorp", "terraform", nil)
fmt.Println(response)
if err != nil {
fmt.Println(err)
} else {
if len(tags) > 0 {
latestTag := tags[0]
fmt.Println(*latestTag.Name)
} else {
fmt.Printf("No tags yet")
}
}

Parsing prometheus metrics from file and updating counters

I've a go application that gets run periodically by a batch. Each run, it should read some prometheus metrics from a file, run its logic, update a success/fail counter, and write metrics back out to a file.
From looking at How to parse Prometheus data as well as the godocs for prometheus, I'm able to read in the file, but I don't know how to update app_processed_total with the value returned by expfmt.ExtractSamples().
This is what I've done so far. Could someone please tell me how should I proceed from here? How can I typecast the Vector I got into a CounterVec?
package main
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
"github.com/prometheus/common/model"
)
var (
fileOnDisk = prometheus.NewRegistry()
processedTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "app_processed_total",
Help: "Number of times ran",
}, []string{"status"})
)
func doInit() {
prometheus.MustRegister(processedTotal)
}
func recordMetrics() {
go func() {
for {
processedTotal.With(prometheus.Labels{"status": "ok"}).Inc()
time.Sleep(5 * time.Second)
}
}()
}
func readExistingMetrics() {
var parser expfmt.TextParser
text := `
# HELP app_processed_total Number of times ran
# TYPE app_processed_total counter
app_processed_total{status="ok"} 300
`
parseText := func() ([]*dto.MetricFamily, error) {
parsed, err := parser.TextToMetricFamilies(strings.NewReader(text))
if err != nil {
return nil, err
}
var result []*dto.MetricFamily
for _, mf := range parsed {
result = append(result, mf)
}
return result, nil
}
gatherers := prometheus.Gatherers{
fileOnDisk,
prometheus.GathererFunc(parseText),
}
gathering, err := gatherers.Gather()
if err != nil {
fmt.Println(err)
}
fmt.Println("gathering: ", gathering)
for _, g := range gathering {
vector, err := expfmt.ExtractSamples(&expfmt.DecodeOptions{
Timestamp: model.Now(),
}, g)
fmt.Println("vector: ", vector)
if err != nil {
fmt.Println(err)
}
// How can I update processedTotal with this new value?
}
}
func main() {
doInit()
readExistingMetrics()
recordMetrics()
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe("localhost:2112", nil)
}
I believe you would need to use processedTotal.WithLabelValues("ok").Inc() or something similar to that.
The more complete example is here
func ExampleCounterVec() {
httpReqs := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
},
[]string{"code", "method"},
)
prometheus.MustRegister(httpReqs)
httpReqs.WithLabelValues("404", "POST").Add(42)
// If you have to access the same set of labels very frequently, it
// might be good to retrieve the metric only once and keep a handle to
// it. But beware of deletion of that metric, see below!
m := httpReqs.WithLabelValues("200", "GET")
for i := 0; i < 1000000; i++ {
m.Inc()
}
// Delete a metric from the vector. If you have previously kept a handle
// to that metric (as above), future updates via that handle will go
// unseen (even if you re-create a metric with the same label set
// later).
httpReqs.DeleteLabelValues("200", "GET")
// Same thing with the more verbose Labels syntax.
httpReqs.Delete(prometheus.Labels{"method": "GET", "code": "200"})
}
This is taken from the Promethus examples on Github
To use the value of vector you can do the following:
vectorFloat, err := strconv.ParseFloat(vector[0].Value.String(), 64)
if err != nil {
panic(err)
}
processedTotal.WithLabelValues("ok").Add(vectorFloat)
This is assuming you will only ever get a single vector value in your response. The value of the vector is stored as a string but you can convert it to a float with the strconv.ParseFloat method.

Testing urfave/cli based applications with go

I'm writing a small CLI application in Golang using urfave/cli framework and I'd like to write tests for it, but I can't find any useful information on how to test CLI applications, specifically written with the urfave/cli library. I have a lot of flags in the application and some of them are mutually exclusive and I'd like a proper test to stay on top of them - does anyone have an idea how to do it the right way?
EDIT:
Consider the following minimal example of application with several flags and restrictions around them. How would you test these flags usage (requirements, exclusivity, etc.) and how they influence the functions when they're set or not?
package main
import (
"errors"
"fmt"
"os"
"github.com/urfave/cli"
)
func doSomething(flag1 string, flag2 string, flag3 bool, flag4 bool) error {
err := errors.New("something")
return err
}
func main() {
app := cli.NewApp()
app.Name = "greet"
app.Usage = "fight the loneliness!"
var flag1, flag2 string
var flag3, flag4 bool
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "flag1",
Value: "",
Usage: "flag1",
Destination: &flag1,
},
cli.StringFlag{
Name: "flag2",
Value: "",
Usage: "flag2",
Destination: &flag2,
},
cli.BoolFlag{
Name: "flag3",
Usage: "flag3",
Destination: &flag3,
},
cli.BoolFlag{
Name: "flag4",
Usage: "flag4",
Destination: &flag4,
},
}
app.Action = func(c *cli.Context) error {
if flag1 != "" && c.NumFlags() > 1 {
fmt.Println("--flag1 flag cannot be used with any other flags")
cli.ShowAppHelp(c)
os.Exit(1)
}
if flag1 == "" && flag2 == "" || c.NumFlags() < 1 {
fmt.Println("--flag2 is required")
cli.ShowAppHelp(c)
os.Exit(1)
}
if flag3 && flag4 {
fmt.Println("--flag3 and --flag4 flags are mutually exclusive")
cli.ShowAppHelp(c)
os.Exit(1)
}
err := doSomething(flag1, flag2, flag3, flag4)
return err
}
}
As Adrian correctly wrote
the same way you test anything else
Given a slightly modified example of the sample code of the project
package main
import (
"fmt"
"log"
"os"
"github.com/urfave/cli"
)
func Friend(c *cli.Context) error {
fmt.Println("Hello friend!")
return nil
}
func main() {
app := cli.NewApp()
app.Name = "greet"
app.Usage = "fight the loneliness!"
app.Action = Friend
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
Since this code actually prints something instead of returning a value you can evaluate, you could use a testable example
func ExampleFriend(){
// Yeah, technically, we can save the error check with the code above
// but this illustrates how you can make sure the output
// is not what the testable example expects.
if err := Friend(nil){
fmt.Printf("Friend: %s",err)
}
// Output:
// Hello friend!
}
Note that Action expects an ActionFunc. Where you define that ActionFunc is pretty much your thing. It could even come from a different package. So it is your design on how good your application will be testable.
Edit The signature of the value Action expects will change in the future, at least according to the docs. I already find it questionable to use interface{} to be able to pass nil to Action, and then check and type assert for ActionFunc, where a no-op ActionFunc would actually serve the same purpose, but removing an error return value really makes me scratch my head. I strongly recommend to have a look at alecthomas/kingpin for smaller to medium size applications or spf13/cobra, which is suitable even for the most complex of cli applications.

How to parse yaml and get value for interface- deep structue

I'm trying to parse this yaml and I want to get the values of the run entry (test1 or test2) without success, here is my working example.
im a bit get lost with the map inside map :( ,
this is given yaml which I couldent change ...
any idea how could I got those values
package main
import (
"fmt"
"log"
"gopkg.in/yaml.v2"
)
var runContent = []byte(`
version: "3.2"
run-parameters:
before:
run-parameters:
run: test1
after:
run-parameters:
run: test2
`)
type FTD struct {
Version string `yaml:"version,omitempty"`
BuildParams *RunParams `yaml:"run-parameters,omitempty"`
}
type RunParams struct {
BeforeExec map[string]interface{} `yaml:"before,omitempty"`
AfterExec map[string]interface{} `yaml:"after,omitempty"`
}
func main() {
runners := &FTD{}
// parse mta yaml
err := yaml.Unmarshal(runContent, runners)
if err != nil {
log.Fatalf("Error : %v", err)
}
for k, v := range runners.BuildParams.BeforeExec {
fmt.Println(k, v.(interface{}))
}
}
This is working example
https://play.golang.org/p/qTqUJy3md0c
also I've tried with
this is working
run := runners.BuildParams.BeforeExec["run-parameters"].(map[interface{}]interface{})["run"]
fmt.Println("run: ", run)
what I've tried is this which works but what happens if the run value is empty or no entry at all,this will cause a dump how can I overcome this ?
what I've tried is this which works but what happens if the run value is empty or no entry at all,this will cause a dump how can I overcome this ?
You can do
runParams, ok := runners.BuildParams.BeforeExec["run-parameters"]
if !ok {
// handle lack of "run-parameters" in BeforeExec
}
runParamsMap, ok := runParams.(map[interface{}]interface{})
if !ok {
// handle "run-parameters" not being a map
}
run, ok := runParamsMap["run"]
if !ok {
// handle lack of "run" inside "run-parameters"
}
runStr, ok := run.(string)
if !ok {
// handle "run" not being a string
}
fmt.Println("run: ", runStr)
This is quite verbose so you could use something like https://github.com/jmoiron/jsonq, where you can specify a "path" to the desired value nested inside several levels of maps. Despite the "json" in the name, this library works with map[string]interface{} and not json files. But note that the library you use for yaml unmarshalling results in map[interface{}]interface{} instead of map[string]interface{} and you will have to use a different one in order for it to work with jsonq.
run, err := jsonq.NewQuery(runners.BuildParams.BeforeExec).String("run-parameters", "run")
if err != nil {
// handle all possible errors in one place
}
fmt.Println("run: ", run)

golang comments and docs fields when doing ast.Inspect — why are they blank?

I'm trying to get at the Docs and Comments of structs and struct fields, but I can't seem to be able to do so, they just turn up empty:
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
func main() {
src := `package test
// Hello
type A struct {
// Where
B int // Are you
}
`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
ast.Inspect(f, func(n ast.Node) bool {
switch t := n.(type) {
case *ast.TypeSpec:
fmt.Println(t.Doc.Text())
case *ast.StructType:
for _, field := range t.Fields.List {
fmt.Println(field.Doc.Text())
fmt.Println(field.Comment.Text())
}
}
return true
})
}
yields three blank lines: https://play.golang.org/p/4Eh9gS-PUg
Saw the similar question Go parser not detecting Doc comments on struct type but when trying to run the accepted example it turns up all empty — so I'm wondering if something has changed since that version.
In order to get comments, you have to pass the parser.ParseComments flag in argument to parser.ParseFile():
parser.ParseFile(fset, "", src, parser.ParseComments)
All possible mode flags are documented here:
https://golang.org/pkg/go/parser/#Mode

Resources